austial 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.
Files changed (91) hide show
  1. austial/__init__.py +136 -0
  2. austial/cli/__init__.py +0 -0
  3. austial/cli/generators/__init__.py +13 -0
  4. austial/cli/generators/app_module_patcher.py +61 -0
  5. austial/cli/generators/base.py +61 -0
  6. austial/cli/generators/controller_gen.py +21 -0
  7. austial/cli/generators/module_gen.py +30 -0
  8. austial/cli/generators/new_project.py +67 -0
  9. austial/cli/generators/resource_gen.py +39 -0
  10. austial/cli/generators/service_gen.py +21 -0
  11. austial/cli/main.py +133 -0
  12. austial/cli/naming.py +33 -0
  13. austial/cli/templates/controller/controller.py.j2 +8 -0
  14. austial/cli/templates/module/module.py.j2 +6 -0
  15. austial/cli/templates/project/app_controller.py.j2 +13 -0
  16. austial/cli/templates/project/app_module.py.j2 +19 -0
  17. austial/cli/templates/project/app_service.py.j2 +7 -0
  18. austial/cli/templates/project/env_example.j2 +2 -0
  19. austial/cli/templates/project/gitignore.j2 +11 -0
  20. austial/cli/templates/project/main.py.j2 +37 -0
  21. austial/cli/templates/project/modules/health/guards/api_key_guard.py.j2 +13 -0
  22. austial/cli/templates/project/modules/health/health_controller.py.j2 +20 -0
  23. austial/cli/templates/project/modules/health/health_dto.py.j2 +10 -0
  24. austial/cli/templates/project/modules/health/health_module.py.j2 +10 -0
  25. austial/cli/templates/project/modules/health/health_service.py.j2 +19 -0
  26. austial/cli/templates/project/pre_commit_config.yaml.j2 +36 -0
  27. austial/cli/templates/project/pyproject.toml.j2 +46 -0
  28. austial/cli/templates/project/readme.j2 +68 -0
  29. austial/cli/templates/project/tests/e2e/app_e2e_spec.py.j2 +38 -0
  30. austial/cli/templates/project/tests/unit/health_service_spec.py.j2 +35 -0
  31. austial/cli/templates/resource/controller.py.j2 +33 -0
  32. austial/cli/templates/resource/create_dto.py.j2 +5 -0
  33. austial/cli/templates/resource/entity.py.j2 +6 -0
  34. austial/cli/templates/resource/module.py.j2 +9 -0
  35. austial/cli/templates/resource/service.py.j2 +39 -0
  36. austial/cli/templates/resource/update_dto.py.j2 +5 -0
  37. austial/cli/templates/service/service.py.j2 +6 -0
  38. austial/common/__init__.py +106 -0
  39. austial/common/decorators/__init__.py +48 -0
  40. austial/common/decorators/controllers.py +109 -0
  41. austial/common/decorators/filters.py +31 -0
  42. austial/common/decorators/guards.py +20 -0
  43. austial/common/decorators/injectable.py +68 -0
  44. austial/common/decorators/interceptors.py +20 -0
  45. austial/common/decorators/modules.py +65 -0
  46. austial/common/decorators/params.py +50 -0
  47. austial/common/decorators/pipes.py +20 -0
  48. austial/common/enums.py +15 -0
  49. austial/common/exceptions/__init__.py +29 -0
  50. austial/common/exceptions/http_exceptions.py +109 -0
  51. austial/common/filters/__init__.py +4 -0
  52. austial/common/filters/all_exceptions_filter.py +31 -0
  53. austial/common/filters/base.py +28 -0
  54. austial/common/guards/__init__.py +3 -0
  55. austial/common/guards/base.py +39 -0
  56. austial/common/interceptors/__init__.py +4 -0
  57. austial/common/interceptors/base.py +30 -0
  58. austial/common/interceptors/transform_interceptor.py +19 -0
  59. austial/common/logger.py +56 -0
  60. austial/common/middleware/__init__.py +4 -0
  61. austial/common/middleware/base.py +60 -0
  62. austial/common/middleware/logging_middleware.py +24 -0
  63. austial/common/pipes/__init__.py +3 -0
  64. austial/common/pipes/validation_pipe.py +51 -0
  65. austial/config/__init__.py +4 -0
  66. austial/config/config_module.py +31 -0
  67. austial/config/config_service.py +22 -0
  68. austial/core/__init__.py +19 -0
  69. austial/core/app.py +155 -0
  70. austial/core/container.py +161 -0
  71. austial/core/dynamic_module.py +22 -0
  72. austial/core/exception_dispatch.py +23 -0
  73. austial/core/factory.py +43 -0
  74. austial/core/metadata.py +65 -0
  75. austial/core/module_scanner.py +62 -0
  76. austial/core/reflector.py +38 -0
  77. austial/core/router_builder.py +184 -0
  78. austial/database/__init__.py +3 -0
  79. austial/database/database_module.py +61 -0
  80. austial/terminus/__init__.py +14 -0
  81. austial/terminus/database_health_indicator.py +26 -0
  82. austial/terminus/health_check_service.py +42 -0
  83. austial/terminus/health_indicator.py +28 -0
  84. austial/terminus/memory_health_indicator.py +20 -0
  85. austial/testing/__init__.py +3 -0
  86. austial/testing/testing_module.py +64 -0
  87. austial-0.1.1.dist-info/METADATA +241 -0
  88. austial-0.1.1.dist-info/RECORD +91 -0
  89. austial-0.1.1.dist-info/WHEEL +4 -0
  90. austial-0.1.1.dist-info/entry_points.txt +2 -0
  91. austial-0.1.1.dist-info/licenses/LICENSE +21 -0
austial/__init__.py ADDED
@@ -0,0 +1,136 @@
1
+ """Austial -- a NestJS-inspired, batteries-included web framework for Python,
2
+ built on top of FastAPI.
3
+
4
+ Quickstart::
5
+
6
+ from austial import Module, Controller, Get, Injectable, AustialFactory
7
+
8
+ Most day-to-day symbols (decorators, exceptions, guards, etc.) are re-exported
9
+ here for convenience, mirroring how ``@nestjs/common`` re-exports everything
10
+ Nest apps need. Deeper framework internals live in ``austial.core``,
11
+ ``austial.config``, ``austial.database``, ``austial.terminus``,
12
+ ``austial.testing`` and ``austial.cli``.
13
+ """
14
+
15
+ from austial.common import (
16
+ AllExceptionsFilter,
17
+ ArgumentMetadata,
18
+ ArgumentsHost,
19
+ BadRequestException,
20
+ Body,
21
+ CallHandler,
22
+ CanActivate,
23
+ Catch,
24
+ ConflictException,
25
+ Controller,
26
+ Delete,
27
+ ExceptionFilter,
28
+ ExecutionContext,
29
+ ForbiddenException,
30
+ Get,
31
+ Global,
32
+ Head,
33
+ Header,
34
+ Headers,
35
+ HttpCode,
36
+ HttpException,
37
+ Inject,
38
+ Injectable,
39
+ InternalServerErrorException,
40
+ Logger,
41
+ LoggingMiddleware,
42
+ MethodNotAllowedException,
43
+ MiddlewareConsumer,
44
+ Module,
45
+ NestInterceptor,
46
+ NestMiddleware,
47
+ NotFoundException,
48
+ NotImplementedException,
49
+ Optional_,
50
+ Options,
51
+ Param,
52
+ Patch,
53
+ PipeTransform,
54
+ Post,
55
+ Put,
56
+ Query,
57
+ Req,
58
+ Res,
59
+ Scope,
60
+ ServiceUnavailableException,
61
+ TooManyRequestsException,
62
+ TransformInterceptor,
63
+ UnauthorizedException,
64
+ UnprocessableEntityException,
65
+ UseFilters,
66
+ UseGuards,
67
+ UseInterceptors,
68
+ UsePipes,
69
+ ValidationPipe,
70
+ )
71
+ from austial.core import DynamicModule, Reflector
72
+ from austial.core.app import AustialApplication
73
+ from austial.core.factory import AustialFactory
74
+
75
+ __version__ = "0.1.0"
76
+
77
+ __all__ = [
78
+ "AllExceptionsFilter",
79
+ "ArgumentMetadata",
80
+ "ArgumentsHost",
81
+ "AustialApplication",
82
+ "AustialFactory",
83
+ "BadRequestException",
84
+ "Body",
85
+ "CallHandler",
86
+ "CanActivate",
87
+ "Catch",
88
+ "ConflictException",
89
+ "Controller",
90
+ "Delete",
91
+ "DynamicModule",
92
+ "ExceptionFilter",
93
+ "ExecutionContext",
94
+ "ForbiddenException",
95
+ "Get",
96
+ "Global",
97
+ "Head",
98
+ "Header",
99
+ "Headers",
100
+ "HttpCode",
101
+ "HttpException",
102
+ "Inject",
103
+ "Injectable",
104
+ "InternalServerErrorException",
105
+ "Logger",
106
+ "LoggingMiddleware",
107
+ "MethodNotAllowedException",
108
+ "MiddlewareConsumer",
109
+ "Module",
110
+ "NestInterceptor",
111
+ "NestMiddleware",
112
+ "NotFoundException",
113
+ "NotImplementedException",
114
+ "Options",
115
+ "Optional_",
116
+ "Param",
117
+ "Patch",
118
+ "PipeTransform",
119
+ "Post",
120
+ "Put",
121
+ "Query",
122
+ "Reflector",
123
+ "Req",
124
+ "Res",
125
+ "Scope",
126
+ "ServiceUnavailableException",
127
+ "TooManyRequestsException",
128
+ "TransformInterceptor",
129
+ "UnauthorizedException",
130
+ "UnprocessableEntityException",
131
+ "UseFilters",
132
+ "UseGuards",
133
+ "UseInterceptors",
134
+ "UsePipes",
135
+ "ValidationPipe",
136
+ ]
File without changes
@@ -0,0 +1,13 @@
1
+ from austial.cli.generators.controller_gen import generate_controller
2
+ from austial.cli.generators.module_gen import generate_module
3
+ from austial.cli.generators.new_project import create_new_project
4
+ from austial.cli.generators.resource_gen import generate_resource
5
+ from austial.cli.generators.service_gen import generate_service
6
+
7
+ __all__ = [
8
+ "create_new_project",
9
+ "generate_controller",
10
+ "generate_module",
11
+ "generate_resource",
12
+ "generate_service",
13
+ ]
@@ -0,0 +1,61 @@
1
+ """Best-effort patcher that registers a newly generated module into the
2
+ nearest ``app_module.py``, mirroring how the real ``nest`` CLI updates
3
+ ``app.module.ts`` after ``nest g module``/``nest g resource``.
4
+
5
+ Text-based and intentionally conservative: if the file doesn't look like what
6
+ `austial new`/`austial generate` produce, it leaves it alone and tells the
7
+ caller so a manual edit instruction can be printed instead.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ from pathlib import Path
14
+
15
+ _IMPORTS_ARRAY_RE = re.compile(r"imports\s*=\s*\[(.*?)\]", re.DOTALL)
16
+
17
+
18
+ def register_module(app_module_path: Path, class_name: str, import_line: str) -> bool:
19
+ """Returns True if the patch was applied, False if it was already present
20
+ or the file couldn't be safely patched."""
21
+ text = app_module_path.read_text()
22
+
23
+ if import_line in text:
24
+ return False # already registered, nothing to do
25
+
26
+ lines = text.splitlines()
27
+
28
+ # Group the new import with its isort "block" (contiguous run of
29
+ # from/import lines sharing the same top-level package as `import_line`,
30
+ # e.g. all `from src...` lines) and re-sort that block alphabetically, so
31
+ # the patched file stays isort-clean instead of just tacking the new line
32
+ # onto the end of the whole import section.
33
+ package = import_line.split()[1].split(".")[0]
34
+ prefixes = (f"from {package}.", f"import {package}.")
35
+ block_indices = [i for i, line in enumerate(lines) if line.startswith(prefixes)]
36
+
37
+ if block_indices:
38
+ first, last = block_indices[0], block_indices[-1]
39
+ block = [lines[i] for i in block_indices] + [import_line]
40
+ block = sorted(set(block))
41
+ lines[first : last + 1] = block
42
+ else:
43
+ last_import_idx = 0
44
+ for i, line in enumerate(lines):
45
+ if line.startswith("from ") or line.startswith("import "):
46
+ last_import_idx = i
47
+ lines.insert(last_import_idx + 1, import_line)
48
+
49
+ text = "\n".join(lines) + "\n"
50
+
51
+ match = _IMPORTS_ARRAY_RE.search(text)
52
+ if not match:
53
+ app_module_path.write_text(text)
54
+ return False
55
+
56
+ inner = match.group(1)
57
+ new_inner = f"{inner.rstrip()}, {class_name}" if inner.strip() else class_name
58
+ text = text[: match.start(1)] + new_inner + text[match.end(1) :]
59
+
60
+ app_module_path.write_text(text)
61
+ return True
@@ -0,0 +1,61 @@
1
+ """Shared helpers for every generator: template rendering + project-root
2
+ discovery, mirroring what the ``nest`` CLI's schematics engine does."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from jinja2 import Environment, FileSystemLoader
10
+
11
+ TEMPLATES_DIR = Path(__file__).resolve().parent.parent / "templates"
12
+
13
+ _env = Environment(
14
+ loader=FileSystemLoader(str(TEMPLATES_DIR)),
15
+ keep_trailing_newline=True,
16
+ trim_blocks=True,
17
+ lstrip_blocks=True,
18
+ )
19
+
20
+
21
+ def render(template_relpath: str, **context: Any) -> str:
22
+ template = _env.get_template(template_relpath)
23
+ return template.render(**context)
24
+
25
+
26
+ def write_file(path: Path, content: str, *, force: bool = False) -> bool:
27
+ """Writes `content` to `path`, creating parent directories as needed.
28
+ Returns False (and leaves the file untouched) if it already exists and
29
+ `force` wasn't given -- mirrors `nest g`'s "already exists" guard."""
30
+ if path.exists() and not force:
31
+ return False
32
+ path.parent.mkdir(parents=True, exist_ok=True)
33
+ path.write_text(content)
34
+ return True
35
+
36
+
37
+ def ensure_package_init(directory: Path) -> None:
38
+ """Creates an empty ``__init__.py`` in `directory` (and does *not* recurse
39
+ -- callers create the full chain explicitly) if one doesn't exist yet."""
40
+ init_file = directory / "__init__.py"
41
+ if not init_file.exists():
42
+ directory.mkdir(parents=True, exist_ok=True)
43
+ init_file.write_text("")
44
+
45
+
46
+ class ProjectNotFoundError(Exception):
47
+ pass
48
+
49
+
50
+ def find_project_root(start: Path | None = None) -> Path:
51
+ """Walks upward from `start` (default: cwd) looking for `src/app_module.py`,
52
+ the marker of an Austial project root."""
53
+ current = (start or Path.cwd()).resolve()
54
+ for candidate in (current, *current.parents):
55
+ if (candidate / "src" / "app_module.py").exists():
56
+ return candidate
57
+ raise ProjectNotFoundError(
58
+ "Couldn't find an Austial project in this or any parent directory "
59
+ "(looked for `src/app_module.py`). Run this inside a project created "
60
+ "with `austial new`, or create `src/app_module.py` by hand first."
61
+ )
@@ -0,0 +1,21 @@
1
+ """``austial generate controller <name>`` -- mirrors ``nest g controller``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from austial.cli.generators.base import find_project_root, render, write_file
8
+ from austial.cli.naming import to_kebab_case, to_pascal_case, to_snake_case
9
+
10
+
11
+ def generate_controller(name: str, *, project_root: Path | None = None) -> Path:
12
+ root = project_root or find_project_root()
13
+ snake_name = to_snake_case(name)
14
+ class_name = to_pascal_case(name)
15
+ ctx = {"snake_name": snake_name, "class_name": class_name, "kebab_name": to_kebab_case(name)}
16
+
17
+ module_dir = root / "src" / "modules" / snake_name
18
+ write_file(module_dir / "__init__.py", "")
19
+ file_path = module_dir / f"{snake_name}_controller.py"
20
+ write_file(file_path, render("controller/controller.py.j2", **ctx))
21
+ return file_path
@@ -0,0 +1,30 @@
1
+ """``austial generate module <name>`` -- mirrors ``nest g module``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from austial.cli.generators.app_module_patcher import register_module
8
+ from austial.cli.generators.base import find_project_root, render, write_file
9
+ from austial.cli.naming import to_kebab_case, to_pascal_case, to_snake_case
10
+
11
+
12
+ def generate_module(name: str, *, project_root: Path | None = None) -> Path:
13
+ root = project_root or find_project_root()
14
+ snake_name = to_snake_case(name)
15
+ class_name = to_pascal_case(name)
16
+ ctx = {"snake_name": snake_name, "class_name": class_name, "kebab_name": to_kebab_case(name)}
17
+
18
+ module_dir = root / "src" / "modules" / snake_name
19
+ write_file(module_dir / "__init__.py", "")
20
+ file_path = module_dir / f"{snake_name}_module.py"
21
+ write_file(file_path, render("module/module.py.j2", **ctx))
22
+
23
+ app_module_path = root / "src" / "app_module.py"
24
+ if app_module_path.exists():
25
+ register_module(
26
+ app_module_path,
27
+ f"{class_name}Module",
28
+ f"from src.modules.{snake_name}.{snake_name}_module import {class_name}Module",
29
+ )
30
+ return file_path
@@ -0,0 +1,67 @@
1
+ """``austial new <name>`` -- mirrors ``nest new <name>``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ from pathlib import Path
7
+
8
+ from austial.cli.generators.base import render, write_file
9
+ from austial.cli.naming import to_kebab_case
10
+
11
+
12
+ def create_new_project(
13
+ name: str,
14
+ *,
15
+ directory: Path | None = None,
16
+ austial_source_path: str | None = None,
17
+ skip_install: bool = False,
18
+ skip_git: bool = False,
19
+ ) -> Path:
20
+ project_dir = (directory or Path.cwd() / name).resolve()
21
+ project_slug = to_kebab_case(name)
22
+ ctx = {"project_name": name, "project_slug": project_slug, "austial_source_path": austial_source_path}
23
+
24
+ write_file(project_dir / "pyproject.toml", render("project/pyproject.toml.j2", **ctx))
25
+ write_file(project_dir / "README.md", render("project/readme.j2", **ctx))
26
+ write_file(project_dir / ".env.example", render("project/env_example.j2", **ctx))
27
+ write_file(project_dir / ".gitignore", render("project/gitignore.j2", **ctx))
28
+ write_file(project_dir / ".pre-commit-config.yaml", render("project/pre_commit_config.yaml.j2", **ctx))
29
+
30
+ write_file(project_dir / "src" / "__init__.py", "")
31
+ write_file(project_dir / "src" / "main.py", render("project/main.py.j2", **ctx))
32
+ write_file(project_dir / "src" / "app_module.py", render("project/app_module.py.j2", **ctx))
33
+ write_file(project_dir / "src" / "app_controller.py", render("project/app_controller.py.j2", **ctx))
34
+ write_file(project_dir / "src" / "app_service.py", render("project/app_service.py.j2", **ctx))
35
+
36
+ write_file(project_dir / "src" / "modules" / "__init__.py", "")
37
+ health_dir = project_dir / "src" / "modules" / "health"
38
+ write_file(health_dir / "__init__.py", "")
39
+ write_file(health_dir / "health_module.py", render("project/modules/health/health_module.py.j2", **ctx))
40
+ write_file(health_dir / "health_controller.py", render("project/modules/health/health_controller.py.j2", **ctx))
41
+ write_file(health_dir / "health_service.py", render("project/modules/health/health_service.py.j2", **ctx))
42
+ write_file(health_dir / "health_dto.py", render("project/modules/health/health_dto.py.j2", **ctx))
43
+ write_file(health_dir / "guards" / "__init__.py", "")
44
+ write_file(
45
+ health_dir / "guards" / "api_key_guard.py",
46
+ render("project/modules/health/guards/api_key_guard.py.j2", **ctx),
47
+ )
48
+
49
+ write_file(project_dir / "tests" / "__init__.py", "")
50
+ write_file(project_dir / "tests" / "unit" / "__init__.py", "")
51
+ write_file(project_dir / "tests" / "e2e" / "__init__.py", "")
52
+ write_file(
53
+ project_dir / "tests" / "unit" / "health_service_spec.py",
54
+ render("project/tests/unit/health_service_spec.py.j2", **ctx),
55
+ )
56
+ write_file(
57
+ project_dir / "tests" / "e2e" / "app_e2e_spec.py",
58
+ render("project/tests/e2e/app_e2e_spec.py.j2", **ctx),
59
+ )
60
+
61
+ if not skip_git:
62
+ subprocess.run(["git", "init", "-q"], cwd=project_dir, check=False)
63
+
64
+ if not skip_install:
65
+ subprocess.run(["uv", "sync"], cwd=project_dir, check=False)
66
+
67
+ return project_dir
@@ -0,0 +1,39 @@
1
+ """``austial generate resource <name>`` -- mirrors ``nest g resource``:
2
+ scaffolds a full CRUD module (module + controller + service + dto + entity)
3
+ in one shot and registers it into the nearest ``app_module.py``."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from pathlib import Path
8
+
9
+ from austial.cli.generators.app_module_patcher import register_module
10
+ from austial.cli.generators.base import find_project_root, render, write_file
11
+ from austial.cli.naming import to_kebab_case, to_pascal_case, to_snake_case
12
+
13
+
14
+ def generate_resource(name: str, *, project_root: Path | None = None) -> Path:
15
+ root = project_root or find_project_root()
16
+ snake_name = to_snake_case(name)
17
+ class_name = to_pascal_case(name)
18
+ ctx = {"snake_name": snake_name, "class_name": class_name, "kebab_name": to_kebab_case(name)}
19
+
20
+ module_dir = root / "src" / "modules" / snake_name
21
+ write_file(module_dir / "__init__.py", "")
22
+ write_file(module_dir / "dto" / "__init__.py", "")
23
+ write_file(module_dir / "entities" / "__init__.py", "")
24
+
25
+ write_file(module_dir / "entities" / f"{snake_name}_entity.py", render("resource/entity.py.j2", **ctx))
26
+ write_file(module_dir / "dto" / f"create_{snake_name}_dto.py", render("resource/create_dto.py.j2", **ctx))
27
+ write_file(module_dir / "dto" / f"update_{snake_name}_dto.py", render("resource/update_dto.py.j2", **ctx))
28
+ write_file(module_dir / f"{snake_name}_service.py", render("resource/service.py.j2", **ctx))
29
+ write_file(module_dir / f"{snake_name}_controller.py", render("resource/controller.py.j2", **ctx))
30
+ write_file(module_dir / f"{snake_name}_module.py", render("resource/module.py.j2", **ctx))
31
+
32
+ app_module_path = root / "src" / "app_module.py"
33
+ if app_module_path.exists():
34
+ register_module(
35
+ app_module_path,
36
+ f"{class_name}Module",
37
+ f"from src.modules.{snake_name}.{snake_name}_module import {class_name}Module",
38
+ )
39
+ return module_dir / f"{snake_name}_module.py"
@@ -0,0 +1,21 @@
1
+ """``austial generate service <name>`` -- mirrors ``nest g service``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from austial.cli.generators.base import find_project_root, render, write_file
8
+ from austial.cli.naming import to_kebab_case, to_pascal_case, to_snake_case
9
+
10
+
11
+ def generate_service(name: str, *, project_root: Path | None = None) -> Path:
12
+ root = project_root or find_project_root()
13
+ snake_name = to_snake_case(name)
14
+ class_name = to_pascal_case(name)
15
+ ctx = {"snake_name": snake_name, "class_name": class_name, "kebab_name": to_kebab_case(name)}
16
+
17
+ module_dir = root / "src" / "modules" / snake_name
18
+ write_file(module_dir / "__init__.py", "")
19
+ file_path = module_dir / f"{snake_name}_service.py"
20
+ write_file(file_path, render("service/service.py.j2", **ctx))
21
+ return file_path
austial/cli/main.py ADDED
@@ -0,0 +1,133 @@
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 serve
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from pathlib import Path
14
+
15
+ import typer
16
+ from rich.console import Console
17
+
18
+ from austial.cli.generators import (
19
+ create_new_project,
20
+ generate_controller,
21
+ generate_module,
22
+ generate_resource,
23
+ generate_service,
24
+ )
25
+ from austial.cli.generators.base import ProjectNotFoundError, find_project_root
26
+
27
+ console = Console()
28
+ app = typer.Typer(
29
+ name="austial",
30
+ help="Austial CLI -- scaffold and run NestJS-style Python projects, built on FastAPI.",
31
+ add_completion=False,
32
+ no_args_is_help=True,
33
+ )
34
+
35
+ _SCHEMATICS = {
36
+ "module": generate_module,
37
+ "controller": generate_controller,
38
+ "service": generate_service,
39
+ "resource": generate_resource,
40
+ }
41
+
42
+
43
+ @app.command("new")
44
+ def new_project(
45
+ name: str = typer.Argument(..., help="Name of the new project/directory to create."),
46
+ directory: str | None = typer.Option(None, "--directory", "-d", help="Target directory (defaults to ./<name>)."),
47
+ skip_install: bool = typer.Option(False, "--skip-install", help="Don't run `uv sync` after scaffolding."),
48
+ skip_git: bool = typer.Option(False, "--skip-git", help="Don't run `git init` after scaffolding."),
49
+ link: str | None = typer.Option(
50
+ None,
51
+ "--link",
52
+ help="Path to a local Austial framework checkout to depend on via `uv`'s editable path sources "
53
+ "(useful before the framework is published to PyPI).",
54
+ ),
55
+ ) -> None:
56
+ """Scaffold a brand-new Austial project -- mirrors `nest new <name>`."""
57
+ console.print(f"[bold green]CREATE[/bold green] {name}/ ...")
58
+ target = Path(directory).resolve() if directory else None
59
+ project_dir = create_new_project(
60
+ name,
61
+ directory=target,
62
+ austial_source_path=link,
63
+ skip_install=skip_install,
64
+ skip_git=skip_git,
65
+ )
66
+ console.print(f"[bold green]\u2714[/bold green] Project created at [bold]{project_dir}[/bold]")
67
+ console.print("\nNext steps:")
68
+ console.print(f" cd {project_dir.name}")
69
+ if skip_install:
70
+ console.print(" uv sync")
71
+ console.print(" cp .env.example .env")
72
+ console.print(" uv run austial serve\n")
73
+
74
+
75
+ def _generate(schematic: str, name: str) -> None:
76
+ schematic = schematic.lower()
77
+ if schematic not in _SCHEMATICS:
78
+ console.print(f"[bold red]Unknown schematic[/bold red] '{schematic}'. Choose one of: {', '.join(_SCHEMATICS)}.")
79
+ raise typer.Exit(code=1)
80
+ try:
81
+ path = _SCHEMATICS[schematic](name)
82
+ except ProjectNotFoundError as exc:
83
+ console.print(f"[bold red]\u2716[/bold red] {exc}")
84
+ raise typer.Exit(code=1) from exc
85
+ console.print(f"[bold green]CREATE[/bold green] {path}")
86
+
87
+
88
+ @app.command("generate")
89
+ def generate(
90
+ schematic: str = typer.Argument(..., help="One of: module, controller, service, resource"),
91
+ name: str = typer.Argument(..., help="Name for the generated artifact, e.g. 'cats'."),
92
+ ) -> None:
93
+ """Generate a module/controller/service/resource -- mirrors `nest generate`."""
94
+ _generate(schematic, name)
95
+
96
+
97
+ @app.command("g", hidden=True)
98
+ def generate_alias(
99
+ schematic: str = typer.Argument(..., help="One of: module, controller, service, resource"),
100
+ name: str = typer.Argument(..., help="Name for the generated artifact, e.g. 'cats'."),
101
+ ) -> None:
102
+ """Alias for `generate` -- mirrors Nest CLI's `nest g`."""
103
+ _generate(schematic, name)
104
+
105
+
106
+ @app.command("serve")
107
+ def serve(
108
+ host: str = typer.Option("0.0.0.0", help="Bind host."),
109
+ port: int = typer.Option(8000, help="Bind port."),
110
+ reload: bool = typer.Option(True, help="Auto-reload on file changes (dev mode)."),
111
+ ) -> None:
112
+ """Runs the current project (mirrors `nest start --watch`)."""
113
+ import uvicorn
114
+
115
+ try:
116
+ project_root = find_project_root()
117
+ except ProjectNotFoundError as exc:
118
+ console.print(f"[bold red]\u2716[/bold red] {exc}")
119
+ raise typer.Exit(code=1) from exc
120
+
121
+ import sys
122
+
123
+ sys.path.insert(0, str(project_root))
124
+ console.print(f"[bold green]\u25b6[/bold green] Serving {project_root.name} on http://{host}:{port}")
125
+ uvicorn.run("src.main:app", host=host, port=port, reload=reload, app_dir=str(project_root))
126
+
127
+
128
+ def run() -> None:
129
+ app()
130
+
131
+
132
+ if __name__ == "__main__":
133
+ run()
austial/cli/naming.py ADDED
@@ -0,0 +1,33 @@
1
+ """Naming helpers shared by every generator -- turns whatever the user typed
2
+ (``cats``, ``cat-breed``, ``CatBreed``, ``cat_breed``) into the forms needed
3
+ for file names, class names and route prefixes, mirroring how ``nest g``
4
+ normalizes its ``<name>`` argument."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import re
9
+
10
+ _BOUNDARY_RE = re.compile(r"[-_\s]+|(?<=[a-z0-9])(?=[A-Z])")
11
+
12
+
13
+ def _words(name: str) -> list[str]:
14
+ return [w for w in _BOUNDARY_RE.split(name.strip()) if w]
15
+
16
+
17
+ def to_snake_case(name: str) -> str:
18
+ return "_".join(w.lower() for w in _words(name))
19
+
20
+
21
+ def to_kebab_case(name: str) -> str:
22
+ return "-".join(w.lower() for w in _words(name))
23
+
24
+
25
+ def to_pascal_case(name: str) -> str:
26
+ return "".join(w.capitalize() for w in _words(name))
27
+
28
+
29
+ def to_camel_case(name: str) -> str:
30
+ words = _words(name)
31
+ if not words:
32
+ return name
33
+ return words[0].lower() + "".join(w.capitalize() for w in words[1:])
@@ -0,0 +1,8 @@
1
+ from austial import Controller, Get
2
+
3
+
4
+ @Controller("{{ kebab_name }}")
5
+ class {{ class_name }}Controller:
6
+ @Get()
7
+ async def find_all(self):
8
+ return []
@@ -0,0 +1,6 @@
1
+ from austial import Module
2
+
3
+
4
+ @Module()
5
+ class {{ class_name }}Module:
6
+ pass
@@ -0,0 +1,13 @@
1
+ from austial import Controller, Get
2
+
3
+ from src.app_service import AppService
4
+
5
+
6
+ @Controller()
7
+ class AppController:
8
+ def __init__(self, app_service: AppService):
9
+ self.app_service = app_service
10
+
11
+ @Get()
12
+ async def get_hello(self):
13
+ return self.app_service.get_hello()
@@ -0,0 +1,19 @@
1
+ """Root application module -- mirrors Nest's ``src/app.module.ts``."""
2
+
3
+ from austial import Module
4
+ from austial.common.middleware import LoggingMiddleware, MiddlewareConsumer
5
+ from austial.config import ConfigModule
6
+
7
+ from src.app_controller import AppController
8
+ from src.app_service import AppService
9
+ from src.modules.health.health_module import HealthModule
10
+
11
+
12
+ @Module(
13
+ imports=[ConfigModule.for_root(), HealthModule],
14
+ controllers=[AppController],
15
+ providers=[AppService],
16
+ )
17
+ class AppModule:
18
+ def configure(self, consumer: MiddlewareConsumer) -> None:
19
+ consumer.apply(LoggingMiddleware).for_routes("*")