malepati/custom_template_working
0
1#!/usr/bin/env python32import json3import os4import re5import sys6from pathlib import Path7from typing import Dict, List, Optional, Tuple8 9REPO_ROOT = Path(__file__).resolve().parent.parent10FASTAPI_DIR = REPO_ROOT / "servers" / "fastapi"11NEXT_DIR = REPO_ROOT / "servers" / "nextjs"12NOTICE_PATH = REPO_ROOT / "NOTICE"13 14PY_LICENSE_CANDIDATES = [15 "LICENSE",16 "LICENSE.txt",17 "LICENSE.md",18 "LICENCE",19 "COPYING",20 "COPYING.txt",21 "NOTICE",22 "NOTICE.txt",23]24 25NODE_LICENSE_CANDIDATES = [26 "LICENSE",27 "LICENSE.txt",28 "LICENSE.md",29 "LICENCE",30 "LICENCE.txt",31 "COPYING",32 "COPYING.txt",33 "NOTICE",34 "NOTICE.txt",35]36 37 38def read_text_safe(path: Path) -> str:39 try:40 return path.read_text(encoding="utf-8", errors="replace").strip()41 except Exception:42 return ""43 44 45def parse_rfc822_metadata(text: str) -> Dict[str, str]:46 data: Dict[str, str] = {}47 key: Optional[str] = None48 for raw_line in text.splitlines():49 if not raw_line:50 key = None51 continue52 if raw_line[0] in " \t" and key:53 data[key] += "\n" + raw_line.strip()54 continue55 if ":" in raw_line:56 k, v = raw_line.split(":", 1)57 key = k.strip()58 data[key] = v.strip()59 return data60 61 62def find_python_site_packages(venv_dir: Path) -> Optional[Path]:63 # Linux/mac64 lib_dir = venv_dir / "lib"65 if lib_dir.exists():66 for child in lib_dir.iterdir():67 if child.is_dir() and child.name.startswith("python"):68 sp = child / "site-packages"69 if sp.exists():70 return sp71 # Windows72 sp = venv_dir / "Lib" / "site-packages"73 if sp.exists():74 return sp75 return None76 77 78def detect_python_venv() -> Optional[Path]:79 env_path = os.environ.get("NOTICE_PYTHON_VENV")80 if env_path:81 v = Path(env_path)82 if v.exists():83 return v84 default = FASTAPI_DIR / ".venv"85 if default.exists():86 return default87 active = os.environ.get("VIRTUAL_ENV")88 if active and FASTAPI_DIR.as_posix() in Path(active).as_posix():89 return Path(active)90 return None91 92 93def scan_python_packages(site_packages_dir: Path) -> List[Dict[str, str]]:94 entries: List[Dict[str, str]] = []95 dist_infos = sorted(site_packages_dir.glob("*.dist-info"))96 for dist in dist_infos:97 metadata_path = dist / "METADATA"98 if not metadata_path.exists():99 continue100 meta = parse_rfc822_metadata(read_text_safe(metadata_path))101 name = meta.get("Name", "").strip()102 version = meta.get("Version", "").strip()103 license_name = meta.get("License", "").strip()104 if not name:105 # Fallback to folder name pattern106 # e.g., requests-2.32.3.dist-info107 base = dist.name[:-10]108 if "-" in base:109 parts = base.rsplit("-", 1)110 if len(parts) == 2:111 name = parts[0]112 version = version or parts[1]113 author = meta.get("Author", meta.get("Maintainer", meta.get("Author-email", ""))).strip()114 115 # License text candidates inside dist-info116 license_text = ""117 for cand in PY_LICENSE_CANDIDATES:118 p = dist / cand119 if p.exists():120 license_text = read_text_safe(p)121 if license_text:122 break123 124 # Search via RECORD for license files elsewhere125 if not license_text:126 record = dist / "RECORD"127 if record.exists():128 for line in read_text_safe(record).splitlines():129 path_part = line.split(",", 1)[0]130 lower = path_part.lower()131 if any(token in lower for token in ["license", "licence", "copying", "notice"]):132 target = site_packages_dir / path_part133 if target.exists():134 license_text = read_text_safe(target)135 if license_text:136 break137 138 # As last resort, embed the License: field content139 if not license_text and license_name:140 license_text = f"License field from METADATA:\n{license_name}"141 142 entries.append({143 "name": name or dist.name,144 "version": version,145 "license": license_name,146 "author": author,147 "license_text": license_text,148 })149 150 # Sort by name for stability151 entries.sort(key=lambda e: (e["name"].lower(), e["version"]))152 return entries153 154 155def find_license_file_in_dir(base_dir: Path, depth_limit: int = 2) -> Optional[Path]:156 # First, try immediate candidates157 for cand in NODE_LICENSE_CANDIDATES:158 p = base_dir / cand159 if p.exists():160 return p161 # case-insensitive check162 for child in base_dir.iterdir():163 if child.is_file() and child.name.lower() == cand.lower():164 return child165 166 # Recursive limited-depth scan excluding nested node_modules167 def walk(dir_path: Path, depth: int) -> Optional[Path]:168 if depth > depth_limit:169 return None170 try:171 it = list(dir_path.iterdir())172 except Exception:173 return None174 for child in it:175 name_lower = child.name.lower()176 if child.is_dir():177 if child.name == "node_modules" or child.name.startswith('.'):178 continue179 found = walk(child, depth + 1)180 if found:181 return found182 else:183 if any(tok in name_lower for tok in ["license", "licence", "copying", "notice"]):184 return child185 return None186 187 return walk(base_dir, 0)188 189 190def scan_node_modules(node_modules_dir: Path) -> List[Dict[str, str]]:191 entries: List[Dict[str, str]] = []192 seen: set[str] = set()193 194 def visit_pkg(pkg_dir: Path):195 pkg_json = pkg_dir / "package.json"196 if not pkg_json.exists():197 return198 try:199 data = json.loads(read_text_safe(pkg_json) or "{}")200 except Exception:201 return202 name = data.get("name") or pkg_dir.name203 version = str(data.get("version") or "")204 key = f"{name}@{version}"205 if key in seen:206 return207 seen.add(key)208 209 license_name = ""210 lic_field = data.get("license")211 if isinstance(lic_field, str):212 license_name = lic_field213 elif isinstance(lic_field, dict):214 license_name = lic_field.get("type", "")215 elif isinstance(data.get("licenses"), list):216 license_name = ", ".join([str(x.get("type", "")) for x in data["licenses"] if isinstance(x, dict)])217 218 author = ""219 a = data.get("author")220 if isinstance(a, str):221 author = a222 elif isinstance(a, dict):223 author = a.get("name", "")224 225 license_text = ""226 lic_file = find_license_file_in_dir(pkg_dir, depth_limit=2)227 if lic_file:228 license_text = read_text_safe(lic_file)229 230 entries.append({231 "name": name,232 "version": version,233 "license": license_name,234 "author": author,235 "license_text": license_text,236 })237 238 def walk_node_modules(base: Path):239 if not base.exists():240 return241 for entry in base.iterdir():242 if not entry.is_dir():243 continue244 if entry.name == ".bin":245 continue246 if entry.name.startswith("@"): # scoped packages247 for scoped in entry.iterdir():248 if scoped.is_dir():249 visit_pkg(scoped)250 # nested node_modules inside the package251 nested = scoped / "node_modules"252 walk_node_modules(nested)253 continue254 visit_pkg(entry)255 nested = entry / "node_modules"256 walk_node_modules(nested)257 258 walk_node_modules(node_modules_dir)259 # Sort by package name260 entries.sort(key=lambda e: (e["name"].lower(), e["version"]))261 return entries262 263 264def format_section(title: str, entries: List[Dict[str, str]]) -> str:265 header = [266 "-------------------------------------",267 title,268 "-------------------------------------",269 "",270 ]271 lines: List[str] = ["\n".join(header)]272 for e in entries:273 block = [274 e.get("name", "").strip(),275 e.get("version", "").strip(),276 e.get("license", "").strip(),277 e.get("author", "").strip(),278 "",279 (e.get("license_text", "") or "LICENSE TEXT NOT FOUND").strip(),280 "",281 "",282 ]283 lines.append("\n".join(block))284 return "".join(lines).rstrip() + "\n"285 286 287def main():288 # Optional CLI overrides289 import argparse290 parser = argparse.ArgumentParser(description="Rebuild NOTICE from installed packages")291 parser.add_argument("--python-venv", dest="python_venv", default=None, help="Path to Python venv to scan")292 parser.add_argument("--node-modules", dest="node_modules", default=None, help="Path to node_modules to scan")293 args = parser.parse_args()294 python_entries: List[Dict[str, str]] = []295 node_entries: List[Dict[str, str]] = []296 297 # Python scan298 venv = Path(args.python_venv) if args.python_venv else detect_python_venv()299 if venv:300 sp = find_python_site_packages(venv)301 if sp and sp.exists():302 python_entries = scan_python_packages(sp)303 else:304 print(f"Warning: site-packages not found under {venv}", file=sys.stderr)305 else:306 print("Warning: Python venv not found. Set NOTICE_PYTHON_VENV or create servers/fastapi/.venv", file=sys.stderr)307 308 # Node scan309 node_modules_dir = Path(args.node_modules or os.environ.get("NOTICE_NODE_MODULES") or (NEXT_DIR / "node_modules"))310 if node_modules_dir.exists():311 node_entries = scan_node_modules(node_modules_dir)312 else:313 print(f"Warning: node_modules not found at {node_modules_dir}", file=sys.stderr)314 315 # Build NOTICE content316 parts: List[str] = []317 if python_entries:318 parts.append(format_section("PYTHON PACKAGES", python_entries))319 if node_entries:320 parts.append(format_section("NODE PACKAGES", node_entries))321 if not parts:322 print("Error: No sections generated. Ensure .venv and node_modules exist.", file=sys.stderr)323 sys.exit(1)324 325 content = "\n".join(parts)326 NOTICE_PATH.write_text(content, encoding="utf-8")327 print("NOTICE rebuilt from installed packages")328 329 330if __name__ == "__main__":331 main()332 333 334 