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,480 @@
1
+ """
2
+ infrakit.core.config.converter
3
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4
+ Convert a config dict (or file) between supported formats.
5
+ Lossy conversions (e.g. YAML list → INI) are handled by stringifying the value
6
+ and recording a warning — no data is silently dropped.
7
+
8
+ Usage:
9
+ from infrakit.core.config.converter import convert_file, convert_dict
10
+
11
+ # File → file
12
+ warnings = convert_file("config.yaml", "config.ini")
13
+
14
+ # Dict → string (useful for previewing output)
15
+ output, warnings = convert_dict(data, from_format="yaml", to_format="ini")
16
+
17
+ # Check for lossy conversions
18
+ for w in warnings:
19
+ print(w)
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ from configparser import ConfigParser
26
+ from io import StringIO
27
+ from pathlib import Path
28
+ from typing import Any
29
+
30
+ try:
31
+ import yaml
32
+ _YAML_AVAILABLE = True
33
+ except ImportError:
34
+ _YAML_AVAILABLE = False
35
+
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # Public types
39
+ # ---------------------------------------------------------------------------
40
+
41
+ ConfigDict = dict[str, Any]
42
+ Format = str # "json" | "yaml" | "ini" | "env"
43
+
44
+ SUPPORTED_FORMATS = {"json", "yaml", "ini", "env"}
45
+
46
+ # Maps file extensions to format names
47
+ _EXT_TO_FORMAT: dict[str, Format] = {
48
+ ".json": "json",
49
+ ".yaml": "yaml",
50
+ ".yml": "yaml",
51
+ ".ini": "ini",
52
+ ".cfg": "ini",
53
+ ".env": "env",
54
+ }
55
+
56
+ # Maps format names to canonical output extensions
57
+ _FORMAT_TO_EXT: dict[Format, str] = {
58
+ "json": ".json",
59
+ "yaml": ".yaml",
60
+ "ini": ".ini",
61
+ "env": ".env",
62
+ }
63
+
64
+
65
+ # ---------------------------------------------------------------------------
66
+ # Exceptions
67
+ # ---------------------------------------------------------------------------
68
+
69
+ class ConversionError(Exception):
70
+ """Raised when a conversion cannot be completed."""
71
+
72
+
73
+ # ---------------------------------------------------------------------------
74
+ # Conversion warnings
75
+ # ---------------------------------------------------------------------------
76
+
77
+ class ConversionWarning:
78
+ """Records a lossy conversion on a single key.
79
+
80
+ Attributes
81
+ ----------
82
+ key:
83
+ The dot-separated config key that was affected.
84
+ original:
85
+ The original Python value.
86
+ stringified:
87
+ The string representation written to the output.
88
+ reason:
89
+ Human-readable explanation of why the conversion was lossy.
90
+ """
91
+
92
+ def __init__(
93
+ self,
94
+ key: str,
95
+ original: Any,
96
+ stringified: str,
97
+ reason: str,
98
+ ) -> None:
99
+ self.key = key
100
+ self.original = original
101
+ self.stringified = stringified
102
+ self.reason = reason
103
+
104
+ def __str__(self) -> str:
105
+ return (
106
+ f"[{self.key}] Lossy conversion: {self.reason}. "
107
+ f"Value {self.original!r} written as {self.stringified!r}."
108
+ )
109
+
110
+ def __repr__(self) -> str:
111
+ return f"ConversionWarning(key={self.key!r}, reason={self.reason!r})"
112
+
113
+
114
+ # ---------------------------------------------------------------------------
115
+ # Public API
116
+ # ---------------------------------------------------------------------------
117
+
118
+ def convert_file(
119
+ source: str | Path,
120
+ target: str | Path,
121
+ *,
122
+ overwrite: bool = False,
123
+ ) -> list[ConversionWarning]:
124
+ """Convert *source* config file to the format implied by *target*'s extension.
125
+
126
+ Parameters
127
+ ----------
128
+ source:
129
+ Path to the input file. Format inferred from extension.
130
+ target:
131
+ Path to the output file. Format inferred from extension.
132
+ The file is created (or overwritten if *overwrite* is True).
133
+ overwrite:
134
+ If False (default), raises :exc:`FileExistsError` when *target* exists.
135
+
136
+ Returns
137
+ -------
138
+ list[ConversionWarning]
139
+ Any lossy conversions that occurred. Empty if conversion was lossless.
140
+
141
+ Raises
142
+ ------
143
+ FileNotFoundError
144
+ If *source* does not exist.
145
+ FileExistsError
146
+ If *target* exists and *overwrite* is False.
147
+ ConversionError
148
+ If the format combination is not supported.
149
+ """
150
+ source = Path(source)
151
+ target = Path(target)
152
+
153
+ if not source.exists():
154
+ raise FileNotFoundError(f"Source file not found: '{source}'")
155
+
156
+ if target.exists() and not overwrite:
157
+ raise FileExistsError(
158
+ f"Target file '{target}' already exists. Pass overwrite=True to replace it."
159
+ )
160
+
161
+ from_fmt = _infer_format(source)
162
+ to_fmt = _infer_format(target)
163
+
164
+ raw = source.read_text(encoding="utf-8")
165
+ data = _parse(raw, from_fmt)
166
+ output, warnings = _serialize(data, to_fmt, source_format=from_fmt)
167
+
168
+ target.parent.mkdir(parents=True, exist_ok=True)
169
+ target.write_text(output, encoding="utf-8")
170
+
171
+ return warnings
172
+
173
+
174
+ def convert_dict(
175
+ data: ConfigDict,
176
+ *,
177
+ from_format: Format,
178
+ to_format: Format,
179
+ ) -> tuple[str, list[ConversionWarning]]:
180
+ """Convert *data* from one format to another, returning the serialized string.
181
+
182
+ Parameters
183
+ ----------
184
+ data:
185
+ A config dict (as returned by the loader).
186
+ from_format:
187
+ The logical origin format of *data* — used to decide which lossy
188
+ conversion rules apply (e.g. INI sections vs flat keys).
189
+ to_format:
190
+ The desired output format.
191
+
192
+ Returns
193
+ -------
194
+ tuple[str, list[ConversionWarning]]
195
+ ``(serialized_string, warnings)``
196
+ """
197
+ if from_format not in SUPPORTED_FORMATS:
198
+ raise ConversionError(
199
+ f"Unknown source format '{from_format}'. "
200
+ f"Supported: {', '.join(sorted(SUPPORTED_FORMATS))}"
201
+ )
202
+ if to_format not in SUPPORTED_FORMATS:
203
+ raise ConversionError(
204
+ f"Unknown target format '{to_format}'. "
205
+ f"Supported: {', '.join(sorted(SUPPORTED_FORMATS))}"
206
+ )
207
+ return _serialize(data, to_format, source_format=from_format)
208
+
209
+
210
+ # ---------------------------------------------------------------------------
211
+ # Format parsers (string → dict)
212
+ # ---------------------------------------------------------------------------
213
+
214
+ def _parse(raw: str, fmt: Format) -> ConfigDict:
215
+ if fmt == "json":
216
+ return _parse_json(raw)
217
+ if fmt == "yaml":
218
+ return _parse_yaml(raw)
219
+ if fmt == "ini":
220
+ return _parse_ini(raw)
221
+ if fmt == "env":
222
+ return _parse_env(raw)
223
+ raise ConversionError(f"No parser for format '{fmt}'")
224
+
225
+
226
+ def _parse_json(raw: str) -> ConfigDict:
227
+ try:
228
+ data = json.loads(raw)
229
+ except json.JSONDecodeError as exc:
230
+ raise ConversionError(f"Invalid JSON: {exc}") from exc
231
+ if not isinstance(data, dict):
232
+ raise ConversionError("JSON root must be an object.")
233
+ return data
234
+
235
+
236
+ def _parse_yaml(raw: str) -> ConfigDict:
237
+ if not _YAML_AVAILABLE:
238
+ raise ConversionError(
239
+ "PyYAML is required for YAML conversion. pip install pyyaml"
240
+ )
241
+ try:
242
+ data = yaml.safe_load(raw)
243
+ except yaml.YAMLError as exc:
244
+ raise ConversionError(f"Invalid YAML: {exc}") from exc
245
+ return data or {}
246
+
247
+
248
+ def _parse_ini(raw: str) -> ConfigDict:
249
+ parser = ConfigParser()
250
+ parser.read_string(raw)
251
+ result: ConfigDict = {}
252
+ if parser.defaults():
253
+ result["DEFAULT"] = dict(parser.defaults())
254
+ for section in parser.sections():
255
+ result[section] = {
256
+ k: parser.get(section, k)
257
+ for k in parser.options(section)
258
+ if k not in parser.defaults()
259
+ }
260
+ return result
261
+
262
+
263
+ def _parse_env(raw: str) -> ConfigDict:
264
+ result: ConfigDict = {}
265
+ for line in raw.splitlines():
266
+ line = line.strip()
267
+ if not line or line.startswith("#"):
268
+ continue
269
+ if "=" not in line:
270
+ continue
271
+ key, _, value = line.partition("=")
272
+ result[key.strip()] = value.strip().strip('"').strip("'")
273
+ return result
274
+
275
+
276
+ # ---------------------------------------------------------------------------
277
+ # Format serializers (dict → string)
278
+ # ---------------------------------------------------------------------------
279
+
280
+ def _serialize(
281
+ data: ConfigDict,
282
+ fmt: Format,
283
+ *,
284
+ source_format: Format,
285
+ ) -> tuple[str, list[ConversionWarning]]:
286
+ if fmt == "json":
287
+ return _serialize_json(data)
288
+ if fmt == "yaml":
289
+ return _serialize_yaml(data)
290
+ if fmt == "ini":
291
+ return _serialize_ini(data, source_format=source_format)
292
+ if fmt == "env":
293
+ return _serialize_env(data, source_format=source_format)
294
+ raise ConversionError(f"No serializer for format '{fmt}'")
295
+
296
+
297
+ def _serialize_json(data: ConfigDict) -> tuple[str, list[ConversionWarning]]:
298
+ """JSON supports all Python types natively — never lossy."""
299
+ return json.dumps(data, indent=2, default=str) + "\n", []
300
+
301
+
302
+ def _serialize_yaml(data: ConfigDict) -> tuple[str, list[ConversionWarning]]:
303
+ if not _YAML_AVAILABLE:
304
+ raise ConversionError(
305
+ "PyYAML is required for YAML serialization. pip install pyyaml"
306
+ )
307
+ output = yaml.dump(data, default_flow_style=False, allow_unicode=True, sort_keys=False)
308
+ return output, []
309
+
310
+
311
+ def _serialize_ini(
312
+ data: ConfigDict,
313
+ *,
314
+ source_format: Format,
315
+ ) -> tuple[str, list[ConversionWarning]]:
316
+ """Serialize to INI format.
317
+
318
+ INI limitations:
319
+ - All values are strings
320
+ - No native list, bool, int, float, or null types
321
+ - Keys must live inside a [section] — top-level flat keys go into [DEFAULT]
322
+
323
+ Lossy conversions are stringified and recorded as warnings.
324
+ """
325
+ warnings: list[ConversionWarning] = []
326
+ parser = ConfigParser()
327
+
328
+ def _add_section(section: str, mapping: dict[str, Any]) -> None:
329
+ parser.add_section(section)
330
+ for k, v in mapping.items():
331
+ str_v, warn = _to_ini_string(f"{section}.{k}", v)
332
+ if warn:
333
+ warnings.append(warn)
334
+ parser.set(section, k, str_v)
335
+
336
+ # If source was already INI-shaped (nested dicts = sections), preserve that.
337
+ # Otherwise, flatten everything into a single [config] section.
338
+ has_sections = source_format in {"ini"} or all(
339
+ isinstance(v, dict) for v in data.values()
340
+ )
341
+
342
+ if has_sections:
343
+ for section, value in data.items():
344
+ if isinstance(value, dict):
345
+ _add_section(section, value)
346
+ else:
347
+ # Top-level scalar — goes into DEFAULT
348
+ if not parser.has_section("DEFAULT"):
349
+ pass # ConfigParser DEFAULT is implicit
350
+ str_v, warn = _to_ini_string(section, value)
351
+ if warn:
352
+ warnings.append(warn)
353
+ parser.defaults()[section] = str_v
354
+ else:
355
+ _add_section("config", data)
356
+
357
+ buf = StringIO()
358
+ parser.write(buf)
359
+ return buf.getvalue(), warnings
360
+
361
+
362
+ def _serialize_env(
363
+ data: ConfigDict,
364
+ *,
365
+ source_format: Format,
366
+ ) -> tuple[str, list[ConversionWarning]]:
367
+ """.env format: KEY=value, flat, all strings. No sections."""
368
+ warnings: list[ConversionWarning] = []
369
+ lines: list[str] = []
370
+
371
+ def _add(original_key: str, output_key: str, value: Any) -> None:
372
+ # original_key used in warnings (preserves source casing)
373
+ # output_key used in the actual .env line (uppercased for non-env sources)
374
+ str_v, warn = _to_env_string(original_key, value)
375
+ if warn:
376
+ warnings.append(warn)
377
+ lines.append(f"{output_key}={str_v}")
378
+
379
+ for k, v in data.items():
380
+ if isinstance(v, dict):
381
+ # Flatten nested dicts: {"db": {"host": "x"}} → DB__HOST=x
382
+ for nested_k, nested_v in v.items():
383
+ flat_key = f"{k.upper()}__{nested_k.upper()}"
384
+ original = f"{k}__{nested_k}"
385
+ _add(original, flat_key, nested_v)
386
+ else:
387
+ out_key = k if source_format == "env" else k.upper()
388
+ _add(k, out_key, v)
389
+
390
+ return "\n".join(lines) + "\n", warnings
391
+
392
+
393
+ # ---------------------------------------------------------------------------
394
+ # Value stringification helpers
395
+ # ---------------------------------------------------------------------------
396
+
397
+ def _to_ini_string(key: str, value: Any) -> tuple[str, ConversionWarning | None]:
398
+ """Convert *value* to an INI-safe string, recording a warning if lossy."""
399
+ if isinstance(value, str):
400
+ return value, None
401
+ if isinstance(value, bool):
402
+ return str(value).lower(), None # true/false — readable
403
+ if isinstance(value, (int, float)):
404
+ return str(value), None
405
+ if value is None:
406
+ return "", None
407
+
408
+ # Lists, dicts, and anything else → stringify + warn
409
+ stringified = _stringify_complex(value)
410
+ return stringified, ConversionWarning(
411
+ key=key,
412
+ original=value,
413
+ stringified=stringified,
414
+ reason=f"INI format does not support {type(value).__name__} values",
415
+ )
416
+
417
+
418
+ def _to_env_string(key: str, value: Any) -> tuple[str, ConversionWarning | None]:
419
+ """.env values are always strings — same lossy rules as INI."""
420
+ if isinstance(value, str):
421
+ # Quote values with spaces
422
+ return f'"{value}"' if " " in value else value, None
423
+ if isinstance(value, bool):
424
+ return str(value).lower(), None
425
+ if isinstance(value, (int, float)):
426
+ return str(value), None
427
+ if value is None:
428
+ return "", None
429
+
430
+ stringified = _stringify_complex(value)
431
+ return stringified, ConversionWarning(
432
+ key=key,
433
+ original=value,
434
+ stringified=stringified,
435
+ reason=f".env format does not support {type(value).__name__} values",
436
+ )
437
+
438
+
439
+ def _stringify_complex(value: Any) -> str:
440
+ """Stringify a complex value (list, dict, etc.) into a compact string.
441
+
442
+ Lists → comma-joined: [1, "a", True] → "1,a,true"
443
+ Dicts → JSON inline: {"a": 1} → '{"a": 1}'
444
+ Other → repr fallback
445
+ """
446
+ if isinstance(value, list):
447
+ return ",".join(_scalar_str(item) for item in value)
448
+ if isinstance(value, dict):
449
+ return json.dumps(value, separators=(",", ":"))
450
+ return repr(value)
451
+
452
+
453
+ def _scalar_str(value: Any) -> str:
454
+ """Compact string representation of a scalar for use inside lists."""
455
+ if isinstance(value, bool):
456
+ return str(value).lower()
457
+ if value is None:
458
+ return ""
459
+ return str(value)
460
+
461
+
462
+ # ---------------------------------------------------------------------------
463
+ # Helpers
464
+ # ---------------------------------------------------------------------------
465
+
466
+ def _infer_format(path: Path) -> Format:
467
+ """Infer format from file extension, handling dotfiles like .env."""
468
+ suffix = path.suffix.lower()
469
+ if not suffix:
470
+ # Dotfile: .env, .envrc, etc.
471
+ name = path.name.lower()
472
+ if name.startswith(".") and "." not in name[1:]:
473
+ suffix = name
474
+ fmt = _EXT_TO_FORMAT.get(suffix)
475
+ if fmt is None:
476
+ raise ConversionError(
477
+ f"Cannot infer format from extension '{suffix}'. "
478
+ f"Supported extensions: {', '.join(sorted(_EXT_TO_FORMAT))}"
479
+ )
480
+ return fmt