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/depfile.py
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
"""
|
|
2
|
+
infrakit.deps.depfile
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
Read and write dependency files: requirements.txt and pyproject.toml.
|
|
5
|
+
Auto-detects which format(s) are present in the project.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
# Data structures
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class PinnedDep:
|
|
22
|
+
"""One dependency entry as found in a dep file."""
|
|
23
|
+
name: str # normalised pip name
|
|
24
|
+
raw: str # original line / string as-is
|
|
25
|
+
version_spec: str = "" # e.g. '>=1.2,<2' or '==1.4.0' or ''
|
|
26
|
+
extras: list[str] = field(default_factory=list) # e.g. ['security']
|
|
27
|
+
markers: str = "" # environment markers
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def normalised(self) -> str:
|
|
31
|
+
"""Lower-cased, hyphens-normalised name for comparison."""
|
|
32
|
+
return self.name.lower().replace("_", "-")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class DepFile:
|
|
37
|
+
"""In-memory representation of a parsed dependency file."""
|
|
38
|
+
path: Path
|
|
39
|
+
format: str # 'requirements' | 'pyproject'
|
|
40
|
+
deps: list[PinnedDep] = field(default_factory=list)
|
|
41
|
+
# For pyproject.toml we preserve the full raw text for round-trip writes
|
|
42
|
+
_raw_text: str = field(default="", repr=False)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# requirements.txt parsing
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
# Matches: package[extras]>=version ; marker # comment
|
|
50
|
+
_REQ_LINE = re.compile(
|
|
51
|
+
r"""
|
|
52
|
+
^
|
|
53
|
+
(?P<name>[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?) # package name
|
|
54
|
+
(?:\[(?P<extras>[^\]]+)\])? # optional [extras]
|
|
55
|
+
(?P<spec>[^;#\n]*) # version specifier
|
|
56
|
+
(?:;(?P<marker>[^#\n]*))? # env marker
|
|
57
|
+
""",
|
|
58
|
+
re.VERBOSE,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _parse_requirements(path: Path) -> DepFile:
|
|
63
|
+
deps: list[PinnedDep] = []
|
|
64
|
+
text = path.read_text(encoding="utf-8")
|
|
65
|
+
|
|
66
|
+
for raw_line in text.splitlines():
|
|
67
|
+
line = raw_line.strip()
|
|
68
|
+
# Skip comments, blank lines, options (-r, --index-url, etc.)
|
|
69
|
+
if not line or line.startswith("#") or line.startswith("-"):
|
|
70
|
+
continue
|
|
71
|
+
# Skip VCS / URL requirements
|
|
72
|
+
if line.startswith(("git+", "http://", "https://", "file://")):
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
m = _REQ_LINE.match(line)
|
|
76
|
+
if not m:
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
name = m.group("name").strip()
|
|
80
|
+
extras_raw = m.group("extras") or ""
|
|
81
|
+
spec = (m.group("spec") or "").strip()
|
|
82
|
+
marker = (m.group("marker") or "").strip()
|
|
83
|
+
extras = [e.strip() for e in extras_raw.split(",") if e.strip()]
|
|
84
|
+
|
|
85
|
+
deps.append(PinnedDep(
|
|
86
|
+
name=name,
|
|
87
|
+
raw=raw_line,
|
|
88
|
+
version_spec=spec,
|
|
89
|
+
extras=extras,
|
|
90
|
+
markers=marker,
|
|
91
|
+
))
|
|
92
|
+
|
|
93
|
+
return DepFile(path=path, format="requirements", deps=deps, _raw_text=text)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# pyproject.toml parsing
|
|
98
|
+
# A lightweight parser — we deliberately avoid requiring `tomllib`/`tomli`
|
|
99
|
+
# here so the module has zero extra deps. We only need the [project]
|
|
100
|
+
# dependencies table and [tool.poetry.dependencies].
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
_TOML_STRING = re.compile(r'"([^"\\]*(?:\\.[^"\\]*)*)"|\'([^\'\\]*(?:\\.[^\'\\]*)*)\'')
|
|
104
|
+
_INLINE_COMMENT = re.compile(r'#.*$')
|
|
105
|
+
|
|
106
|
+
# Matches: "package>=1.0" or package = ">=1.0" (poetry style)
|
|
107
|
+
_PEP508 = re.compile(
|
|
108
|
+
r"""
|
|
109
|
+
^
|
|
110
|
+
(?P<name>[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?)
|
|
111
|
+
(?:\[(?P<extras>[^\]]+)\])?
|
|
112
|
+
(?P<spec>[^;#\n]*)
|
|
113
|
+
(?:;(?P<marker>[^#\n]*))?
|
|
114
|
+
""",
|
|
115
|
+
re.VERBOSE,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _strip_toml_quotes(s: str) -> str:
|
|
120
|
+
s = s.strip()
|
|
121
|
+
if (s.startswith('"') and s.endswith('"')) or \
|
|
122
|
+
(s.startswith("'") and s.endswith("'")):
|
|
123
|
+
return s[1:-1]
|
|
124
|
+
return s
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _parse_pep508(raw: str) -> Optional[PinnedDep]:
|
|
128
|
+
raw = raw.strip().strip('"\'').strip()
|
|
129
|
+
if not raw or raw.startswith("#"):
|
|
130
|
+
return None
|
|
131
|
+
# Skip VCS / URL
|
|
132
|
+
if any(raw.startswith(p) for p in ("git+", "http://", "https://", "file://")):
|
|
133
|
+
return None
|
|
134
|
+
m = _PEP508.match(raw)
|
|
135
|
+
if not m:
|
|
136
|
+
return None
|
|
137
|
+
name = m.group("name").strip()
|
|
138
|
+
if not name:
|
|
139
|
+
return None
|
|
140
|
+
extras_raw = m.group("extras") or ""
|
|
141
|
+
spec = (m.group("spec") or "").strip()
|
|
142
|
+
marker = (m.group("marker") or "").strip()
|
|
143
|
+
extras = [e.strip() for e in extras_raw.split(",") if e.strip()]
|
|
144
|
+
return PinnedDep(name=name, raw=raw, version_spec=spec, extras=extras, markers=marker)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _parse_pyproject(path: Path) -> DepFile:
|
|
148
|
+
text = path.read_text(encoding="utf-8")
|
|
149
|
+
deps: list[PinnedDep] = []
|
|
150
|
+
|
|
151
|
+
lines = text.splitlines()
|
|
152
|
+
n = len(lines)
|
|
153
|
+
i = 0
|
|
154
|
+
|
|
155
|
+
# We look for three patterns:
|
|
156
|
+
# 1. [project] → dependencies = [ ... ]
|
|
157
|
+
# 2. [project.optional-dependencies.*] → list
|
|
158
|
+
# 3. [tool.poetry.dependencies] → key = "version"
|
|
159
|
+
# 4. [tool.poetry.*.dependencies] → same
|
|
160
|
+
|
|
161
|
+
in_section: Optional[str] = None # 'pep621' | 'poetry' | None
|
|
162
|
+
in_array = False
|
|
163
|
+
|
|
164
|
+
while i < n:
|
|
165
|
+
line = lines[i]
|
|
166
|
+
stripped = line.strip()
|
|
167
|
+
comment_stripped = _INLINE_COMMENT.sub("", stripped).strip()
|
|
168
|
+
|
|
169
|
+
# Detect section headers
|
|
170
|
+
if stripped.startswith("["):
|
|
171
|
+
header = stripped.strip("[]").strip()
|
|
172
|
+
if header == "project" or header.startswith("project.optional"):
|
|
173
|
+
in_section = "pep621"
|
|
174
|
+
elif "poetry" in header and "dependencies" in header:
|
|
175
|
+
in_section = "poetry"
|
|
176
|
+
else:
|
|
177
|
+
in_section = None
|
|
178
|
+
in_array = False
|
|
179
|
+
i += 1
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
if in_section == "pep621":
|
|
183
|
+
# Look for: dependencies = [
|
|
184
|
+
if re.match(r'dependencies\s*=\s*\[', comment_stripped):
|
|
185
|
+
in_array = True
|
|
186
|
+
# Check if array closes on same line
|
|
187
|
+
rest = comment_stripped[comment_stripped.index("[") + 1:]
|
|
188
|
+
if "]" in rest:
|
|
189
|
+
# single-line array
|
|
190
|
+
items = rest[: rest.index("]")]
|
|
191
|
+
for raw in re.split(r',', items):
|
|
192
|
+
d = _parse_pep508(raw)
|
|
193
|
+
if d:
|
|
194
|
+
deps.append(d)
|
|
195
|
+
in_array = False
|
|
196
|
+
i += 1
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
if in_array:
|
|
200
|
+
if "]" in comment_stripped:
|
|
201
|
+
# last item possibly before ]
|
|
202
|
+
item = comment_stripped[: comment_stripped.index("]")]
|
|
203
|
+
d = _parse_pep508(item)
|
|
204
|
+
if d:
|
|
205
|
+
deps.append(d)
|
|
206
|
+
in_array = False
|
|
207
|
+
else:
|
|
208
|
+
d = _parse_pep508(comment_stripped)
|
|
209
|
+
if d:
|
|
210
|
+
deps.append(d)
|
|
211
|
+
i += 1
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
elif in_section == "poetry":
|
|
215
|
+
# Skip python = "..." entries
|
|
216
|
+
if comment_stripped.lower().startswith("python"):
|
|
217
|
+
i += 1
|
|
218
|
+
continue
|
|
219
|
+
# key = "version_or_constraint"
|
|
220
|
+
m = re.match(r'^([A-Za-z0-9][A-Za-z0-9._-]*)\s*=\s*(.+)$', comment_stripped)
|
|
221
|
+
if m:
|
|
222
|
+
name = m.group(1)
|
|
223
|
+
val = _strip_toml_quotes(m.group(2))
|
|
224
|
+
# poetry can have inline table: {version = "...", extras = [...]}
|
|
225
|
+
if val.startswith("{"):
|
|
226
|
+
# extract version from inline table
|
|
227
|
+
vm = re.search(r'version\s*=\s*["\']([^"\']+)["\']', val)
|
|
228
|
+
spec = vm.group(1) if vm else ""
|
|
229
|
+
em = re.search(r'extras\s*=\s*\[([^\]]+)\]', val)
|
|
230
|
+
extras = [e.strip().strip('"\'') for e in em.group(1).split(",") if e.strip()] if em else []
|
|
231
|
+
else:
|
|
232
|
+
spec = val if val not in ("*", "latest") else ""
|
|
233
|
+
extras = []
|
|
234
|
+
deps.append(PinnedDep(
|
|
235
|
+
name=name,
|
|
236
|
+
raw=comment_stripped,
|
|
237
|
+
version_spec=spec,
|
|
238
|
+
extras=extras,
|
|
239
|
+
))
|
|
240
|
+
i += 1
|
|
241
|
+
continue
|
|
242
|
+
|
|
243
|
+
i += 1
|
|
244
|
+
|
|
245
|
+
return DepFile(path=path, format="pyproject", deps=deps, _raw_text=text)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# ---------------------------------------------------------------------------
|
|
249
|
+
# Auto-detection
|
|
250
|
+
# ---------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
def find_dep_files(root: Path) -> list[DepFile]:
|
|
253
|
+
"""Find and parse all dependency files in *root* (non-recursive)."""
|
|
254
|
+
found: list[DepFile] = []
|
|
255
|
+
|
|
256
|
+
req = root / "requirements.txt"
|
|
257
|
+
if req.exists():
|
|
258
|
+
found.append(_parse_requirements(req))
|
|
259
|
+
|
|
260
|
+
# Also check common variants
|
|
261
|
+
for name in ("requirements-dev.txt", "requirements_dev.txt",
|
|
262
|
+
"requirements-test.txt", "requirements_test.txt"):
|
|
263
|
+
p = root / name
|
|
264
|
+
if p.exists():
|
|
265
|
+
found.append(_parse_requirements(p))
|
|
266
|
+
|
|
267
|
+
pyproj = root / "pyproject.toml"
|
|
268
|
+
if pyproj.exists():
|
|
269
|
+
found.append(_parse_pyproject(pyproj))
|
|
270
|
+
|
|
271
|
+
return found
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def all_declared_packages(dep_files: list[DepFile]) -> dict[str, PinnedDep]:
|
|
275
|
+
"""Return dict of normalised-name → PinnedDep from all dep files combined."""
|
|
276
|
+
out: dict[str, PinnedDep] = {}
|
|
277
|
+
for df in dep_files:
|
|
278
|
+
for dep in df.deps:
|
|
279
|
+
out[dep.normalised] = dep
|
|
280
|
+
return out
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# ---------------------------------------------------------------------------
|
|
284
|
+
# Writers
|
|
285
|
+
# ---------------------------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
def write_requirements(
|
|
288
|
+
packages: list[str], # pip names to include
|
|
289
|
+
declared: dict[str, PinnedDep],
|
|
290
|
+
output_path: Path,
|
|
291
|
+
keep_versions: bool = True,
|
|
292
|
+
) -> None:
|
|
293
|
+
"""Write a clean requirements.txt with only *packages*."""
|
|
294
|
+
lines: list[str] = [
|
|
295
|
+
"# Generated by infrakit deps export",
|
|
296
|
+
"# Only packages actively used in this project",
|
|
297
|
+
"",
|
|
298
|
+
]
|
|
299
|
+
for pkg in sorted(packages, key=str.lower):
|
|
300
|
+
norm = pkg.lower().replace("_", "-")
|
|
301
|
+
pinned = declared.get(norm)
|
|
302
|
+
if pinned and keep_versions and pinned.version_spec:
|
|
303
|
+
extras = f"[{','.join(pinned.extras)}]" if pinned.extras else ""
|
|
304
|
+
marker = f" ; {pinned.markers}" if pinned.markers else ""
|
|
305
|
+
pinned.version_spec = pinned.version_spec.strip().strip('",')
|
|
306
|
+
lines.append(f"{pkg}{extras}{pinned.version_spec}{marker}")
|
|
307
|
+
else:
|
|
308
|
+
lines.append(pkg)
|
|
309
|
+
|
|
310
|
+
output_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def update_requirements_inplace(
|
|
314
|
+
dep_file: DepFile,
|
|
315
|
+
used_packages: set[str], # normalised names
|
|
316
|
+
) -> None:
|
|
317
|
+
"""Rewrite requirements.txt keeping only used packages (preserves ordering & comments)."""
|
|
318
|
+
assert dep_file.format == "requirements"
|
|
319
|
+
original = dep_file.path.read_text(encoding="utf-8")
|
|
320
|
+
out_lines: list[str] = []
|
|
321
|
+
|
|
322
|
+
for line in original.splitlines():
|
|
323
|
+
stripped = line.strip()
|
|
324
|
+
if not stripped or stripped.startswith("#") or stripped.startswith("-"):
|
|
325
|
+
out_lines.append(line)
|
|
326
|
+
continue
|
|
327
|
+
m = _REQ_LINE.match(stripped)
|
|
328
|
+
if not m:
|
|
329
|
+
out_lines.append(line)
|
|
330
|
+
continue
|
|
331
|
+
name = m.group("name").strip().lower().replace("_", "-")
|
|
332
|
+
if name in used_packages:
|
|
333
|
+
out_lines.append(line)
|
|
334
|
+
# else: drop the line (unused dep)
|
|
335
|
+
|
|
336
|
+
dep_file.path.write_text("\n".join(out_lines) + "\n", encoding="utf-8")
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def update_pyproject_inplace(
|
|
340
|
+
dep_file: DepFile,
|
|
341
|
+
used_packages: set[str], # normalised names
|
|
342
|
+
) -> None:
|
|
343
|
+
"""
|
|
344
|
+
Rewrite pyproject.toml removing deps not in used_packages.
|
|
345
|
+
Uses line-level editing to avoid toml-write library requirement.
|
|
346
|
+
"""
|
|
347
|
+
assert dep_file.format == "pyproject"
|
|
348
|
+
text = dep_file._raw_text
|
|
349
|
+
lines = text.splitlines()
|
|
350
|
+
out: list[str] = []
|
|
351
|
+
|
|
352
|
+
in_dep_array = False
|
|
353
|
+
in_poetry_deps = False
|
|
354
|
+
|
|
355
|
+
for line in lines:
|
|
356
|
+
stripped = line.strip()
|
|
357
|
+
comment_stripped = _INLINE_COMMENT.sub("", stripped).strip()
|
|
358
|
+
|
|
359
|
+
# Section transitions
|
|
360
|
+
if stripped.startswith("["):
|
|
361
|
+
in_dep_array = False
|
|
362
|
+
in_poetry_deps = False
|
|
363
|
+
header = stripped.strip("[]").strip()
|
|
364
|
+
if "poetry" in header and "dependencies" in header:
|
|
365
|
+
in_poetry_deps = True
|
|
366
|
+
out.append(line)
|
|
367
|
+
continue
|
|
368
|
+
|
|
369
|
+
# PEP 621 dependency array
|
|
370
|
+
if re.match(r'dependencies\s*=\s*\[', comment_stripped):
|
|
371
|
+
in_dep_array = True
|
|
372
|
+
if "]" in comment_stripped:
|
|
373
|
+
in_dep_array = False
|
|
374
|
+
out.append(line)
|
|
375
|
+
continue
|
|
376
|
+
|
|
377
|
+
if in_dep_array:
|
|
378
|
+
if "]" in comment_stripped:
|
|
379
|
+
in_dep_array = False
|
|
380
|
+
out.append(line)
|
|
381
|
+
continue
|
|
382
|
+
d = _parse_pep508(comment_stripped)
|
|
383
|
+
if d is None:
|
|
384
|
+
out.append(line)
|
|
385
|
+
elif d.normalised in used_packages:
|
|
386
|
+
out.append(line)
|
|
387
|
+
# else: drop unused dep line
|
|
388
|
+
continue
|
|
389
|
+
|
|
390
|
+
if in_poetry_deps:
|
|
391
|
+
if stripped.startswith("["):
|
|
392
|
+
in_poetry_deps = False
|
|
393
|
+
out.append(line)
|
|
394
|
+
continue
|
|
395
|
+
m = re.match(r'^([A-Za-z0-9][A-Za-z0-9._-]*)\s*=', comment_stripped)
|
|
396
|
+
if m:
|
|
397
|
+
name = m.group(1).lower().replace("_", "-")
|
|
398
|
+
if name == "python" or name in used_packages:
|
|
399
|
+
out.append(line)
|
|
400
|
+
# else: drop
|
|
401
|
+
continue
|
|
402
|
+
|
|
403
|
+
out.append(line)
|
|
404
|
+
|
|
405
|
+
dep_file.path.write_text("\n".join(out) + "\n", encoding="utf-8")
|