python-infrakit-dev 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. infrakit/__init__.py +0 -0
  2. infrakit/cli/__init__.py +1 -0
  3. infrakit/cli/commands/__init__.py +1 -0
  4. infrakit/cli/commands/deps.py +530 -0
  5. infrakit/cli/commands/init.py +129 -0
  6. infrakit/cli/commands/llm.py +295 -0
  7. infrakit/cli/commands/logger.py +160 -0
  8. infrakit/cli/commands/module.py +342 -0
  9. infrakit/cli/commands/time.py +81 -0
  10. infrakit/cli/main.py +65 -0
  11. infrakit/core/__init__.py +0 -0
  12. infrakit/core/config/__init__.py +0 -0
  13. infrakit/core/config/converter.py +480 -0
  14. infrakit/core/config/exporter.py +304 -0
  15. infrakit/core/config/loader.py +713 -0
  16. infrakit/core/config/validator.py +389 -0
  17. infrakit/core/logger/__init__.py +21 -0
  18. infrakit/core/logger/formatters.py +143 -0
  19. infrakit/core/logger/handlers.py +322 -0
  20. infrakit/core/logger/retention.py +176 -0
  21. infrakit/core/logger/setup.py +314 -0
  22. infrakit/deps/__init__.py +239 -0
  23. infrakit/deps/clean.py +141 -0
  24. infrakit/deps/depfile.py +405 -0
  25. infrakit/deps/health.py +357 -0
  26. infrakit/deps/optimizer.py +642 -0
  27. infrakit/deps/scanner.py +550 -0
  28. infrakit/llm/__init__.py +35 -0
  29. infrakit/llm/batch.py +165 -0
  30. infrakit/llm/client.py +575 -0
  31. infrakit/llm/key_manager.py +728 -0
  32. infrakit/llm/llm_readme.md +306 -0
  33. infrakit/llm/models.py +148 -0
  34. infrakit/llm/providers/__init__.py +5 -0
  35. infrakit/llm/providers/base.py +112 -0
  36. infrakit/llm/providers/gemini.py +164 -0
  37. infrakit/llm/providers/openai.py +168 -0
  38. infrakit/llm/rate_limiter.py +54 -0
  39. infrakit/scaffolder/__init__.py +31 -0
  40. infrakit/scaffolder/ai.py +508 -0
  41. infrakit/scaffolder/backend.py +555 -0
  42. infrakit/scaffolder/cli_tool.py +386 -0
  43. infrakit/scaffolder/generator.py +338 -0
  44. infrakit/scaffolder/pipeline.py +562 -0
  45. infrakit/scaffolder/registry.py +121 -0
  46. infrakit/time/__init__.py +60 -0
  47. infrakit/time/profiler.py +511 -0
  48. python_infrakit_dev-0.1.0.dist-info/METADATA +124 -0
  49. python_infrakit_dev-0.1.0.dist-info/RECORD +51 -0
  50. python_infrakit_dev-0.1.0.dist-info/WHEEL +4 -0
  51. python_infrakit_dev-0.1.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,357 @@
1
+ """
2
+ infrakit.deps._health
3
+ ~~~~~~~~~~~~~~~~~~~~~
4
+ Dependency health checks:
5
+ - outdated packages (via PyPI JSON API)
6
+ - security vulnerabilities (via pip-audit subprocess)
7
+ - license compatibility (via pip show / importlib.metadata)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import subprocess
14
+ import sys
15
+ import urllib.request
16
+ import urllib.error
17
+ from concurrent.futures import ThreadPoolExecutor, as_completed
18
+ from dataclasses import dataclass, field
19
+ from typing import Optional
20
+ import importlib.metadata
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Data structures
25
+ # ---------------------------------------------------------------------------
26
+
27
+ @dataclass
28
+ class OutdatedPackage:
29
+ name: str
30
+ current: str
31
+ latest: str
32
+ status: str # 'outdated' | 'up-to-date' | 'unknown' | 'not-installed'
33
+
34
+
35
+ @dataclass
36
+ class Vulnerability:
37
+ package: str
38
+ installed_version: str
39
+ vuln_id: str
40
+ description: str
41
+ fix_version: str = ""
42
+
43
+
44
+ @dataclass
45
+ class LicenseInfo:
46
+ package: str
47
+ version: str
48
+ license: str
49
+ compatible: Optional[bool] = None
50
+ notes: str = ""
51
+
52
+
53
+ @dataclass
54
+ class HealthReport:
55
+ outdated: list[OutdatedPackage] = field(default_factory=list)
56
+ vulnerabilities: list[Vulnerability] = field(default_factory=list)
57
+ licenses: list[LicenseInfo] = field(default_factory=list)
58
+ errors: list[str] = field(default_factory=list)
59
+
60
+
61
+ # ---------------------------------------------------------------------------
62
+ # Helpers
63
+ # ---------------------------------------------------------------------------
64
+
65
+ def _norm(name: str) -> str:
66
+ """Normalise package name: lowercase, underscores -> hyphens."""
67
+ return name.lower().replace("_", "-")
68
+
69
+
70
+ # ---------------------------------------------------------------------------
71
+ # Installed package versions
72
+ # Handles both classic pip venvs and uv-managed venvs (where pip itself is
73
+ # not installed and importlib.metadata may return an incomplete picture).
74
+ # ---------------------------------------------------------------------------
75
+
76
+ def get_all_installed() -> dict[str, str]:
77
+ """
78
+ Return dict of normalised_package_name -> version for every installed dist.
79
+
80
+ Strategy (tried in order):
81
+ 1. importlib.metadata.distributions() -- fast, works when packages are
82
+ on sys.path (standard pip venvs, system Python)
83
+ 2. ``uv pip list --format=json`` -- covers uv-managed venvs where pip
84
+ itself may not be installed and importlib.metadata returns nothing
85
+ 3. ``python -m pip list --format=json`` -- classic fallback
86
+
87
+ Returns an empty dict only if all three fail.
88
+ """
89
+ result: dict[str, str] = {}
90
+
91
+ # 1. importlib.metadata
92
+ try:
93
+ for dist in importlib.metadata.distributions():
94
+ try:
95
+ meta = dist.metadata
96
+ name = meta.get("Name")
97
+ version = meta.get("Version")
98
+ if name and version:
99
+ result[_norm(str(name))] = str(version)
100
+ except Exception:
101
+ continue
102
+ except Exception:
103
+ pass
104
+
105
+ if result:
106
+ return result
107
+
108
+ # 2 & 3: subprocess fallbacks
109
+ for cmd in (
110
+ ["uv", "pip", "list", "--format=json"],
111
+ [sys.executable, "-m", "pip", "list", "--format=json"],
112
+ ):
113
+ try:
114
+ proc = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
115
+ if proc.returncode == 0 and proc.stdout.strip():
116
+ for entry in json.loads(proc.stdout):
117
+ n = entry.get("name", "")
118
+ v = entry.get("version", "")
119
+ if n and v:
120
+ result[_norm(n)] = v
121
+ if result:
122
+ return result
123
+ except Exception:
124
+ continue
125
+
126
+ return result
127
+
128
+
129
+ def get_installed_version(package: str) -> Optional[str]:
130
+ """
131
+ Return the installed version of a package, or None if not found.
132
+ Fast path via importlib.metadata; falls back to get_all_installed()
133
+ to handle uv venvs where importlib.metadata may be incomplete.
134
+ """
135
+ # Fast path
136
+ for candidate in (package, _norm(package)):
137
+ try:
138
+ return importlib.metadata.version(candidate)
139
+ except importlib.metadata.PackageNotFoundError:
140
+ pass
141
+
142
+ # Slow path (handles uv-managed venvs)
143
+ return get_all_installed().get(_norm(package))
144
+
145
+
146
+ # ---------------------------------------------------------------------------
147
+ # Outdated check (PyPI JSON API)
148
+ # ---------------------------------------------------------------------------
149
+
150
+ def _fetch_pypi_latest(package: str, timeout: int = 8) -> Optional[str]:
151
+ """Query PyPI JSON API for the latest stable version of a package."""
152
+ url = f"https://pypi.org/pypi/{package}/json"
153
+ try:
154
+ req = urllib.request.Request(url, headers={"User-Agent": "infrakit/1.0"})
155
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
156
+ data = json.loads(resp.read())
157
+ return data["info"]["version"]
158
+ except urllib.error.HTTPError:
159
+ return None
160
+ except Exception:
161
+ return None
162
+
163
+
164
+ def _version_is_older(current: str, latest: str) -> bool:
165
+ """
166
+ Return True if current < latest.
167
+ Uses packaging.version when available; falls back to pure-stdlib
168
+ numeric tuple comparison.
169
+ """
170
+ try:
171
+ from packaging.version import Version # type: ignore
172
+ return Version(current) < Version(latest)
173
+ except Exception:
174
+ pass
175
+
176
+ def _segments(v: str) -> tuple:
177
+ parts = []
178
+ for seg in v.split("."):
179
+ digits = ""
180
+ for ch in seg:
181
+ if ch.isdigit():
182
+ digits += ch
183
+ else:
184
+ break
185
+ parts.append(int(digits) if digits else 0)
186
+ return tuple(parts)
187
+
188
+ try:
189
+ return _segments(current) < _segments(latest)
190
+ except Exception:
191
+ return current != latest
192
+
193
+
194
+ def check_outdated(packages: list[str], workers: int = 10) -> list[OutdatedPackage]:
195
+ """Check PyPI for the latest version of each package."""
196
+ installed = get_all_installed()
197
+
198
+ def _check(pkg: str) -> OutdatedPackage:
199
+ norm = _norm(pkg)
200
+ current = installed.get(norm)
201
+ if current is None:
202
+ return OutdatedPackage(pkg, "not installed", "unknown", "not-installed")
203
+ latest = _fetch_pypi_latest(pkg)
204
+ if latest is None:
205
+ return OutdatedPackage(pkg, current, "unknown", "unknown")
206
+ status = "outdated" if _version_is_older(current, latest) else "up-to-date"
207
+ return OutdatedPackage(pkg, current, latest, status)
208
+
209
+ results: list[OutdatedPackage] = []
210
+ with ThreadPoolExecutor(max_workers=workers) as pool:
211
+ futures = {pool.submit(_check, p): p for p in packages}
212
+ for fut in as_completed(futures):
213
+ try:
214
+ results.append(fut.result())
215
+ except Exception as exc:
216
+ pkg = futures[fut]
217
+ results.append(OutdatedPackage(pkg, "error", "error", f"error: {exc}"))
218
+
219
+ return sorted(results, key=lambda x: (x.status != "outdated", x.name.lower()))
220
+
221
+
222
+ # ---------------------------------------------------------------------------
223
+ # Security vulnerability scan (pip-audit)
224
+ # ---------------------------------------------------------------------------
225
+
226
+ def _parse_pip_audit_json(raw: str) -> list[Vulnerability]:
227
+ vulns: list[Vulnerability] = []
228
+ try:
229
+ data = json.loads(raw)
230
+ except json.JSONDecodeError:
231
+ return vulns
232
+ for entry in data:
233
+ pkg_name = entry.get("name", "")
234
+ pkg_ver = entry.get("version", "")
235
+ for v in entry.get("vulns", []):
236
+ fix_vers = v.get("fix_versions", [])
237
+ vulns.append(Vulnerability(
238
+ package=pkg_name,
239
+ installed_version=pkg_ver,
240
+ vuln_id=v.get("id", ""),
241
+ description=v.get("description", ""),
242
+ fix_version=", ".join(fix_vers) if fix_vers else "no fix available",
243
+ ))
244
+ return vulns
245
+
246
+
247
+ def check_vulnerabilities(
248
+ packages: Optional[list[str]] = None,
249
+ ) -> tuple[list[Vulnerability], Optional[str]]:
250
+ """Run pip-audit and return (vulnerabilities, error_message)."""
251
+ try:
252
+ chk = subprocess.run(
253
+ [sys.executable, "-m", "pip_audit", "--version"],
254
+ capture_output=True, text=True, timeout=15,
255
+ )
256
+ if chk.returncode != 0:
257
+ return [], "pip-audit is not installed. Run: pip install pip-audit"
258
+ except FileNotFoundError:
259
+ return [], "pip-audit is not installed. Run: pip install pip-audit"
260
+ except subprocess.TimeoutExpired:
261
+ return [], "pip-audit version check timed out"
262
+
263
+ cmd = [sys.executable, "-m", "pip_audit", "--format=json", "--progress-spinner=off"]
264
+ try:
265
+ proc = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
266
+ vulns = _parse_pip_audit_json(proc.stdout or proc.stderr)
267
+ if packages:
268
+ pkg_lower = {_norm(p) for p in packages}
269
+ vulns = [v for v in vulns if _norm(v.package) in pkg_lower]
270
+ return vulns, None
271
+ except subprocess.TimeoutExpired:
272
+ return [], "pip-audit timed out after 120s"
273
+ except Exception as exc:
274
+ return [], f"pip-audit error: {exc}"
275
+
276
+
277
+ # ---------------------------------------------------------------------------
278
+ # License check
279
+ # ---------------------------------------------------------------------------
280
+
281
+ _LICENSE_NOTES: dict[str, tuple[Optional[bool], str]] = {
282
+ "mit": (True, "Permissive — safe for most projects"),
283
+ "apache-2.0": (True, "Permissive with patent protection"),
284
+ "apache 2.0": (True, "Permissive with patent protection"),
285
+ "apache software": (True, "Permissive with patent protection"),
286
+ "bsd-2-clause": (True, "Permissive"),
287
+ "bsd-3-clause": (True, "Permissive"),
288
+ "bsd": (True, "Permissive"),
289
+ "isc": (True, "Permissive"),
290
+ "python-2.0": (True, "Permissive (PSF)"),
291
+ "psf": (True, "Python Software Foundation — permissive"),
292
+ "unlicense": (True, "Public domain"),
293
+ "cc0": (True, "Public domain"),
294
+ "mpl-2.0": (None, "Weak copyleft — file-level, review for your use case"),
295
+ "lgpl-2.0": (None, "Weak copyleft — dynamic linking usually OK"),
296
+ "lgpl-2.1": (None, "Weak copyleft — dynamic linking usually OK"),
297
+ "lgpl-3.0": (None, "Weak copyleft — dynamic linking usually OK"),
298
+ "gpl-2.0": (False, "Strong copyleft — may require source disclosure"),
299
+ "gpl-3.0": (False, "Strong copyleft — may require source disclosure"),
300
+ "agpl-3.0": (False, "Network copyleft — very restrictive"),
301
+ "proprietary": (False, "Proprietary — check license terms"),
302
+ "commercial": (False, "Commercial — check license terms"),
303
+ }
304
+
305
+
306
+ def _normalise_license(raw: str) -> str:
307
+ return raw.lower().strip().replace(" license", "").replace("license", "").strip()
308
+
309
+
310
+ def _get_package_license(package: str) -> tuple[str, str]:
311
+ for candidate in (package, _norm(package)):
312
+ try:
313
+ meta = importlib.metadata.metadata(candidate)
314
+ version = str(meta.get("Version", "unknown"))
315
+ lic = str(meta.get("License", "") or "")
316
+ if not lic:
317
+ for c in (meta.get_all("Classifier") or []):
318
+ if "License" in c:
319
+ lic = c.split("::")[-1].strip()
320
+ break
321
+ return lic or "UNKNOWN", version
322
+ except importlib.metadata.PackageNotFoundError:
323
+ continue
324
+ return "UNKNOWN", "unknown"
325
+
326
+
327
+ def check_licenses(packages: list[str]) -> list[LicenseInfo]:
328
+ results: list[LicenseInfo] = []
329
+ for pkg in packages:
330
+ lic, ver = _get_package_license(pkg)
331
+ norm = _normalise_license(lic)
332
+ compatible, notes = _LICENSE_NOTES.get(norm, (None, "Unknown license — review manually"))
333
+ results.append(LicenseInfo(package=pkg, version=ver, license=lic, compatible=compatible, notes=notes))
334
+ return sorted(results, key=lambda x: (x.compatible is not False, x.package.lower()))
335
+
336
+
337
+ # ---------------------------------------------------------------------------
338
+ # Combined health check
339
+ # ---------------------------------------------------------------------------
340
+
341
+ def run_health_check(
342
+ packages: list[str],
343
+ check_outdated_flag: bool = True,
344
+ check_vulns_flag: bool = True,
345
+ check_licenses_flag: bool = True,
346
+ ) -> HealthReport:
347
+ report = HealthReport()
348
+ if check_outdated_flag:
349
+ report.outdated = check_outdated(packages)
350
+ if check_vulns_flag:
351
+ vulns, err = check_vulnerabilities(packages)
352
+ report.vulnerabilities = vulns
353
+ if err:
354
+ report.errors.append(f"Security scan: {err}")
355
+ if check_licenses_flag:
356
+ report.licenses = check_licenses(packages)
357
+ return report