pyoas 0.1.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.
- pyoas-0.1.0/PKG-INFO +253 -0
- pyoas-0.1.0/README.md +232 -0
- pyoas-0.1.0/pyproject.toml +63 -0
- pyoas-0.1.0/src/pyoas/__init__.py +0 -0
- pyoas-0.1.0/src/pyoas/claude/__init__.py +3 -0
- pyoas-0.1.0/src/pyoas/claude/scaffolder.py +111 -0
- pyoas-0.1.0/src/pyoas/claude/templates/add_test_case.md.jinja2 +45 -0
- pyoas-0.1.0/src/pyoas/claude/templates/implement_services.md.jinja2 +48 -0
- pyoas-0.1.0/src/pyoas/claude/templates/implement_tests.md.jinja2 +66 -0
- pyoas-0.1.0/src/pyoas/claude/templates/review_generated.md.jinja2 +29 -0
- pyoas-0.1.0/src/pyoas/core/__init__.py +30 -0
- pyoas-0.1.0/src/pyoas/core/analysis.py +377 -0
- pyoas-0.1.0/src/pyoas/core/cli.py +568 -0
- pyoas-0.1.0/src/pyoas/core/config.py +218 -0
- pyoas-0.1.0/src/pyoas/core/parser/__init__.py +60 -0
- pyoas-0.1.0/src/pyoas/core/renderer/__init__.py +47 -0
- pyoas-0.1.0/src/pyoas/core/resolver/__init__.py +72 -0
- pyoas-0.1.0/src/pyoas/core/tags/__init__.py +86 -0
- pyoas-0.1.0/src/pyoas/core/utils.py +127 -0
- pyoas-0.1.0/src/pyoas/fastapi/__init__.py +13 -0
- pyoas-0.1.0/src/pyoas/fastapi/deps_scaffold.py +123 -0
- pyoas-0.1.0/src/pyoas/fastapi/generator.py +443 -0
- pyoas-0.1.0/src/pyoas/fastapi/params.py +423 -0
- pyoas-0.1.0/src/pyoas/fastapi/scaffold.py +422 -0
- pyoas-0.1.0/src/pyoas/fastapi/servicetestscaffold.py +263 -0
- pyoas-0.1.0/src/pyoas/fastapi/templates/conftest.py.jinja2 +20 -0
- pyoas-0.1.0/src/pyoas/fastapi/templates/dependency_auth.py.jinja2 +63 -0
- pyoas-0.1.0/src/pyoas/fastapi/templates/init.py.jinja2 +6 -0
- pyoas-0.1.0/src/pyoas/fastapi/templates/router.py.jinja2 +68 -0
- pyoas-0.1.0/src/pyoas/fastapi/templates/service.py.jinja2 +51 -0
- pyoas-0.1.0/src/pyoas/fastapi/templates/test.py.jinja2 +156 -0
- pyoas-0.1.0/src/pyoas/fastapi/templates/test_service.py.jinja2 +44 -0
- pyoas-0.1.0/src/pyoas/fastapi/testscaffold.py +1127 -0
- pyoas-0.1.0/src/pyoas/models/__init__.py +3 -0
- pyoas-0.1.0/src/pyoas/models/classifier.py +98 -0
- pyoas-0.1.0/src/pyoas/models/context.py +174 -0
- pyoas-0.1.0/src/pyoas/models/generator.py +238 -0
- pyoas-0.1.0/src/pyoas/models/schema_renderer.py +324 -0
- pyoas-0.1.0/src/pyoas/models/templates/init.py.jinja2 +8 -0
- pyoas-0.1.0/src/pyoas/models/templates/model.py.jinja2 +63 -0
- pyoas-0.1.0/src/pyoas/models/types.py +278 -0
pyoas-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: pyoas
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generate Pydantic v2 models and FastAPI routers from OpenAPI 3.0/3.1 specs
|
|
5
|
+
Requires-Dist: jinja2>=3.1
|
|
6
|
+
Requires-Dist: jsonref>=1.1
|
|
7
|
+
Requires-Dist: openapi-spec-validator>=0.7
|
|
8
|
+
Requires-Dist: pydantic>=2.0
|
|
9
|
+
Requires-Dist: pyyaml>=6.0
|
|
10
|
+
Requires-Dist: ruff>=0.4
|
|
11
|
+
Requires-Dist: typer>=0.12
|
|
12
|
+
Requires-Dist: watchdog>=3.0
|
|
13
|
+
Requires-Dist: polyfactory>=2.0
|
|
14
|
+
Requires-Dist: pyoas[fastapi,claude] ; extra == 'all'
|
|
15
|
+
Requires-Dist: fastapi>=0.110 ; extra == 'fastapi'
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Provides-Extra: all
|
|
18
|
+
Provides-Extra: claude
|
|
19
|
+
Provides-Extra: fastapi
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# pyoas
|
|
23
|
+
|
|
24
|
+
Generate Pydantic v2 models and FastAPI routers from an OpenAPI spec. Organized as a uv workspace with independent, installable packages.
|
|
25
|
+
|
|
26
|
+
## Packages
|
|
27
|
+
|
|
28
|
+
| Package | Purpose |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `pyoas` | Foundation: spec loading, ref resolution, tag extraction, Jinja2 rendering, CLI |
|
|
31
|
+
| `pyoas` | Pydantic v2 model generation from OpenAPI schemas |
|
|
32
|
+
| `pyoas[fastapi]` | FastAPI router + service stub generation + test scaffolding |
|
|
33
|
+
| `pyoas[claude]` | Claude Code skill generation (optional) |
|
|
34
|
+
|
|
35
|
+
`pyoas[fastapi]` depends on `pyoas`; both depend on `pyoas`. `pyoas[claude]` depends on `pyoas`.
|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
38
|
+
|
|
39
|
+
```shell
|
|
40
|
+
# Install
|
|
41
|
+
uv add pyoas[fastapi]
|
|
42
|
+
|
|
43
|
+
# Create a minimal config
|
|
44
|
+
cat > pyoas.yaml << 'EOF'
|
|
45
|
+
spec: openapi.yaml
|
|
46
|
+
output:
|
|
47
|
+
models: src/generated/models
|
|
48
|
+
routers: src/generated/routers
|
|
49
|
+
EOF
|
|
50
|
+
|
|
51
|
+
# Generate Pydantic models and FastAPI routers
|
|
52
|
+
uv run pyoas generate
|
|
53
|
+
|
|
54
|
+
# Optionally scaffold service stubs and test files
|
|
55
|
+
uv run pyoas scaffold services
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
A more complete config with all optional features:
|
|
59
|
+
|
|
60
|
+
```yaml
|
|
61
|
+
spec: openapi.yaml
|
|
62
|
+
output:
|
|
63
|
+
models: src/generated/models
|
|
64
|
+
routers: src/generated/routers
|
|
65
|
+
services:
|
|
66
|
+
generate: true
|
|
67
|
+
output: src/services
|
|
68
|
+
import_path: myapp.services # Python import path for service module
|
|
69
|
+
tests:
|
|
70
|
+
generate: true
|
|
71
|
+
output: tests/generated
|
|
72
|
+
not_found_exception: "HTTPException(status_code=404, detail='Not found')"
|
|
73
|
+
skills:
|
|
74
|
+
generate: true # requires pyoas[claude]
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Then:
|
|
78
|
+
|
|
79
|
+
```shell
|
|
80
|
+
uv run pyoas generate # models + routers + service stubs + tests + skills
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Configuration reference (`pyoas.yaml`)
|
|
84
|
+
|
|
85
|
+
### `spec`
|
|
86
|
+
|
|
87
|
+
Path to the OpenAPI 3.0/3.1 spec file (YAML or JSON). Resolved relative to the config file. **Required.**
|
|
88
|
+
|
|
89
|
+
### `output`
|
|
90
|
+
|
|
91
|
+
| Key | Default | Description |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| `models` | `src/generated/models` | Output directory for generated model files |
|
|
94
|
+
| `routers` | `src/generated/routers` | Output directory for generated router files |
|
|
95
|
+
| `models_import` | _(derived)_ | Python import path for models; derived from `models` path if omitted |
|
|
96
|
+
| `routers_import` | _(derived)_ | Python import path for routers; derived from `routers` path if omitted |
|
|
97
|
+
| `source_root` | `src` | Filesystem prefix stripped when deriving Python import paths |
|
|
98
|
+
|
|
99
|
+
### `default_tag`
|
|
100
|
+
|
|
101
|
+
Default: `"default"`. Operations with no tag are grouped under this name.
|
|
102
|
+
|
|
103
|
+
### `model_config`
|
|
104
|
+
|
|
105
|
+
| Key | Default | Description |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| `extra` | `"ignore"` | Pydantic `extra` setting for response/shared models |
|
|
108
|
+
| `request_extra` | `"forbid"` | Pydantic `extra` setting for request-only models |
|
|
109
|
+
| `frozen` | `false` | Makes generated models immutable |
|
|
110
|
+
| `populate_by_name` | `true` | Allow populating fields by Python name as well as alias |
|
|
111
|
+
|
|
112
|
+
### `fields`
|
|
113
|
+
|
|
114
|
+
| Key | Default | Description |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| `snake_case` | `true` | Convert camelCase field names to snake_case with an alias |
|
|
117
|
+
| `enums_as_literals` | `true` | Render small enums as `Literal[...]` instead of `Enum` subclasses |
|
|
118
|
+
|
|
119
|
+
### `format`
|
|
120
|
+
|
|
121
|
+
| Key | Default | Description |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| `enabled` | `true` | Run `ruff format` on generated files after writing |
|
|
124
|
+
|
|
125
|
+
### `templates`
|
|
126
|
+
|
|
127
|
+
| Key | Default | Description |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| `models` | `null` | Path to a directory of custom Jinja2 templates overriding model templates |
|
|
130
|
+
| `routers` | `null` | Path to a directory of custom Jinja2 templates overriding router templates |
|
|
131
|
+
|
|
132
|
+
### `services`
|
|
133
|
+
|
|
134
|
+
| Key | Default | Description |
|
|
135
|
+
|---|---|---|
|
|
136
|
+
| `generate` | `false` | Scaffold service stub files |
|
|
137
|
+
| `output` | `src/services` | Output directory for service files |
|
|
138
|
+
| `overwrite` | `false` | Overwrite existing service files on re-run |
|
|
139
|
+
| `import_path` | `""` | Python import path used by routers to import the service (e.g. `myapp.services`) |
|
|
140
|
+
|
|
141
|
+
### `tests`
|
|
142
|
+
|
|
143
|
+
| Key | Default | Description |
|
|
144
|
+
|---|---|---|
|
|
145
|
+
| `generate` | `false` | Scaffold pytest test stub files |
|
|
146
|
+
| `output` | `tests/generated` | Output directory for test files |
|
|
147
|
+
| `overwrite` | `false` | Overwrite existing test files on re-run (default: append new test classes only) |
|
|
148
|
+
| `not_found_exception` | `null` | Exception expression used in `test_not_found` stubs (e.g. `HTTPException(status_code=404)`) |
|
|
149
|
+
|
|
150
|
+
### `skills`
|
|
151
|
+
|
|
152
|
+
Requires `pyoas[claude]` to be installed.
|
|
153
|
+
|
|
154
|
+
| Key | Default | Description |
|
|
155
|
+
|---|---|---|
|
|
156
|
+
| `generate` | `false` | Generate Claude Code skill files |
|
|
157
|
+
| `output` | `.claude/commands` | Output directory for skill files |
|
|
158
|
+
| `overwrite` | `false` | Overwrite existing skill files on re-run |
|
|
159
|
+
|
|
160
|
+
## Generated output
|
|
161
|
+
|
|
162
|
+
### Models (`pyoas`)
|
|
163
|
+
|
|
164
|
+
One file per tag: `{models_output}/{tag}/models.py`. Schemas referenced by multiple tags go to `{models_output}/shared/models.py`.
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
src/generated/models/
|
|
168
|
+
__init__.py
|
|
169
|
+
pets/
|
|
170
|
+
models.py # Pet, PetCreate, PetList, ...
|
|
171
|
+
shared/
|
|
172
|
+
models.py # schemas used by more than one tag
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Routers (`pyoas[fastapi]`)
|
|
176
|
+
|
|
177
|
+
One file per tag: `{routers_output}/{tag}/router.py`. An `__init__.py` at the root re-exports all routers.
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
src/generated/routers/
|
|
181
|
+
__init__.py # from .pets import router as pets_router; ...
|
|
182
|
+
pets/
|
|
183
|
+
router.py # APIRouter with typed endpoint stubs
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Service stubs
|
|
187
|
+
|
|
188
|
+
One file per tag: `{services_output}/{tag}.py`. Scaffolded once; never overwritten by default.
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
src/services/
|
|
192
|
+
pets.py # PetsService class with async method stubs
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Test scaffolding
|
|
196
|
+
|
|
197
|
+
One test file per tag plus a shared `conftest.py` with model factories.
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
tests/generated/
|
|
201
|
+
conftest.py # make_pet(), make_pet_list(), ...
|
|
202
|
+
test_pets.py # TestListPets, TestCreatePet, TestGetPet, ...
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Each test class covers one endpoint and includes:
|
|
206
|
+
- `test_endpoint_exists` — verifies the route returns something other than 404/405
|
|
207
|
+
- Validation tests for required fields, numeric bounds, string constraints, enum violations
|
|
208
|
+
- `test_not_found` — verifies 404 when the service raises the configured exception
|
|
209
|
+
- `test_success` — happy-path stub (auto-implemented for GET/DELETE, stubbed for others)
|
|
210
|
+
|
|
211
|
+
## CLI reference
|
|
212
|
+
|
|
213
|
+
```shell
|
|
214
|
+
pyoas models # generate Pydantic models only
|
|
215
|
+
pyoas fastapi # generate FastAPI routers only
|
|
216
|
+
pyoas generate # generate models + routers (+ services/tests/skills if configured)
|
|
217
|
+
pyoas scaffold services # scaffold service stubs (skips existing files)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
All commands accept:
|
|
221
|
+
- `--config PATH` — path to config file (default: `pyoas.yaml`)
|
|
222
|
+
- `--tags TAG1,TAG2` — limit generation to specific tags
|
|
223
|
+
- `--clean` — purge output directory before generating
|
|
224
|
+
|
|
225
|
+
## Claude Code integration (`pyoas[claude]`)
|
|
226
|
+
|
|
227
|
+
Install `pyoas[claude]` and set `skills.generate: true` in your config. Running `pyoas generate` will write Claude Code skill files to `.claude/commands/`:
|
|
228
|
+
|
|
229
|
+
| Skill | Invocation | Purpose |
|
|
230
|
+
|---|---|---|
|
|
231
|
+
| `implement-tests.md` | `/implement-tests tests/generated/test_pets.py` | Implement all `pytest.skip("implement me")` stubs in a test file |
|
|
232
|
+
| `add-test-case.md` | `/add-test-case tests/generated/test_pets.py "scenario"` | Add a new test method for the described scenario |
|
|
233
|
+
| `review-generated.md` | `/review-generated` | Cross-reference generated code against the OpenAPI spec and flag issues |
|
|
234
|
+
|
|
235
|
+
## Development
|
|
236
|
+
|
|
237
|
+
```shell
|
|
238
|
+
# Install all workspace packages in editable mode
|
|
239
|
+
uv sync --all-packages
|
|
240
|
+
|
|
241
|
+
# Run all tests
|
|
242
|
+
uv run pytest
|
|
243
|
+
|
|
244
|
+
# Run tests for a single package
|
|
245
|
+
uv run pytest packages/pyoas[fastapi]/
|
|
246
|
+
|
|
247
|
+
# Update snapshots
|
|
248
|
+
uv run pytest packages/pyoas[fastapi]/ --snapshot-update
|
|
249
|
+
|
|
250
|
+
# Lint and type-check
|
|
251
|
+
uv run ruff check packages/
|
|
252
|
+
uv run mypy packages/
|
|
253
|
+
```
|
pyoas-0.1.0/README.md
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# pyoas
|
|
2
|
+
|
|
3
|
+
Generate Pydantic v2 models and FastAPI routers from an OpenAPI spec. Organized as a uv workspace with independent, installable packages.
|
|
4
|
+
|
|
5
|
+
## Packages
|
|
6
|
+
|
|
7
|
+
| Package | Purpose |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `pyoas` | Foundation: spec loading, ref resolution, tag extraction, Jinja2 rendering, CLI |
|
|
10
|
+
| `pyoas` | Pydantic v2 model generation from OpenAPI schemas |
|
|
11
|
+
| `pyoas[fastapi]` | FastAPI router + service stub generation + test scaffolding |
|
|
12
|
+
| `pyoas[claude]` | Claude Code skill generation (optional) |
|
|
13
|
+
|
|
14
|
+
`pyoas[fastapi]` depends on `pyoas`; both depend on `pyoas`. `pyoas[claude]` depends on `pyoas`.
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```shell
|
|
19
|
+
# Install
|
|
20
|
+
uv add pyoas[fastapi]
|
|
21
|
+
|
|
22
|
+
# Create a minimal config
|
|
23
|
+
cat > pyoas.yaml << 'EOF'
|
|
24
|
+
spec: openapi.yaml
|
|
25
|
+
output:
|
|
26
|
+
models: src/generated/models
|
|
27
|
+
routers: src/generated/routers
|
|
28
|
+
EOF
|
|
29
|
+
|
|
30
|
+
# Generate Pydantic models and FastAPI routers
|
|
31
|
+
uv run pyoas generate
|
|
32
|
+
|
|
33
|
+
# Optionally scaffold service stubs and test files
|
|
34
|
+
uv run pyoas scaffold services
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
A more complete config with all optional features:
|
|
38
|
+
|
|
39
|
+
```yaml
|
|
40
|
+
spec: openapi.yaml
|
|
41
|
+
output:
|
|
42
|
+
models: src/generated/models
|
|
43
|
+
routers: src/generated/routers
|
|
44
|
+
services:
|
|
45
|
+
generate: true
|
|
46
|
+
output: src/services
|
|
47
|
+
import_path: myapp.services # Python import path for service module
|
|
48
|
+
tests:
|
|
49
|
+
generate: true
|
|
50
|
+
output: tests/generated
|
|
51
|
+
not_found_exception: "HTTPException(status_code=404, detail='Not found')"
|
|
52
|
+
skills:
|
|
53
|
+
generate: true # requires pyoas[claude]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Then:
|
|
57
|
+
|
|
58
|
+
```shell
|
|
59
|
+
uv run pyoas generate # models + routers + service stubs + tests + skills
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Configuration reference (`pyoas.yaml`)
|
|
63
|
+
|
|
64
|
+
### `spec`
|
|
65
|
+
|
|
66
|
+
Path to the OpenAPI 3.0/3.1 spec file (YAML or JSON). Resolved relative to the config file. **Required.**
|
|
67
|
+
|
|
68
|
+
### `output`
|
|
69
|
+
|
|
70
|
+
| Key | Default | Description |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| `models` | `src/generated/models` | Output directory for generated model files |
|
|
73
|
+
| `routers` | `src/generated/routers` | Output directory for generated router files |
|
|
74
|
+
| `models_import` | _(derived)_ | Python import path for models; derived from `models` path if omitted |
|
|
75
|
+
| `routers_import` | _(derived)_ | Python import path for routers; derived from `routers` path if omitted |
|
|
76
|
+
| `source_root` | `src` | Filesystem prefix stripped when deriving Python import paths |
|
|
77
|
+
|
|
78
|
+
### `default_tag`
|
|
79
|
+
|
|
80
|
+
Default: `"default"`. Operations with no tag are grouped under this name.
|
|
81
|
+
|
|
82
|
+
### `model_config`
|
|
83
|
+
|
|
84
|
+
| Key | Default | Description |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| `extra` | `"ignore"` | Pydantic `extra` setting for response/shared models |
|
|
87
|
+
| `request_extra` | `"forbid"` | Pydantic `extra` setting for request-only models |
|
|
88
|
+
| `frozen` | `false` | Makes generated models immutable |
|
|
89
|
+
| `populate_by_name` | `true` | Allow populating fields by Python name as well as alias |
|
|
90
|
+
|
|
91
|
+
### `fields`
|
|
92
|
+
|
|
93
|
+
| Key | Default | Description |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| `snake_case` | `true` | Convert camelCase field names to snake_case with an alias |
|
|
96
|
+
| `enums_as_literals` | `true` | Render small enums as `Literal[...]` instead of `Enum` subclasses |
|
|
97
|
+
|
|
98
|
+
### `format`
|
|
99
|
+
|
|
100
|
+
| Key | Default | Description |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| `enabled` | `true` | Run `ruff format` on generated files after writing |
|
|
103
|
+
|
|
104
|
+
### `templates`
|
|
105
|
+
|
|
106
|
+
| Key | Default | Description |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| `models` | `null` | Path to a directory of custom Jinja2 templates overriding model templates |
|
|
109
|
+
| `routers` | `null` | Path to a directory of custom Jinja2 templates overriding router templates |
|
|
110
|
+
|
|
111
|
+
### `services`
|
|
112
|
+
|
|
113
|
+
| Key | Default | Description |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| `generate` | `false` | Scaffold service stub files |
|
|
116
|
+
| `output` | `src/services` | Output directory for service files |
|
|
117
|
+
| `overwrite` | `false` | Overwrite existing service files on re-run |
|
|
118
|
+
| `import_path` | `""` | Python import path used by routers to import the service (e.g. `myapp.services`) |
|
|
119
|
+
|
|
120
|
+
### `tests`
|
|
121
|
+
|
|
122
|
+
| Key | Default | Description |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| `generate` | `false` | Scaffold pytest test stub files |
|
|
125
|
+
| `output` | `tests/generated` | Output directory for test files |
|
|
126
|
+
| `overwrite` | `false` | Overwrite existing test files on re-run (default: append new test classes only) |
|
|
127
|
+
| `not_found_exception` | `null` | Exception expression used in `test_not_found` stubs (e.g. `HTTPException(status_code=404)`) |
|
|
128
|
+
|
|
129
|
+
### `skills`
|
|
130
|
+
|
|
131
|
+
Requires `pyoas[claude]` to be installed.
|
|
132
|
+
|
|
133
|
+
| Key | Default | Description |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| `generate` | `false` | Generate Claude Code skill files |
|
|
136
|
+
| `output` | `.claude/commands` | Output directory for skill files |
|
|
137
|
+
| `overwrite` | `false` | Overwrite existing skill files on re-run |
|
|
138
|
+
|
|
139
|
+
## Generated output
|
|
140
|
+
|
|
141
|
+
### Models (`pyoas`)
|
|
142
|
+
|
|
143
|
+
One file per tag: `{models_output}/{tag}/models.py`. Schemas referenced by multiple tags go to `{models_output}/shared/models.py`.
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
src/generated/models/
|
|
147
|
+
__init__.py
|
|
148
|
+
pets/
|
|
149
|
+
models.py # Pet, PetCreate, PetList, ...
|
|
150
|
+
shared/
|
|
151
|
+
models.py # schemas used by more than one tag
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Routers (`pyoas[fastapi]`)
|
|
155
|
+
|
|
156
|
+
One file per tag: `{routers_output}/{tag}/router.py`. An `__init__.py` at the root re-exports all routers.
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
src/generated/routers/
|
|
160
|
+
__init__.py # from .pets import router as pets_router; ...
|
|
161
|
+
pets/
|
|
162
|
+
router.py # APIRouter with typed endpoint stubs
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Service stubs
|
|
166
|
+
|
|
167
|
+
One file per tag: `{services_output}/{tag}.py`. Scaffolded once; never overwritten by default.
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
src/services/
|
|
171
|
+
pets.py # PetsService class with async method stubs
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Test scaffolding
|
|
175
|
+
|
|
176
|
+
One test file per tag plus a shared `conftest.py` with model factories.
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
tests/generated/
|
|
180
|
+
conftest.py # make_pet(), make_pet_list(), ...
|
|
181
|
+
test_pets.py # TestListPets, TestCreatePet, TestGetPet, ...
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Each test class covers one endpoint and includes:
|
|
185
|
+
- `test_endpoint_exists` — verifies the route returns something other than 404/405
|
|
186
|
+
- Validation tests for required fields, numeric bounds, string constraints, enum violations
|
|
187
|
+
- `test_not_found` — verifies 404 when the service raises the configured exception
|
|
188
|
+
- `test_success` — happy-path stub (auto-implemented for GET/DELETE, stubbed for others)
|
|
189
|
+
|
|
190
|
+
## CLI reference
|
|
191
|
+
|
|
192
|
+
```shell
|
|
193
|
+
pyoas models # generate Pydantic models only
|
|
194
|
+
pyoas fastapi # generate FastAPI routers only
|
|
195
|
+
pyoas generate # generate models + routers (+ services/tests/skills if configured)
|
|
196
|
+
pyoas scaffold services # scaffold service stubs (skips existing files)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
All commands accept:
|
|
200
|
+
- `--config PATH` — path to config file (default: `pyoas.yaml`)
|
|
201
|
+
- `--tags TAG1,TAG2` — limit generation to specific tags
|
|
202
|
+
- `--clean` — purge output directory before generating
|
|
203
|
+
|
|
204
|
+
## Claude Code integration (`pyoas[claude]`)
|
|
205
|
+
|
|
206
|
+
Install `pyoas[claude]` and set `skills.generate: true` in your config. Running `pyoas generate` will write Claude Code skill files to `.claude/commands/`:
|
|
207
|
+
|
|
208
|
+
| Skill | Invocation | Purpose |
|
|
209
|
+
|---|---|---|
|
|
210
|
+
| `implement-tests.md` | `/implement-tests tests/generated/test_pets.py` | Implement all `pytest.skip("implement me")` stubs in a test file |
|
|
211
|
+
| `add-test-case.md` | `/add-test-case tests/generated/test_pets.py "scenario"` | Add a new test method for the described scenario |
|
|
212
|
+
| `review-generated.md` | `/review-generated` | Cross-reference generated code against the OpenAPI spec and flag issues |
|
|
213
|
+
|
|
214
|
+
## Development
|
|
215
|
+
|
|
216
|
+
```shell
|
|
217
|
+
# Install all workspace packages in editable mode
|
|
218
|
+
uv sync --all-packages
|
|
219
|
+
|
|
220
|
+
# Run all tests
|
|
221
|
+
uv run pytest
|
|
222
|
+
|
|
223
|
+
# Run tests for a single package
|
|
224
|
+
uv run pytest packages/pyoas[fastapi]/
|
|
225
|
+
|
|
226
|
+
# Update snapshots
|
|
227
|
+
uv run pytest packages/pyoas[fastapi]/ --snapshot-update
|
|
228
|
+
|
|
229
|
+
# Lint and type-check
|
|
230
|
+
uv run ruff check packages/
|
|
231
|
+
uv run mypy packages/
|
|
232
|
+
```
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pyoas"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Generate Pydantic v2 models and FastAPI routers from OpenAPI 3.0/3.1 specs"
|
|
5
|
+
requires-python = ">=3.12"
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"jinja2>=3.1",
|
|
9
|
+
"jsonref>=1.1",
|
|
10
|
+
"openapi-spec-validator>=0.7",
|
|
11
|
+
"pydantic>=2.0",
|
|
12
|
+
"pyyaml>=6.0",
|
|
13
|
+
"ruff>=0.4",
|
|
14
|
+
"typer>=0.12",
|
|
15
|
+
"watchdog>=3.0",
|
|
16
|
+
"polyfactory>=2.0",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
fastapi = ["fastapi>=0.110"]
|
|
21
|
+
claude = []
|
|
22
|
+
all = ["pyoas[fastapi,claude]"]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
pyoas = "pyoas.core.cli:app"
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = ["uv_build>=0.11.8,<0.12"]
|
|
29
|
+
build-backend = "uv_build"
|
|
30
|
+
|
|
31
|
+
[tool.uv.build-backend]
|
|
32
|
+
module-name = "pyoas"
|
|
33
|
+
|
|
34
|
+
[dependency-groups]
|
|
35
|
+
dev = [
|
|
36
|
+
"pytest>=8.0",
|
|
37
|
+
"syrupy>=4.0",
|
|
38
|
+
"types-pyyaml>=6.0.12.20260408",
|
|
39
|
+
"httpx>=0.27",
|
|
40
|
+
"mkdocs-material>=9.7.6",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[tool.ruff]
|
|
44
|
+
target-version = "py312"
|
|
45
|
+
line-length = 88
|
|
46
|
+
|
|
47
|
+
[tool.ruff.lint]
|
|
48
|
+
select = ["E", "F", "I", "UP"]
|
|
49
|
+
ignore = ["E501"]
|
|
50
|
+
|
|
51
|
+
[tool.mypy]
|
|
52
|
+
python_version = "3.12"
|
|
53
|
+
ignore_missing_imports = true
|
|
54
|
+
disable_error_code = ["import-untyped"]
|
|
55
|
+
mypy_path = ["src"]
|
|
56
|
+
exclude = [
|
|
57
|
+
"conftest\\.py$",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
[tool.pytest.ini_options]
|
|
61
|
+
testpaths = ["tests"]
|
|
62
|
+
addopts = "--import-mode=importlib"
|
|
63
|
+
filterwarnings = ["ignore::pytest.PytestCollectionWarning"]
|
|
File without changes
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SkillScaffolder — writes Claude Code skill files for pyoas projects.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from pyoas.core.config import Config
|
|
13
|
+
from pyoas.core.renderer import Renderer
|
|
14
|
+
from pyoas.core.utils import derive_import_path
|
|
15
|
+
|
|
16
|
+
_DEFAULT_TEMPLATES = Path(__file__).parent / "templates"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _rel(path_str: str) -> str:
|
|
20
|
+
"""Return path_str as a relative path to cwd when possible."""
|
|
21
|
+
p = Path(path_str)
|
|
22
|
+
if not p.is_absolute():
|
|
23
|
+
return path_str
|
|
24
|
+
try:
|
|
25
|
+
return str(p.relative_to(Path.cwd()))
|
|
26
|
+
except ValueError:
|
|
27
|
+
return path_str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SkillScaffolder:
|
|
31
|
+
def __init__(self, config: Config) -> None:
|
|
32
|
+
self._config = config
|
|
33
|
+
|
|
34
|
+
def scaffold(self) -> None:
|
|
35
|
+
if not self._config.skills.generate:
|
|
36
|
+
return
|
|
37
|
+
output_dir = Path(self._config.skills.output)
|
|
38
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
context = self._build_context()
|
|
40
|
+
renderer = Renderer(default_templates_dir=_DEFAULT_TEMPLATES)
|
|
41
|
+
self._scaffold_implement_tests(output_dir, context, renderer)
|
|
42
|
+
self._scaffold_add_test_case(output_dir, context, renderer)
|
|
43
|
+
self._scaffold_review_generated(output_dir, context, renderer)
|
|
44
|
+
self._scaffold_implement_services(output_dir, context, renderer)
|
|
45
|
+
|
|
46
|
+
def _scaffold_implement_tests(
|
|
47
|
+
self, output_dir: Path, context: dict[str, Any], renderer: Renderer
|
|
48
|
+
) -> None:
|
|
49
|
+
skill_file = output_dir / "implement-tests.md"
|
|
50
|
+
if skill_file.exists() and not self._config.skills.overwrite:
|
|
51
|
+
typer.echo(f"Skipping {skill_file} (already exists)")
|
|
52
|
+
return
|
|
53
|
+
src = renderer.render("implement_tests.md.jinja2", context)
|
|
54
|
+
skill_file.write_text(src, encoding="utf-8")
|
|
55
|
+
typer.echo(f"Wrote {skill_file}")
|
|
56
|
+
|
|
57
|
+
def _scaffold_add_test_case(
|
|
58
|
+
self, output_dir: Path, context: dict[str, Any], renderer: Renderer
|
|
59
|
+
) -> None:
|
|
60
|
+
skill_file = output_dir / "add-test-case.md"
|
|
61
|
+
if skill_file.exists() and not self._config.skills.overwrite:
|
|
62
|
+
typer.echo(f"Skipping {skill_file} (already exists)")
|
|
63
|
+
return
|
|
64
|
+
src = renderer.render("add_test_case.md.jinja2", context)
|
|
65
|
+
skill_file.write_text(src, encoding="utf-8")
|
|
66
|
+
typer.echo(f"Wrote {skill_file}")
|
|
67
|
+
|
|
68
|
+
def _scaffold_review_generated(
|
|
69
|
+
self, output_dir: Path, context: dict[str, Any], renderer: Renderer
|
|
70
|
+
) -> None:
|
|
71
|
+
skill_file = output_dir / "review-generated.md"
|
|
72
|
+
if skill_file.exists() and not self._config.skills.overwrite:
|
|
73
|
+
typer.echo(f"Skipping {skill_file} (already exists)")
|
|
74
|
+
return
|
|
75
|
+
src = renderer.render("review_generated.md.jinja2", context)
|
|
76
|
+
skill_file.write_text(src, encoding="utf-8")
|
|
77
|
+
typer.echo(f"Wrote {skill_file}")
|
|
78
|
+
|
|
79
|
+
def _scaffold_implement_services(
|
|
80
|
+
self, output_dir: Path, context: dict[str, Any], renderer: Renderer
|
|
81
|
+
) -> None:
|
|
82
|
+
if not context["has_services"]:
|
|
83
|
+
return
|
|
84
|
+
skill_file = output_dir / "implement-services.md"
|
|
85
|
+
if skill_file.exists() and not self._config.skills.overwrite:
|
|
86
|
+
typer.echo(f"Skipping {skill_file} (already exists)")
|
|
87
|
+
return
|
|
88
|
+
src = renderer.render("implement_services.md.jinja2", context)
|
|
89
|
+
skill_file.write_text(src, encoding="utf-8")
|
|
90
|
+
typer.echo(f"Wrote {skill_file}")
|
|
91
|
+
|
|
92
|
+
def _build_context(self) -> dict[str, Any]:
|
|
93
|
+
cfg = self._config
|
|
94
|
+
tests_output = _rel(cfg.tests.output)
|
|
95
|
+
services_output = _rel(cfg.services.output) if cfg.services.generate else None
|
|
96
|
+
models_import = cfg.output.models_import or derive_import_path(
|
|
97
|
+
cfg.output.models, cfg.output.source_root
|
|
98
|
+
)
|
|
99
|
+
return {
|
|
100
|
+
"spec": _rel(cfg.spec),
|
|
101
|
+
"tests_output": tests_output,
|
|
102
|
+
"services_output": services_output,
|
|
103
|
+
"models_output": _rel(cfg.output.models),
|
|
104
|
+
"routers_output": _rel(cfg.output.routers),
|
|
105
|
+
"models_import_path": models_import,
|
|
106
|
+
"router_import_path": cfg.output.routers_import
|
|
107
|
+
or derive_import_path(cfg.output.routers, cfg.output.source_root),
|
|
108
|
+
"has_services": bool(cfg.services.import_path),
|
|
109
|
+
"not_found_exception": cfg.tests.not_found_exception,
|
|
110
|
+
"services_pattern": cfg.skills.services_pattern,
|
|
111
|
+
}
|