austial-cli 0.2.0__tar.gz → 0.2.1__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.
- {austial_cli-0.2.0 → austial_cli-0.2.1}/.gitignore +3 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/PKG-INFO +1 -1
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/generators/__init__.py +6 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/generators/base.py +27 -0
- austial_cli-0.2.1/src/austial/cli/generators/component_gen.py +55 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/generators/module_gen.py +2 -2
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/generators/resource_gen.py +2 -2
- austial_cli-0.2.1/src/austial/cli/generators/workspace_gen.py +42 -0
- austial_cli-0.2.1/src/austial/cli/main.py +317 -0
- austial_cli-0.2.1/src/austial/cli/templates/app/app_module.py.j2 +6 -0
- austial_cli-0.2.1/src/austial/cli/templates/app/main.py.j2 +7 -0
- austial_cli-0.2.1/src/austial/cli/templates/class/class.py.j2 +2 -0
- austial_cli-0.2.1/src/austial/cli/templates/decorator/decorator.py.j2 +11 -0
- austial_cli-0.2.1/src/austial/cli/templates/filter/filter.py.j2 +11 -0
- austial_cli-0.2.1/src/austial/cli/templates/guard/guard.py.j2 +8 -0
- austial_cli-0.2.1/src/austial/cli/templates/interceptor/interceptor.py.j2 +12 -0
- austial_cli-0.2.1/src/austial/cli/templates/library/module.py.j2 +8 -0
- austial_cli-0.2.1/src/austial/cli/templates/library/service.py.j2 +6 -0
- austial_cli-0.2.1/src/austial/cli/templates/middleware/middleware.py.j2 +11 -0
- austial_cli-0.2.1/src/austial/cli/templates/pipe/pipe.py.j2 +9 -0
- austial_cli-0.2.1/src/austial/cli/tools.py +91 -0
- austial_cli-0.2.1/test/cli_tools_spec.py +134 -0
- austial_cli-0.2.1/test/generate_components_spec.py +150 -0
- austial_cli-0.2.0/src/austial/cli/main.py +0 -139
- {austial_cli-0.2.0 → austial_cli-0.2.1}/pyproject.toml +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/__init__.py +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/generators/agent_files.py +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/generators/app_module_patcher.py +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/generators/controller_gen.py +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/generators/new_project.py +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/generators/service_gen.py +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/naming.py +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/templates/controller/controller.py.j2 +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/templates/module/module.py.j2 +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/templates/project/agents_md.j2 +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/templates/project/app_controller.py.j2 +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/templates/project/app_module.py.j2 +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/templates/project/app_service.py.j2 +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/templates/project/env_example.j2 +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/templates/project/gitignore.j2 +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/templates/project/main.py.j2 +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/templates/project/modules/health/guards/api_key_guard.py.j2 +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/templates/project/modules/health/health_controller.py.j2 +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/templates/project/modules/health/health_dto.py.j2 +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/templates/project/modules/health/health_module.py.j2 +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/templates/project/modules/health/health_service.py.j2 +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/templates/project/pre_commit_config.yaml.j2 +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/templates/project/pyproject.toml.j2 +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/templates/project/readme.j2 +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/templates/project/tests/e2e/app_e2e_spec.py.j2 +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/templates/project/tests/unit/health_service_spec.py.j2 +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/templates/resource/controller.py.j2 +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/templates/resource/create_dto.py.j2 +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/templates/resource/entity.py.j2 +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/templates/resource/module.py.j2 +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/templates/resource/service.py.j2 +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/templates/resource/update_dto.py.j2 +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/src/austial/cli/templates/service/service.py.j2 +0 -0
- {austial_cli-0.2.0 → austial_cli-0.2.1}/test/new_project_spec.py +0 -0
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
from austial.cli.generators.component_gen import COMPONENT_KINDS, generate_component
|
|
1
2
|
from austial.cli.generators.controller_gen import generate_controller
|
|
2
3
|
from austial.cli.generators.module_gen import generate_module
|
|
3
4
|
from austial.cli.generators.new_project import create_new_project
|
|
4
5
|
from austial.cli.generators.resource_gen import generate_resource
|
|
5
6
|
from austial.cli.generators.service_gen import generate_service
|
|
7
|
+
from austial.cli.generators.workspace_gen import generate_app, generate_library
|
|
6
8
|
|
|
7
9
|
__all__ = [
|
|
10
|
+
"COMPONENT_KINDS",
|
|
11
|
+
"generate_app",
|
|
12
|
+
"generate_component",
|
|
13
|
+
"generate_library",
|
|
8
14
|
"create_new_project",
|
|
9
15
|
"generate_controller",
|
|
10
16
|
"generate_module",
|
|
@@ -3,6 +3,9 @@ discovery, mirroring what the ``nest`` CLI's schematics engine does."""
|
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
+
from collections.abc import Iterator
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
from contextvars import ContextVar
|
|
6
9
|
from pathlib import Path
|
|
7
10
|
from typing import Any
|
|
8
11
|
|
|
@@ -23,12 +26,36 @@ def render(template_relpath: str, **context: Any) -> str:
|
|
|
23
26
|
return template.render(**context)
|
|
24
27
|
|
|
25
28
|
|
|
29
|
+
_dry_run: ContextVar[list[Path] | None] = ContextVar("austial_dry_run", default=None)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@contextmanager
|
|
33
|
+
def dry_run() -> Iterator[list[Path]]:
|
|
34
|
+
"""Inside the block nothing is written; the paths that *would* be created are collected instead
|
|
35
|
+
(``austial generate ... --dry-run``)."""
|
|
36
|
+
created: list[Path] = []
|
|
37
|
+
token = _dry_run.set(created)
|
|
38
|
+
try:
|
|
39
|
+
yield created
|
|
40
|
+
finally:
|
|
41
|
+
_dry_run.reset(token)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def is_dry_run() -> bool:
|
|
45
|
+
return _dry_run.get() is not None
|
|
46
|
+
|
|
47
|
+
|
|
26
48
|
def write_file(path: Path, content: str, *, force: bool = False) -> bool:
|
|
27
49
|
"""Writes `content` to `path`, creating parent directories as needed.
|
|
28
50
|
Returns False (and leaves the file untouched) if it already exists and
|
|
29
51
|
`force` wasn't given -- mirrors `nest g`'s "already exists" guard."""
|
|
30
52
|
if path.exists() and not force:
|
|
31
53
|
return False
|
|
54
|
+
collector = _dry_run.get()
|
|
55
|
+
if collector is not None:
|
|
56
|
+
if path.name != "__init__.py": # package markers are noise in the report
|
|
57
|
+
collector.append(path)
|
|
58
|
+
return True
|
|
32
59
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
33
60
|
path.write_text(content)
|
|
34
61
|
return True
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""``austial generate guard|pipe|interceptor|filter|middleware|decorator|class <name>`` -- mirrors the
|
|
2
|
+
same ``nest g`` schematics.
|
|
3
|
+
|
|
4
|
+
``<name>`` may carry a path: ``auth/api-key`` creates ``src/modules/auth/api_key/api_key_guard.py``
|
|
5
|
+
(``--flat`` drops the innermost folder: ``src/modules/auth/api_key_guard.py``).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from austial.cli.generators.base import find_project_root, render, write_file
|
|
13
|
+
from austial.cli.naming import to_kebab_case, to_pascal_case, to_snake_case
|
|
14
|
+
|
|
15
|
+
# kind -> (template, file suffix)
|
|
16
|
+
COMPONENT_KINDS: dict[str, tuple[str, str]] = {
|
|
17
|
+
"guard": ("guard/guard.py.j2", "guard"),
|
|
18
|
+
"pipe": ("pipe/pipe.py.j2", "pipe"),
|
|
19
|
+
"interceptor": ("interceptor/interceptor.py.j2", "interceptor"),
|
|
20
|
+
"filter": ("filter/filter.py.j2", "filter"),
|
|
21
|
+
"middleware": ("middleware/middleware.py.j2", "middleware"),
|
|
22
|
+
"decorator": ("decorator/decorator.py.j2", "decorator"),
|
|
23
|
+
"class": ("class/class.py.j2", ""),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def generate_component(kind: str, name: str, *, flat: bool = False, project_root: Path | None = None) -> Path:
|
|
28
|
+
template, suffix = COMPONENT_KINDS[kind]
|
|
29
|
+
root = project_root or find_project_root()
|
|
30
|
+
|
|
31
|
+
*folders, leaf = [part for part in name.replace("\\", "/").split("/") if part]
|
|
32
|
+
snake_name = to_snake_case(leaf)
|
|
33
|
+
class_name = to_pascal_case(leaf)
|
|
34
|
+
ctx = {
|
|
35
|
+
"snake_name": snake_name,
|
|
36
|
+
"class_name": class_name,
|
|
37
|
+
"kebab_name": to_kebab_case(leaf),
|
|
38
|
+
"constant_name": snake_name.upper(),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
base = root / "src" / "modules"
|
|
42
|
+
directory = base.joinpath(*(to_snake_case(f) for f in folders))
|
|
43
|
+
if not flat:
|
|
44
|
+
directory = directory / snake_name
|
|
45
|
+
|
|
46
|
+
# Every folder between src/modules and the file is a package.
|
|
47
|
+
for folder in (directory, *directory.parents):
|
|
48
|
+
if folder == base.parent or folder == base.parent.parent:
|
|
49
|
+
break
|
|
50
|
+
write_file(folder / "__init__.py", "")
|
|
51
|
+
|
|
52
|
+
file_name = f"{snake_name}_{suffix}.py" if suffix else f"{snake_name}.py"
|
|
53
|
+
file_path = directory / file_name
|
|
54
|
+
write_file(file_path, render(template, **ctx))
|
|
55
|
+
return file_path
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
7
|
from austial.cli.generators.app_module_patcher import register_module
|
|
8
|
-
from austial.cli.generators.base import find_project_root, render, write_file
|
|
8
|
+
from austial.cli.generators.base import find_project_root, is_dry_run, render, write_file
|
|
9
9
|
from austial.cli.naming import to_kebab_case, to_pascal_case, to_snake_case
|
|
10
10
|
|
|
11
11
|
|
|
@@ -21,7 +21,7 @@ def generate_module(name: str, *, project_root: Path | None = None) -> Path:
|
|
|
21
21
|
write_file(file_path, render("module/module.py.j2", **ctx))
|
|
22
22
|
|
|
23
23
|
app_module_path = root / "src" / "app_module.py"
|
|
24
|
-
if app_module_path.exists():
|
|
24
|
+
if app_module_path.exists() and not is_dry_run():
|
|
25
25
|
register_module(
|
|
26
26
|
app_module_path,
|
|
27
27
|
f"{class_name}Module",
|
|
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
9
|
from austial.cli.generators.app_module_patcher import register_module
|
|
10
|
-
from austial.cli.generators.base import find_project_root, render, write_file
|
|
10
|
+
from austial.cli.generators.base import find_project_root, is_dry_run, render, write_file
|
|
11
11
|
from austial.cli.naming import to_kebab_case, to_pascal_case, to_snake_case
|
|
12
12
|
|
|
13
13
|
|
|
@@ -30,7 +30,7 @@ def generate_resource(name: str, *, project_root: Path | None = None) -> Path:
|
|
|
30
30
|
write_file(module_dir / f"{snake_name}_module.py", render("resource/module.py.j2", **ctx))
|
|
31
31
|
|
|
32
32
|
app_module_path = root / "src" / "app_module.py"
|
|
33
|
-
if app_module_path.exists():
|
|
33
|
+
if app_module_path.exists() and not is_dry_run():
|
|
34
34
|
register_module(
|
|
35
35
|
app_module_path,
|
|
36
36
|
f"{class_name}Module",
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""``austial generate library|app <name>`` -- mirrors Nest's monorepo ``libraries`` and ``applications``.
|
|
2
|
+
|
|
3
|
+
* ``library`` -> ``libs/<name>/`` : a module + service other apps import (``from libs.<name>... import``).
|
|
4
|
+
* ``app`` -> ``apps/<name>/`` : an additional application with its own ``main.py``, next to ``src/``.
|
|
5
|
+
|
|
6
|
+
Both live at the project root, which ``austial serve`` and pytest already have on the import path.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from austial.cli.generators.base import find_project_root, render, write_file
|
|
14
|
+
from austial.cli.naming import to_kebab_case, to_pascal_case, to_snake_case
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _context(name: str) -> dict[str, str]:
|
|
18
|
+
return {"snake_name": to_snake_case(name), "class_name": to_pascal_case(name), "kebab_name": to_kebab_case(name)}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def generate_library(name: str, *, project_root: Path | None = None) -> Path:
|
|
22
|
+
root = project_root or find_project_root()
|
|
23
|
+
ctx = _context(name)
|
|
24
|
+
folder = root / "libs" / ctx["snake_name"]
|
|
25
|
+
write_file(root / "libs" / "__init__.py", "")
|
|
26
|
+
write_file(folder / "__init__.py", "")
|
|
27
|
+
write_file(folder / f"{ctx['snake_name']}_service.py", render("library/service.py.j2", **ctx))
|
|
28
|
+
path = folder / f"{ctx['snake_name']}_module.py"
|
|
29
|
+
write_file(path, render("library/module.py.j2", **ctx))
|
|
30
|
+
return path
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def generate_app(name: str, *, project_root: Path | None = None) -> Path:
|
|
34
|
+
root = project_root or find_project_root()
|
|
35
|
+
ctx = _context(name)
|
|
36
|
+
folder = root / "apps" / ctx["snake_name"]
|
|
37
|
+
write_file(root / "apps" / "__init__.py", "")
|
|
38
|
+
write_file(folder / "__init__.py", "")
|
|
39
|
+
write_file(folder / "app_module.py", render("app/app_module.py.j2", **ctx))
|
|
40
|
+
path = folder / "main.py"
|
|
41
|
+
write_file(path, render("app/main.py.j2", **ctx))
|
|
42
|
+
return path
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""``austial`` -- the CLI, mirrors the ``nest`` command.
|
|
2
|
+
|
|
3
|
+
austial new my-app
|
|
4
|
+
austial generate module cats (alias: austial g module cats)
|
|
5
|
+
austial generate controller cats
|
|
6
|
+
austial generate service cats
|
|
7
|
+
austial generate resource cats
|
|
8
|
+
austial generate guard auth/api-key [--flat] [--dry-run]
|
|
9
|
+
(also: pipe, interceptor, filter, middleware, decorator, class)
|
|
10
|
+
austial info
|
|
11
|
+
austial serve
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import typer
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
|
|
21
|
+
from austial.cli.generators import (
|
|
22
|
+
COMPONENT_KINDS,
|
|
23
|
+
create_new_project,
|
|
24
|
+
generate_app,
|
|
25
|
+
generate_component,
|
|
26
|
+
generate_controller,
|
|
27
|
+
generate_library,
|
|
28
|
+
generate_module,
|
|
29
|
+
generate_resource,
|
|
30
|
+
generate_service,
|
|
31
|
+
)
|
|
32
|
+
from austial.cli.generators.base import ProjectNotFoundError, dry_run, find_project_root
|
|
33
|
+
|
|
34
|
+
console = Console()
|
|
35
|
+
app = typer.Typer(
|
|
36
|
+
name="austial",
|
|
37
|
+
help="Austial CLI -- scaffold and run NestJS-style Python projects, built on FastAPI.",
|
|
38
|
+
add_completion=False,
|
|
39
|
+
no_args_is_help=True,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
_SCHEMATICS = {
|
|
43
|
+
"module": generate_module,
|
|
44
|
+
"controller": generate_controller,
|
|
45
|
+
"service": generate_service,
|
|
46
|
+
"provider": generate_service, # Nest calls a service a "provider"; same schematic
|
|
47
|
+
"resource": generate_resource,
|
|
48
|
+
"library": generate_library,
|
|
49
|
+
"app": generate_app,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Schematics that accept `--flat` (the ones that create a single file rather than a feature folder).
|
|
53
|
+
_FLAT_SCHEMATICS = set(COMPONENT_KINDS)
|
|
54
|
+
|
|
55
|
+
# `nest g` short aliases.
|
|
56
|
+
_ALIASES = {
|
|
57
|
+
"mo": "module",
|
|
58
|
+
"co": "controller",
|
|
59
|
+
"s": "service",
|
|
60
|
+
"pr": "provider",
|
|
61
|
+
"r": "resource",
|
|
62
|
+
"gu": "guard",
|
|
63
|
+
"pi": "pipe",
|
|
64
|
+
"in": "interceptor",
|
|
65
|
+
"f": "filter",
|
|
66
|
+
"mi": "middleware",
|
|
67
|
+
"d": "decorator",
|
|
68
|
+
"cl": "class",
|
|
69
|
+
"lib": "library",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
_ALL_SCHEMATICS = sorted({*_SCHEMATICS, *COMPONENT_KINDS})
|
|
73
|
+
_SCHEMATIC_HELP = "One of: " + ", ".join(_ALL_SCHEMATICS) + " (or a short alias: " + ", ".join(sorted(_ALIASES)) + ")"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@app.command("new")
|
|
77
|
+
def new_project(
|
|
78
|
+
name: str = typer.Argument(..., help="Name of the new project/directory to create."),
|
|
79
|
+
directory: str | None = typer.Option(None, "--directory", "-d", help="Target directory (defaults to ./<name>)."),
|
|
80
|
+
skip_install: bool = typer.Option(False, "--skip-install", help="Don't run `uv sync` after scaffolding."),
|
|
81
|
+
skip_git: bool = typer.Option(False, "--skip-git", help="Don't run `git init` after scaffolding."),
|
|
82
|
+
skip_agents: bool = typer.Option(
|
|
83
|
+
False,
|
|
84
|
+
"--skip-agents",
|
|
85
|
+
help="Don't write AGENTS.md or the per-tool symlinks (CLAUDE.md, GEMINI.md, ...).",
|
|
86
|
+
),
|
|
87
|
+
link: str | None = typer.Option(
|
|
88
|
+
None,
|
|
89
|
+
"--link",
|
|
90
|
+
help="Path to a local Austial framework checkout to depend on via `uv`'s editable path sources "
|
|
91
|
+
"(useful before the framework is published to PyPI).",
|
|
92
|
+
),
|
|
93
|
+
) -> None:
|
|
94
|
+
"""Scaffold a brand-new Austial project -- mirrors `nest new <name>`."""
|
|
95
|
+
console.print(f"[bold green]CREATE[/bold green] {name}/ ...")
|
|
96
|
+
target = Path(directory).resolve() if directory else None
|
|
97
|
+
project_dir = create_new_project(
|
|
98
|
+
name,
|
|
99
|
+
directory=target,
|
|
100
|
+
austial_source_path=link,
|
|
101
|
+
skip_install=skip_install,
|
|
102
|
+
skip_git=skip_git,
|
|
103
|
+
skip_agents=skip_agents,
|
|
104
|
+
)
|
|
105
|
+
console.print(f"[bold green]\u2714[/bold green] Project created at [bold]{project_dir}[/bold]")
|
|
106
|
+
console.print("\nNext steps:")
|
|
107
|
+
console.print(f" cd {project_dir.name}")
|
|
108
|
+
if skip_install:
|
|
109
|
+
console.print(" uv sync")
|
|
110
|
+
console.print(" cp .env.example .env")
|
|
111
|
+
console.print(" uv run austial serve\n")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _generate(schematic: str, name: str, *, flat: bool = False, dry: bool = False) -> None:
|
|
115
|
+
schematic = _ALIASES.get(schematic.lower(), schematic.lower())
|
|
116
|
+
if schematic not in _ALL_SCHEMATICS:
|
|
117
|
+
console.print(
|
|
118
|
+
f"[bold red]Unknown schematic[/bold red] '{schematic}'. Choose one of: {', '.join(_ALL_SCHEMATICS)}."
|
|
119
|
+
)
|
|
120
|
+
raise typer.Exit(code=1)
|
|
121
|
+
if flat and schematic not in _FLAT_SCHEMATICS:
|
|
122
|
+
console.print(f"[bold red]\u2716[/bold red] --flat applies to: {', '.join(sorted(_FLAT_SCHEMATICS))}.")
|
|
123
|
+
raise typer.Exit(code=1)
|
|
124
|
+
|
|
125
|
+
def run() -> Path:
|
|
126
|
+
if schematic in COMPONENT_KINDS:
|
|
127
|
+
return generate_component(schematic, name, flat=flat)
|
|
128
|
+
return _SCHEMATICS[schematic](name)
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
if dry:
|
|
132
|
+
with dry_run() as created:
|
|
133
|
+
run()
|
|
134
|
+
for path in created:
|
|
135
|
+
console.print(f"[bold yellow]CREATE[/bold yellow] {path} [dim](dry run)[/dim]")
|
|
136
|
+
console.print("[dim]Dry run: nothing was written.[/dim]")
|
|
137
|
+
return
|
|
138
|
+
path = run()
|
|
139
|
+
except ProjectNotFoundError as exc:
|
|
140
|
+
console.print(f"[bold red]\u2716[/bold red] {exc}")
|
|
141
|
+
raise typer.Exit(code=1) from exc
|
|
142
|
+
console.print(f"[bold green]CREATE[/bold green] {path}")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
_FLAT_HELP = "Don't create a folder for the file (guard, pipe, interceptor, filter, middleware, decorator, class)."
|
|
146
|
+
_DRY_HELP = "Show what would be created without writing anything."
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@app.command("generate")
|
|
150
|
+
def generate(
|
|
151
|
+
schematic: str = typer.Argument(..., help=_SCHEMATIC_HELP),
|
|
152
|
+
name: str = typer.Argument(..., help="Name, optionally with a path: 'cats' or 'auth/api-key'."),
|
|
153
|
+
flat: bool = typer.Option(False, "--flat", help=_FLAT_HELP),
|
|
154
|
+
dry: bool = typer.Option(False, "--dry-run", "-d", help=_DRY_HELP),
|
|
155
|
+
) -> None:
|
|
156
|
+
"""Generate an artifact -- mirrors `nest generate`."""
|
|
157
|
+
_generate(schematic, name, flat=flat, dry=dry)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@app.command("g", hidden=True)
|
|
161
|
+
def generate_alias(
|
|
162
|
+
schematic: str = typer.Argument(..., help=_SCHEMATIC_HELP),
|
|
163
|
+
name: str = typer.Argument(..., help="Name, optionally with a path: 'cats' or 'auth/api-key'."),
|
|
164
|
+
flat: bool = typer.Option(False, "--flat", help=_FLAT_HELP),
|
|
165
|
+
dry: bool = typer.Option(False, "--dry-run", "-d", help=_DRY_HELP),
|
|
166
|
+
) -> None:
|
|
167
|
+
"""Alias for `generate` -- mirrors Nest CLI's `nest g`."""
|
|
168
|
+
_generate(schematic, name, flat=flat, dry=dry)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@app.command("info")
|
|
172
|
+
def info() -> None:
|
|
173
|
+
"""Prints Python, platform and installed Austial package versions -- mirrors `nest info`."""
|
|
174
|
+
import importlib.metadata as metadata
|
|
175
|
+
import platform
|
|
176
|
+
|
|
177
|
+
console.print(f"[bold]Python[/bold] {platform.python_version()} ({platform.system()} {platform.machine()})")
|
|
178
|
+
packages = sorted(
|
|
179
|
+
(dist.metadata["Name"], dist.version)
|
|
180
|
+
for dist in metadata.distributions()
|
|
181
|
+
if (dist.metadata["Name"] or "").lower().startswith("austial")
|
|
182
|
+
)
|
|
183
|
+
console.print("[bold]Austial packages[/bold]")
|
|
184
|
+
for name, version in packages:
|
|
185
|
+
console.print(f" {name:<22} {version}")
|
|
186
|
+
try:
|
|
187
|
+
console.print(f"[bold]Project[/bold] {find_project_root()}")
|
|
188
|
+
except ProjectNotFoundError:
|
|
189
|
+
console.print("[bold]Project[/bold] [dim](not inside an Austial project)[/dim]")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _project_root_or_exit() -> Path:
|
|
193
|
+
try:
|
|
194
|
+
return find_project_root()
|
|
195
|
+
except ProjectNotFoundError as exc:
|
|
196
|
+
console.print(f"[bold red]\u2716[/bold red] {exc}")
|
|
197
|
+
raise typer.Exit(code=1) from exc
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@app.command("serve")
|
|
201
|
+
def serve(
|
|
202
|
+
host: str = typer.Option("0.0.0.0", help="Bind host."),
|
|
203
|
+
port: int = typer.Option(8000, help="Bind port."),
|
|
204
|
+
reload: bool = typer.Option(True, help="Auto-reload on file changes (dev mode)."),
|
|
205
|
+
) -> None:
|
|
206
|
+
"""Runs the current project with auto-reload (mirrors `nest start --watch`)."""
|
|
207
|
+
_serve(host, port, reload)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@app.command("start")
|
|
211
|
+
def start(
|
|
212
|
+
host: str = typer.Option("0.0.0.0", help="Bind host."),
|
|
213
|
+
port: int = typer.Option(8000, help="Bind port."),
|
|
214
|
+
watch: bool = typer.Option(False, "--watch", "-w", help="Auto-reload on file changes."),
|
|
215
|
+
target: str = typer.Option("src.main:app", "--app", help="ASGI app to run, e.g. apps.admin.main:app."),
|
|
216
|
+
) -> None:
|
|
217
|
+
"""Runs the project (mirrors `nest start`; add --watch for reload, --app for an app in apps/)."""
|
|
218
|
+
_serve(host, port, watch, target)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@app.command("build")
|
|
222
|
+
def build() -> None:
|
|
223
|
+
"""Byte-compiles the project (src, libs, apps) and checks the module graph imports and scans cleanly."""
|
|
224
|
+
from austial.cli.tools import TargetError, build_project, load_target, module_graph_for
|
|
225
|
+
|
|
226
|
+
root = _project_root_or_exit()
|
|
227
|
+
failed = build_project(root)
|
|
228
|
+
if failed:
|
|
229
|
+
console.print(f"[bold red]\u2716[/bold red] Compilation failed in: {', '.join(failed)}")
|
|
230
|
+
raise typer.Exit(code=1)
|
|
231
|
+
try:
|
|
232
|
+
graph = module_graph_for(load_target("src.app_module:AppModule", root))
|
|
233
|
+
except TargetError as exc:
|
|
234
|
+
console.print(f"[bold red]\u2716[/bold red] {exc}")
|
|
235
|
+
raise typer.Exit(code=1) from exc
|
|
236
|
+
console.print(f"[bold green]\u2714[/bold green] Build OK: {len(graph.records)} modules scanned")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@app.command("add")
|
|
240
|
+
def add(
|
|
241
|
+
package: str = typer.Argument(..., help="Optional package, e.g. cache, events, websockets (see `austial add x`)."),
|
|
242
|
+
dry: bool = typer.Option(False, "--dry-run", "-d", help="Print the command without running it."),
|
|
243
|
+
) -> None:
|
|
244
|
+
"""Installs an optional Austial package into the project with uv (mirrors `nest add`)."""
|
|
245
|
+
import subprocess
|
|
246
|
+
|
|
247
|
+
from austial.cli.tools import TargetError, add_command
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
command = add_command(package)
|
|
251
|
+
except TargetError as exc:
|
|
252
|
+
console.print(f"[bold red]\u2716[/bold red] {exc}")
|
|
253
|
+
raise typer.Exit(code=1) from exc
|
|
254
|
+
console.print(f"[bold]$ {' '.join(command)}[/bold]")
|
|
255
|
+
if dry:
|
|
256
|
+
return
|
|
257
|
+
raise typer.Exit(code=subprocess.run(command, cwd=_project_root_or_exit()).returncode)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@app.command("graph")
|
|
261
|
+
def graph(
|
|
262
|
+
target: str = typer.Argument("src.app_module:AppModule", help="module.path:RootModule"),
|
|
263
|
+
fmt: str = typer.Option("mermaid", "--format", "-f", help="mermaid or json."),
|
|
264
|
+
) -> None:
|
|
265
|
+
"""Prints the module graph (imports, providers, controllers, exports), like Nest's devtools graph."""
|
|
266
|
+
import json
|
|
267
|
+
|
|
268
|
+
from austial.cli.tools import TargetError, load_target, module_graph_for
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
module_graph = module_graph_for(load_target(target, _project_root_or_exit()))
|
|
272
|
+
except TargetError as exc:
|
|
273
|
+
console.print(f"[bold red]\u2716[/bold red] {exc}")
|
|
274
|
+
raise typer.Exit(code=1) from exc
|
|
275
|
+
typer.echo(json.dumps(module_graph.to_dict(), indent=2) if fmt == "json" else module_graph.to_mermaid())
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@app.command("repl")
|
|
279
|
+
def repl(target: str = typer.Argument("src.app_module:AppModule", help="module.path:RootModule")) -> None:
|
|
280
|
+
"""Starts a Python REPL with the app booted: get(Token), run(coroutine). Mirrors Nest's REPL."""
|
|
281
|
+
import code
|
|
282
|
+
|
|
283
|
+
from austial.cli.tools import ReplSession, TargetError, load_target
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
session = ReplSession(load_target(target, _project_root_or_exit()))
|
|
287
|
+
except TargetError as exc:
|
|
288
|
+
console.print(f"[bold red]\u2716[/bold red] {exc}")
|
|
289
|
+
raise typer.Exit(code=1) from exc
|
|
290
|
+
try:
|
|
291
|
+
code.interact(banner=session.banner(), local=session.namespace)
|
|
292
|
+
finally:
|
|
293
|
+
session.close()
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _serve(host: str, port: int, reload: bool, target: str = "src.main:app") -> None:
|
|
297
|
+
import uvicorn
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
project_root = find_project_root()
|
|
301
|
+
except ProjectNotFoundError as exc:
|
|
302
|
+
console.print(f"[bold red]\u2716[/bold red] {exc}")
|
|
303
|
+
raise typer.Exit(code=1) from exc
|
|
304
|
+
|
|
305
|
+
import sys
|
|
306
|
+
|
|
307
|
+
sys.path.insert(0, str(project_root))
|
|
308
|
+
console.print(f"[bold green]\u25b6[/bold green] Serving {project_root.name} on http://{host}:{port}")
|
|
309
|
+
uvicorn.run(target, host=host, port=port, reload=reload, app_dir=str(project_root))
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def run() -> None:
|
|
313
|
+
app()
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
if __name__ == "__main__":
|
|
317
|
+
run()
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Entry point of the `{{ snake_name }}` app: `uvicorn apps.{{ snake_name }}.main:app` (from the repository root)."""
|
|
2
|
+
|
|
3
|
+
from austial import AustialFactory
|
|
4
|
+
|
|
5
|
+
from apps.{{ snake_name }}.app_module import {{ class_name }}AppModule
|
|
6
|
+
|
|
7
|
+
app = AustialFactory.create({{ class_name }}AppModule, title="{{ class_name }}")
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from austial.core.metadata import set_metadata
|
|
2
|
+
|
|
3
|
+
{{ constant_name }}_KEY = "{{ snake_name }}"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def {{ class_name }}(*values: str):
|
|
7
|
+
"""Marks a controller or handler: ``@{{ class_name }}("a", "b")``.
|
|
8
|
+
|
|
9
|
+
Read it back with ``Reflector.get({{ constant_name }}_KEY, context.get_handler())`` in a guard or interceptor.
|
|
10
|
+
"""
|
|
11
|
+
return set_metadata({{ constant_name }}_KEY, list(values))
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from austial import ArgumentsHost, Catch, ExceptionFilter, HttpException, Injectable
|
|
2
|
+
from starlette.responses import JSONResponse
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@Injectable()
|
|
6
|
+
@Catch(HttpException)
|
|
7
|
+
class {{ class_name }}Filter(ExceptionFilter):
|
|
8
|
+
async def catch(self, exception: HttpException, host: ArgumentsHost) -> JSONResponse:
|
|
9
|
+
request = host.get_request()
|
|
10
|
+
body = {**exception.get_response(), "path": request.url.path}
|
|
11
|
+
return JSONResponse(status_code=exception.get_status(), content=body)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from austial import CanActivate, ExecutionContext, Injectable
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@Injectable()
|
|
5
|
+
class {{ class_name }}Guard(CanActivate):
|
|
6
|
+
async def can_activate(self, context: ExecutionContext) -> bool:
|
|
7
|
+
request = context.get_request() # noqa: F841 - inspect headers, state, etc.
|
|
8
|
+
return True
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from austial import CallHandler, ExecutionContext, Injectable, NestInterceptor
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@Injectable()
|
|
7
|
+
class {{ class_name }}Interceptor(NestInterceptor):
|
|
8
|
+
async def intercept(self, context: ExecutionContext, next: CallHandler) -> Any:
|
|
9
|
+
# Runs before the handler...
|
|
10
|
+
result = await next.handle()
|
|
11
|
+
# ...and after it.
|
|
12
|
+
return result
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from collections.abc import Awaitable, Callable
|
|
2
|
+
|
|
3
|
+
from austial import Injectable, NestMiddleware
|
|
4
|
+
from starlette.requests import Request
|
|
5
|
+
from starlette.responses import Response
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@Injectable()
|
|
9
|
+
class {{ class_name }}Middleware(NestMiddleware):
|
|
10
|
+
async def use(self, request: Request, call_next: Callable[[], Awaitable[Response]]) -> Response:
|
|
11
|
+
return await call_next()
|