specx 0.0.0a1__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.
- specx-0.0.0a1/LICENSE.md +21 -0
- specx-0.0.0a1/PKG-INFO +228 -0
- specx-0.0.0a1/README.md +207 -0
- specx-0.0.0a1/pyproject.toml +84 -0
- specx-0.0.0a1/src/specx/__init__.py +1 -0
- specx-0.0.0a1/src/specx/_internal/__init__.py +1 -0
- specx-0.0.0a1/src/specx/_internal/exceptions.py +5 -0
- specx-0.0.0a1/src/specx/_internal/python_ast/__init__.py +1 -0
- specx-0.0.0a1/src/specx/_internal/python_ast/scanner.py +84 -0
- specx-0.0.0a1/src/specx/foundation/__init__.py +1 -0
- specx-0.0.0a1/src/specx/foundation/capability.py +9 -0
- specx-0.0.0a1/src/specx/foundation/command.py +16 -0
- specx-0.0.0a1/src/specx/foundation/configurator.py +8 -0
- specx-0.0.0a1/src/specx/foundation/delivery/__init__.py +1 -0
- specx-0.0.0a1/src/specx/foundation/delivery/controller.py +18 -0
- specx-0.0.0a1/src/specx/foundation/delivery/fastapi/__init__.py +1 -0
- specx-0.0.0a1/src/specx/foundation/delivery/fastapi/schema.py +15 -0
- specx-0.0.0a1/src/specx/foundation/delivery/service.py +8 -0
- specx-0.0.0a1/src/specx/foundation/dto.py +15 -0
- specx-0.0.0a1/src/specx/foundation/effect_service.py +12 -0
- specx-0.0.0a1/src/specx/foundation/entity.py +15 -0
- specx-0.0.0a1/src/specx/foundation/enums.py +10 -0
- specx-0.0.0a1/src/specx/foundation/exceptions.py +16 -0
- specx-0.0.0a1/src/specx/foundation/factory.py +8 -0
- specx-0.0.0a1/src/specx/foundation/gateway.py +9 -0
- specx-0.0.0a1/src/specx/foundation/infrastructure/__init__.py +1 -0
- specx-0.0.0a1/src/specx/foundation/infrastructure/sqlalchemy/__init__.py +1 -0
- specx-0.0.0a1/src/specx/foundation/infrastructure/sqlalchemy/model.py +23 -0
- specx-0.0.0a1/src/specx/foundation/pure_service.py +12 -0
- specx-0.0.0a1/src/specx/foundation/query.py +16 -0
- specx-0.0.0a1/src/specx/foundation/read_service.py +12 -0
- specx-0.0.0a1/src/specx/foundation/repository.py +8 -0
- specx-0.0.0a1/src/specx/foundation/settings.py +17 -0
- specx-0.0.0a1/src/specx/foundation/unit_of_work.py +7 -0
- specx-0.0.0a1/src/specx/foundation/unit_of_work_manager.py +29 -0
- specx-0.0.0a1/src/specx/foundation/use_case.py +8 -0
- specx-0.0.0a1/src/specx/py.typed +1 -0
- specx-0.0.0a1/src/specx/testing/__init__.py +1 -0
- specx-0.0.0a1/src/specx/testing/architecture/__init__.py +28 -0
- specx-0.0.0a1/src/specx/testing/architecture/checker.py +40 -0
- specx-0.0.0a1/src/specx/testing/architecture/context.py +722 -0
- specx-0.0.0a1/src/specx/testing/architecture/models.py +102 -0
- specx-0.0.0a1/src/specx/testing/architecture/registry.py +55 -0
- specx-0.0.0a1/src/specx/testing/architecture/rule.py +19 -0
- specx-0.0.0a1/src/specx/testing/architecture/rule_id.py +68 -0
- specx-0.0.0a1/src/specx/testing/architecture/rules.py +1951 -0
specx-0.0.0a1/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Maksim Zayats
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
specx-0.0.0a1/PKG-INFO
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: specx
|
|
3
|
+
Version: 0.0.0a1
|
|
4
|
+
Summary: Rule-based architecture guardrails for Specx Python services.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE.md
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Framework :: Pytest
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Dist: pydantic>=2.13.0
|
|
17
|
+
Requires-Dist: pydantic-settings>=2.14.0
|
|
18
|
+
Requires-Dist: sqlalchemy>=2.0.0
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# Specx
|
|
23
|
+
|
|
24
|
+
**Codex skills for building Python services with explicit architectural boundaries.**
|
|
25
|
+
|
|
26
|
+
Specx is a skill catalog for generating and evolving backend services that keep
|
|
27
|
+
application code readable under agent-driven development. It gives Codex a
|
|
28
|
+
shared vocabulary for packaged foundation bases, scoped core packages, delivery
|
|
29
|
+
adapters, unit-of-work lifecycles, dependency injection, migrations, and
|
|
30
|
+
architecture tests.
|
|
31
|
+
|
|
32
|
+
[Install](#install) ·
|
|
33
|
+
[Skills](#skills) ·
|
|
34
|
+
[Generated Architecture](#generated-architecture) ·
|
|
35
|
+
[Contribute](CONTRIBUTING.md)
|
|
36
|
+
|
|
37
|
+
## Key Features
|
|
38
|
+
|
|
39
|
+
- **Skill-based service scaffolding.** Specx replaces a one-off code template
|
|
40
|
+
with composable skills for project structure, foundation usage, tooling,
|
|
41
|
+
DI, use cases, services, delivery controllers, infrastructure adapters,
|
|
42
|
+
settings, migrations, and tests.
|
|
43
|
+
- **Explicit class boundaries.** Generated services use class-based use cases,
|
|
44
|
+
services, controllers, repositories, gateways, capabilities, DTOs, entities,
|
|
45
|
+
units of work, and factories with packaged `specx.foundation` bases.
|
|
46
|
+
- **Clear transaction ownership.** Use cases open `UnitOfWorkManager` scopes.
|
|
47
|
+
Read/effect services may use an active UoW passed by the use case, but they do
|
|
48
|
+
not own transaction lifecycle.
|
|
49
|
+
- **Guardrails for agent work.** The `specx` Python package ships rule-based
|
|
50
|
+
architecture tests that reject layer leaks, entity returns from use cases,
|
|
51
|
+
schema bootstrap calls, bare classes, wrong suffixes, and hidden transaction
|
|
52
|
+
ownership.
|
|
53
|
+
- **Reusable runtime bases.** Generated services import small base classes from
|
|
54
|
+
`specx.foundation` instead of vendoring a foundation tree. Projects add
|
|
55
|
+
local `foundation/` modules only when a real missing category needs one.
|
|
56
|
+
- **Alembic-first persistence.** SQLAlchemy projects use real Alembic
|
|
57
|
+
migrations and drift checks instead of `metadata.create_all()` bootstraps.
|
|
58
|
+
|
|
59
|
+
## Install
|
|
60
|
+
|
|
61
|
+
Install every Specx skill for Codex:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
npx skills add maksimzayats/specx --skill '*' --agent codex -y
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
List skills from a local checkout:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
npx skills add . --list --full-depth
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Validate the catalog:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
make check
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Use the architecture package from generated projects:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from pathlib import Path
|
|
83
|
+
|
|
84
|
+
from specx.testing.architecture import SpecxArchitectureConfig, assert_specx_architecture
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_specx_architecture() -> None:
|
|
88
|
+
assert_specx_architecture(
|
|
89
|
+
SpecxArchitectureConfig(
|
|
90
|
+
project_root=Path(__file__).resolve().parents[3],
|
|
91
|
+
package_name="order_service",
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## What You Get
|
|
97
|
+
|
|
98
|
+
- A reusable Codex skill catalog under `skills/`.
|
|
99
|
+
- A typed Python guardrail package under `src/specx/`.
|
|
100
|
+
- A generated reference service under `samples/task-db-service/`.
|
|
101
|
+
- Rule-based architecture guardrails exposed through
|
|
102
|
+
`specx.testing.architecture`.
|
|
103
|
+
- A compatibility renderer that writes the tiny generated-project pytest
|
|
104
|
+
wrapper with the correct package name.
|
|
105
|
+
- Root `AGENTS.md` guidance for agents working on this catalog.
|
|
106
|
+
- Generated-project `AGENTS.md` guidance that projects should carry with them.
|
|
107
|
+
|
|
108
|
+
## Skills
|
|
109
|
+
|
|
110
|
+
- `specx-project-structure` creates the initial `core`, `delivery`,
|
|
111
|
+
`infrastructure`, `ioc`, optional local `foundation`, optional `shared`,
|
|
112
|
+
migrations, tests, and generated-project agent instructions.
|
|
113
|
+
- `specx-foundation` teaches packaged base usage and project-local extensions
|
|
114
|
+
for real missing base categories.
|
|
115
|
+
- `specx-project-tooling` adds `uv`, Ruff, mypy, pytest, Makefile targets, and
|
|
116
|
+
local validation commands.
|
|
117
|
+
- `specx-component-architecture` decides where code belongs across scopes,
|
|
118
|
+
boundaries, capabilities, gateways, DTOs, schemas, adapters, and shared code.
|
|
119
|
+
- `specx-diwire-composition` wires `diwire.Container`, `Injected[...]`, private
|
|
120
|
+
registrations, app factories, and test overrides.
|
|
121
|
+
- `specx-add-core-use-case` adds command/query-driven use cases that return
|
|
122
|
+
DTOs and own UoW scopes when persistence is needed.
|
|
123
|
+
- `specx-add-core-service` adds focused reusable core behavior without hiding
|
|
124
|
+
transaction lifecycle.
|
|
125
|
+
- `specx-add-infrastructure-adapter` adds repositories, gateways, UoW
|
|
126
|
+
implementations, SQLAlchemy, Redis, HTTP, SDK, and other technical adapters.
|
|
127
|
+
- `specx-sqlalchemy-migrations` adds async Alembic configuration, revisions,
|
|
128
|
+
migration commands, and drift tests.
|
|
129
|
+
- `specx-add-delivery-controller` adds top-level FastAPI controllers, schemas,
|
|
130
|
+
route registration, and delivery-only helpers.
|
|
131
|
+
- `specx-settings` adds `pydantic-settings` configuration without direct
|
|
132
|
+
environment reads in core code.
|
|
133
|
+
- `specx-tests` adds unit, integration, end-to-end, DI, migration, and
|
|
134
|
+
architecture tests backed by the `specx` package.
|
|
135
|
+
|
|
136
|
+
## Generated Architecture
|
|
137
|
+
|
|
138
|
+
Specx projects import foundation bases from `specx.foundation` and organize
|
|
139
|
+
application code around scoped core packages:
|
|
140
|
+
|
|
141
|
+
```text
|
|
142
|
+
src/<package>/
|
|
143
|
+
core/
|
|
144
|
+
<scope>/
|
|
145
|
+
capabilities/
|
|
146
|
+
dtos/
|
|
147
|
+
entities/
|
|
148
|
+
exceptions/
|
|
149
|
+
gateways/
|
|
150
|
+
repositories/
|
|
151
|
+
services/
|
|
152
|
+
use_cases/
|
|
153
|
+
infrastructure/
|
|
154
|
+
delivery/
|
|
155
|
+
fastapi/
|
|
156
|
+
__main__.py
|
|
157
|
+
factory.py
|
|
158
|
+
controllers/
|
|
159
|
+
schemas/
|
|
160
|
+
services/
|
|
161
|
+
infrastructure/
|
|
162
|
+
ioc/
|
|
163
|
+
shared/
|
|
164
|
+
migrations/
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`core/<scope>/delivery/` is intentionally not part of the structure. Delivery
|
|
168
|
+
lives at the top level, while core packages stay framework-free.
|
|
169
|
+
|
|
170
|
+
Create `src/<package>/foundation/` only when a real class category is missing
|
|
171
|
+
from `specx.foundation`. Local foundation module filenames are not
|
|
172
|
+
`base_`-prefixed, but class names stay prefixed, for example `clock.py` defines
|
|
173
|
+
`BaseClock`.
|
|
174
|
+
|
|
175
|
+
## Core Rules
|
|
176
|
+
|
|
177
|
+
- Every project class inherits an explicit packaged or project-local foundation
|
|
178
|
+
base.
|
|
179
|
+
- Use cases accept exactly one same-file `Command` or `Query` and return DTOs,
|
|
180
|
+
not entities.
|
|
181
|
+
- Commands represent state-changing operations. Queries are read-only, even
|
|
182
|
+
when the input is empty.
|
|
183
|
+
- Commands, queries, DTOs, entities, and other core data classes should use
|
|
184
|
+
`@dataclass(frozen=True, kw_only=True, slots=True)` unless the user asks for
|
|
185
|
+
another model type. Keep Pydantic for delivery schemas and settings.
|
|
186
|
+
- Core services inherit `BasePureService`, `BaseReadService`, or
|
|
187
|
+
`BaseEffectService`; do not add a generic `BaseService`.
|
|
188
|
+
- Small injectable collaborators inherit `BaseCapability`, live under
|
|
189
|
+
`core/<scope>/capabilities/`, and do not pretend to be services,
|
|
190
|
+
repositories, gateways, helpers, or managers.
|
|
191
|
+
- Gateway ports inherit `BaseGateway`, live under `core/<scope>/gateways/`,
|
|
192
|
+
declare external effects, use business language, and do not return entities.
|
|
193
|
+
- Persistence use cases inject a `UnitOfWorkManager` and open an active
|
|
194
|
+
`UnitOfWork` inside `execute(...)`.
|
|
195
|
+
- Services may receive an active UoW as a method argument, but they do not open
|
|
196
|
+
UoW scopes, commit, or roll back.
|
|
197
|
+
- SQLAlchemy schema is managed by Alembic migrations, not application schema
|
|
198
|
+
bootstrap calls.
|
|
199
|
+
- `diwire.Container` belongs in `ioc`, top-level delivery factory/entrypoint
|
|
200
|
+
code, and tests only.
|
|
201
|
+
|
|
202
|
+
## Reference Service
|
|
203
|
+
|
|
204
|
+
The sample service under `samples/task-db-service/` is a working generated
|
|
205
|
+
project used to validate the skills. It includes:
|
|
206
|
+
|
|
207
|
+
- FastAPI delivery with `task_db_service.delivery.fastapi.__main__:app`.
|
|
208
|
+
- Task use cases with command/query inputs and DTO outputs.
|
|
209
|
+
- Split pure/read/effect services.
|
|
210
|
+
- SQLAlchemy repositories and UoW manager.
|
|
211
|
+
- Alembic migrations and drift checks.
|
|
212
|
+
- Architecture tests that call the rule-based `specx` guardrail package.
|
|
213
|
+
|
|
214
|
+
Run it from the sample directory:
|
|
215
|
+
|
|
216
|
+
```sh
|
|
217
|
+
cd samples/task-db-service
|
|
218
|
+
make check
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Contributing
|
|
222
|
+
|
|
223
|
+
Developer setup, architecture notes, sample regeneration expectations, and
|
|
224
|
+
validation workflow live in [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
Specx is released under the [MIT License](LICENSE.md).
|
specx-0.0.0a1/README.md
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Specx
|
|
2
|
+
|
|
3
|
+
**Codex skills for building Python services with explicit architectural boundaries.**
|
|
4
|
+
|
|
5
|
+
Specx is a skill catalog for generating and evolving backend services that keep
|
|
6
|
+
application code readable under agent-driven development. It gives Codex a
|
|
7
|
+
shared vocabulary for packaged foundation bases, scoped core packages, delivery
|
|
8
|
+
adapters, unit-of-work lifecycles, dependency injection, migrations, and
|
|
9
|
+
architecture tests.
|
|
10
|
+
|
|
11
|
+
[Install](#install) ·
|
|
12
|
+
[Skills](#skills) ·
|
|
13
|
+
[Generated Architecture](#generated-architecture) ·
|
|
14
|
+
[Contribute](CONTRIBUTING.md)
|
|
15
|
+
|
|
16
|
+
## Key Features
|
|
17
|
+
|
|
18
|
+
- **Skill-based service scaffolding.** Specx replaces a one-off code template
|
|
19
|
+
with composable skills for project structure, foundation usage, tooling,
|
|
20
|
+
DI, use cases, services, delivery controllers, infrastructure adapters,
|
|
21
|
+
settings, migrations, and tests.
|
|
22
|
+
- **Explicit class boundaries.** Generated services use class-based use cases,
|
|
23
|
+
services, controllers, repositories, gateways, capabilities, DTOs, entities,
|
|
24
|
+
units of work, and factories with packaged `specx.foundation` bases.
|
|
25
|
+
- **Clear transaction ownership.** Use cases open `UnitOfWorkManager` scopes.
|
|
26
|
+
Read/effect services may use an active UoW passed by the use case, but they do
|
|
27
|
+
not own transaction lifecycle.
|
|
28
|
+
- **Guardrails for agent work.** The `specx` Python package ships rule-based
|
|
29
|
+
architecture tests that reject layer leaks, entity returns from use cases,
|
|
30
|
+
schema bootstrap calls, bare classes, wrong suffixes, and hidden transaction
|
|
31
|
+
ownership.
|
|
32
|
+
- **Reusable runtime bases.** Generated services import small base classes from
|
|
33
|
+
`specx.foundation` instead of vendoring a foundation tree. Projects add
|
|
34
|
+
local `foundation/` modules only when a real missing category needs one.
|
|
35
|
+
- **Alembic-first persistence.** SQLAlchemy projects use real Alembic
|
|
36
|
+
migrations and drift checks instead of `metadata.create_all()` bootstraps.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
Install every Specx skill for Codex:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
npx skills add maksimzayats/specx --skill '*' --agent codex -y
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
List skills from a local checkout:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
npx skills add . --list --full-depth
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Validate the catalog:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
make check
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Use the architecture package from generated projects:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from pathlib import Path
|
|
62
|
+
|
|
63
|
+
from specx.testing.architecture import SpecxArchitectureConfig, assert_specx_architecture
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_specx_architecture() -> None:
|
|
67
|
+
assert_specx_architecture(
|
|
68
|
+
SpecxArchitectureConfig(
|
|
69
|
+
project_root=Path(__file__).resolve().parents[3],
|
|
70
|
+
package_name="order_service",
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## What You Get
|
|
76
|
+
|
|
77
|
+
- A reusable Codex skill catalog under `skills/`.
|
|
78
|
+
- A typed Python guardrail package under `src/specx/`.
|
|
79
|
+
- A generated reference service under `samples/task-db-service/`.
|
|
80
|
+
- Rule-based architecture guardrails exposed through
|
|
81
|
+
`specx.testing.architecture`.
|
|
82
|
+
- A compatibility renderer that writes the tiny generated-project pytest
|
|
83
|
+
wrapper with the correct package name.
|
|
84
|
+
- Root `AGENTS.md` guidance for agents working on this catalog.
|
|
85
|
+
- Generated-project `AGENTS.md` guidance that projects should carry with them.
|
|
86
|
+
|
|
87
|
+
## Skills
|
|
88
|
+
|
|
89
|
+
- `specx-project-structure` creates the initial `core`, `delivery`,
|
|
90
|
+
`infrastructure`, `ioc`, optional local `foundation`, optional `shared`,
|
|
91
|
+
migrations, tests, and generated-project agent instructions.
|
|
92
|
+
- `specx-foundation` teaches packaged base usage and project-local extensions
|
|
93
|
+
for real missing base categories.
|
|
94
|
+
- `specx-project-tooling` adds `uv`, Ruff, mypy, pytest, Makefile targets, and
|
|
95
|
+
local validation commands.
|
|
96
|
+
- `specx-component-architecture` decides where code belongs across scopes,
|
|
97
|
+
boundaries, capabilities, gateways, DTOs, schemas, adapters, and shared code.
|
|
98
|
+
- `specx-diwire-composition` wires `diwire.Container`, `Injected[...]`, private
|
|
99
|
+
registrations, app factories, and test overrides.
|
|
100
|
+
- `specx-add-core-use-case` adds command/query-driven use cases that return
|
|
101
|
+
DTOs and own UoW scopes when persistence is needed.
|
|
102
|
+
- `specx-add-core-service` adds focused reusable core behavior without hiding
|
|
103
|
+
transaction lifecycle.
|
|
104
|
+
- `specx-add-infrastructure-adapter` adds repositories, gateways, UoW
|
|
105
|
+
implementations, SQLAlchemy, Redis, HTTP, SDK, and other technical adapters.
|
|
106
|
+
- `specx-sqlalchemy-migrations` adds async Alembic configuration, revisions,
|
|
107
|
+
migration commands, and drift tests.
|
|
108
|
+
- `specx-add-delivery-controller` adds top-level FastAPI controllers, schemas,
|
|
109
|
+
route registration, and delivery-only helpers.
|
|
110
|
+
- `specx-settings` adds `pydantic-settings` configuration without direct
|
|
111
|
+
environment reads in core code.
|
|
112
|
+
- `specx-tests` adds unit, integration, end-to-end, DI, migration, and
|
|
113
|
+
architecture tests backed by the `specx` package.
|
|
114
|
+
|
|
115
|
+
## Generated Architecture
|
|
116
|
+
|
|
117
|
+
Specx projects import foundation bases from `specx.foundation` and organize
|
|
118
|
+
application code around scoped core packages:
|
|
119
|
+
|
|
120
|
+
```text
|
|
121
|
+
src/<package>/
|
|
122
|
+
core/
|
|
123
|
+
<scope>/
|
|
124
|
+
capabilities/
|
|
125
|
+
dtos/
|
|
126
|
+
entities/
|
|
127
|
+
exceptions/
|
|
128
|
+
gateways/
|
|
129
|
+
repositories/
|
|
130
|
+
services/
|
|
131
|
+
use_cases/
|
|
132
|
+
infrastructure/
|
|
133
|
+
delivery/
|
|
134
|
+
fastapi/
|
|
135
|
+
__main__.py
|
|
136
|
+
factory.py
|
|
137
|
+
controllers/
|
|
138
|
+
schemas/
|
|
139
|
+
services/
|
|
140
|
+
infrastructure/
|
|
141
|
+
ioc/
|
|
142
|
+
shared/
|
|
143
|
+
migrations/
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`core/<scope>/delivery/` is intentionally not part of the structure. Delivery
|
|
147
|
+
lives at the top level, while core packages stay framework-free.
|
|
148
|
+
|
|
149
|
+
Create `src/<package>/foundation/` only when a real class category is missing
|
|
150
|
+
from `specx.foundation`. Local foundation module filenames are not
|
|
151
|
+
`base_`-prefixed, but class names stay prefixed, for example `clock.py` defines
|
|
152
|
+
`BaseClock`.
|
|
153
|
+
|
|
154
|
+
## Core Rules
|
|
155
|
+
|
|
156
|
+
- Every project class inherits an explicit packaged or project-local foundation
|
|
157
|
+
base.
|
|
158
|
+
- Use cases accept exactly one same-file `Command` or `Query` and return DTOs,
|
|
159
|
+
not entities.
|
|
160
|
+
- Commands represent state-changing operations. Queries are read-only, even
|
|
161
|
+
when the input is empty.
|
|
162
|
+
- Commands, queries, DTOs, entities, and other core data classes should use
|
|
163
|
+
`@dataclass(frozen=True, kw_only=True, slots=True)` unless the user asks for
|
|
164
|
+
another model type. Keep Pydantic for delivery schemas and settings.
|
|
165
|
+
- Core services inherit `BasePureService`, `BaseReadService`, or
|
|
166
|
+
`BaseEffectService`; do not add a generic `BaseService`.
|
|
167
|
+
- Small injectable collaborators inherit `BaseCapability`, live under
|
|
168
|
+
`core/<scope>/capabilities/`, and do not pretend to be services,
|
|
169
|
+
repositories, gateways, helpers, or managers.
|
|
170
|
+
- Gateway ports inherit `BaseGateway`, live under `core/<scope>/gateways/`,
|
|
171
|
+
declare external effects, use business language, and do not return entities.
|
|
172
|
+
- Persistence use cases inject a `UnitOfWorkManager` and open an active
|
|
173
|
+
`UnitOfWork` inside `execute(...)`.
|
|
174
|
+
- Services may receive an active UoW as a method argument, but they do not open
|
|
175
|
+
UoW scopes, commit, or roll back.
|
|
176
|
+
- SQLAlchemy schema is managed by Alembic migrations, not application schema
|
|
177
|
+
bootstrap calls.
|
|
178
|
+
- `diwire.Container` belongs in `ioc`, top-level delivery factory/entrypoint
|
|
179
|
+
code, and tests only.
|
|
180
|
+
|
|
181
|
+
## Reference Service
|
|
182
|
+
|
|
183
|
+
The sample service under `samples/task-db-service/` is a working generated
|
|
184
|
+
project used to validate the skills. It includes:
|
|
185
|
+
|
|
186
|
+
- FastAPI delivery with `task_db_service.delivery.fastapi.__main__:app`.
|
|
187
|
+
- Task use cases with command/query inputs and DTO outputs.
|
|
188
|
+
- Split pure/read/effect services.
|
|
189
|
+
- SQLAlchemy repositories and UoW manager.
|
|
190
|
+
- Alembic migrations and drift checks.
|
|
191
|
+
- Architecture tests that call the rule-based `specx` guardrail package.
|
|
192
|
+
|
|
193
|
+
Run it from the sample directory:
|
|
194
|
+
|
|
195
|
+
```sh
|
|
196
|
+
cd samples/task-db-service
|
|
197
|
+
make check
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Contributing
|
|
201
|
+
|
|
202
|
+
Developer setup, architecture notes, sample regeneration expectations, and
|
|
203
|
+
validation workflow live in [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
204
|
+
|
|
205
|
+
## License
|
|
206
|
+
|
|
207
|
+
Specx is released under the [MIT License](LICENSE.md).
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "specx"
|
|
3
|
+
version = "0.0.0a1"
|
|
4
|
+
description = "Rule-based architecture guardrails for Specx Python services."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE.md"]
|
|
9
|
+
dependencies = [
|
|
10
|
+
"pydantic>=2.13.0",
|
|
11
|
+
"pydantic-settings>=2.14.0",
|
|
12
|
+
"sqlalchemy>=2.0.0",
|
|
13
|
+
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Framework :: Pytest",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Programming Language :: Python :: 3.14",
|
|
23
|
+
"Typing :: Typed",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[dependency-groups]
|
|
27
|
+
dev = [
|
|
28
|
+
"mypy>=2.1.0",
|
|
29
|
+
"pytest>=9.1.0",
|
|
30
|
+
"pyright>=1.1.407",
|
|
31
|
+
"pyyaml>=6.0.3",
|
|
32
|
+
"ruff>=0.15.0",
|
|
33
|
+
"types-PyYAML>=6.0.12",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[build-system]
|
|
37
|
+
requires = ["uv_build>=0.11.0,<0.12.0"]
|
|
38
|
+
build-backend = "uv_build"
|
|
39
|
+
|
|
40
|
+
[tool.uv.build-backend]
|
|
41
|
+
module-name = "specx"
|
|
42
|
+
|
|
43
|
+
[tool.pytest.ini_options]
|
|
44
|
+
minversion = "9.0"
|
|
45
|
+
addopts = "-vv --strict-config --strict-markers --import-mode=importlib"
|
|
46
|
+
pythonpath = ["src"]
|
|
47
|
+
testpaths = ["tests"]
|
|
48
|
+
xfail_strict = true
|
|
49
|
+
|
|
50
|
+
[tool.ruff]
|
|
51
|
+
target-version = "py311"
|
|
52
|
+
line-length = 100
|
|
53
|
+
src = ["src", "tests", "scripts"]
|
|
54
|
+
|
|
55
|
+
[tool.ruff.lint]
|
|
56
|
+
select = [
|
|
57
|
+
"A",
|
|
58
|
+
"B",
|
|
59
|
+
"C4",
|
|
60
|
+
"E",
|
|
61
|
+
"F",
|
|
62
|
+
"I",
|
|
63
|
+
"N",
|
|
64
|
+
"PIE",
|
|
65
|
+
"PT",
|
|
66
|
+
"PTH",
|
|
67
|
+
"RET",
|
|
68
|
+
"RUF",
|
|
69
|
+
"SIM",
|
|
70
|
+
"UP",
|
|
71
|
+
]
|
|
72
|
+
ignore = []
|
|
73
|
+
|
|
74
|
+
[tool.mypy]
|
|
75
|
+
python_version = "3.11"
|
|
76
|
+
strict = true
|
|
77
|
+
warn_unreachable = true
|
|
78
|
+
extra_checks = true
|
|
79
|
+
mypy_path = "src"
|
|
80
|
+
|
|
81
|
+
[tool.pyright]
|
|
82
|
+
typeCheckingMode = "strict"
|
|
83
|
+
pythonVersion = "3.11"
|
|
84
|
+
include = ["src", "tests"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from fnmatch import fnmatch
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, kw_only=True, slots=True)
|
|
10
|
+
class PythonSourceFile:
|
|
11
|
+
"""Parsed Python file and its lightweight static metadata."""
|
|
12
|
+
|
|
13
|
+
path: Path
|
|
14
|
+
tree: ast.Module
|
|
15
|
+
imports: frozenset[str]
|
|
16
|
+
aliases: dict[str, str]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, kw_only=True, slots=True)
|
|
20
|
+
class PythonAstProject:
|
|
21
|
+
"""Static Python source index for a project under inspection."""
|
|
22
|
+
|
|
23
|
+
files: dict[Path, PythonSourceFile]
|
|
24
|
+
|
|
25
|
+
def source_file(self, path: Path) -> PythonSourceFile:
|
|
26
|
+
return self.files[path]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True, kw_only=True, slots=True)
|
|
30
|
+
class PythonAstScanner:
|
|
31
|
+
"""Parse project Python files once so rules can share AST metadata."""
|
|
32
|
+
|
|
33
|
+
project_root: Path
|
|
34
|
+
excluded_patterns: tuple[str, ...] = ()
|
|
35
|
+
|
|
36
|
+
def scan(self, roots: tuple[Path, ...]) -> PythonAstProject:
|
|
37
|
+
files: dict[Path, PythonSourceFile] = {}
|
|
38
|
+
for root in roots:
|
|
39
|
+
if not root.exists():
|
|
40
|
+
continue
|
|
41
|
+
for path in sorted(root.rglob("*.py")):
|
|
42
|
+
if self._is_excluded(path):
|
|
43
|
+
continue
|
|
44
|
+
text = path.read_text(encoding="utf-8")
|
|
45
|
+
tree = ast.parse(text, filename=str(path))
|
|
46
|
+
files[path] = PythonSourceFile(
|
|
47
|
+
path=path,
|
|
48
|
+
tree=tree,
|
|
49
|
+
imports=_imports(tree),
|
|
50
|
+
aliases=_import_aliases(tree),
|
|
51
|
+
)
|
|
52
|
+
return PythonAstProject(files=files)
|
|
53
|
+
|
|
54
|
+
def _is_excluded(self, path: Path) -> bool:
|
|
55
|
+
relative = path.relative_to(self.project_root).as_posix()
|
|
56
|
+
return any(fnmatch(relative, pattern) for pattern in self.excluded_patterns)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _imports(tree: ast.Module) -> frozenset[str]:
|
|
60
|
+
modules: set[str] = set()
|
|
61
|
+
for node in ast.walk(tree):
|
|
62
|
+
if isinstance(node, ast.Import):
|
|
63
|
+
modules.update(alias.name for alias in node.names)
|
|
64
|
+
elif isinstance(node, ast.ImportFrom):
|
|
65
|
+
module_name = "." * node.level + (node.module or "")
|
|
66
|
+
if module_name:
|
|
67
|
+
modules.add(module_name)
|
|
68
|
+
separator = "" if module_name == "" or module_name.endswith(".") else "."
|
|
69
|
+
modules.update(
|
|
70
|
+
f"{module_name}{separator}{alias.name}" for alias in node.names if alias.name != "*"
|
|
71
|
+
)
|
|
72
|
+
return frozenset(modules)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _import_aliases(tree: ast.Module) -> dict[str, str]:
|
|
76
|
+
aliases: dict[str, str] = {}
|
|
77
|
+
for node in ast.walk(tree):
|
|
78
|
+
if isinstance(node, ast.Import):
|
|
79
|
+
for alias in node.names:
|
|
80
|
+
aliases[alias.asname or alias.name.split(".")[0]] = alias.name.split(".")[-1]
|
|
81
|
+
elif isinstance(node, ast.ImportFrom):
|
|
82
|
+
for alias in node.names:
|
|
83
|
+
aliases[alias.asname or alias.name] = alias.name
|
|
84
|
+
return aliases
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
class BaseCapability:
|
|
2
|
+
"""Base for small injectable collaborators narrower than services.
|
|
3
|
+
|
|
4
|
+
Capabilities do one focused thing, may be injected or faked, and do not own
|
|
5
|
+
application workflows, unit-of-work scopes, repositories, or gateways.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
SlugGeneratingCapability creates slugs for display labels.
|
|
9
|
+
"""
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from specx.foundation.dto import BaseDTO
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True, kw_only=True, slots=True)
|
|
7
|
+
class BaseCommand(BaseDTO):
|
|
8
|
+
"""Base for use-case inputs that request state-changing work.
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, kw_only=True, slots=True)
|
|
14
|
+
class CreateTaskCommand(BaseCommand):
|
|
15
|
+
title: str
|
|
16
|
+
"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|