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,295 @@
1
+ """
2
+ infrakit/cli/commands/llm.py
3
+ -----------------------------
4
+ Typer command group for infrakit.llm — key status and quota management.
5
+
6
+ Commands
7
+ --------
8
+ ik llm status
9
+ ik llm status --provider openai
10
+ ik llm status --key sk-abc123
11
+
12
+ ik llm quota set --provider openai --key sk-abc123 --rpm 60
13
+ ik llm quota set --provider gemini --key AIza-abc1 --model gemini-2.5-pro --daily 250000
14
+ ik llm quota set --provider gemini --key AIza-abc1 --daily 1500000 # default for all models
15
+
16
+ Connecting to your main CLI
17
+ ----------------------------
18
+ Typer root::
19
+
20
+ from infrakit.cli.commands.llm import app as llm_app
21
+ root_app.add_typer(llm_app, name="llm")
22
+
23
+ Click root::
24
+
25
+ from infrakit.cli.commands.llm import click_group as llm_group
26
+ cli.add_command(llm_group, name="llm")
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import json
32
+ from enum import Enum
33
+ from pathlib import Path
34
+ from typing import Optional
35
+
36
+ import typer
37
+ from typing_extensions import Annotated
38
+
39
+
40
+ # ── enums ──────────────────────────────────────────────────────────────────
41
+
42
+ class ProviderChoice(str, Enum):
43
+ openai = "openai"
44
+ gemini = "gemini"
45
+
46
+
47
+ # ── shared option types ────────────────────────────────────────────────────
48
+
49
+ _StorageDirOption = Annotated[
50
+ Optional[Path],
51
+ typer.Option(
52
+ "--storage-dir", "-d",
53
+ help=(
54
+ "Directory where key state is persisted. "
55
+ "Defaults to ~/.infrakit/llm/"
56
+ ),
57
+ ),
58
+ ]
59
+
60
+ _QuotaFileOption = Annotated[
61
+ Optional[Path],
62
+ typer.Option(
63
+ "--quota-file", "-q",
64
+ help=(
65
+ "Path to quotas.json. "
66
+ "Defaults to ~/.infrakit/llm/quotas.json if that file exists."
67
+ ),
68
+ ),
69
+ ]
70
+
71
+ _KeysFileOption = Annotated[
72
+ Optional[Path],
73
+ typer.Option(
74
+ "--keys-file", "-k",
75
+ help=(
76
+ 'JSON file containing API keys: {"openai_keys": [...], "gemini_keys": [...]}. '
77
+ "Only needed to register new keys; omit when inspecting persisted state."
78
+ ),
79
+ ),
80
+ ]
81
+
82
+ _ProviderFilterOption = Annotated[
83
+ Optional[ProviderChoice],
84
+ typer.Option("--provider", "-p", help="Filter to a specific provider."),
85
+ ]
86
+
87
+ _KeyFilterOption = Annotated[
88
+ Optional[str],
89
+ typer.Option("--key", "-K", help="Filter to a specific key (first 8 chars)."),
90
+ ]
91
+
92
+
93
+ # ── apps ───────────────────────────────────────────────────────────────────
94
+
95
+ app = typer.Typer(
96
+ name="llm",
97
+ help="Manage infrakit LLM keys, quotas, and usage.",
98
+ no_args_is_help=True,
99
+ rich_markup_mode="markdown",
100
+ )
101
+
102
+ quota_app = typer.Typer(
103
+ name="quota",
104
+ help="Manage quota limits for LLM API keys.",
105
+ no_args_is_help=True,
106
+ rich_markup_mode="markdown",
107
+ )
108
+ app.add_typer(quota_app, name="quota")
109
+
110
+
111
+ # ── helpers ────────────────────────────────────────────────────────────────
112
+
113
+ def _load_client(
114
+ storage_dir: Optional[Path],
115
+ quota_file: Optional[Path],
116
+ keys_file: Optional[Path],
117
+ ):
118
+ from infrakit.llm import LLMClient
119
+
120
+ keys: dict = {"openai_keys": [], "gemini_keys": []}
121
+ if keys_file is not None:
122
+ if keys_file.exists():
123
+ with open(keys_file) as f:
124
+ try:
125
+ keys = json.load(f)
126
+ except json.JSONDecodeError as exc:
127
+ typer.echo(f"[error] Cannot parse keys file: {exc}", err=True)
128
+ raise typer.Exit(1)
129
+ else:
130
+ typer.echo(f"[warn] Keys file not found: {keys_file}", err=True)
131
+
132
+ try:
133
+ return LLMClient(
134
+ keys=keys,
135
+ storage_dir=storage_dir,
136
+ quota_file=quota_file,
137
+ )
138
+ except Exception as exc:
139
+ typer.echo(f"[error] Failed to initialise LLMClient: {exc}", err=True)
140
+ raise typer.Exit(1)
141
+
142
+
143
+ def _prov(provider: Optional[ProviderChoice]) -> Optional[str]:
144
+ return provider.value if provider is not None else None
145
+
146
+
147
+ # ── ik llm status ──────────────────────────────────────────────────────────
148
+
149
+ @app.command("status")
150
+ def status(
151
+ storage_dir: _StorageDirOption = None,
152
+ quota_file: _QuotaFileOption = None,
153
+ keys_file: _KeysFileOption = None,
154
+ provider: _ProviderFilterOption = None,
155
+ key: _KeyFilterOption = None,
156
+ output_json: Annotated[
157
+ bool,
158
+ typer.Option("--json", help="Output raw JSON.", is_flag=True),
159
+ ] = False,
160
+ ):
161
+ """
162
+ Show quota and usage status for LLM API keys.
163
+
164
+ Deactivation is tracked per model — a key can have gemini-2.5-pro
165
+ exhausted while gemini-2.0-flash is still active.
166
+
167
+ **Examples**
168
+
169
+ ik llm status
170
+
171
+ ik llm status --provider gemini
172
+
173
+ ik llm status --key AIza-abc1
174
+
175
+ ik llm status --json
176
+ """
177
+ client = _load_client(storage_dir, quota_file, keys_file)
178
+ rows = client.status(provider=_prov(provider), key_id=key)
179
+
180
+ if not rows:
181
+ typer.echo(
182
+ "No keys found. Pass --keys-file to register keys on first use."
183
+ )
184
+ raise typer.Exit(0)
185
+
186
+ if output_json:
187
+ typer.echo(json.dumps(rows, indent=2, default=str))
188
+ return
189
+
190
+ client.print_status(provider=_prov(provider), key_id=key)
191
+
192
+
193
+ # ── ik llm quota set ───────────────────────────────────────────────────────
194
+
195
+ @quota_app.command("set")
196
+ def quota_set(
197
+ provider: Annotated[
198
+ ProviderChoice,
199
+ typer.Option("--provider", "-p", help="Provider the key belongs to."),
200
+ ],
201
+ key: Annotated[
202
+ str,
203
+ typer.Option("--key", "-K", help="Key ID (first 8 chars of the API key)."),
204
+ ],
205
+ storage_dir: _StorageDirOption = None,
206
+ quota_file: _QuotaFileOption = None,
207
+ keys_file: _KeysFileOption = None,
208
+ model: Annotated[
209
+ Optional[str],
210
+ typer.Option(
211
+ "--model", "-m",
212
+ help=(
213
+ "Scope quota to a specific model "
214
+ "(e.g. gemini-2.5-pro, gpt-4o). "
215
+ "Omit to set a default that applies to all models on this key."
216
+ ),
217
+ ),
218
+ ] = None,
219
+ rpm: Annotated[
220
+ Optional[int],
221
+ typer.Option("--rpm", help="Requests-per-minute limit (key-level)."),
222
+ ] = None,
223
+ tpm: Annotated[
224
+ Optional[int],
225
+ typer.Option("--tpm", help="Tokens-per-minute limit (model-level)."),
226
+ ] = None,
227
+ daily: Annotated[
228
+ Optional[int],
229
+ typer.Option("--daily", help="Daily token limit (model-level)."),
230
+ ] = None,
231
+ reset_hour: Annotated[
232
+ int,
233
+ typer.Option(
234
+ "--reset-hour",
235
+ help="UTC hour (0-23) when daily quota resets.",
236
+ min=0, max=23, show_default=True,
237
+ ),
238
+ ] = 0,
239
+ ):
240
+ """
241
+ Set quota limits for a specific API key, optionally scoped to one model.
242
+
243
+ Omitting **--model** sets a default that applies to all models on the key
244
+ that don't have their own entry. Providing **--model** overrides only
245
+ that model.
246
+
247
+ **Examples**
248
+
249
+ # default for all models on this key
250
+ ik llm quota set --provider gemini --key AIza-abc1 --rpm 15 --daily 1500000
251
+
252
+ # tighter limit for one expensive model
253
+ ik llm quota set --provider gemini --key AIza-abc1 \\
254
+ --model gemini-2.5-pro --daily 250000 --reset-hour 0
255
+
256
+ # openai key-level RPM
257
+ ik llm quota set --provider openai --key sk-abc123 --rpm 60 --tpm 90000
258
+ """
259
+ from infrakit.llm import QuotaConfig
260
+
261
+ client = _load_client(storage_dir, quota_file, keys_file)
262
+
263
+ quota = QuotaConfig(
264
+ model=model,
265
+ rpm_limit=rpm,
266
+ tpm_limit=tpm,
267
+ daily_token_limit=daily,
268
+ reset_hour_utc=reset_hour,
269
+ )
270
+
271
+ try:
272
+ client.set_quota(provider=provider.value, key_id=key, quota=quota)
273
+ except KeyError:
274
+ typer.echo(
275
+ f"[error] Key '{key}' not found for provider '{provider.value}'.\n"
276
+ "Tip: key IDs are the first 8 characters of the raw API key.",
277
+ err=True,
278
+ )
279
+ raise typer.Exit(1)
280
+
281
+ scope = f"model '{model}'" if model else "all models (default)"
282
+ lines = [f"Quota updated for {provider.value} key '{key}...' ({scope}):"]
283
+ lines.append(f" RPM limit : {rpm if rpm is not None else '(unchanged)'}")
284
+ lines.append(f" TPM limit : {tpm if tpm is not None else '(unchanged)'}")
285
+ lines.append(f" Daily limit : {daily if daily is not None else '(unchanged)'}")
286
+ lines.append(f" Reset hour : {reset_hour:02d}:00 UTC")
287
+ typer.echo("\n".join(lines))
288
+
289
+
290
+ # ── click shim ─────────────────────────────────────────────────────────────
291
+
292
+ def _make_click_group():
293
+ return typer.main.get_command(app)
294
+
295
+ click_group = _make_click_group()
@@ -0,0 +1,160 @@
1
+ """
2
+ infrakit.cli.commands.logger
3
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4
+ ``infrakit logger`` subcommand group.
5
+
6
+ Commands
7
+ --------
8
+ infrakit logger check — show active INFRAKIT_LOG_* env vars
9
+ infrakit logger clean <log_dir> — manually run a retention sweep
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from pathlib import Path
15
+
16
+ import typer
17
+
18
+ logger_app = typer.Typer(
19
+ name="logger",
20
+ help="Inspect and maintain infrakit logger state.",
21
+ no_args_is_help=True,
22
+ )
23
+
24
+ # ── helpers ───────────────────────────────────────────────────────────────────
25
+
26
+ def _abort(msg: str) -> None:
27
+ typer.echo(typer.style(f"✗ {msg}", fg=typer.colors.RED), err=True)
28
+ raise typer.Exit(1)
29
+
30
+
31
+ def _ok(msg: str) -> None:
32
+ typer.echo(typer.style(f"✓ {msg}", fg=typer.colors.GREEN))
33
+
34
+
35
+ # ── infrakit logger check ─────────────────────────────────────────────────────
36
+
37
+ _ENV_VARS = [
38
+ ("INFRAKIT_LOG_DIR", "log_dir", "logs"),
39
+ ("INFRAKIT_LOG_STRATEGY", "strategy", "date"),
40
+ ("INFRAKIT_LOG_STREAM", "stream", "stdout"),
41
+ ("INFRAKIT_LOG_FORMAT", "format", "human"),
42
+ ("INFRAKIT_LOG_LEVEL", "level", "DEBUG"),
43
+ ("INFRAKIT_LOG_SESSION", "session", "<timestamped>"),
44
+ ("INFRAKIT_LOG_RETENTION", "retention_days", "<none>"),
45
+ ("INFRAKIT_LOG_DRY_RUN", "dry_run", "false"),
46
+ ]
47
+
48
+
49
+ @logger_app.command("check")
50
+ def cmd_check() -> None:
51
+ """
52
+ Show the current INFRAKIT_LOG_* environment variables.
53
+
54
+ Useful for verifying your env before running an application —
55
+ whatever is shown here is exactly what setup() will pick up.
56
+
57
+ \b
58
+ Examples
59
+ --------
60
+ ik logger check
61
+ """
62
+ import os
63
+
64
+ typer.echo(typer.style("infrakit logger — env configuration", bold=True))
65
+ typer.echo()
66
+
67
+ any_set = False
68
+ for env_key, label, default in _ENV_VARS:
69
+ raw = os.environ.get(env_key)
70
+ if raw is not None:
71
+ any_set = True
72
+ val_str = typer.style(raw, fg=typer.colors.GREEN)
73
+ src = typer.style("(env)", fg=typer.colors.BRIGHT_BLACK)
74
+ else:
75
+ val_str = typer.style(default, fg=typer.colors.BRIGHT_BLACK)
76
+ src = typer.style("(default)", fg=typer.colors.BRIGHT_BLACK)
77
+
78
+ label_str = typer.style(f"{label:<20}", fg=typer.colors.CYAN)
79
+ typer.echo(f" {label_str} {val_str} {src}")
80
+
81
+ typer.echo()
82
+ if any_set:
83
+ _ok("Some INFRAKIT_LOG_* vars are set — setup() will use them automatically.")
84
+ else:
85
+ typer.echo(typer.style(
86
+ " No INFRAKIT_LOG_* vars set — all defaults will apply.",
87
+ fg=typer.colors.BRIGHT_BLACK,
88
+ ))
89
+
90
+
91
+ # ── infrakit logger clean ─────────────────────────────────────────────────────
92
+
93
+ @logger_app.command("clean")
94
+ def cmd_clean(
95
+ log_dir: Path = typer.Argument(..., help="Log directory to sweep."),
96
+ days: int = typer.Option(..., "--days", "-d", help="Delete files older than this many days."),
97
+ dry_run: bool = typer.Option(False, "--dry-run", help="Preview what would be deleted without removing anything."),
98
+ confirm: bool = typer.Option(True, "--confirm/--no-confirm", help="Prompt before deleting (default: on)."),
99
+ ) -> None:
100
+ """
101
+ Run a retention sweep on a log directory.
102
+
103
+ Always does a dry-run preview first, then (if --no-confirm is not set)
104
+ prompts before actually deleting.
105
+
106
+ \b
107
+ Examples
108
+ --------
109
+ ik logger clean ./logs --days 7
110
+ ik logger clean /var/log/myapp --days 30 --dry-run
111
+ ik logger clean ./logs --days 3 --no-confirm
112
+ """
113
+ if not log_dir.exists():
114
+ _abort(f"Directory not found: {log_dir}")
115
+
116
+ if days < 1:
117
+ _abort("--days must be at least 1.")
118
+
119
+ from infrakit.core.logger.retention import sweep
120
+
121
+ # ── dry-run pass: find candidates without deleting ────────────────────────
122
+ try:
123
+ preview = sweep(log_dir, retention_days=days, dry_run=True)
124
+ except Exception as exc: # noqa: BLE001
125
+ _abort(f"Retention scan failed: {exc}")
126
+ return
127
+
128
+ if not preview.deleted:
129
+ _ok(f"No log files older than {days} day(s) found in {log_dir}.")
130
+ return
131
+
132
+ typer.echo(typer.style(
133
+ f" Found {len(preview.deleted)} file(s) older than {days} day(s):",
134
+ bold=True,
135
+ ))
136
+ for p in preview.deleted:
137
+ typer.echo(f" {typer.style(str(p), fg=typer.colors.YELLOW)}")
138
+ typer.echo()
139
+
140
+ # ── dry-run mode: stop here ───────────────────────────────────────────────
141
+ if dry_run:
142
+ typer.echo(typer.style(" Dry-run — nothing deleted.", fg=typer.colors.BRIGHT_BLACK))
143
+ return
144
+
145
+ # ── confirm prompt ────────────────────────────────────────────────────────
146
+ if confirm:
147
+ typer.confirm(f" Delete {len(preview.deleted)} file(s)?", abort=True)
148
+
149
+ # ── live pass: actually delete ────────────────────────────────────────────
150
+ try:
151
+ result = sweep(log_dir, retention_days=days, dry_run=False)
152
+ except Exception as exc: # noqa: BLE001
153
+ _abort(f"Deletion failed: {exc}")
154
+ return
155
+
156
+ _ok(f"Deleted {len(result.deleted)} file(s).")
157
+
158
+ if result.errors:
159
+ for path, exc in result.errors:
160
+ typer.echo(typer.style(f"⚠ Error on {path}: {exc}", fg=typer.colors.YELLOW), err=True)