"""Every third-party import must be declared in requirements.txt or pyproject.toml. `requests` — the HTTP client behind EVERY COSMOS call, imported at module level in `cosmos/client.py` — was in neither file. A clean `pip install -r requirements.txt` then failed on import, not at the first request. `openpyxl`, which reads the competitor workbook on the default path, was missing too. Meanwhile `openai` was declared and never imported: the real import is `langchain-openai`, so declaring `openai` alone did not enable the LLM path. These tests parse the actual imports instead of trusting a hand-maintained list, so adding a dependency without declaring it fails here rather than on someone else's machine. """ from __future__ import annotations import ast import pathlib import re import sys ROOT = pathlib.Path(__file__).resolve().parents[1] SKIP_DIRS = {"__pycache__", ".venv", "new_archtetcure_and frontend", ".mojibake_backup"} LOCAL = {"pricing_agent", "dashboard", "config", "app", "legacy_app", "backend", "tests"} # import name -> distribution name, where they differ. DIST = { "yaml": "pyyaml", "dotenv": "python-dotenv", "apify_client": "apify-client", "langchain_openai": "langchain-openai", "pydantic_settings": "pydantic-settings", "google": "google-auth", "sp_api": "python-amazon-sp-api", "PIL": "pillow", } # Test-only, and declared under [dev]. TEST_ONLY = {"pytest"} def _sources(): for f in ROOT.rglob("*.py"): if not SKIP_DIRS.intersection(f.parts): yield f def _third_party_imports() -> dict[str, set[str]]: found: dict[str, set[str]] = {} for f in _sources(): try: tree = ast.parse(f.read_text(encoding="utf-8")) except SyntaxError: # not our problem here continue for node in ast.walk(tree): mods: list[str] = [] if isinstance(node, ast.Import): mods = [a.name.split(".")[0] for a in node.names] elif isinstance(node, ast.ImportFrom) and node.module and node.level == 0: mods = [node.module.split(".")[0]] for m in mods: if m and m not in sys.stdlib_module_names and m not in LOCAL: found.setdefault(m, set()).add(str(f.relative_to(ROOT))) return found def _declared() -> set[str]: text = (ROOT / "requirements.txt").read_text(encoding="utf-8") text += (ROOT / "pyproject.toml").read_text(encoding="utf-8") names = re.findall(r"^\s*[\"']?([A-Za-z][A-Za-z0-9._-]*)\s*(?:[><=!~]|[\"']|$)", text, re.M) return {n.lower().replace("_", "-") for n in names} def test_every_import_is_declared_somewhere(): declared = _declared() missing = {} for mod, files in _third_party_imports().items(): if mod in TEST_ONLY: continue dist = DIST.get(mod, mod).lower().replace("_", "-") if dist not in declared and mod.lower().replace("_", "-") not in declared: missing[dist] = sorted(files)[:3] assert not missing, f"undeclared dependencies: {missing}" def test_requests_is_in_requirements_txt(): """Module-level in cosmos/client.py — the dashboard cannot import without it.""" req = (ROOT / "requirements.txt").read_text(encoding="utf-8") assert re.search(r"^requests\b", req, re.M), "requests missing from requirements.txt" def test_openpyxl_is_in_requirements_txt(): """The default competitor path reads a workbook; without this it raises at runtime.""" req = (ROOT / "requirements.txt").read_text(encoding="utf-8") assert re.search(r"^openpyxl\b", req, re.M), "openpyxl missing from requirements.txt" def test_the_llm_extra_names_the_package_actually_imported(): """`openai` alone does not enable the narrative — the import is `langchain_openai`.""" req = (ROOT / "requirements.txt").read_text(encoding="utf-8") assert re.search(r"^langchain-openai\b", req, re.M) def test_dashboard_requirements_stay_a_subset_of_the_package(): """Anything the dashboard needs must also be installable via `pip install -e .`.""" req_names = { m.group(1).lower() for m in re.finditer(r"^([A-Za-z][A-Za-z0-9._-]*)", (ROOT / "requirements.txt").read_text(encoding="utf-8"), re.M) } proj = (ROOT / "pyproject.toml").read_text(encoding="utf-8").lower() # streamlit/plotly/pandas/numpy live under the [ui] extra; everything else is a core dep. for name in req_names: assert name in proj, f"{name} is in requirements.txt but not in pyproject.toml"