schematico 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.
- schematico/__init__.py +27 -0
- schematico/cli/__init__.py +0 -0
- schematico/cli/main.py +375 -0
- schematico/cli/progress.py +31 -0
- schematico/cli/projects.py +173 -0
- schematico/cli/runner.py +125 -0
- schematico/cli/wizard.py +177 -0
- schematico/discovery.py +102 -0
- schematico/generator.py +97 -0
- schematico/helpers.py +38 -0
- schematico/logging.py +34 -0
- schematico/models.py +140 -0
- schematico/providers.py +95 -0
- schematico/tools/tavily_tools.py +79 -0
- schematico-0.1.0.dist-info/METADATA +289 -0
- schematico-0.1.0.dist-info/RECORD +19 -0
- schematico-0.1.0.dist-info/WHEEL +4 -0
- schematico-0.1.0.dist-info/entry_points.txt +2 -0
- schematico-0.1.0.dist-info/licenses/LICENSE +21 -0
schematico/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from importlib.metadata import PackageNotFoundError, version as _version
|
|
2
|
+
|
|
3
|
+
from schematico.generator import run_generation
|
|
4
|
+
from schematico.discovery import run_discovery
|
|
5
|
+
from schematico.models import build_batch_model, model_from_dict, model_from_json
|
|
6
|
+
from schematico.providers import (
|
|
7
|
+
DEFAULT_MODEL,
|
|
8
|
+
SchematicoModel,
|
|
9
|
+
get_llm_model,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
__version__ = _version("schematico")
|
|
14
|
+
except PackageNotFoundError: # running from a source tree without an install
|
|
15
|
+
__version__ = "0.0.0+unknown"
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"__version__",
|
|
19
|
+
"DEFAULT_MODEL",
|
|
20
|
+
"SchematicoModel",
|
|
21
|
+
"build_batch_model",
|
|
22
|
+
"get_llm_model",
|
|
23
|
+
"model_from_dict",
|
|
24
|
+
"model_from_json",
|
|
25
|
+
"run_generation",
|
|
26
|
+
"run_discovery",
|
|
27
|
+
]
|
|
File without changes
|
schematico/cli/main.py
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from dotenv import load_dotenv
|
|
7
|
+
|
|
8
|
+
from schematico import __version__
|
|
9
|
+
from schematico.cli import runner
|
|
10
|
+
from schematico.cli.projects import (
|
|
11
|
+
Mode,
|
|
12
|
+
ProjectNotFoundError,
|
|
13
|
+
config_path,
|
|
14
|
+
delete_project,
|
|
15
|
+
find_projects_by_name,
|
|
16
|
+
get_default,
|
|
17
|
+
list_projects,
|
|
18
|
+
parse_config_filename,
|
|
19
|
+
resolve_active_project,
|
|
20
|
+
save_project,
|
|
21
|
+
set_default,
|
|
22
|
+
)
|
|
23
|
+
from schematico.cli.wizard import run_wizard
|
|
24
|
+
from schematico.logging import get_logger
|
|
25
|
+
|
|
26
|
+
load_dotenv()
|
|
27
|
+
logger = get_logger("cli.main")
|
|
28
|
+
|
|
29
|
+
app = typer.Typer(
|
|
30
|
+
name="schematico",
|
|
31
|
+
help=(
|
|
32
|
+
"Generate or discover synthetic data from a JSON schema.\n\n"
|
|
33
|
+
"Run `schematico help` to see every command and flag in one place."
|
|
34
|
+
),
|
|
35
|
+
no_args_is_help=True,
|
|
36
|
+
add_completion=False,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _version_callback(value: bool) -> None:
|
|
41
|
+
if value:
|
|
42
|
+
typer.echo(f"schematico {__version__}")
|
|
43
|
+
raise typer.Exit()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@app.callback()
|
|
47
|
+
def _root(
|
|
48
|
+
version: bool = typer.Option(
|
|
49
|
+
False,
|
|
50
|
+
"--version",
|
|
51
|
+
"-V",
|
|
52
|
+
help="Show the version and exit.",
|
|
53
|
+
callback=_version_callback,
|
|
54
|
+
is_eager=True,
|
|
55
|
+
),
|
|
56
|
+
) -> None:
|
|
57
|
+
"""schematico — generate or discover data from a JSON schema."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _make_mode_app(mode: Mode) -> typer.Typer:
|
|
61
|
+
sub = typer.Typer(
|
|
62
|
+
name=mode,
|
|
63
|
+
help=f"{mode.capitalize()} records using a project config.",
|
|
64
|
+
invoke_without_command=True,
|
|
65
|
+
no_args_is_help=False,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@sub.callback()
|
|
69
|
+
def _default(
|
|
70
|
+
ctx: typer.Context,
|
|
71
|
+
config: str | None = typer.Option(
|
|
72
|
+
None,
|
|
73
|
+
"--config",
|
|
74
|
+
"-c",
|
|
75
|
+
help="Project config name to use (overrides default).",
|
|
76
|
+
),
|
|
77
|
+
output_path: str | None = typer.Option(
|
|
78
|
+
None, "--output", "-o", help="Override output path."
|
|
79
|
+
),
|
|
80
|
+
count: int | None = typer.Option(
|
|
81
|
+
None, "--count", "-n", help="Override record count."
|
|
82
|
+
),
|
|
83
|
+
model: str | None = typer.Option(
|
|
84
|
+
None, "--model", "-m", help="Override AI model."
|
|
85
|
+
),
|
|
86
|
+
) -> None:
|
|
87
|
+
if ctx.invoked_subcommand is not None:
|
|
88
|
+
ctx.obj = {"config": config}
|
|
89
|
+
return
|
|
90
|
+
try:
|
|
91
|
+
cfg = resolve_active_project(mode, name_override=config)
|
|
92
|
+
except ProjectNotFoundError as e:
|
|
93
|
+
typer.echo(f"schematico: error: {e}", err=True)
|
|
94
|
+
raise typer.Exit(1)
|
|
95
|
+
runner.run(
|
|
96
|
+
cfg,
|
|
97
|
+
output_override=output_path,
|
|
98
|
+
count_override=count,
|
|
99
|
+
model_override=model,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
@sub.command("list", help=f"List {mode} configs.")
|
|
103
|
+
def _list() -> None:
|
|
104
|
+
default_name = get_default(mode)
|
|
105
|
+
paths = list_projects(mode)
|
|
106
|
+
if not paths:
|
|
107
|
+
typer.echo(f"No {mode} configs found in ./.schematico/.")
|
|
108
|
+
typer.echo("Run `schematico new` to create one.")
|
|
109
|
+
return
|
|
110
|
+
for p in paths:
|
|
111
|
+
parsed = parse_config_filename(p.name)
|
|
112
|
+
name = parsed[0] if parsed else p.name
|
|
113
|
+
marker = "*" if name == default_name else " "
|
|
114
|
+
typer.echo(f"{marker} {name} ({p})")
|
|
115
|
+
|
|
116
|
+
@sub.command(
|
|
117
|
+
"use",
|
|
118
|
+
help=f"Select default {mode} config (`use <name>`) or set model (`use model <id>`).",
|
|
119
|
+
)
|
|
120
|
+
def _use(args: list[str] = typer.Argument(None)) -> None:
|
|
121
|
+
if not args:
|
|
122
|
+
typer.echo(
|
|
123
|
+
f"Usage: schematico {mode} use <name> | use model <id>", err=True
|
|
124
|
+
)
|
|
125
|
+
raise typer.Exit(2)
|
|
126
|
+
if args[0] == "model":
|
|
127
|
+
if len(args) != 2:
|
|
128
|
+
typer.echo(f"Usage: schematico {mode} use model <id>", err=True)
|
|
129
|
+
raise typer.Exit(2)
|
|
130
|
+
try:
|
|
131
|
+
cfg = resolve_active_project(mode)
|
|
132
|
+
except ProjectNotFoundError as e:
|
|
133
|
+
typer.echo(f"schematico: error: {e}", err=True)
|
|
134
|
+
raise typer.Exit(1)
|
|
135
|
+
cfg.model = args[1]
|
|
136
|
+
save_project(cfg)
|
|
137
|
+
typer.echo(f"Set model={args[1]} in '{cfg.name}.{mode}.toml'.")
|
|
138
|
+
return
|
|
139
|
+
if len(args) != 1:
|
|
140
|
+
typer.echo(f"Usage: schematico {mode} use <name>", err=True)
|
|
141
|
+
raise typer.Exit(2)
|
|
142
|
+
name = args[0]
|
|
143
|
+
if not config_path(name, mode).exists():
|
|
144
|
+
typer.echo(
|
|
145
|
+
f"schematico: error: config '{name}.{mode}.toml' not found.", err=True
|
|
146
|
+
)
|
|
147
|
+
raise typer.Exit(1)
|
|
148
|
+
set_default(mode, name)
|
|
149
|
+
typer.echo(f"Default {mode} config set to '{name}'.")
|
|
150
|
+
|
|
151
|
+
@sub.command("delete", help=f"Delete a {mode} config.")
|
|
152
|
+
def _delete(
|
|
153
|
+
name: str = typer.Argument(...),
|
|
154
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."),
|
|
155
|
+
) -> None:
|
|
156
|
+
p = config_path(name, mode)
|
|
157
|
+
if not p.exists():
|
|
158
|
+
typer.echo(
|
|
159
|
+
f"schematico: error: config '{name}.{mode}.toml' not found.",
|
|
160
|
+
err=True,
|
|
161
|
+
)
|
|
162
|
+
raise typer.Exit(1)
|
|
163
|
+
if not yes and not typer.confirm(f"Delete {p}?", default=False):
|
|
164
|
+
typer.echo("Aborted.")
|
|
165
|
+
raise typer.Exit(0)
|
|
166
|
+
was_default = delete_project(name, mode)
|
|
167
|
+
msg = f"Deleted '{name}.{mode}.toml'."
|
|
168
|
+
if was_default:
|
|
169
|
+
msg += f" Cleared default for `schematico {mode}`."
|
|
170
|
+
typer.echo(msg)
|
|
171
|
+
|
|
172
|
+
@sub.command("output")
|
|
173
|
+
def _set_output(path: str = typer.Argument(...)) -> None:
|
|
174
|
+
try:
|
|
175
|
+
cfg = resolve_active_project(mode)
|
|
176
|
+
except ProjectNotFoundError as e:
|
|
177
|
+
typer.echo(f"schematico: error: {e}", err=True)
|
|
178
|
+
raise typer.Exit(1)
|
|
179
|
+
cfg.output_path = path
|
|
180
|
+
save_project(cfg)
|
|
181
|
+
typer.echo(f"Set output_path={path} in '{cfg.name}.{mode}.toml'.")
|
|
182
|
+
|
|
183
|
+
schema_app = typer.Typer(
|
|
184
|
+
help="Manage the schema for the active config (`import` or `path`).",
|
|
185
|
+
no_args_is_help=True,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
@schema_app.command("import")
|
|
189
|
+
def _schema_import(
|
|
190
|
+
file: str = typer.Argument(..., help="Path to a JSON schema file.")
|
|
191
|
+
) -> None:
|
|
192
|
+
"""Embed a JSON schema file into the active config's [schema] table."""
|
|
193
|
+
import json as _json
|
|
194
|
+
|
|
195
|
+
from schematico.models import model_from_dict
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
cfg = resolve_active_project(mode)
|
|
199
|
+
except ProjectNotFoundError as e:
|
|
200
|
+
typer.echo(f"schematico: error: {e}", err=True)
|
|
201
|
+
raise typer.Exit(1)
|
|
202
|
+
p = Path(file)
|
|
203
|
+
if not p.exists():
|
|
204
|
+
typer.echo(f"schematico: error: file '{file}' not found.", err=True)
|
|
205
|
+
raise typer.Exit(1)
|
|
206
|
+
try:
|
|
207
|
+
raw = _json.loads(p.read_text(encoding="utf-8"))
|
|
208
|
+
except _json.JSONDecodeError as e:
|
|
209
|
+
typer.echo(f"schematico: error: invalid JSON in '{file}': {e}", err=True)
|
|
210
|
+
raise typer.Exit(1)
|
|
211
|
+
try:
|
|
212
|
+
model_from_dict(raw)
|
|
213
|
+
except ValueError as e:
|
|
214
|
+
typer.echo(f"schematico: error: {e}", err=True)
|
|
215
|
+
raise typer.Exit(1)
|
|
216
|
+
cfg.record_schema = raw
|
|
217
|
+
cfg.schema_path = ""
|
|
218
|
+
save_project(cfg)
|
|
219
|
+
typer.echo(
|
|
220
|
+
f"Imported schema from '{file}' into '{cfg.name}.{mode}.toml' "
|
|
221
|
+
f"({len(raw.get('fields', []))} fields)."
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
@schema_app.command("path")
|
|
225
|
+
def _schema_path(
|
|
226
|
+
file: str = typer.Argument(..., help="Path to a JSON schema file to reference.")
|
|
227
|
+
) -> None:
|
|
228
|
+
"""Reference an external JSON schema file (not embedded)."""
|
|
229
|
+
from schematico.models import model_from_json
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
cfg = resolve_active_project(mode)
|
|
233
|
+
except ProjectNotFoundError as e:
|
|
234
|
+
typer.echo(f"schematico: error: {e}", err=True)
|
|
235
|
+
raise typer.Exit(1)
|
|
236
|
+
try:
|
|
237
|
+
model_from_json(file)
|
|
238
|
+
except (FileNotFoundError, ValueError) as e:
|
|
239
|
+
typer.echo(f"schematico: error: {e}", err=True)
|
|
240
|
+
raise typer.Exit(1)
|
|
241
|
+
cfg.schema_path = file
|
|
242
|
+
cfg.record_schema = {}
|
|
243
|
+
save_project(cfg)
|
|
244
|
+
typer.echo(f"Set schema_path={file} in '{cfg.name}.{mode}.toml'.")
|
|
245
|
+
|
|
246
|
+
sub.add_typer(schema_app, name="schema")
|
|
247
|
+
|
|
248
|
+
return sub
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
app.add_typer(_make_mode_app("generate"), name="generate")
|
|
252
|
+
app.add_typer(_make_mode_app("discover"), name="discover")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@app.command("new")
|
|
256
|
+
def new_cmd() -> None:
|
|
257
|
+
"""Interactively create a new project config."""
|
|
258
|
+
run_wizard()
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@app.command("delete", help="Delete a project config by name (auto-detects mode).")
|
|
262
|
+
def delete_cmd(
|
|
263
|
+
name: str = typer.Argument(..., help="Project name (without .toml suffix)."),
|
|
264
|
+
mode: str | None = typer.Option(
|
|
265
|
+
None,
|
|
266
|
+
"--mode",
|
|
267
|
+
"-m",
|
|
268
|
+
help="Required when the name exists for both `generate` and `discover`.",
|
|
269
|
+
),
|
|
270
|
+
all_modes: bool = typer.Option(
|
|
271
|
+
False, "--all", "-a", help="Delete the project for every mode it exists in."
|
|
272
|
+
),
|
|
273
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."),
|
|
274
|
+
) -> None:
|
|
275
|
+
matches = find_projects_by_name(name)
|
|
276
|
+
if not matches:
|
|
277
|
+
typer.echo(f"schematico: error: no project named '{name}'.", err=True)
|
|
278
|
+
raise typer.Exit(1)
|
|
279
|
+
|
|
280
|
+
if mode is not None:
|
|
281
|
+
if mode not in ("generate", "discover"):
|
|
282
|
+
typer.echo(
|
|
283
|
+
f"schematico: error: --mode must be 'generate' or 'discover'.",
|
|
284
|
+
err=True,
|
|
285
|
+
)
|
|
286
|
+
raise typer.Exit(2)
|
|
287
|
+
if mode not in matches:
|
|
288
|
+
typer.echo(
|
|
289
|
+
f"schematico: error: no {mode} project named '{name}'.", err=True
|
|
290
|
+
)
|
|
291
|
+
raise typer.Exit(1)
|
|
292
|
+
targets: list[Mode] = [mode] # type: ignore[list-item]
|
|
293
|
+
elif all_modes:
|
|
294
|
+
targets = matches
|
|
295
|
+
elif len(matches) == 1:
|
|
296
|
+
targets = matches
|
|
297
|
+
else:
|
|
298
|
+
typer.echo(
|
|
299
|
+
f"schematico: error: '{name}' exists for both modes "
|
|
300
|
+
f"({', '.join(matches)}). Pass --mode <generate|discover> or --all.",
|
|
301
|
+
err=True,
|
|
302
|
+
)
|
|
303
|
+
raise typer.Exit(1)
|
|
304
|
+
|
|
305
|
+
paths = [config_path(name, m) for m in targets]
|
|
306
|
+
if not yes:
|
|
307
|
+
typer.echo("About to delete:")
|
|
308
|
+
for p in paths:
|
|
309
|
+
typer.echo(f" - {p}")
|
|
310
|
+
if not typer.confirm("Proceed?", default=False):
|
|
311
|
+
typer.echo("Aborted.")
|
|
312
|
+
raise typer.Exit(0)
|
|
313
|
+
|
|
314
|
+
for m in targets:
|
|
315
|
+
was_default = delete_project(name, m)
|
|
316
|
+
msg = f"Deleted '{name}.{m}.toml'."
|
|
317
|
+
if was_default:
|
|
318
|
+
msg += f" Cleared default for `schematico {m}`."
|
|
319
|
+
typer.echo(msg)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@app.command("list")
|
|
323
|
+
def list_cmd() -> None:
|
|
324
|
+
"""List all schematico project configs (both discover and generate)."""
|
|
325
|
+
rows: list[tuple[str, str, str, str]] = [] # (marker, mode, name, path)
|
|
326
|
+
for mode in ("discover", "generate"):
|
|
327
|
+
default_name = get_default(mode) # type: ignore[arg-type]
|
|
328
|
+
for p in list_projects(mode): # type: ignore[arg-type]
|
|
329
|
+
parsed = parse_config_filename(p.name)
|
|
330
|
+
name = parsed[0] if parsed else p.name
|
|
331
|
+
marker = "*" if name == default_name else " "
|
|
332
|
+
rows.append((marker, mode, name, str(p)))
|
|
333
|
+
|
|
334
|
+
if not rows:
|
|
335
|
+
typer.echo("No project configs found in ./.schematico/.")
|
|
336
|
+
typer.echo("Run `schematico new` to create one.")
|
|
337
|
+
return
|
|
338
|
+
|
|
339
|
+
mode_w = max(len(r[1]) for r in rows)
|
|
340
|
+
name_w = max(len(r[2]) for r in rows)
|
|
341
|
+
for marker, mode, name, path in rows:
|
|
342
|
+
typer.echo(f"{marker} [{mode:<{mode_w}}] {name:<{name_w}} ({path})")
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _walk_help(cmd, ctx, path: str) -> None:
|
|
346
|
+
import click
|
|
347
|
+
|
|
348
|
+
typer.echo("")
|
|
349
|
+
typer.echo("=" * 72)
|
|
350
|
+
typer.echo(path)
|
|
351
|
+
typer.echo("=" * 72)
|
|
352
|
+
typer.echo(cmd.get_help(ctx))
|
|
353
|
+
if isinstance(cmd, click.Group):
|
|
354
|
+
for name, sub in cmd.commands.items():
|
|
355
|
+
sub_ctx = click.Context(sub, info_name=name, parent=ctx)
|
|
356
|
+
_walk_help(sub, sub_ctx, f"{path} {name}")
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
@app.command("help")
|
|
360
|
+
def help_cmd() -> None:
|
|
361
|
+
"""Show help for every command and subcommand (full tree)."""
|
|
362
|
+
import click
|
|
363
|
+
from typer.main import get_command
|
|
364
|
+
|
|
365
|
+
cli = get_command(app)
|
|
366
|
+
ctx = click.Context(cli, info_name="schematico")
|
|
367
|
+
_walk_help(cli, ctx, "schematico")
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def main() -> None:
|
|
371
|
+
app()
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
if __name__ == "__main__":
|
|
375
|
+
main()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ProgressReporter:
|
|
7
|
+
def __init__(self, table: str) -> None:
|
|
8
|
+
self._table = table
|
|
9
|
+
self._last_len = 0
|
|
10
|
+
print(
|
|
11
|
+
f"Generating data for table: '{table}'...",
|
|
12
|
+
file=sys.stderr,
|
|
13
|
+
flush=True,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
def update(self, found: int, total: int, event: str) -> None:
|
|
17
|
+
if event == "duplicate":
|
|
18
|
+
msg = f"\r Found duplicate — retrying... "
|
|
19
|
+
else:
|
|
20
|
+
msg = f"\r {found} of {total} entries found "
|
|
21
|
+
|
|
22
|
+
print(msg, end="", file=sys.stderr, flush=True)
|
|
23
|
+
self._last_len = len(msg)
|
|
24
|
+
|
|
25
|
+
def done(self, count: int) -> None:
|
|
26
|
+
print(
|
|
27
|
+
f"\r Found data — {count} records generated. ",
|
|
28
|
+
file=sys.stderr,
|
|
29
|
+
flush=True,
|
|
30
|
+
)
|
|
31
|
+
print("", file=sys.stderr)
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import tomllib
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
|
|
8
|
+
import tomli_w
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
10
|
+
|
|
11
|
+
Mode = Literal["generate", "discover"]
|
|
12
|
+
MODES: tuple[Mode, ...] = ("generate", "discover")
|
|
13
|
+
|
|
14
|
+
CONFIG_DIRNAME = ".schematico"
|
|
15
|
+
STATE_FILENAME = "state.toml"
|
|
16
|
+
DEFAULT_ENV_KEY = "PYDANTIC_AI_GATEWAY_API_KEY"
|
|
17
|
+
DEFAULT_COUNT = 25
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ProjectConfig(BaseModel):
|
|
21
|
+
model_config = ConfigDict(populate_by_name=True, protected_namespaces=())
|
|
22
|
+
|
|
23
|
+
name: str
|
|
24
|
+
mode: Mode
|
|
25
|
+
model: str = ""
|
|
26
|
+
env_key: str = DEFAULT_ENV_KEY
|
|
27
|
+
base_url: str = ""
|
|
28
|
+
output_path: str = f"./{CONFIG_DIRNAME}/output"
|
|
29
|
+
count: int = DEFAULT_COUNT
|
|
30
|
+
logfire_token: str = ""
|
|
31
|
+
schema_path: str = ""
|
|
32
|
+
record_schema: dict[str, Any] = Field(default_factory=dict, alias="schema")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def config_dir(cwd: Path | None = None) -> Path:
|
|
36
|
+
base = cwd if cwd is not None else Path.cwd()
|
|
37
|
+
return base / CONFIG_DIRNAME
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def ensure_config_dir(cwd: Path | None = None) -> Path:
|
|
41
|
+
d = config_dir(cwd)
|
|
42
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
return d
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def config_filename(name: str, mode: Mode) -> str:
|
|
47
|
+
return f"{name}.{mode}.toml"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def config_path(name: str, mode: Mode, cwd: Path | None = None) -> Path:
|
|
51
|
+
return config_dir(cwd) / config_filename(name, mode)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def state_path(cwd: Path | None = None) -> Path:
|
|
55
|
+
return config_dir(cwd) / STATE_FILENAME
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def load_state(cwd: Path | None = None) -> dict[str, str]:
|
|
59
|
+
p = state_path(cwd)
|
|
60
|
+
if not p.exists():
|
|
61
|
+
return {}
|
|
62
|
+
with p.open("rb") as f:
|
|
63
|
+
return tomllib.load(f)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def save_state(state: dict[str, str], cwd: Path | None = None) -> None:
|
|
67
|
+
ensure_config_dir(cwd)
|
|
68
|
+
p = state_path(cwd)
|
|
69
|
+
with p.open("wb") as f:
|
|
70
|
+
tomli_w.dump(state, f)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_default(mode: Mode, cwd: Path | None = None) -> str | None:
|
|
74
|
+
return load_state(cwd).get(f"{mode}_default")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def set_default(mode: Mode, name: str, cwd: Path | None = None) -> None:
|
|
78
|
+
state = load_state(cwd)
|
|
79
|
+
state[f"{mode}_default"] = name
|
|
80
|
+
save_state(state, cwd)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def clear_default(mode: Mode, cwd: Path | None = None) -> bool:
|
|
84
|
+
state = load_state(cwd)
|
|
85
|
+
key = f"{mode}_default"
|
|
86
|
+
if key not in state:
|
|
87
|
+
return False
|
|
88
|
+
del state[key]
|
|
89
|
+
save_state(state, cwd)
|
|
90
|
+
return True
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def delete_project(name: str, mode: Mode, cwd: Path | None = None) -> bool:
|
|
94
|
+
"""Delete the config file and clear the mode default if it pointed here.
|
|
95
|
+
|
|
96
|
+
Returns True if the deleted project was the current default for its mode.
|
|
97
|
+
Raises FileNotFoundError if no such config exists.
|
|
98
|
+
"""
|
|
99
|
+
p = config_path(name, mode, cwd)
|
|
100
|
+
if not p.exists():
|
|
101
|
+
raise FileNotFoundError(p)
|
|
102
|
+
p.unlink()
|
|
103
|
+
if get_default(mode, cwd) == name:
|
|
104
|
+
clear_default(mode, cwd)
|
|
105
|
+
return True
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def find_projects_by_name(name: str, cwd: Path | None = None) -> list[Mode]:
|
|
110
|
+
"""Return modes that have a project with the given name."""
|
|
111
|
+
return [m for m in MODES if config_path(name, m, cwd).exists()]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def load_project(path: Path) -> ProjectConfig:
|
|
115
|
+
with path.open("rb") as f:
|
|
116
|
+
raw = tomllib.load(f)
|
|
117
|
+
return ProjectConfig(**raw)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def save_project(config: ProjectConfig, cwd: Path | None = None) -> Path:
|
|
121
|
+
ensure_config_dir(cwd)
|
|
122
|
+
p = config_path(config.name, config.mode, cwd)
|
|
123
|
+
data = config.model_dump(by_alias=True)
|
|
124
|
+
with p.open("wb") as f:
|
|
125
|
+
tomli_w.dump(data, f)
|
|
126
|
+
return p
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def list_projects(mode: Mode | None = None, cwd: Path | None = None) -> list[Path]:
|
|
130
|
+
d = config_dir(cwd)
|
|
131
|
+
if not d.exists():
|
|
132
|
+
return []
|
|
133
|
+
suffix = f".{mode}.toml" if mode else ".toml"
|
|
134
|
+
return sorted(
|
|
135
|
+
p for p in d.iterdir() if p.name.endswith(suffix) and p.name != STATE_FILENAME
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def parse_config_filename(filename: str) -> tuple[str, Mode] | None:
|
|
140
|
+
m = re.fullmatch(r"(.+)\.(generate|discover)\.toml", filename)
|
|
141
|
+
if not m:
|
|
142
|
+
return None
|
|
143
|
+
return m.group(1), m.group(2) # type: ignore[return-value]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def next_available_name(base: str, mode: Mode, cwd: Path | None = None) -> str:
|
|
147
|
+
if not config_path(base, mode, cwd).exists():
|
|
148
|
+
return base
|
|
149
|
+
n = 2
|
|
150
|
+
while config_path(f"{base}_{n}", mode, cwd).exists():
|
|
151
|
+
n += 1
|
|
152
|
+
return f"{base}_{n}"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class ProjectNotFoundError(Exception):
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def resolve_active_project(
|
|
160
|
+
mode: Mode,
|
|
161
|
+
name_override: str | None = None,
|
|
162
|
+
cwd: Path | None = None,
|
|
163
|
+
) -> ProjectConfig:
|
|
164
|
+
name = name_override or get_default(mode, cwd)
|
|
165
|
+
if not name:
|
|
166
|
+
raise ProjectNotFoundError(
|
|
167
|
+
f"No active {mode} project. Run `schematico new` to create one, "
|
|
168
|
+
f"or `schematico {mode} use <name>` to select an existing config."
|
|
169
|
+
)
|
|
170
|
+
p = config_path(name, mode, cwd)
|
|
171
|
+
if not p.exists():
|
|
172
|
+
raise ProjectNotFoundError(f"Config '{p.name}' not found in {config_dir(cwd)}.")
|
|
173
|
+
return load_project(p)
|