argus-forge 0.1.1__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.
- argus_forge/__init__.py +16 -0
- argus_forge/_version.py +24 -0
- argus_forge/cli.py +374 -0
- argus_forge/core.py +302 -0
- argus_forge/emitters/__init__.py +43 -0
- argus_forge/emitters/base.py +147 -0
- argus_forge/emitters/diffusers.py +98 -0
- argus_forge/emitters/kohya.py +133 -0
- argus_forge/emitters/onetrainer.py +98 -0
- argus_forge/heuristics.py +131 -0
- argus_forge/manifest.py +242 -0
- argus_forge/models.py +366 -0
- argus_forge/py.typed +1 -0
- argus_forge/runner.py +194 -0
- argus_forge/server/__init__.py +25 -0
- argus_forge/server/app.py +471 -0
- argus_forge/server/jobs.py +341 -0
- argus_forge-0.1.1.dist-info/METADATA +247 -0
- argus_forge-0.1.1.dist-info/RECORD +22 -0
- argus_forge-0.1.1.dist-info/WHEEL +4 -0
- argus_forge-0.1.1.dist-info/entry_points.txt +2 -0
- argus_forge-0.1.1.dist-info/licenses/LICENSE +21 -0
argus_forge/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""argus-forge — Training bridge: turn curated dataset exports into ready-to-run LoRA training configs (kohya_ss / OneTrainer / diffusers)"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
# Written by hatch-vcs at build time (see pyproject [tool.hatch.build.hooks.vcs]).
|
|
7
|
+
from argus_forge._version import __version__
|
|
8
|
+
except ImportError: # running from a source checkout that hasn't been built
|
|
9
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
__version__ = version("argus-forge")
|
|
13
|
+
except PackageNotFoundError:
|
|
14
|
+
__version__ = "0.0.0+unknown"
|
|
15
|
+
|
|
16
|
+
__all__ = ["__version__"]
|
argus_forge/_version.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.1.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 1)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
argus_forge/cli.py
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"""argus-forge CLI — ``config``, ``run``, ``inspect``, ``trainers``, ``schema``, ``serve``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import structlog
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
import typer
|
|
15
|
+
from typer import Argument, Option
|
|
16
|
+
except ImportError as _exc: # pragma: no cover
|
|
17
|
+
print("CLI requires: pip install argus-forge[cli]", file=sys.stderr)
|
|
18
|
+
raise SystemExit(1) from _exc
|
|
19
|
+
|
|
20
|
+
app = typer.Typer(
|
|
21
|
+
name="argus-forge",
|
|
22
|
+
help="Training bridge: turn curated dataset exports into ready-to-run LoRA training configs.",
|
|
23
|
+
no_args_is_help=True,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@app.callback()
|
|
28
|
+
def _cli(verbose: bool = Option(False, "--verbose", "-v", help="Show info/debug logs")) -> None:
|
|
29
|
+
"""Keep stdout clean for --json output; ``serve`` re-enables info logs."""
|
|
30
|
+
level = logging.DEBUG if verbose else logging.WARNING
|
|
31
|
+
structlog.configure(wrapper_class=structlog.make_filtering_bound_logger(level))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@app.command()
|
|
35
|
+
def config(
|
|
36
|
+
export_dir: Path = Argument(..., help="Curator export dir (images + manifest.jsonl + .txt sidecars)"),
|
|
37
|
+
trainer: str = Option("kohya", "--trainer", "-t", help="kohya | onetrainer | diffusers"),
|
|
38
|
+
base_model: str | None = Option(None, "--base-model", help="Checkpoint/repo (default: manifest, else SDXL base)"),
|
|
39
|
+
trigger: str | None = Option(None, "--trigger", help="Concept token (default: slug of the export dir name)"),
|
|
40
|
+
output_name: str | None = Option(None, "--output-name", help="LoRA filename stem (default: <dir>-lora)"),
|
|
41
|
+
category: str | None = Option(
|
|
42
|
+
None, "--category", help="Override manifest category: identity|wardrobe|pose_composition|setting"
|
|
43
|
+
),
|
|
44
|
+
repeats: int | None = Option(None, "--repeats", help="Override per-image repeats"),
|
|
45
|
+
epochs: int | None = Option(None, "--epochs", help="Override epochs"),
|
|
46
|
+
network_dim: int | None = Option(None, "--network-dim", help="Override LoRA rank"),
|
|
47
|
+
network_alpha: int | None = Option(None, "--network-alpha", help="Override LoRA alpha"),
|
|
48
|
+
unet_lr: float | None = Option(None, "--unet-lr", help="Override UNet learning rate"),
|
|
49
|
+
text_encoder_lr: float | None = Option(None, "--text-encoder-lr", help="Override text-encoder learning rate"),
|
|
50
|
+
optimizer: str | None = Option(None, "--optimizer", help="Override optimizer (kohya name, e.g. AdamW8bit)"),
|
|
51
|
+
scheduler: str | None = Option(None, "--scheduler", help="Override LR scheduler"),
|
|
52
|
+
resolution: int | None = Option(None, "--resolution", help="Override training resolution"),
|
|
53
|
+
batch_size: int | None = Option(None, "--batch-size", help="Override batch size"),
|
|
54
|
+
precision: str | None = Option(None, "--precision", help="Override mixed precision (bf16/fp16)"),
|
|
55
|
+
collect_captions: bool = Option(
|
|
56
|
+
True,
|
|
57
|
+
"--collect-captions/--no-collect-captions",
|
|
58
|
+
help="Copy .txt sidecars from manifest source paths into the export",
|
|
59
|
+
),
|
|
60
|
+
path_map: list[str] = Option(
|
|
61
|
+
[],
|
|
62
|
+
"--path-map",
|
|
63
|
+
help="Rewrite a path prefix in emitted configs, as CONTAINER=HOST (one entry per flag; also FORGE_PATH_MAP env)",
|
|
64
|
+
),
|
|
65
|
+
dry_run: bool = Option(False, "--dry-run", help="Print rendered files without writing anything"),
|
|
66
|
+
as_json: bool = Option(False, "--json", help="Print the full ForgeResult as JSON"),
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Emit a ready-to-run training config for a curated export."""
|
|
69
|
+
from argus_forge.core import forge_config, path_map_entry
|
|
70
|
+
from argus_forge.emitters.base import map_path
|
|
71
|
+
from argus_forge.models import ForgeError, ForgeRequest, ParamOverrides
|
|
72
|
+
|
|
73
|
+
overrides = ParamOverrides(
|
|
74
|
+
repeats=repeats,
|
|
75
|
+
epochs=epochs,
|
|
76
|
+
network_dim=network_dim,
|
|
77
|
+
network_alpha=network_alpha,
|
|
78
|
+
unet_lr=unet_lr,
|
|
79
|
+
text_encoder_lr=text_encoder_lr,
|
|
80
|
+
optimizer=optimizer,
|
|
81
|
+
scheduler=scheduler,
|
|
82
|
+
resolution=resolution,
|
|
83
|
+
batch_size=batch_size,
|
|
84
|
+
precision=precision,
|
|
85
|
+
)
|
|
86
|
+
try:
|
|
87
|
+
req = ForgeRequest(
|
|
88
|
+
export_dir=str(export_dir),
|
|
89
|
+
trainer=trainer, # type: ignore[arg-type] (pydantic validates the literal)
|
|
90
|
+
base_model=base_model,
|
|
91
|
+
trigger=trigger,
|
|
92
|
+
output_name=output_name,
|
|
93
|
+
category=category, # type: ignore[arg-type]
|
|
94
|
+
overrides=overrides,
|
|
95
|
+
collect_captions=collect_captions,
|
|
96
|
+
# One entry per flag, parsed individually — paths may contain commas.
|
|
97
|
+
path_map=dict(path_map_entry(entry, source="--path-map") for entry in path_map),
|
|
98
|
+
dry_run=dry_run,
|
|
99
|
+
)
|
|
100
|
+
result = forge_config(req)
|
|
101
|
+
except (ForgeError, ValueError) as exc:
|
|
102
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
103
|
+
raise typer.Exit(1) from exc
|
|
104
|
+
|
|
105
|
+
if as_json:
|
|
106
|
+
typer.echo(result.model_dump_json(indent=2))
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
p = result.params
|
|
110
|
+
typer.echo(f"Forged {result.trainer} config for {result.export_dir}")
|
|
111
|
+
typer.echo(
|
|
112
|
+
f" {p.images} images x {p.repeats} repeats x {p.epochs} epochs "
|
|
113
|
+
f"= {p.total_steps} samples ({p.optimizer_steps} steps @ batch {p.batch_size})"
|
|
114
|
+
)
|
|
115
|
+
typer.echo(f" dim/alpha {p.network_dim}/{p.network_alpha} · lr {p.unet_lr}/{p.text_encoder_lr} · {p.precision}")
|
|
116
|
+
typer.echo(f" base model: {result.base_model}")
|
|
117
|
+
typer.echo(f" trigger: {result.trigger}")
|
|
118
|
+
if result.captions_collected:
|
|
119
|
+
typer.echo(f" captions collected from sources: {result.captions_collected}")
|
|
120
|
+
for w in result.warnings:
|
|
121
|
+
typer.echo(f" ! {w}")
|
|
122
|
+
typer.echo("")
|
|
123
|
+
if dry_run:
|
|
124
|
+
for f in result.files:
|
|
125
|
+
typer.echo(f"----- {f.name} -----")
|
|
126
|
+
typer.echo(f.content)
|
|
127
|
+
else:
|
|
128
|
+
for f in result.files:
|
|
129
|
+
typer.echo(f" wrote {f.path}")
|
|
130
|
+
run_hint = next((f.path for f in result.files if f.name.endswith("train.sh")), None)
|
|
131
|
+
if run_hint:
|
|
132
|
+
# The next step runs where the *mapped* paths live, so hint with those
|
|
133
|
+
# even though the "wrote" lines above are forge's own (real) locations.
|
|
134
|
+
from argus_forge.core import resolve_path_map
|
|
135
|
+
|
|
136
|
+
effective_map = resolve_path_map(req.path_map)
|
|
137
|
+
typer.echo(
|
|
138
|
+
f"\nNext: review {map_path(result.out_dir, effective_map)}/README.md, "
|
|
139
|
+
f"then run {map_path(run_hint, effective_map)}"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _run_exit_status(returncode: int | None, error_seen: bool) -> int:
|
|
144
|
+
"""The CLI's own exit code for a run: the trainer's return code (128+N for a
|
|
145
|
+
signal death, per shell convention), or 1 if the run errored without a clean
|
|
146
|
+
exit. Keeps a signal-killed or errored run from ever reporting success."""
|
|
147
|
+
if returncode is None:
|
|
148
|
+
return 1 if error_seen else 0
|
|
149
|
+
if returncode < 0: # killed by signal -N
|
|
150
|
+
return 128 + (-returncode)
|
|
151
|
+
return returncode or (1 if error_seen else 0)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@app.command()
|
|
155
|
+
def run(
|
|
156
|
+
export_dir: Path = Argument(..., help="Forged export dir (contains forge/<trainer>/train.sh)"),
|
|
157
|
+
trainer: str = Option("kohya", "--trainer", "-t", help="kohya | diffusers (onetrainer has no train.sh)"),
|
|
158
|
+
env: list[str] = Option(
|
|
159
|
+
[],
|
|
160
|
+
"--env",
|
|
161
|
+
help="Extra env for the trainer as KEY=VALUE (e.g. SD_SCRIPTS_DIR=~/kohya-ss/sd-scripts); one per flag",
|
|
162
|
+
),
|
|
163
|
+
dry_run: bool = Option(False, "--dry-run", help="Print the command that would run, without executing it"),
|
|
164
|
+
as_json: bool = Option(False, "--json", help="Stream raw NDJSON RunEvents instead of human-readable output"),
|
|
165
|
+
) -> None:
|
|
166
|
+
"""Run a forged training config: shell out to the trainer, streaming progress."""
|
|
167
|
+
from argus_forge.models import ForgeError, RunRequest
|
|
168
|
+
from argus_forge.runner import astream_run
|
|
169
|
+
|
|
170
|
+
env_map: dict[str, str] = {}
|
|
171
|
+
for item in env:
|
|
172
|
+
key, sep, val = item.partition("=")
|
|
173
|
+
if not sep or not key:
|
|
174
|
+
typer.echo(f"Error: --env expects KEY=VALUE, got {item!r}", err=True)
|
|
175
|
+
raise typer.Exit(1)
|
|
176
|
+
env_map[key] = val
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
req = RunRequest(
|
|
180
|
+
export_dir=str(export_dir),
|
|
181
|
+
trainer=trainer, # type: ignore[arg-type] (pydantic validates the literal)
|
|
182
|
+
env=env_map,
|
|
183
|
+
dry_run=dry_run,
|
|
184
|
+
)
|
|
185
|
+
except ValueError as exc: # pydantic ValidationError, e.g. an unknown --trainer
|
|
186
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
187
|
+
raise typer.Exit(1) from exc
|
|
188
|
+
|
|
189
|
+
async def drive() -> int:
|
|
190
|
+
returncode: int | None = None
|
|
191
|
+
error_seen = False
|
|
192
|
+
async for ev in astream_run(req):
|
|
193
|
+
if as_json:
|
|
194
|
+
typer.echo(ev.model_dump_json())
|
|
195
|
+
continue
|
|
196
|
+
if ev.type == "start":
|
|
197
|
+
typer.echo(f"▶ run {ev.run_id}: {' '.join(ev.command or [])} (cwd {ev.cwd})")
|
|
198
|
+
if dry_run:
|
|
199
|
+
typer.echo(" dry run — not executing")
|
|
200
|
+
elif ev.type == "log":
|
|
201
|
+
typer.echo(ev.message or "")
|
|
202
|
+
elif ev.type == "exit":
|
|
203
|
+
returncode = ev.returncode
|
|
204
|
+
typer.echo(f"{'✔' if returncode == 0 else '✗'} run {ev.run_id} finished (exit {returncode})")
|
|
205
|
+
elif ev.type == "error":
|
|
206
|
+
error_seen = True
|
|
207
|
+
typer.echo(f"Error: {ev.message}", err=True)
|
|
208
|
+
return _run_exit_status(returncode, error_seen)
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
exit_code = asyncio.run(drive())
|
|
212
|
+
except ForgeError as exc:
|
|
213
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
214
|
+
raise typer.Exit(1) from exc
|
|
215
|
+
raise typer.Exit(exit_code)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@app.command()
|
|
219
|
+
def inspect(
|
|
220
|
+
export_dir: Path = Argument(..., help="Curator export dir to inspect"),
|
|
221
|
+
category: str | None = Option(None, "--category", help="Override manifest category"),
|
|
222
|
+
as_json: bool = Option(False, "--json", help="Print DatasetInfo as JSON"),
|
|
223
|
+
) -> None:
|
|
224
|
+
"""Summarise an export dir: images, captions, manifest, suggested params."""
|
|
225
|
+
from argus_forge.manifest import inspect_export
|
|
226
|
+
from argus_forge.models import ForgeError
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
info, _ = inspect_export(export_dir, category=category) # type: ignore[arg-type]
|
|
230
|
+
except ForgeError as exc:
|
|
231
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
232
|
+
raise typer.Exit(1) from exc
|
|
233
|
+
|
|
234
|
+
if as_json:
|
|
235
|
+
typer.echo(info.model_dump_json(indent=2))
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
typer.echo(f"Export: {info.export_dir}")
|
|
239
|
+
typer.echo(f" images: {info.image_count} ({info.caption_count} captioned)")
|
|
240
|
+
if info.manifest_present:
|
|
241
|
+
typer.echo(f" manifest: v{info.manifest_version}, {info.manifest_rows} rows, {info.missing_from_disk} missing")
|
|
242
|
+
else:
|
|
243
|
+
typer.echo(" manifest: none (bare folder mode)")
|
|
244
|
+
tp = info.target_profile
|
|
245
|
+
typer.echo(f" profile: {tp.target_category} / {tp.target_style} / {tp.target_backend or '-'}")
|
|
246
|
+
typer.echo(f" size: [{info.size_hint.tone}] {info.size_hint.text}")
|
|
247
|
+
p = info.suggested
|
|
248
|
+
typer.echo(
|
|
249
|
+
f" suggest: {p.repeats} repeats x {p.epochs} epochs = {p.total_steps} samples, "
|
|
250
|
+
f"dim/alpha {p.network_dim}/{p.network_alpha}, lr {p.unet_lr}/{p.text_encoder_lr}"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@app.command()
|
|
255
|
+
def trainers() -> None:
|
|
256
|
+
"""List supported trainers and what forge emits for each."""
|
|
257
|
+
from argus_forge.emitters import TRAINER_INFO
|
|
258
|
+
|
|
259
|
+
for info in TRAINER_INFO.values():
|
|
260
|
+
typer.echo(f"{info.id:12s} {info.label}")
|
|
261
|
+
typer.echo(f"{'':12s} files: {', '.join(info.files)}")
|
|
262
|
+
typer.echo(f"{'':12s} {info.notes}")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
DEFAULT_SCHEMA_PATH = Path("schema/forge-wire.schema.json")
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@app.command()
|
|
269
|
+
def schema(
|
|
270
|
+
output: Path = Option(DEFAULT_SCHEMA_PATH, "--output", "-o", help="Where to write the JSON Schema"),
|
|
271
|
+
check: bool = Option(False, "--check", help="Exit non-zero if the committed schema is stale (for CI)"),
|
|
272
|
+
) -> None:
|
|
273
|
+
"""Emit the wire-contract JSON Schema consumers codegen against."""
|
|
274
|
+
import json
|
|
275
|
+
|
|
276
|
+
from argus_forge.models import wire_schema
|
|
277
|
+
|
|
278
|
+
rendered = json.dumps(wire_schema(), indent=2, sort_keys=True) + "\n"
|
|
279
|
+
|
|
280
|
+
if check:
|
|
281
|
+
existing = output.read_text(encoding="utf-8") if output.exists() else ""
|
|
282
|
+
if existing != rendered:
|
|
283
|
+
typer.echo(f"{output} is stale — run `argus-forge schema` and commit the result.", err=True)
|
|
284
|
+
raise typer.Exit(1)
|
|
285
|
+
typer.echo(f"{output} is up to date.")
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
289
|
+
output.write_text(rendered, encoding="utf-8")
|
|
290
|
+
typer.echo(f"Wrote wire schema -> {output}")
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@app.command()
|
|
294
|
+
def serve(
|
|
295
|
+
port: int = Option(8103, "--port", "-p", help="Port to listen on"),
|
|
296
|
+
host: str = Option("0.0.0.0", "--host", help="Host to bind to"),
|
|
297
|
+
cors: bool = Option(False, "--cors", help="Enable CORS for the localhost:3000 studio frontend"),
|
|
298
|
+
cors_origin: list[str] = Option(
|
|
299
|
+
[], "--cors-origin", help="Allowed CORS origin (repeatable; implies CORS; or FORGE_CORS_ORIGINS)"
|
|
300
|
+
),
|
|
301
|
+
cors_any: bool = Option(
|
|
302
|
+
False,
|
|
303
|
+
"--cors-any",
|
|
304
|
+
help="Allow ANY origin, credential-less (public demos; pair with --cors-origin to let your own frontend write)",
|
|
305
|
+
),
|
|
306
|
+
export_root: str | None = Option(
|
|
307
|
+
None,
|
|
308
|
+
"--export-root",
|
|
309
|
+
help="Contain request export_dir paths under this directory (or ARGUS_FORGE_EXPORT_ROOT); required by the API",
|
|
310
|
+
),
|
|
311
|
+
no_run: bool = Option(
|
|
312
|
+
False,
|
|
313
|
+
"--no-run",
|
|
314
|
+
help="Demo-safe mode: /config renders (dry-run only) but /run is refused with 403. Or ARGUS_FORGE_READONLY=1.",
|
|
315
|
+
),
|
|
316
|
+
) -> None:
|
|
317
|
+
"""Start the argus-forge micro-server (FastAPI) on :8103."""
|
|
318
|
+
try:
|
|
319
|
+
import uvicorn
|
|
320
|
+
except ImportError as _exc: # pragma: no cover
|
|
321
|
+
typer.echo("Server requires: pip install argus-forge[server]", err=True)
|
|
322
|
+
raise typer.Exit(1) from _exc
|
|
323
|
+
|
|
324
|
+
from argus_forge.core import env_path_map
|
|
325
|
+
from argus_forge.models import ARGUS_ROOT_ENV, CORS_ORIGINS_ENV, LEGACY_ROOT_ENV, ForgeError
|
|
326
|
+
from argus_forge.server import create_app, env_readonly
|
|
327
|
+
|
|
328
|
+
structlog.configure(wrapper_class=structlog.make_filtering_bound_logger(logging.INFO))
|
|
329
|
+
try:
|
|
330
|
+
env_path_map() # fail fast at startup, not as a 400 on every /config
|
|
331
|
+
except ForgeError as exc:
|
|
332
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
333
|
+
raise typer.Exit(1) from exc
|
|
334
|
+
|
|
335
|
+
# These warnings must describe the app create_app will actually build, so
|
|
336
|
+
# they resolve the same env fallbacks it does — a warning that contradicts
|
|
337
|
+
# the running server sends the operator to debug a working config.
|
|
338
|
+
resolved_root = export_root or os.environ.get(ARGUS_ROOT_ENV) or os.environ.get(LEGACY_ROOT_ENV)
|
|
339
|
+
# Normalised the way create_app normalises them, so a value that only *looks*
|
|
340
|
+
# like an origin list (",", whitespace) is correctly reported as no CORS at
|
|
341
|
+
# all rather than silently suppressing the warning for a server that has none.
|
|
342
|
+
env_origins = [o for o in (os.environ.get(CORS_ORIGINS_ENV) or "").split(",") if o.strip().rstrip("/")]
|
|
343
|
+
cors_on = bool(cors or cors_origin or cors_any or env_origins)
|
|
344
|
+
readonly = no_run or env_readonly()
|
|
345
|
+
|
|
346
|
+
if not cors_on:
|
|
347
|
+
typer.echo(
|
|
348
|
+
"CORS is disabled — browser clients (e.g. the argus-studio frontend on :3000) "
|
|
349
|
+
f"will fail with 'Failed to fetch'; pass --cors (or set {CORS_ORIGINS_ENV}) to allow them.",
|
|
350
|
+
err=True,
|
|
351
|
+
)
|
|
352
|
+
if readonly:
|
|
353
|
+
typer.echo(
|
|
354
|
+
"Demo-safe mode: /run is refused with 403 and /config renders dry-run only (nothing is written).",
|
|
355
|
+
err=True,
|
|
356
|
+
)
|
|
357
|
+
if not resolved_root:
|
|
358
|
+
typer.echo(
|
|
359
|
+
"No export root — /inspect, /config and /run will refuse every request with a 400. "
|
|
360
|
+
f"Pass --export-root (or set {ARGUS_ROOT_ENV}) to the dir holding curator exports.",
|
|
361
|
+
err=True,
|
|
362
|
+
)
|
|
363
|
+
application = create_app(
|
|
364
|
+
cors=cors,
|
|
365
|
+
cors_origins=list(cors_origin) or None,
|
|
366
|
+
cors_allow_any=cors_any,
|
|
367
|
+
export_root=export_root,
|
|
368
|
+
allow_run=not readonly,
|
|
369
|
+
)
|
|
370
|
+
uvicorn.run(application, host=host, port=port)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
if __name__ == "__main__":
|
|
374
|
+
app()
|