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,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")