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.
Files changed (41) hide show
  1. pyoas-0.1.0/PKG-INFO +253 -0
  2. pyoas-0.1.0/README.md +232 -0
  3. pyoas-0.1.0/pyproject.toml +63 -0
  4. pyoas-0.1.0/src/pyoas/__init__.py +0 -0
  5. pyoas-0.1.0/src/pyoas/claude/__init__.py +3 -0
  6. pyoas-0.1.0/src/pyoas/claude/scaffolder.py +111 -0
  7. pyoas-0.1.0/src/pyoas/claude/templates/add_test_case.md.jinja2 +45 -0
  8. pyoas-0.1.0/src/pyoas/claude/templates/implement_services.md.jinja2 +48 -0
  9. pyoas-0.1.0/src/pyoas/claude/templates/implement_tests.md.jinja2 +66 -0
  10. pyoas-0.1.0/src/pyoas/claude/templates/review_generated.md.jinja2 +29 -0
  11. pyoas-0.1.0/src/pyoas/core/__init__.py +30 -0
  12. pyoas-0.1.0/src/pyoas/core/analysis.py +377 -0
  13. pyoas-0.1.0/src/pyoas/core/cli.py +568 -0
  14. pyoas-0.1.0/src/pyoas/core/config.py +218 -0
  15. pyoas-0.1.0/src/pyoas/core/parser/__init__.py +60 -0
  16. pyoas-0.1.0/src/pyoas/core/renderer/__init__.py +47 -0
  17. pyoas-0.1.0/src/pyoas/core/resolver/__init__.py +72 -0
  18. pyoas-0.1.0/src/pyoas/core/tags/__init__.py +86 -0
  19. pyoas-0.1.0/src/pyoas/core/utils.py +127 -0
  20. pyoas-0.1.0/src/pyoas/fastapi/__init__.py +13 -0
  21. pyoas-0.1.0/src/pyoas/fastapi/deps_scaffold.py +123 -0
  22. pyoas-0.1.0/src/pyoas/fastapi/generator.py +443 -0
  23. pyoas-0.1.0/src/pyoas/fastapi/params.py +423 -0
  24. pyoas-0.1.0/src/pyoas/fastapi/scaffold.py +422 -0
  25. pyoas-0.1.0/src/pyoas/fastapi/servicetestscaffold.py +263 -0
  26. pyoas-0.1.0/src/pyoas/fastapi/templates/conftest.py.jinja2 +20 -0
  27. pyoas-0.1.0/src/pyoas/fastapi/templates/dependency_auth.py.jinja2 +63 -0
  28. pyoas-0.1.0/src/pyoas/fastapi/templates/init.py.jinja2 +6 -0
  29. pyoas-0.1.0/src/pyoas/fastapi/templates/router.py.jinja2 +68 -0
  30. pyoas-0.1.0/src/pyoas/fastapi/templates/service.py.jinja2 +51 -0
  31. pyoas-0.1.0/src/pyoas/fastapi/templates/test.py.jinja2 +156 -0
  32. pyoas-0.1.0/src/pyoas/fastapi/templates/test_service.py.jinja2 +44 -0
  33. pyoas-0.1.0/src/pyoas/fastapi/testscaffold.py +1127 -0
  34. pyoas-0.1.0/src/pyoas/models/__init__.py +3 -0
  35. pyoas-0.1.0/src/pyoas/models/classifier.py +98 -0
  36. pyoas-0.1.0/src/pyoas/models/context.py +174 -0
  37. pyoas-0.1.0/src/pyoas/models/generator.py +238 -0
  38. pyoas-0.1.0/src/pyoas/models/schema_renderer.py +324 -0
  39. pyoas-0.1.0/src/pyoas/models/templates/init.py.jinja2 +8 -0
  40. pyoas-0.1.0/src/pyoas/models/templates/model.py.jinja2 +63 -0
  41. 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,3 @@
1
+ from pyoas.claude.scaffolder import SkillScaffolder
2
+
3
+ __all__ = ["SkillScaffolder"]
@@ -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
+ }