python-infrakit-dev 0.1.2__tar.gz → 0.1.4__tar.gz

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 (77) hide show
  1. python_infrakit_dev-0.1.4/.claude/settings.local.json +7 -0
  2. python_infrakit_dev-0.1.4/PKG-INFO +318 -0
  3. python_infrakit_dev-0.1.4/README.md +301 -0
  4. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/scaffolder/ai.py +37 -16
  5. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/scaffolder/backend.py +106 -2
  6. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/scaffolder/cli_tool.py +1 -1
  7. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/scaffolder/generator.py +93 -28
  8. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/scaffolder/pipeline.py +1 -1
  9. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/pyproject.toml +1 -1
  10. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/tests/test_llm.py +262 -36
  11. python_infrakit_dev-0.1.4/tests/test_scaffolders.py +495 -0
  12. python_infrakit_dev-0.1.2/PKG-INFO +0 -124
  13. python_infrakit_dev-0.1.2/README.md +0 -107
  14. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/.gitignore +0 -0
  15. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/.python-version +0 -0
  16. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/__init__.py +0 -0
  17. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/cli/__init__.py +0 -0
  18. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/cli/commands/__init__.py +0 -0
  19. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/cli/commands/config.py +0 -0
  20. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/cli/commands/deps.py +0 -0
  21. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/cli/commands/init.py +0 -0
  22. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/cli/commands/llm.py +0 -0
  23. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/cli/commands/logger.py +0 -0
  24. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/cli/commands/module.py +0 -0
  25. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/cli/commands/time.py +0 -0
  26. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/cli/main.py +0 -0
  27. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/core/__init__.py +0 -0
  28. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/core/config/__init__.py +0 -0
  29. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/core/config/converter.py +0 -0
  30. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/core/config/exporter.py +0 -0
  31. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/core/config/loader.py +0 -0
  32. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/core/config/validator.py +0 -0
  33. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/core/logger/__init__.py +0 -0
  34. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/core/logger/formatters.py +0 -0
  35. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/core/logger/handlers.py +0 -0
  36. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/core/logger/retention.py +0 -0
  37. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/core/logger/setup.py +0 -0
  38. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/deps/__init__.py +0 -0
  39. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/deps/clean.py +0 -0
  40. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/deps/depfile.py +0 -0
  41. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/deps/health.py +0 -0
  42. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/deps/optimizer.py +0 -0
  43. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/deps/scanner.py +0 -0
  44. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/llm/__init__.py +0 -0
  45. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/llm/batch.py +0 -0
  46. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/llm/client.py +0 -0
  47. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/llm/key_manager.py +0 -0
  48. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/llm/llm_readme.md +0 -0
  49. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/llm/models.py +0 -0
  50. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/llm/providers/__init__.py +0 -0
  51. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/llm/providers/base.py +0 -0
  52. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/llm/providers/gemini.py +0 -0
  53. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/llm/providers/openai.py +0 -0
  54. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/llm/rate_limiter.py +0 -0
  55. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/scaffolder/__init__.py +0 -0
  56. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/scaffolder/registry.py +0 -0
  57. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/time/__init__.py +0 -0
  58. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/infrakit/time/profiler.py +0 -0
  59. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/tests/__init__.py +0 -0
  60. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/tests/cli/__init__.py +0 -0
  61. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/tests/cli/conftest.py +0 -0
  62. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/tests/cli/test_config.py +0 -0
  63. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/tests/cli/test_init.py +0 -0
  64. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/tests/cli/test_logger.py +0 -0
  65. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/tests/cli/test_module.py +0 -0
  66. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/tests/core/__init__.py +0 -0
  67. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/tests/core/config/__init__.py +0 -0
  68. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/tests/core/config/test_converter.py +0 -0
  69. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/tests/core/config/test_exporter.py +0 -0
  70. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/tests/core/config/test_loader.py +0 -0
  71. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/tests/core/config/test_validator.py +0 -0
  72. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/tests/core/logger/__init__.py +0 -0
  73. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/tests/core/logger/test_formatters.py +0 -0
  74. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/tests/core/logger/test_handler.py +0 -0
  75. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/tests/core/logger/test_retention.py +0 -0
  76. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/tests/core/logger/test_setup.py +0 -0
  77. {python_infrakit_dev-0.1.2 → python_infrakit_dev-0.1.4}/tests/test_time.py +0 -0
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(python -c \":*)"
5
+ ]
6
+ }
7
+ }
@@ -0,0 +1,318 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-infrakit-dev
3
+ Version: 0.1.4
4
+ Summary: A comprehensive Python developer infrastructure toolkit
5
+ Project-URL: Homepage, https://github.com/chiragg21/infrakit
6
+ Project-URL: Repository, https://github.com/chiragg21/infrakit
7
+ Requires-Python: >=3.13
8
+ Requires-Dist: google-genai>=1.69.0
9
+ Requires-Dist: isort>=8.0.1
10
+ Requires-Dist: openai>=2.30.0
11
+ Requires-Dist: pydantic>=2.12.5
12
+ Requires-Dist: python-dotenv>=1.2.2
13
+ Requires-Dist: pyyaml>=6.0.3
14
+ Requires-Dist: tqdm>=4.67.3
15
+ Requires-Dist: typer>=0.24.1
16
+ Description-Content-Type: text/markdown
17
+
18
+ # Infrakit
19
+
20
+ A modular developer toolkit for Python — project scaffolding, logging, config loading, a multi-provider LLM client, and dependency utilities.
21
+
22
+ ```bash
23
+ pip install infrakit
24
+ ```
25
+
26
+ The CLI is available as `ik`.
27
+
28
+ ---
29
+
30
+ ## Scaffolding
31
+
32
+ Bootstrap a new project in one command:
33
+
34
+ ```bash
35
+ ik init my-project # basic layout
36
+ ik init my-api -t backend # FastAPI service
37
+ ik init my-model -t ai # AI/ML project with pipelines and notebooks
38
+ ik init my-etl -t pipeline # ETL/data pipeline
39
+ ik init my-cli -t cli_tool # distributable CLI app
40
+ ```
41
+
42
+ **All flags:**
43
+
44
+ | Flag | Values | Default | Description |
45
+ |---|---|---|---|
46
+ | `-t` / `--template` | `basic`, `backend`, `ai`, `pipeline`, `cli_tool` | `basic` | Project template |
47
+ | `-v` / `--version` | e.g. `0.1.0` | `0.1.0` | Starting version string |
48
+ | `--description` | string | `""` | Short description added to pyproject.toml and README |
49
+ | `--author` | string | `""` | Author line in pyproject.toml |
50
+ | `--config` | `env`, `yaml`, `json` | `env` | Config file format to generate |
51
+ | `--deps` | `toml`, `requirements` | `toml` | Dependency file style |
52
+ | `--include-llm` | flag | off | Add `utils/llm.py` wired to `infrakit.llm` |
53
+
54
+ Every template generates a config file pre-populated with the variables its utilities need (logger settings, LLM keys, app settings). Re-running over an existing directory skips files already present.
55
+
56
+ ---
57
+
58
+ ## Core — Config & Logger
59
+
60
+ ### Config loader
61
+
62
+ ```python
63
+ from infrakit.core.config.loader import load, load_env
64
+
65
+ cfg = load_env(".env", cast_values=True) # "true" → bool, "42" → int
66
+ cfg = load("config.yaml")
67
+ cfg = load("config.json")
68
+
69
+ port = cfg.get("APP_PORT", 8000)
70
+ ```
71
+
72
+ **`load(path, *, ...)`** — load a config file (JSON, YAML, INI, or `.env`), format inferred from extension.
73
+
74
+ | Parameter | Default | Description |
75
+ |---|---|---|
76
+ | `path` | — | Path to the config file |
77
+ | `env_override` | `False` | Let existing environment variables override file values |
78
+ | `env_file` | `".env"` | `.env` file to merge in alongside the config |
79
+ | `inject_new` | `False` | Add any new keys from the env file into the result |
80
+ | `interpolate` | `True` | Expand `${VAR}` references within values |
81
+ | `cast_values` | `True` | Convert strings to int, float, bool where possible |
82
+
83
+ **`load_env(path, *, cast_values)`** — convenience wrapper to load a `.env` file directly. `cast_values` defaults to `False`.
84
+
85
+ ---
86
+
87
+ ### Logger
88
+
89
+ ```python
90
+ from infrakit.core.logger import setup, get_logger
91
+
92
+ setup(log_dir="logs", strategy="date", stream="stdout", fmt="human", level="INFO")
93
+
94
+ log = get_logger(__name__)
95
+ log.info("started on port %d", 8080)
96
+ ```
97
+
98
+ **`setup(*, ...)`** — configure logging once at startup. All parameters are keyword-only.
99
+
100
+ | Parameter | Default | Description |
101
+ |---|---|---|
102
+ | `level` | `"DEBUG"` | Minimum log level (`DEBUG`, `INFO`, `WARNING`, `ERROR`) |
103
+ | `fmt` | `"human"` | Console format — `"human"` for readable text, `"json"` for structured |
104
+ | `file_fmt` | `"human"` | Format used in log files (same options as `fmt`) |
105
+ | `strategy` | `"date"` | File rotation strategy — `"date"`, `"date_level"`, or `"single"` |
106
+ | `stream` | `"stdout"` | Where to echo logs — `"stdout"`, `"stderr"`, or `None` to disable |
107
+ | `log_dir` | `"logs"` | Directory where log files are written |
108
+ | `session` | `None` | Label this run — adds a session prefix to log filenames |
109
+ | `retention` | `30` | Days to keep old log files before deletion |
110
+ | `max_bytes` | `10MB` | Max size of a single log file before rotation |
111
+ | `force` | `False` | Re-apply setup even if already configured |
112
+
113
+ **`get_logger(name)`** — returns a stdlib `Logger`. Always call as `get_logger(__name__)`. Safe to call before `setup()`.
114
+
115
+ ---
116
+
117
+ ## LLM Client
118
+
119
+ A unified client for **OpenAI** and **Gemini** with key rotation, rate limiting, quota tracking, and async/batch generation.
120
+
121
+ ```python
122
+ from infrakit.llm import LLMClient, Prompt
123
+
124
+ client = LLMClient(
125
+ keys={"openai_keys": ["sk-..."], "gemini_keys": ["AIza..."]},
126
+ storage_dir="./llm_state", # persists key usage across restarts
127
+ )
128
+
129
+ response = client.generate(Prompt(user="What is 2+2?"), provider="openai")
130
+ print(response.content)
131
+ ```
132
+
133
+ **`LLMClient(keys, ...)`**
134
+
135
+ | Parameter | Default | Description |
136
+ |---|---|---|
137
+ | `keys` | — | Dict with `openai_keys` and/or `gemini_keys` lists of API key strings |
138
+ | `storage_dir` | `~/.infrakit/llm` | Directory where key state is persisted across restarts |
139
+ | `quota_file` | `None` | Path to a `quotas.json` file with per-provider/model limits |
140
+ | `mode` | `"async"` | Batch execution mode — `"async"` or `"threaded"` |
141
+ | `max_concurrent` | `3` | Max parallel requests in batch calls |
142
+ | `key_retries` | `3` | How many keys to try before giving up on a request |
143
+ | `schema_retries` | `2` | How many times to retry structured output parsing on schema mismatch |
144
+ | `meta_window` | `200` | Number of recent requests to keep in memory per key |
145
+ | `openai_model` | `"gpt-4o-mini"` | Default OpenAI model |
146
+ | `gemini_model` | `"gemini-2.0-flash"` | Default Gemini model |
147
+ | `show_progress` | `True` | Show a progress bar during batch generation |
148
+
149
+ ---
150
+
151
+ **`generate(prompt, provider, response_model, **kwargs)`** — blocking single call, safe in any context.
152
+
153
+ **`async_generate(prompt, provider, response_model, **kwargs)`** — async version, use inside `async` functions.
154
+
155
+ | Parameter | Default | Description |
156
+ |---|---|---|
157
+ | `prompt` | — | `Prompt(system=..., user=...)` — `system` is optional |
158
+ | `provider` | — | `"openai"` or `"gemini"` |
159
+ | `response_model` | `None` | Pydantic `BaseModel` subclass for structured output parsing |
160
+
161
+ ---
162
+
163
+ **`batch_generate(prompts, provider, ...)`** — run many prompts; results match input order.
164
+
165
+ **`async_batch_generate(prompts, provider, ...)`** — async version for use inside `async` functions.
166
+
167
+ | Parameter | Default | Description |
168
+ |---|---|---|
169
+ | `prompts` | — | List of `Prompt` objects |
170
+ | `provider` | — | `"openai"` or `"gemini"` |
171
+ | `response_model` | `None` | Pydantic model for structured output on every item |
172
+ | `max_concurrent` | client default | Override the client-level concurrency limit for this batch |
173
+ | `show_progress` | client default | Override the client-level progress bar setting |
174
+
175
+ ```python
176
+ batch = client.batch_generate(prompts, provider="openai")
177
+ print(batch.success_count, batch.failure_count, batch.total_tokens)
178
+ for r in batch.results:
179
+ print(r.content if not r.error else r.error)
180
+ ```
181
+
182
+ ---
183
+
184
+ **Structured output:**
185
+
186
+ ```python
187
+ from pydantic import BaseModel
188
+
189
+ class Summary(BaseModel):
190
+ title: str
191
+ bullets: list[str]
192
+
193
+ response = client.generate(Prompt(user="Summarise: ..."), provider="openai", response_model=Summary)
194
+ if response.schema_matched:
195
+ print(response.parsed.bullets) # typed Summary instance
196
+ ```
197
+
198
+ ---
199
+
200
+ **`set_quota(provider, key_id, quota)`** — set limits for a specific key.
201
+
202
+ ```python
203
+ from infrakit.llm import QuotaConfig
204
+
205
+ # key-level RPM (applies to all models on this key)
206
+ client.set_quota(provider="openai", key_id="sk-key1", quota=QuotaConfig(rpm_limit=60))
207
+
208
+ # per-model daily token limit
209
+ client.set_quota(provider="gemini", key_id="AIza-key1", quota=QuotaConfig(model="gemini-2.5-pro", daily_token_limit=250_000))
210
+ ```
211
+
212
+ `QuotaConfig` fields: `model` (None = all models on the key), `rpm_limit`, `daily_token_limit`, `tpm_limit`.
213
+
214
+ Quota defaults can also be set in a `quotas.json` file — pass the path via `quota_file=` in the constructor.
215
+
216
+ ---
217
+
218
+ **`status(provider, key_id)`** — return key status as a list of dicts. **`print_status(provider, key_id)`** — same but pretty-printed to stdout.
219
+
220
+ | Parameter | Default | Description |
221
+ |---|---|---|
222
+ | `provider` | `None` | Filter to `"openai"` or `"gemini"`; `None` returns all |
223
+ | `key_id` | `None` | Filter to a specific key (first 8 chars); `None` returns all |
224
+
225
+ ```python
226
+ rows = client.status(provider="openai")
227
+ # each row: provider, key_id, status, rpm_limit, current_rpm, models
228
+ ```
229
+
230
+ **CLI:**
231
+
232
+ ```bash
233
+ ik llm status --storage-dir ./llm_state
234
+ ik llm quota set --provider openai --key sk-abc --rpm 60 --storage-dir ./llm_state
235
+ ```
236
+
237
+ ---
238
+
239
+ ## Other Features
240
+
241
+ ### Dependency management (`infrakit.deps`)
242
+
243
+ ```bash
244
+ ik deps scan . # list packages your code actually imports
245
+ ik deps export . --format toml # sync pyproject.toml / requirements.txt
246
+ ik deps check --packages numpy # outdated, security, and license checks
247
+ ik deps clean . --dry-run # find unused installed packages
248
+ ik deps optimise . # sort and clean imports (isort)
249
+ ```
250
+
251
+ **`scan(root, include_notebooks, use_gitignore)`**
252
+
253
+ | Parameter | Default | Description |
254
+ |---|---|---|
255
+ | `root` | — | Project root to scan |
256
+ | `include_notebooks` | `False` | Also scan `.ipynb` notebook files |
257
+ | `use_gitignore` | `True` | Skip paths matched by `.gitignore` |
258
+
259
+ **`export(root, output, inplace, keep_versions, include_notebooks, use_gitignore)`**
260
+
261
+ | Parameter | Default | Description |
262
+ |---|---|---|
263
+ | `root` | — | Project root to scan |
264
+ | `output` | `None` | Write to this path instead of the detected dependency file |
265
+ | `inplace` | `False` | Update the existing file in place |
266
+ | `keep_versions` | `True` | Preserve pinned version specifiers already in the file |
267
+ | `include_notebooks` | `False` | Also scan `.ipynb` files |
268
+ | `use_gitignore` | `True` | Skip gitignored paths |
269
+
270
+ **`check(root, packages, outdated, security, licenses)`**
271
+
272
+ | Parameter | Default | Description |
273
+ |---|---|---|
274
+ | `root` | `None` | Auto-scan this root for packages (used if `packages` is `None`) |
275
+ | `packages` | `None` | Explicit list of package names to check |
276
+ | `outdated` | `True` | Check for newer versions on PyPI |
277
+ | `security` | `True` | Run vulnerability checks |
278
+ | `licenses` | `True` | Check license compatibility |
279
+
280
+ Returns a `HealthReport` with `.outdated`, `.vulnerable`, `.licenses`, and `.errors` lists.
281
+
282
+ **`clean(root, protected, dry_run)`**
283
+
284
+ | Parameter | Default | Description |
285
+ |---|---|---|
286
+ | `root` | — | Project root (used to determine what is actually imported) |
287
+ | `protected` | `None` | Set of package names to never remove |
288
+ | `dry_run` | `True` | Preview only — pass `False` to actually uninstall |
289
+
290
+ Returns a `CleanResult` with `.to_remove`, `.removed`, `.skipped`, and `.errors` lists.
291
+
292
+ **`optimise(root, files, convert_to, use_isort, dry_run)`**
293
+
294
+ | Parameter | Default | Description |
295
+ |---|---|---|
296
+ | `root` | — | Project root |
297
+ | `files` | `None` | Specific files to process; `None` processes all `.py` files |
298
+ | `convert_to` | `None` | Rewrite import style (e.g. `"absolute"`) |
299
+ | `use_isort` | `True` | Run `isort` on each file |
300
+ | `dry_run` | `False` | Preview changes without writing files |
301
+
302
+ ---
303
+
304
+ ### Profiling (`infrakit.time`)
305
+
306
+ ```bash
307
+ ik time run script.py
308
+ ```
309
+
310
+ ```python
311
+ from infrakit.time import pipeline_profiler, track
312
+
313
+ @pipeline_profiler("My Pipeline")
314
+ def main(): ...
315
+
316
+ @track(name="Load Step")
317
+ def load_data(): ...
318
+ ```
@@ -0,0 +1,301 @@
1
+ # Infrakit
2
+
3
+ A modular developer toolkit for Python — project scaffolding, logging, config loading, a multi-provider LLM client, and dependency utilities.
4
+
5
+ ```bash
6
+ pip install infrakit
7
+ ```
8
+
9
+ The CLI is available as `ik`.
10
+
11
+ ---
12
+
13
+ ## Scaffolding
14
+
15
+ Bootstrap a new project in one command:
16
+
17
+ ```bash
18
+ ik init my-project # basic layout
19
+ ik init my-api -t backend # FastAPI service
20
+ ik init my-model -t ai # AI/ML project with pipelines and notebooks
21
+ ik init my-etl -t pipeline # ETL/data pipeline
22
+ ik init my-cli -t cli_tool # distributable CLI app
23
+ ```
24
+
25
+ **All flags:**
26
+
27
+ | Flag | Values | Default | Description |
28
+ |---|---|---|---|
29
+ | `-t` / `--template` | `basic`, `backend`, `ai`, `pipeline`, `cli_tool` | `basic` | Project template |
30
+ | `-v` / `--version` | e.g. `0.1.0` | `0.1.0` | Starting version string |
31
+ | `--description` | string | `""` | Short description added to pyproject.toml and README |
32
+ | `--author` | string | `""` | Author line in pyproject.toml |
33
+ | `--config` | `env`, `yaml`, `json` | `env` | Config file format to generate |
34
+ | `--deps` | `toml`, `requirements` | `toml` | Dependency file style |
35
+ | `--include-llm` | flag | off | Add `utils/llm.py` wired to `infrakit.llm` |
36
+
37
+ Every template generates a config file pre-populated with the variables its utilities need (logger settings, LLM keys, app settings). Re-running over an existing directory skips files already present.
38
+
39
+ ---
40
+
41
+ ## Core — Config & Logger
42
+
43
+ ### Config loader
44
+
45
+ ```python
46
+ from infrakit.core.config.loader import load, load_env
47
+
48
+ cfg = load_env(".env", cast_values=True) # "true" → bool, "42" → int
49
+ cfg = load("config.yaml")
50
+ cfg = load("config.json")
51
+
52
+ port = cfg.get("APP_PORT", 8000)
53
+ ```
54
+
55
+ **`load(path, *, ...)`** — load a config file (JSON, YAML, INI, or `.env`), format inferred from extension.
56
+
57
+ | Parameter | Default | Description |
58
+ |---|---|---|
59
+ | `path` | — | Path to the config file |
60
+ | `env_override` | `False` | Let existing environment variables override file values |
61
+ | `env_file` | `".env"` | `.env` file to merge in alongside the config |
62
+ | `inject_new` | `False` | Add any new keys from the env file into the result |
63
+ | `interpolate` | `True` | Expand `${VAR}` references within values |
64
+ | `cast_values` | `True` | Convert strings to int, float, bool where possible |
65
+
66
+ **`load_env(path, *, cast_values)`** — convenience wrapper to load a `.env` file directly. `cast_values` defaults to `False`.
67
+
68
+ ---
69
+
70
+ ### Logger
71
+
72
+ ```python
73
+ from infrakit.core.logger import setup, get_logger
74
+
75
+ setup(log_dir="logs", strategy="date", stream="stdout", fmt="human", level="INFO")
76
+
77
+ log = get_logger(__name__)
78
+ log.info("started on port %d", 8080)
79
+ ```
80
+
81
+ **`setup(*, ...)`** — configure logging once at startup. All parameters are keyword-only.
82
+
83
+ | Parameter | Default | Description |
84
+ |---|---|---|
85
+ | `level` | `"DEBUG"` | Minimum log level (`DEBUG`, `INFO`, `WARNING`, `ERROR`) |
86
+ | `fmt` | `"human"` | Console format — `"human"` for readable text, `"json"` for structured |
87
+ | `file_fmt` | `"human"` | Format used in log files (same options as `fmt`) |
88
+ | `strategy` | `"date"` | File rotation strategy — `"date"`, `"date_level"`, or `"single"` |
89
+ | `stream` | `"stdout"` | Where to echo logs — `"stdout"`, `"stderr"`, or `None` to disable |
90
+ | `log_dir` | `"logs"` | Directory where log files are written |
91
+ | `session` | `None` | Label this run — adds a session prefix to log filenames |
92
+ | `retention` | `30` | Days to keep old log files before deletion |
93
+ | `max_bytes` | `10MB` | Max size of a single log file before rotation |
94
+ | `force` | `False` | Re-apply setup even if already configured |
95
+
96
+ **`get_logger(name)`** — returns a stdlib `Logger`. Always call as `get_logger(__name__)`. Safe to call before `setup()`.
97
+
98
+ ---
99
+
100
+ ## LLM Client
101
+
102
+ A unified client for **OpenAI** and **Gemini** with key rotation, rate limiting, quota tracking, and async/batch generation.
103
+
104
+ ```python
105
+ from infrakit.llm import LLMClient, Prompt
106
+
107
+ client = LLMClient(
108
+ keys={"openai_keys": ["sk-..."], "gemini_keys": ["AIza..."]},
109
+ storage_dir="./llm_state", # persists key usage across restarts
110
+ )
111
+
112
+ response = client.generate(Prompt(user="What is 2+2?"), provider="openai")
113
+ print(response.content)
114
+ ```
115
+
116
+ **`LLMClient(keys, ...)`**
117
+
118
+ | Parameter | Default | Description |
119
+ |---|---|---|
120
+ | `keys` | — | Dict with `openai_keys` and/or `gemini_keys` lists of API key strings |
121
+ | `storage_dir` | `~/.infrakit/llm` | Directory where key state is persisted across restarts |
122
+ | `quota_file` | `None` | Path to a `quotas.json` file with per-provider/model limits |
123
+ | `mode` | `"async"` | Batch execution mode — `"async"` or `"threaded"` |
124
+ | `max_concurrent` | `3` | Max parallel requests in batch calls |
125
+ | `key_retries` | `3` | How many keys to try before giving up on a request |
126
+ | `schema_retries` | `2` | How many times to retry structured output parsing on schema mismatch |
127
+ | `meta_window` | `200` | Number of recent requests to keep in memory per key |
128
+ | `openai_model` | `"gpt-4o-mini"` | Default OpenAI model |
129
+ | `gemini_model` | `"gemini-2.0-flash"` | Default Gemini model |
130
+ | `show_progress` | `True` | Show a progress bar during batch generation |
131
+
132
+ ---
133
+
134
+ **`generate(prompt, provider, response_model, **kwargs)`** — blocking single call, safe in any context.
135
+
136
+ **`async_generate(prompt, provider, response_model, **kwargs)`** — async version, use inside `async` functions.
137
+
138
+ | Parameter | Default | Description |
139
+ |---|---|---|
140
+ | `prompt` | — | `Prompt(system=..., user=...)` — `system` is optional |
141
+ | `provider` | — | `"openai"` or `"gemini"` |
142
+ | `response_model` | `None` | Pydantic `BaseModel` subclass for structured output parsing |
143
+
144
+ ---
145
+
146
+ **`batch_generate(prompts, provider, ...)`** — run many prompts; results match input order.
147
+
148
+ **`async_batch_generate(prompts, provider, ...)`** — async version for use inside `async` functions.
149
+
150
+ | Parameter | Default | Description |
151
+ |---|---|---|
152
+ | `prompts` | — | List of `Prompt` objects |
153
+ | `provider` | — | `"openai"` or `"gemini"` |
154
+ | `response_model` | `None` | Pydantic model for structured output on every item |
155
+ | `max_concurrent` | client default | Override the client-level concurrency limit for this batch |
156
+ | `show_progress` | client default | Override the client-level progress bar setting |
157
+
158
+ ```python
159
+ batch = client.batch_generate(prompts, provider="openai")
160
+ print(batch.success_count, batch.failure_count, batch.total_tokens)
161
+ for r in batch.results:
162
+ print(r.content if not r.error else r.error)
163
+ ```
164
+
165
+ ---
166
+
167
+ **Structured output:**
168
+
169
+ ```python
170
+ from pydantic import BaseModel
171
+
172
+ class Summary(BaseModel):
173
+ title: str
174
+ bullets: list[str]
175
+
176
+ response = client.generate(Prompt(user="Summarise: ..."), provider="openai", response_model=Summary)
177
+ if response.schema_matched:
178
+ print(response.parsed.bullets) # typed Summary instance
179
+ ```
180
+
181
+ ---
182
+
183
+ **`set_quota(provider, key_id, quota)`** — set limits for a specific key.
184
+
185
+ ```python
186
+ from infrakit.llm import QuotaConfig
187
+
188
+ # key-level RPM (applies to all models on this key)
189
+ client.set_quota(provider="openai", key_id="sk-key1", quota=QuotaConfig(rpm_limit=60))
190
+
191
+ # per-model daily token limit
192
+ client.set_quota(provider="gemini", key_id="AIza-key1", quota=QuotaConfig(model="gemini-2.5-pro", daily_token_limit=250_000))
193
+ ```
194
+
195
+ `QuotaConfig` fields: `model` (None = all models on the key), `rpm_limit`, `daily_token_limit`, `tpm_limit`.
196
+
197
+ Quota defaults can also be set in a `quotas.json` file — pass the path via `quota_file=` in the constructor.
198
+
199
+ ---
200
+
201
+ **`status(provider, key_id)`** — return key status as a list of dicts. **`print_status(provider, key_id)`** — same but pretty-printed to stdout.
202
+
203
+ | Parameter | Default | Description |
204
+ |---|---|---|
205
+ | `provider` | `None` | Filter to `"openai"` or `"gemini"`; `None` returns all |
206
+ | `key_id` | `None` | Filter to a specific key (first 8 chars); `None` returns all |
207
+
208
+ ```python
209
+ rows = client.status(provider="openai")
210
+ # each row: provider, key_id, status, rpm_limit, current_rpm, models
211
+ ```
212
+
213
+ **CLI:**
214
+
215
+ ```bash
216
+ ik llm status --storage-dir ./llm_state
217
+ ik llm quota set --provider openai --key sk-abc --rpm 60 --storage-dir ./llm_state
218
+ ```
219
+
220
+ ---
221
+
222
+ ## Other Features
223
+
224
+ ### Dependency management (`infrakit.deps`)
225
+
226
+ ```bash
227
+ ik deps scan . # list packages your code actually imports
228
+ ik deps export . --format toml # sync pyproject.toml / requirements.txt
229
+ ik deps check --packages numpy # outdated, security, and license checks
230
+ ik deps clean . --dry-run # find unused installed packages
231
+ ik deps optimise . # sort and clean imports (isort)
232
+ ```
233
+
234
+ **`scan(root, include_notebooks, use_gitignore)`**
235
+
236
+ | Parameter | Default | Description |
237
+ |---|---|---|
238
+ | `root` | — | Project root to scan |
239
+ | `include_notebooks` | `False` | Also scan `.ipynb` notebook files |
240
+ | `use_gitignore` | `True` | Skip paths matched by `.gitignore` |
241
+
242
+ **`export(root, output, inplace, keep_versions, include_notebooks, use_gitignore)`**
243
+
244
+ | Parameter | Default | Description |
245
+ |---|---|---|
246
+ | `root` | — | Project root to scan |
247
+ | `output` | `None` | Write to this path instead of the detected dependency file |
248
+ | `inplace` | `False` | Update the existing file in place |
249
+ | `keep_versions` | `True` | Preserve pinned version specifiers already in the file |
250
+ | `include_notebooks` | `False` | Also scan `.ipynb` files |
251
+ | `use_gitignore` | `True` | Skip gitignored paths |
252
+
253
+ **`check(root, packages, outdated, security, licenses)`**
254
+
255
+ | Parameter | Default | Description |
256
+ |---|---|---|
257
+ | `root` | `None` | Auto-scan this root for packages (used if `packages` is `None`) |
258
+ | `packages` | `None` | Explicit list of package names to check |
259
+ | `outdated` | `True` | Check for newer versions on PyPI |
260
+ | `security` | `True` | Run vulnerability checks |
261
+ | `licenses` | `True` | Check license compatibility |
262
+
263
+ Returns a `HealthReport` with `.outdated`, `.vulnerable`, `.licenses`, and `.errors` lists.
264
+
265
+ **`clean(root, protected, dry_run)`**
266
+
267
+ | Parameter | Default | Description |
268
+ |---|---|---|
269
+ | `root` | — | Project root (used to determine what is actually imported) |
270
+ | `protected` | `None` | Set of package names to never remove |
271
+ | `dry_run` | `True` | Preview only — pass `False` to actually uninstall |
272
+
273
+ Returns a `CleanResult` with `.to_remove`, `.removed`, `.skipped`, and `.errors` lists.
274
+
275
+ **`optimise(root, files, convert_to, use_isort, dry_run)`**
276
+
277
+ | Parameter | Default | Description |
278
+ |---|---|---|
279
+ | `root` | — | Project root |
280
+ | `files` | `None` | Specific files to process; `None` processes all `.py` files |
281
+ | `convert_to` | `None` | Rewrite import style (e.g. `"absolute"`) |
282
+ | `use_isort` | `True` | Run `isort` on each file |
283
+ | `dry_run` | `False` | Preview changes without writing files |
284
+
285
+ ---
286
+
287
+ ### Profiling (`infrakit.time`)
288
+
289
+ ```bash
290
+ ik time run script.py
291
+ ```
292
+
293
+ ```python
294
+ from infrakit.time import pipeline_profiler, track
295
+
296
+ @pipeline_profiler("My Pipeline")
297
+ def main(): ...
298
+
299
+ @track(name="Load Step")
300
+ def load_data(): ...
301
+ ```