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.
- infrakit/__init__.py +0 -0
- infrakit/cli/__init__.py +1 -0
- infrakit/cli/commands/__init__.py +1 -0
- infrakit/cli/commands/deps.py +530 -0
- infrakit/cli/commands/init.py +129 -0
- infrakit/cli/commands/llm.py +295 -0
- infrakit/cli/commands/logger.py +160 -0
- infrakit/cli/commands/module.py +342 -0
- infrakit/cli/commands/time.py +81 -0
- infrakit/cli/main.py +65 -0
- infrakit/core/__init__.py +0 -0
- infrakit/core/config/__init__.py +0 -0
- infrakit/core/config/converter.py +480 -0
- infrakit/core/config/exporter.py +304 -0
- infrakit/core/config/loader.py +713 -0
- infrakit/core/config/validator.py +389 -0
- infrakit/core/logger/__init__.py +21 -0
- infrakit/core/logger/formatters.py +143 -0
- infrakit/core/logger/handlers.py +322 -0
- infrakit/core/logger/retention.py +176 -0
- infrakit/core/logger/setup.py +314 -0
- infrakit/deps/__init__.py +239 -0
- infrakit/deps/clean.py +141 -0
- infrakit/deps/depfile.py +405 -0
- infrakit/deps/health.py +357 -0
- infrakit/deps/optimizer.py +642 -0
- infrakit/deps/scanner.py +550 -0
- infrakit/llm/__init__.py +35 -0
- infrakit/llm/batch.py +165 -0
- infrakit/llm/client.py +575 -0
- infrakit/llm/key_manager.py +728 -0
- infrakit/llm/llm_readme.md +306 -0
- infrakit/llm/models.py +148 -0
- infrakit/llm/providers/__init__.py +5 -0
- infrakit/llm/providers/base.py +112 -0
- infrakit/llm/providers/gemini.py +164 -0
- infrakit/llm/providers/openai.py +168 -0
- infrakit/llm/rate_limiter.py +54 -0
- infrakit/scaffolder/__init__.py +31 -0
- infrakit/scaffolder/ai.py +508 -0
- infrakit/scaffolder/backend.py +555 -0
- infrakit/scaffolder/cli_tool.py +386 -0
- infrakit/scaffolder/generator.py +338 -0
- infrakit/scaffolder/pipeline.py +562 -0
- infrakit/scaffolder/registry.py +121 -0
- infrakit/time/__init__.py +60 -0
- infrakit/time/profiler.py +511 -0
- python_infrakit_dev-0.1.0.dist-info/METADATA +124 -0
- python_infrakit_dev-0.1.0.dist-info/RECORD +51 -0
- python_infrakit_dev-0.1.0.dist-info/WHEEL +4 -0
- python_infrakit_dev-0.1.0.dist-info/entry_points.txt +3 -0
infrakit/deps/health.py
ADDED
|
@@ -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
|