austial-cli 0.1.5__tar.gz → 0.2.0__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.1.5 → austial_cli-0.2.0}/.gitignore +1 -0
- {austial_cli-0.1.5 → austial_cli-0.2.0}/PKG-INFO +2 -2
- austial_cli-0.2.0/src/austial/cli/generators/agent_files.py +45 -0
- {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/generators/new_project.py +12 -1
- {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/main.py +6 -0
- austial_cli-0.2.0/src/austial/cli/templates/project/agents_md.j2 +162 -0
- {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/project/readme.j2 +9 -0
- {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/resource/controller.py.j2 +5 -5
- austial_cli-0.2.0/src/austial/cli/templates/resource/entity.py.j2 +7 -0
- austial_cli-0.2.0/src/austial/cli/templates/resource/module.py.j2 +15 -0
- austial_cli-0.2.0/src/austial/cli/templates/resource/service.py.j2 +33 -0
- austial_cli-0.2.0/test/new_project_spec.py +68 -0
- austial_cli-0.1.5/src/austial/cli/templates/resource/entity.py.j2 +0 -6
- austial_cli-0.1.5/src/austial/cli/templates/resource/module.py.j2 +0 -9
- austial_cli-0.1.5/src/austial/cli/templates/resource/service.py.j2 +0 -39
- {austial_cli-0.1.5 → austial_cli-0.2.0}/pyproject.toml +0 -0
- {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/__init__.py +0 -0
- {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/generators/__init__.py +0 -0
- {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/generators/app_module_patcher.py +0 -0
- {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/generators/base.py +0 -0
- {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/generators/controller_gen.py +0 -0
- {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/generators/module_gen.py +0 -0
- {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/generators/resource_gen.py +0 -0
- {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/generators/service_gen.py +0 -0
- {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/naming.py +0 -0
- {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/controller/controller.py.j2 +0 -0
- {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/module/module.py.j2 +0 -0
- {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/project/app_controller.py.j2 +0 -0
- {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/project/app_module.py.j2 +0 -0
- {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/project/app_service.py.j2 +0 -0
- {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/project/env_example.j2 +0 -0
- {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/project/gitignore.j2 +0 -0
- {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/project/main.py.j2 +0 -0
- {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/project/modules/health/guards/api_key_guard.py.j2 +0 -0
- {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/project/modules/health/health_controller.py.j2 +0 -0
- {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/project/modules/health/health_dto.py.j2 +0 -0
- {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/project/modules/health/health_module.py.j2 +0 -0
- {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/project/modules/health/health_service.py.j2 +0 -0
- {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/project/pre_commit_config.yaml.j2 +0 -0
- {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/project/pyproject.toml.j2 +0 -0
- {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/project/tests/e2e/app_e2e_spec.py.j2 +0 -0
- {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/project/tests/unit/health_service_spec.py.j2 +0 -0
- {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/resource/create_dto.py.j2 +0 -0
- {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/resource/update_dto.py.j2 +0 -0
- {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/service/service.py.j2 +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: austial-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: The `austial` scaffolding CLI (new/generate) and its Jinja2 project templates -- mirrors the separate @nestjs/cli repo.
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: jinja2>=3.1.0
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""AI-tool instruction files for a new project.
|
|
2
|
+
|
|
3
|
+
One canonical ``AGENTS.md`` is written, and every other tool's instructions
|
|
4
|
+
file is a symlink to it, so there is a single file to edit and no drift. Where
|
|
5
|
+
the filesystem can't create symlinks (Windows without Developer Mode), the
|
|
6
|
+
file is copied instead. See https://docs.austial.com/ai/setup.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import shutil
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
AGENTS_FILE = "AGENTS.md"
|
|
16
|
+
|
|
17
|
+
# link path (relative to the project root) -> symlink target (relative to the link's own directory)
|
|
18
|
+
AGENT_LINKS: dict[str, str] = {
|
|
19
|
+
"CLAUDE.md": "AGENTS.md",
|
|
20
|
+
"GEMINI.md": "AGENTS.md",
|
|
21
|
+
"CONVENTIONS.md": "AGENTS.md",
|
|
22
|
+
".windsurfrules": "AGENTS.md",
|
|
23
|
+
".clinerules": "AGENTS.md",
|
|
24
|
+
".rules": "AGENTS.md",
|
|
25
|
+
".github/copilot-instructions.md": "../AGENTS.md",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def link_agent_files(project_dir: Path) -> list[str]:
|
|
30
|
+
"""Points every tool-specific instructions file at ``AGENTS.md``. Returns
|
|
31
|
+
the link paths that had to be copied because symlinks aren't available.
|
|
32
|
+
Existing files are left untouched."""
|
|
33
|
+
source = project_dir / AGENTS_FILE
|
|
34
|
+
copied: list[str] = []
|
|
35
|
+
for link, target in AGENT_LINKS.items():
|
|
36
|
+
path = project_dir / link
|
|
37
|
+
if path.exists() or path.is_symlink():
|
|
38
|
+
continue
|
|
39
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
try:
|
|
41
|
+
os.symlink(target, path)
|
|
42
|
+
except (OSError, NotImplementedError):
|
|
43
|
+
shutil.copyfile(source, path)
|
|
44
|
+
copied.append(link)
|
|
45
|
+
return copied
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import subprocess
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
+
from austial.cli.generators.agent_files import link_agent_files
|
|
8
9
|
from austial.cli.generators.base import render, write_file
|
|
9
10
|
from austial.cli.naming import to_kebab_case
|
|
10
11
|
|
|
@@ -16,10 +17,16 @@ def create_new_project(
|
|
|
16
17
|
austial_source_path: str | None = None,
|
|
17
18
|
skip_install: bool = False,
|
|
18
19
|
skip_git: bool = False,
|
|
20
|
+
skip_agents: bool = False,
|
|
19
21
|
) -> Path:
|
|
20
22
|
project_dir = (directory or Path.cwd() / name).resolve()
|
|
21
23
|
project_slug = to_kebab_case(name)
|
|
22
|
-
ctx = {
|
|
24
|
+
ctx = {
|
|
25
|
+
"project_name": name,
|
|
26
|
+
"project_slug": project_slug,
|
|
27
|
+
"austial_source_path": austial_source_path,
|
|
28
|
+
"agents": not skip_agents,
|
|
29
|
+
}
|
|
23
30
|
|
|
24
31
|
write_file(project_dir / "pyproject.toml", render("project/pyproject.toml.j2", **ctx))
|
|
25
32
|
write_file(project_dir / "README.md", render("project/readme.j2", **ctx))
|
|
@@ -27,6 +34,10 @@ def create_new_project(
|
|
|
27
34
|
write_file(project_dir / ".gitignore", render("project/gitignore.j2", **ctx))
|
|
28
35
|
write_file(project_dir / ".pre-commit-config.yaml", render("project/pre_commit_config.yaml.j2", **ctx))
|
|
29
36
|
|
|
37
|
+
if not skip_agents:
|
|
38
|
+
write_file(project_dir / "AGENTS.md", render("project/agents_md.j2", **ctx))
|
|
39
|
+
link_agent_files(project_dir)
|
|
40
|
+
|
|
30
41
|
write_file(project_dir / "src" / "__init__.py", "")
|
|
31
42
|
write_file(project_dir / "src" / "main.py", render("project/main.py.j2", **ctx))
|
|
32
43
|
write_file(project_dir / "src" / "app_module.py", render("project/app_module.py.j2", **ctx))
|
|
@@ -46,6 +46,11 @@ def new_project(
|
|
|
46
46
|
directory: str | None = typer.Option(None, "--directory", "-d", help="Target directory (defaults to ./<name>)."),
|
|
47
47
|
skip_install: bool = typer.Option(False, "--skip-install", help="Don't run `uv sync` after scaffolding."),
|
|
48
48
|
skip_git: bool = typer.Option(False, "--skip-git", help="Don't run `git init` after scaffolding."),
|
|
49
|
+
skip_agents: bool = typer.Option(
|
|
50
|
+
False,
|
|
51
|
+
"--skip-agents",
|
|
52
|
+
help="Don't write AGENTS.md or the per-tool symlinks (CLAUDE.md, GEMINI.md, ...).",
|
|
53
|
+
),
|
|
49
54
|
link: str | None = typer.Option(
|
|
50
55
|
None,
|
|
51
56
|
"--link",
|
|
@@ -62,6 +67,7 @@ def new_project(
|
|
|
62
67
|
austial_source_path=link,
|
|
63
68
|
skip_install=skip_install,
|
|
64
69
|
skip_git=skip_git,
|
|
70
|
+
skip_agents=skip_agents,
|
|
65
71
|
)
|
|
66
72
|
console.print(f"[bold green]\u2714[/bold green] Project created at [bold]{project_dir}[/bold]")
|
|
67
73
|
console.print("\nNext steps:")
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
This is an **Austial** application: a NestJS-style, batteries-included web
|
|
4
|
+
framework for Python, built on FastAPI. Austial is not FastAPI, Django, or
|
|
5
|
+
NestJS. It looks like all three, and guessing from them produces code that
|
|
6
|
+
does not run. When you are unsure of an API, read the docs instead of
|
|
7
|
+
inventing one:
|
|
8
|
+
|
|
9
|
+
- Docs: https://docs.austial.com
|
|
10
|
+
- Whole doc set as one file for LLMs: https://docs.austial.com/llms-full.txt
|
|
11
|
+
- Page index: https://docs.austial.com/llms.txt
|
|
12
|
+
|
|
13
|
+
## Project facts (fill these in)
|
|
14
|
+
|
|
15
|
+
- App name / purpose: {{ project_name }} -- <what this service does>
|
|
16
|
+
- Database: <postgres | sqlite> via `austial.orm`
|
|
17
|
+
- Auth: <JWT via austial.auth | API key | none>
|
|
18
|
+
- Multi-tenant: <yes | no>
|
|
19
|
+
- Deployed as: <uvicorn src.main:app on ... >
|
|
20
|
+
|
|
21
|
+
## Commands
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uv sync --extra dev # install, including dev tools
|
|
25
|
+
cp .env.example .env # first time only
|
|
26
|
+
uv run austial serve # dev server on :8000 with reload
|
|
27
|
+
uv run austial g resource <name> # scaffold a full CRUD feature module
|
|
28
|
+
uv run ruff check . --fix && uv run ruff format .
|
|
29
|
+
uv run mypy src tests
|
|
30
|
+
uv run pytest -q
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Definition of done:** `ruff check`, `ruff format --check`, `mypy src tests`
|
|
34
|
+
and `pytest` all pass. Never report work as finished without running them.
|
|
35
|
+
|
|
36
|
+
## Layout
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
src/
|
|
40
|
+
├── main.py # AustialFactory.create(AppModule) + global pipes/filters
|
|
41
|
+
├── app_module.py # root @Module: imports every feature module
|
|
42
|
+
└── modules/<snake_name>/
|
|
43
|
+
├── <name>_module.py
|
|
44
|
+
├── <name>_controller.py
|
|
45
|
+
├── <name>_service.py
|
|
46
|
+
├── entities/<name>_entity.py
|
|
47
|
+
└── dto/create_<name>_dto.py, update_<name>_dto.py
|
|
48
|
+
tests/unit/, tests/e2e/ # files named *_spec.py
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- One folder per feature. File names use underscore suffixes
|
|
52
|
+
(`cats_controller.py`), never dots.
|
|
53
|
+
- Imports are absolute from the project root: `from src.modules.cats...`.
|
|
54
|
+
- Prefer `uv run austial g resource <name>` over hand-writing a feature. It
|
|
55
|
+
also patches `app_module.py`.
|
|
56
|
+
|
|
57
|
+
## The mental model
|
|
58
|
+
|
|
59
|
+
Request pipeline, in order: **guards -> pipes -> interceptors -> handler ->
|
|
60
|
+
exception filters**. Middleware runs before all of it.
|
|
61
|
+
|
|
62
|
+
1. **Everything must be registered.** A controller goes in a module's
|
|
63
|
+
`controllers=[...]`, a provider in `providers=[...]`, and the module in
|
|
64
|
+
an `imports=[...]` chain that reaches `AppModule`. A missing registration
|
|
65
|
+
shows up as a missing route or a DI error, not an import error.
|
|
66
|
+
2. **Dependency injection is by `__init__` type hint.** Providers are
|
|
67
|
+
singletons (`Scope.DEFAULT`) unless marked `Scope.TRANSIENT`. There is no
|
|
68
|
+
request scope. Never instantiate a service yourself with `Foo()`.
|
|
69
|
+
3. **Repositories** are injected as
|
|
70
|
+
`repo: Repository[Cat] = InjectRepository(Cat)`, and the owning module
|
|
71
|
+
needs `OrmModule.for_feature([Cat])`. The root needs one
|
|
72
|
+
`OrmModule.for_root(...)`.
|
|
73
|
+
4. **Request data** uses marker defaults: `Body()`, `Param()`, `Query()`,
|
|
74
|
+
`Headers()`. DTOs are plain pydantic `BaseModel`s. Route paths use `:id`
|
|
75
|
+
and `:key*` (catch-all that also matches `/`), not `{id}`.
|
|
76
|
+
5. **Errors:** raise Austial exceptions (`NotFoundException`,
|
|
77
|
+
`BadRequestException`, `ConflictException`, ...). Do not hand-build error
|
|
78
|
+
responses. The global `AllExceptionsFilter` renders
|
|
79
|
+
`statusCode/message/error/timestamp/path`.
|
|
80
|
+
6. **Guards are the security boundary.** Every non-public route must be
|
|
81
|
+
covered by `AuthGuard` (and `RolesGuard` / `TenantGuard` where relevant).
|
|
82
|
+
Mark intentionally open routes with `@Public()`.
|
|
83
|
+
|
|
84
|
+
## Import map
|
|
85
|
+
|
|
86
|
+
| You need | Import from |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `Module Controller Injectable Get Post Put Patch Delete Body Param Query Headers UseGuards UseInterceptors UsePipes UseFilters HttpCode Header Scope Global Inject`, all `*Exception` classes, `CanActivate ExecutionContext NestInterceptor NestMiddleware PipeTransform ExceptionFilter Catch ValidationPipe AustialFactory Reflector` | `austial` |
|
|
89
|
+
| `ConfigModule ConfigService` | `austial.config` |
|
|
90
|
+
| `OrmModule DataSource Entity Column PrimaryGeneratedColumn Repository InjectRepository FindOptions In NotIn IsNull NotNull ManyToOne OneToMany` | `austial.orm` |
|
|
91
|
+
| `AuthModule AuthConfig AuthGuard AuthService CurrentUser Public JwtStrategy Argon2Hasher TotpStrategy` | `austial.auth` |
|
|
92
|
+
| `AuthzModule RolesGuard Roles Permissions AuthorizeObject PermissionService` | `austial.authz` |
|
|
93
|
+
| `TenancyModule TenantGuard TenantContext RequireTenant Tenanted` | `austial.tenancy` |
|
|
94
|
+
| `SecurityModule SecurityConfig RateLimit RateLimitGuard IdempotencyKeyGuard CsrfInterceptor SecureHeadersMiddleware PIIRedactor EnvelopeEncryptor` | `austial.security` |
|
|
95
|
+
| `AuditModule AuditConfig AuditLogService AuditEvent Audited OrmAuditStore` | `austial.audit` |
|
|
96
|
+
| `JobsModule JobsConfig JobQueue Job Cron Worker` | `austial.jobs` |
|
|
97
|
+
| `StorageModule StorageConfig StorageService SignedUrlService` | `austial.storage` |
|
|
98
|
+
| `MigrationsModule MigrationsConfig MigrationRunner` | `austial.migrations` |
|
|
99
|
+
| `I18nModule I18nConfig I18nService LocaleMiddleware` | `austial.i18n` |
|
|
100
|
+
| `HealthCheckService MemoryHealthIndicator DatabaseHealthIndicator` | `austial.terminus` |
|
|
101
|
+
| `Test` (testing module) | `austial.testing` |
|
|
102
|
+
|
|
103
|
+
If a symbol is not in this table, check
|
|
104
|
+
https://docs.austial.com/llms-full.txt before using it.
|
|
105
|
+
|
|
106
|
+
## Do
|
|
107
|
+
|
|
108
|
+
- Write async handlers and services. The ORM is async end to end.
|
|
109
|
+
- Return typed values or DTOs from handlers, and validate input with DTOs.
|
|
110
|
+
- Filter every query on a `Tenanted` entity by `tenant_id`, and set it
|
|
111
|
+
explicitly on insert from `TenantContext.require()`.
|
|
112
|
+
- Use `repository.insert(...)` when the primary key is assigned by the
|
|
113
|
+
application (UUIDs). `save()` would issue an UPDATE.
|
|
114
|
+
- Use `IsNull()` / `NotNull()` to filter on NULL. `{"col": None}` compiles to
|
|
115
|
+
`= NULL` and never matches.
|
|
116
|
+
- Use `append_only=True` entities for audit and ledger data.
|
|
117
|
+
- Write a test for every new route (`austial.testing.Test`, `httpx`
|
|
118
|
+
`ASGITransport`) and assert error bodies, not just 200s.
|
|
119
|
+
- Add a new Alembic revision for every schema change. Read the generated
|
|
120
|
+
file before committing it.
|
|
121
|
+
|
|
122
|
+
## Do not
|
|
123
|
+
|
|
124
|
+
- Do not use FastAPI `Depends()` for things Austial's DI already provides.
|
|
125
|
+
- Do not import SQLAlchemy in application code, or build SQL with f-strings.
|
|
126
|
+
Use `Repository`, `FindOptions` and `QueryBuilder` with bound params.
|
|
127
|
+
- Do not log or audit-log PII, tokens or secrets. Pass records through
|
|
128
|
+
`PIIRedactor` first.
|
|
129
|
+
- Do not edit a migration that has been applied. Add a new one.
|
|
130
|
+
- Do not hardcode secrets. Read them with `ConfigService.get_or_throw(...)`.
|
|
131
|
+
- Do not rely on `synchronize=True` outside local development and tests.
|
|
132
|
+
- Do not ship the in-memory defaults (see below) to production.
|
|
133
|
+
|
|
134
|
+
## Not production-safe by default
|
|
135
|
+
|
|
136
|
+
These ship as in-memory or local reference implementations. Replace each with
|
|
137
|
+
a persistent or shared one before running more than one worker or surviving a
|
|
138
|
+
restart:
|
|
139
|
+
|
|
140
|
+
| Component | Default | Production need |
|
|
141
|
+
|---|---|---|
|
|
142
|
+
| Refresh tokens | `InMemoryRefreshTokenStore` | ORM/Redis-backed `RefreshTokenStore` |
|
|
143
|
+
| Rate limiting | `InMemoryRateLimitStore` | shared store, plus `trusted_proxy_hops` behind a proxy |
|
|
144
|
+
| Idempotency keys | `InMemoryIdempotencyStore` | persistent store |
|
|
145
|
+
| Audit log | `InMemoryAuditStore` | `OrmAuditStore` or a SIEM store |
|
|
146
|
+
| Background jobs | in-memory `JobBackend` (process-local) | persistent `JobBackend` |
|
|
147
|
+
| File storage | `driver="local"` | `driver="s3"` |
|
|
148
|
+
| Schema | `synchronize=True` | Alembic migrations (`austial-migrate`) |
|
|
149
|
+
|
|
150
|
+
## Production gates
|
|
151
|
+
|
|
152
|
+
Before calling a feature production-ready, confirm:
|
|
153
|
+
|
|
154
|
+
1. All checks in "Definition of done" pass.
|
|
155
|
+
2. Secrets come from the environment and `jwt_secret` /
|
|
156
|
+
`signed_url_secret` are 32+ characters.
|
|
157
|
+
3. `DataSource.initialize()` runs in a `startup` event handler, because
|
|
158
|
+
`uvicorn src.main:app` never calls `bootstrap()`.
|
|
159
|
+
4. `GET /health` is wired to `DatabaseHealthIndicator`.
|
|
160
|
+
5. Global `ValidationPipe` and `AllExceptionsFilter` are registered, and
|
|
161
|
+
`AuthGuard` is global or on every non-public controller.
|
|
162
|
+
6. Full checklist: https://docs.austial.com/ai/production
|
|
@@ -66,3 +66,12 @@ uv run pre-commit run --all-files # run every hook once, on demand
|
|
|
66
66
|
Once installed, `git commit` runs formatting/linting/type-checks automatically,
|
|
67
67
|
and `git push` also runs the test suite -- mirroring a typical Nest project's
|
|
68
68
|
husky + lint-staged setup.
|
|
69
|
+
{% if agents %}
|
|
70
|
+
|
|
71
|
+
## AI coding tools
|
|
72
|
+
|
|
73
|
+
`AGENTS.md` holds the rules every AI tool should follow in this project (commands, layout, framework
|
|
74
|
+
conventions, production gates). `CLAUDE.md`, `GEMINI.md`, `.windsurfrules`, `.clinerules`, `.rules`,
|
|
75
|
+
`CONVENTIONS.md` and `.github/copilot-instructions.md` are symlinks to it, so there is one file to edit.
|
|
76
|
+
Fill in its **Project facts** block first. See https://docs.austial.com/ai/setup.
|
|
77
|
+
{% endif %}
|
|
@@ -13,21 +13,21 @@ class {{ class_name }}Controller:
|
|
|
13
13
|
|
|
14
14
|
@Post()
|
|
15
15
|
async def create(self, dto: Create{{ class_name }}Dto = Body()) -> {{ class_name }}:
|
|
16
|
-
return self.service.create(dto)
|
|
16
|
+
return await self.service.create(dto)
|
|
17
17
|
|
|
18
18
|
@Get()
|
|
19
19
|
async def find_all(self) -> list[{{ class_name }}]:
|
|
20
|
-
return self.service.find_all()
|
|
20
|
+
return await self.service.find_all()
|
|
21
21
|
|
|
22
22
|
@Get(":id")
|
|
23
23
|
async def find_one(self, id: int) -> {{ class_name }}:
|
|
24
|
-
return self.service.find_one(id)
|
|
24
|
+
return await self.service.find_one(id)
|
|
25
25
|
|
|
26
26
|
@Patch(":id")
|
|
27
27
|
async def update(self, id: int, dto: Update{{ class_name }}Dto = Body()) -> {{ class_name }}:
|
|
28
|
-
return self.service.update(id, dto)
|
|
28
|
+
return await self.service.update(id, dto)
|
|
29
29
|
|
|
30
30
|
@Delete(":id")
|
|
31
31
|
async def remove(self, id: int) -> dict:
|
|
32
|
-
self.service.remove(id)
|
|
32
|
+
await self.service.remove(id)
|
|
33
33
|
return {"deleted": True}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from austial import Module
|
|
2
|
+
from austial.orm import OrmModule
|
|
3
|
+
|
|
4
|
+
from src.modules.{{ snake_name }}.{{ snake_name }}_controller import {{ class_name }}Controller
|
|
5
|
+
from src.modules.{{ snake_name }}.{{ snake_name }}_service import {{ class_name }}Service
|
|
6
|
+
from src.modules.{{ snake_name }}.entities.{{ snake_name }}_entity import {{ class_name }}
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@Module(
|
|
10
|
+
imports=[OrmModule.for_feature([{{ class_name }}])],
|
|
11
|
+
controllers=[{{ class_name }}Controller],
|
|
12
|
+
providers=[{{ class_name }}Service],
|
|
13
|
+
)
|
|
14
|
+
class {{ class_name }}Module:
|
|
15
|
+
pass
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from austial import Injectable, NotFoundException
|
|
2
|
+
from austial.orm import InjectRepository, Repository
|
|
3
|
+
|
|
4
|
+
from src.modules.{{ snake_name }}.dto.create_{{ snake_name }}_dto import Create{{ class_name }}Dto
|
|
5
|
+
from src.modules.{{ snake_name }}.dto.update_{{ snake_name }}_dto import Update{{ class_name }}Dto
|
|
6
|
+
from src.modules.{{ snake_name }}.entities.{{ snake_name }}_entity import {{ class_name }}
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@Injectable()
|
|
10
|
+
class {{ class_name }}Service:
|
|
11
|
+
def __init__(self, repository: Repository[{{ class_name }}] = InjectRepository({{ class_name }})):
|
|
12
|
+
self.repository = repository
|
|
13
|
+
|
|
14
|
+
async def create(self, dto: Create{{ class_name }}Dto) -> {{ class_name }}:
|
|
15
|
+
return await self.repository.save({{ class_name }}(**dto.model_dump()))
|
|
16
|
+
|
|
17
|
+
async def find_all(self) -> list[{{ class_name }}]:
|
|
18
|
+
return await self.repository.find()
|
|
19
|
+
|
|
20
|
+
async def find_one(self, id: int) -> {{ class_name }}:
|
|
21
|
+
item = await self.repository.find_one_by({"id": id})
|
|
22
|
+
if item is None:
|
|
23
|
+
raise NotFoundException(f"{{ class_name }} #{id} not found")
|
|
24
|
+
return item
|
|
25
|
+
|
|
26
|
+
async def update(self, id: int, dto: Update{{ class_name }}Dto) -> {{ class_name }}:
|
|
27
|
+
await self.find_one(id)
|
|
28
|
+
await self.repository.update(id, dto.model_dump(exclude_unset=True))
|
|
29
|
+
return await self.find_one(id)
|
|
30
|
+
|
|
31
|
+
async def remove(self, id: int) -> None:
|
|
32
|
+
await self.find_one(id)
|
|
33
|
+
await self.repository.delete(id)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""``austial new`` writes one AGENTS.md and points every AI tool's instructions file at it."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from austial.cli.generators import create_new_project
|
|
11
|
+
from austial.cli.generators.agent_files import AGENT_LINKS
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _scaffold(tmp_path: Path, **kwargs) -> Path:
|
|
15
|
+
return create_new_project("my-app", directory=tmp_path / "my-app", skip_install=True, skip_git=True, **kwargs)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_new_project_writes_agents_md_with_project_name(tmp_path):
|
|
19
|
+
project = _scaffold(tmp_path)
|
|
20
|
+
|
|
21
|
+
text = (project / "AGENTS.md").read_text()
|
|
22
|
+
assert "my-app -- <what this service does>" in text
|
|
23
|
+
assert "{{" not in text and "{%" not in text
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.mark.skipif(not hasattr(os, "symlink"), reason="platform has no symlink support")
|
|
27
|
+
def test_every_tool_file_is_a_symlink_that_resolves_to_agents_md(tmp_path):
|
|
28
|
+
project = _scaffold(tmp_path)
|
|
29
|
+
|
|
30
|
+
for link in AGENT_LINKS:
|
|
31
|
+
path = project / link
|
|
32
|
+
assert path.is_symlink(), link
|
|
33
|
+
assert path.resolve() == (project / "AGENTS.md").resolve(), link
|
|
34
|
+
assert path.read_text() == (project / "AGENTS.md").read_text()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_skip_agents_writes_none_of_them(tmp_path):
|
|
38
|
+
project = _scaffold(tmp_path, skip_agents=True)
|
|
39
|
+
|
|
40
|
+
assert not (project / "AGENTS.md").exists()
|
|
41
|
+
for link in AGENT_LINKS:
|
|
42
|
+
assert not (project / link).exists() and not (project / link).is_symlink()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_falls_back_to_copies_when_symlinks_are_unavailable(tmp_path, monkeypatch):
|
|
46
|
+
def _no_symlinks(*args, **kwargs):
|
|
47
|
+
raise OSError("symlinks not permitted")
|
|
48
|
+
|
|
49
|
+
monkeypatch.setattr(os, "symlink", _no_symlinks)
|
|
50
|
+
|
|
51
|
+
project = _scaffold(tmp_path)
|
|
52
|
+
|
|
53
|
+
for link in AGENT_LINKS:
|
|
54
|
+
path = project / link
|
|
55
|
+
assert path.is_file() and not path.is_symlink(), link
|
|
56
|
+
assert path.read_text() == (project / "AGENTS.md").read_text()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_existing_tool_file_is_left_untouched(tmp_path):
|
|
60
|
+
from austial.cli.generators.agent_files import link_agent_files
|
|
61
|
+
|
|
62
|
+
(tmp_path / "AGENTS.md").write_text("canonical")
|
|
63
|
+
(tmp_path / "CLAUDE.md").write_text("mine")
|
|
64
|
+
|
|
65
|
+
link_agent_files(tmp_path)
|
|
66
|
+
|
|
67
|
+
assert (tmp_path / "CLAUDE.md").read_text() == "mine"
|
|
68
|
+
assert (tmp_path / "GEMINI.md").read_text() == "canonical"
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
from austial import Module
|
|
2
|
-
|
|
3
|
-
from src.modules.{{ snake_name }}.{{ snake_name }}_controller import {{ class_name }}Controller
|
|
4
|
-
from src.modules.{{ snake_name }}.{{ snake_name }}_service import {{ class_name }}Service
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
@Module(controllers=[{{ class_name }}Controller], providers=[{{ class_name }}Service])
|
|
8
|
-
class {{ class_name }}Module:
|
|
9
|
-
pass
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
from austial import Injectable, NotFoundException
|
|
2
|
-
|
|
3
|
-
from src.modules.{{ snake_name }}.dto.create_{{ snake_name }}_dto import Create{{ class_name }}Dto
|
|
4
|
-
from src.modules.{{ snake_name }}.dto.update_{{ snake_name }}_dto import Update{{ class_name }}Dto
|
|
5
|
-
from src.modules.{{ snake_name }}.entities.{{ snake_name }}_entity import {{ class_name }}
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@Injectable()
|
|
9
|
-
class {{ class_name }}Service:
|
|
10
|
-
"""In-memory CRUD store -- swap this out for a real repository backed by
|
|
11
|
-
``austial.orm`` (SQLAlchemy) whenever you're ready."""
|
|
12
|
-
|
|
13
|
-
def __init__(self):
|
|
14
|
-
self._items: list[{{ class_name }}] = []
|
|
15
|
-
self._next_id = 1
|
|
16
|
-
|
|
17
|
-
def create(self, dto: Create{{ class_name }}Dto) -> {{ class_name }}:
|
|
18
|
-
item = {{ class_name }}(id=self._next_id, **dto.model_dump())
|
|
19
|
-
self._next_id += 1
|
|
20
|
-
self._items.append(item)
|
|
21
|
-
return item
|
|
22
|
-
|
|
23
|
-
def find_all(self) -> list[{{ class_name }}]:
|
|
24
|
-
return self._items
|
|
25
|
-
|
|
26
|
-
def find_one(self, id: int) -> {{ class_name }}:
|
|
27
|
-
for item in self._items:
|
|
28
|
-
if item.id == id:
|
|
29
|
-
return item
|
|
30
|
-
raise NotFoundException(f"{{ class_name }} #{id} not found")
|
|
31
|
-
|
|
32
|
-
def update(self, id: int, dto: Update{{ class_name }}Dto) -> {{ class_name }}:
|
|
33
|
-
item = self.find_one(id)
|
|
34
|
-
updated = item.model_copy(update=dto.model_dump(exclude_unset=True))
|
|
35
|
-
self._items[self._items.index(item)] = updated
|
|
36
|
-
return updated
|
|
37
|
-
|
|
38
|
-
def remove(self, id: int) -> None:
|
|
39
|
-
self._items.remove(self.find_one(id))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/controller/controller.py.j2
RENAMED
|
File without changes
|
|
File without changes
|
{austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/project/app_controller.py.j2
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/project/pre_commit_config.yaml.j2
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|