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.
Files changed (45) hide show
  1. {austial_cli-0.1.5 → austial_cli-0.2.0}/.gitignore +1 -0
  2. {austial_cli-0.1.5 → austial_cli-0.2.0}/PKG-INFO +2 -2
  3. austial_cli-0.2.0/src/austial/cli/generators/agent_files.py +45 -0
  4. {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/generators/new_project.py +12 -1
  5. {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/main.py +6 -0
  6. austial_cli-0.2.0/src/austial/cli/templates/project/agents_md.j2 +162 -0
  7. {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/project/readme.j2 +9 -0
  8. {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/resource/controller.py.j2 +5 -5
  9. austial_cli-0.2.0/src/austial/cli/templates/resource/entity.py.j2 +7 -0
  10. austial_cli-0.2.0/src/austial/cli/templates/resource/module.py.j2 +15 -0
  11. austial_cli-0.2.0/src/austial/cli/templates/resource/service.py.j2 +33 -0
  12. austial_cli-0.2.0/test/new_project_spec.py +68 -0
  13. austial_cli-0.1.5/src/austial/cli/templates/resource/entity.py.j2 +0 -6
  14. austial_cli-0.1.5/src/austial/cli/templates/resource/module.py.j2 +0 -9
  15. austial_cli-0.1.5/src/austial/cli/templates/resource/service.py.j2 +0 -39
  16. {austial_cli-0.1.5 → austial_cli-0.2.0}/pyproject.toml +0 -0
  17. {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/__init__.py +0 -0
  18. {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/generators/__init__.py +0 -0
  19. {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/generators/app_module_patcher.py +0 -0
  20. {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/generators/base.py +0 -0
  21. {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/generators/controller_gen.py +0 -0
  22. {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/generators/module_gen.py +0 -0
  23. {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/generators/resource_gen.py +0 -0
  24. {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/generators/service_gen.py +0 -0
  25. {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/naming.py +0 -0
  26. {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/controller/controller.py.j2 +0 -0
  27. {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/module/module.py.j2 +0 -0
  28. {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/project/app_controller.py.j2 +0 -0
  29. {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/project/app_module.py.j2 +0 -0
  30. {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/project/app_service.py.j2 +0 -0
  31. {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/project/env_example.j2 +0 -0
  32. {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/project/gitignore.j2 +0 -0
  33. {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/project/main.py.j2 +0 -0
  34. {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
  35. {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/project/modules/health/health_controller.py.j2 +0 -0
  36. {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/project/modules/health/health_dto.py.j2 +0 -0
  37. {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/project/modules/health/health_module.py.j2 +0 -0
  38. {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/project/modules/health/health_service.py.j2 +0 -0
  39. {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/project/pre_commit_config.yaml.j2 +0 -0
  40. {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/project/pyproject.toml.j2 +0 -0
  41. {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/project/tests/e2e/app_e2e_spec.py.j2 +0 -0
  42. {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/project/tests/unit/health_service_spec.py.j2 +0 -0
  43. {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/resource/create_dto.py.j2 +0 -0
  44. {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/resource/update_dto.py.j2 +0 -0
  45. {austial_cli-0.1.5 → austial_cli-0.2.0}/src/austial/cli/templates/service/service.py.j2 +0 -0
@@ -9,3 +9,4 @@ htmlcov/
9
9
  dist/
10
10
  build/
11
11
  *.egg-info/
12
+ .DS_Store
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: austial-cli
3
- Version: 0.1.5
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 = {"project_name": name, "project_slug": project_slug, "austial_source_path": austial_source_path}
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,7 @@
1
+ from austial.orm import Column, Entity, PrimaryGeneratedColumn
2
+
3
+
4
+ @Entity()
5
+ class {{ class_name }}:
6
+ id: int = PrimaryGeneratedColumn()
7
+ name: str = Column()
@@ -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,6 +0,0 @@
1
- from pydantic import BaseModel
2
-
3
-
4
- class {{ class_name }}(BaseModel):
5
- id: int
6
- name: str
@@ -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