nabu-forge 1.0.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.
- nabu_forge-1.0.0/.github/workflows/ci.yml +56 -0
- nabu_forge-1.0.0/.gitignore +58 -0
- nabu_forge-1.0.0/LICENSE +21 -0
- nabu_forge-1.0.0/PKG-INFO +228 -0
- nabu_forge-1.0.0/README.md +209 -0
- nabu_forge-1.0.0/architecture/assets/compiler-pipeline.drawio.png +0 -0
- nabu_forge-1.0.0/architecture/compiler-pipeline.md +128 -0
- nabu_forge-1.0.0/architecture/development-plan.md +151 -0
- nabu_forge-1.0.0/architecture/diagnostics-and-cli.md +142 -0
- nabu_forge-1.0.0/architecture/generated-package.md +234 -0
- nabu_forge-1.0.0/architecture/overview.md +44 -0
- nabu_forge-1.0.0/architecture/product-requirements.md +113 -0
- nabu_forge-1.0.0/pyproject.toml +43 -0
- nabu_forge-1.0.0/samples/university/nabu.toml +7 -0
- nabu_forge-1.0.0/samples/university/operations/courses.graphql +36 -0
- nabu_forge-1.0.0/samples/university/operations/fragments.graphql +42 -0
- nabu_forge-1.0.0/samples/university/operations/get_student.graphql +14 -0
- nabu_forge-1.0.0/samples/university/operations/get_student_details.graphql +38 -0
- nabu_forge-1.0.0/samples/university/operations/list_students.graphql +8 -0
- nabu_forge-1.0.0/samples/university/operations/mutations.graphql +80 -0
- nabu_forge-1.0.0/samples/university/operations/search.graphql +32 -0
- nabu_forge-1.0.0/samples/university/schema.graphqls +199 -0
- nabu_forge-1.0.0/src/nabu/__init__.py +0 -0
- nabu_forge-1.0.0/src/nabu/analysis/__init__.py +0 -0
- nabu_forge-1.0.0/src/nabu/analysis/analyser.py +289 -0
- nabu_forge-1.0.0/src/nabu/analysis/index.py +61 -0
- nabu_forge-1.0.0/src/nabu/backends/__init__.py +0 -0
- nabu_forge-1.0.0/src/nabu/backends/python/__init__.py +7 -0
- nabu_forge-1.0.0/src/nabu/backends/python/codegen/__init__.py +0 -0
- nabu_forge-1.0.0/src/nabu/backends/python/codegen/client_gen.py +124 -0
- nabu_forge-1.0.0/src/nabu/backends/python/codegen/engine.py +31 -0
- nabu_forge-1.0.0/src/nabu/backends/python/codegen/enum_gen.py +13 -0
- nabu_forge-1.0.0/src/nabu/backends/python/codegen/exceptions_gen.py +5 -0
- nabu_forge-1.0.0/src/nabu/backends/python/codegen/exports_gen.py +26 -0
- nabu_forge-1.0.0/src/nabu/backends/python/codegen/fields.py +115 -0
- nabu_forge-1.0.0/src/nabu/backends/python/codegen/input_gen.py +19 -0
- nabu_forge-1.0.0/src/nabu/backends/python/codegen/model_gen.py +44 -0
- nabu_forge-1.0.0/src/nabu/backends/python/codegen/operation_gen.py +271 -0
- nabu_forge-1.0.0/src/nabu/backends/python/codegen/ordering.py +36 -0
- nabu_forge-1.0.0/src/nabu/backends/python/codegen/scalars_gen.py +25 -0
- nabu_forge-1.0.0/src/nabu/backends/python/codegen/templates/client.py.jinja +57 -0
- nabu_forge-1.0.0/src/nabu/backends/python/codegen/templates/enum.py.jinja +11 -0
- nabu_forge-1.0.0/src/nabu/backends/python/codegen/templates/exceptions.py.jinja +11 -0
- nabu_forge-1.0.0/src/nabu/backends/python/codegen/templates/init.py.jinja +10 -0
- nabu_forge-1.0.0/src/nabu/backends/python/codegen/templates/input.py.jinja +17 -0
- nabu_forge-1.0.0/src/nabu/backends/python/codegen/templates/model.py.jinja +26 -0
- nabu_forge-1.0.0/src/nabu/backends/python/codegen/templates/operation_model.py.jinja +35 -0
- nabu_forge-1.0.0/src/nabu/backends/python/codegen/templates/scalars.py.jinja +10 -0
- nabu_forge-1.0.0/src/nabu/backends/python/codegen/templates/transport.py.jinja +25 -0
- nabu_forge-1.0.0/src/nabu/backends/python/codegen/transport_gen.py +5 -0
- nabu_forge-1.0.0/src/nabu/backends/python/codegen/writer.py +50 -0
- nabu_forge-1.0.0/src/nabu/backends/python/mapping/__init__.py +0 -0
- nabu_forge-1.0.0/src/nabu/backends/python/mapping/imports.py +47 -0
- nabu_forge-1.0.0/src/nabu/backends/python/mapping/names.py +14 -0
- nabu_forge-1.0.0/src/nabu/backends/python/mapping/scalars.py +11 -0
- nabu_forge-1.0.0/src/nabu/backends/python/mapping/type_mapper.py +15 -0
- nabu_forge-1.0.0/src/nabu/cli/__init__.py +17 -0
- nabu_forge-1.0.0/src/nabu/cli/callbacks.py +13 -0
- nabu_forge-1.0.0/src/nabu/cli/commands.py +75 -0
- nabu_forge-1.0.0/src/nabu/config/__init__.py +3 -0
- nabu_forge-1.0.0/src/nabu/config/loader.py +55 -0
- nabu_forge-1.0.0/src/nabu/context.py +96 -0
- nabu_forge-1.0.0/src/nabu/diagnostics/__init__.py +6 -0
- nabu_forge-1.0.0/src/nabu/diagnostics/codes.py +29 -0
- nabu_forge-1.0.0/src/nabu/diagnostics/diagnostic.py +45 -0
- nabu_forge-1.0.0/src/nabu/diagnostics/reporter.py +29 -0
- nabu_forge-1.0.0/src/nabu/diagnostics/result.py +16 -0
- nabu_forge-1.0.0/src/nabu/graphql/__init__.py +0 -0
- nabu_forge-1.0.0/src/nabu/graphql/schema_utils.py +15 -0
- nabu_forge-1.0.0/src/nabu/ir/__init__.py +0 -0
- nabu_forge-1.0.0/src/nabu/ir/definitions.py +62 -0
- nabu_forge-1.0.0/src/nabu/ir/document.py +26 -0
- nabu_forge-1.0.0/src/nabu/ir/location.py +17 -0
- nabu_forge-1.0.0/src/nabu/ir/operations.py +70 -0
- nabu_forge-1.0.0/src/nabu/ir/transformer.py +250 -0
- nabu_forge-1.0.0/src/nabu/ir/types.py +37 -0
- nabu_forge-1.0.0/src/nabu/loader/__init__.py +3 -0
- nabu_forge-1.0.0/src/nabu/loader/files.py +39 -0
- nabu_forge-1.0.0/src/nabu/log.py +3 -0
- nabu_forge-1.0.0/src/nabu/parser/__init__.py +5 -0
- nabu_forge-1.0.0/src/nabu/parser/operations.py +46 -0
- nabu_forge-1.0.0/src/nabu/parser/schema.py +30 -0
- nabu_forge-1.0.0/src/nabu/parser/traverse.py +65 -0
- nabu_forge-1.0.0/tests/__init__.py +0 -0
- nabu_forge-1.0.0/tests/analysis/__init__.py +0 -0
- nabu_forge-1.0.0/tests/analysis/test_analyser.py +237 -0
- nabu_forge-1.0.0/tests/backends/__init__.py +0 -0
- nabu_forge-1.0.0/tests/backends/python/__init__.py +0 -0
- nabu_forge-1.0.0/tests/backends/python/codegen/__init__.py +0 -0
- nabu_forge-1.0.0/tests/backends/python/codegen/test_client_gen.py +133 -0
- nabu_forge-1.0.0/tests/backends/python/codegen/test_generators.py +713 -0
- nabu_forge-1.0.0/tests/backends/python/mapping/__init__.py +0 -0
- nabu_forge-1.0.0/tests/backends/python/mapping/test_imports.py +59 -0
- nabu_forge-1.0.0/tests/backends/python/mapping/test_names.py +42 -0
- nabu_forge-1.0.0/tests/backends/python/mapping/test_type_mapper.py +82 -0
- nabu_forge-1.0.0/tests/ir/__init__.py +0 -0
- nabu_forge-1.0.0/tests/ir/test_transformer.py +175 -0
- nabu_forge-1.0.0/tests/ir/test_types.py +43 -0
- nabu_forge-1.0.0/tests/parser/__init__.py +0 -0
- nabu_forge-1.0.0/tests/parser/test_operations.py +91 -0
- nabu_forge-1.0.0/tests/parser/test_schema.py +53 -0
- nabu_forge-1.0.0/tests/test_cli.py +79 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ master ]
|
|
6
|
+
tags: [ "v*" ]
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: [ "3.11", "3.12", "3.13" ]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
19
|
+
uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ matrix.python-version }}
|
|
22
|
+
|
|
23
|
+
- name: Install project and dev dependencies
|
|
24
|
+
run: pip install -e ".[dev]"
|
|
25
|
+
|
|
26
|
+
- name: Lint (ruff)
|
|
27
|
+
run: ruff check src/
|
|
28
|
+
|
|
29
|
+
- name: Format check (ruff)
|
|
30
|
+
run: ruff format --check src/
|
|
31
|
+
|
|
32
|
+
- name: Tests
|
|
33
|
+
run: pytest -q
|
|
34
|
+
|
|
35
|
+
publish:
|
|
36
|
+
name: Publish to PyPI
|
|
37
|
+
needs: test
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
40
|
+
environment: pypi
|
|
41
|
+
permissions:
|
|
42
|
+
id-token: write
|
|
43
|
+
steps:
|
|
44
|
+
- uses: actions/checkout@v4
|
|
45
|
+
|
|
46
|
+
- uses: actions/setup-python@v5
|
|
47
|
+
with:
|
|
48
|
+
python-version: "3.12"
|
|
49
|
+
|
|
50
|
+
- name: Build distribution
|
|
51
|
+
run: |
|
|
52
|
+
pip install build
|
|
53
|
+
python -m build
|
|
54
|
+
|
|
55
|
+
- name: Publish to PyPI
|
|
56
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.pyo
|
|
5
|
+
*.pyd
|
|
6
|
+
*.egg
|
|
7
|
+
*.egg-info/
|
|
8
|
+
dist/
|
|
9
|
+
build/
|
|
10
|
+
wheels/
|
|
11
|
+
sdist/
|
|
12
|
+
.eggs/
|
|
13
|
+
MANIFEST
|
|
14
|
+
|
|
15
|
+
# Virtual environments
|
|
16
|
+
.venv/
|
|
17
|
+
venv/
|
|
18
|
+
env/
|
|
19
|
+
ENV/
|
|
20
|
+
|
|
21
|
+
# uv
|
|
22
|
+
.uv/
|
|
23
|
+
uv.lock
|
|
24
|
+
|
|
25
|
+
# Distribution / packaging
|
|
26
|
+
pip-wheel-metadata/
|
|
27
|
+
share/python-wheels/
|
|
28
|
+
|
|
29
|
+
# Testing
|
|
30
|
+
.pytest_cache/
|
|
31
|
+
.coverage
|
|
32
|
+
coverage.xml
|
|
33
|
+
htmlcov/
|
|
34
|
+
.tox/
|
|
35
|
+
|
|
36
|
+
# Type checking
|
|
37
|
+
.mypy_cache/
|
|
38
|
+
.pyright/
|
|
39
|
+
pyrightconfig.json
|
|
40
|
+
|
|
41
|
+
# Ruff
|
|
42
|
+
.ruff_cache/
|
|
43
|
+
|
|
44
|
+
# IDEs
|
|
45
|
+
.idea/
|
|
46
|
+
.vscode/
|
|
47
|
+
*.iml
|
|
48
|
+
|
|
49
|
+
# macOS
|
|
50
|
+
.DS_Store
|
|
51
|
+
|
|
52
|
+
# Generated output (development artefacts, not committed)
|
|
53
|
+
generated_client/
|
|
54
|
+
|
|
55
|
+
architecture/phases/
|
|
56
|
+
architecture/assets/*.drawio
|
|
57
|
+
|
|
58
|
+
samples/euporie/
|
nabu_forge-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ivan Nikolov
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: nabu-forge
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A schema-driven compiler that transforms GraphQL SDL and operation definitions into type-safe Python client packages.
|
|
5
|
+
Project-URL: Repository, https://github.com/ivanNikolov2910/nabu-forge
|
|
6
|
+
Author: Ivan Nikolov
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Requires-Dist: graphql-core>=3.2
|
|
11
|
+
Requires-Dist: httpx>=0.26.2
|
|
12
|
+
Requires-Dist: jinja2>=3.1.6
|
|
13
|
+
Requires-Dist: pydantic>=2.0
|
|
14
|
+
Requires-Dist: typer>=0.27.1
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest>=9.1.1; extra == 'dev'
|
|
17
|
+
Requires-Dist: ruff>=0.16.1; extra == 'dev'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# Nabu Forge
|
|
21
|
+
|
|
22
|
+
A schema-driven compiler that transforms GraphQL SDL and operation definitions into type-safe Python client packages.
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
GraphQL SDL + operations -> Nabu Forge -> Typed Python client package
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## What it does
|
|
29
|
+
|
|
30
|
+
Given a GraphQL schema and operation documents, Nabu Forge generates a complete Python package with:
|
|
31
|
+
|
|
32
|
+
- Pydantic v2 models for every response shape, with correct list/nullability handling
|
|
33
|
+
- Discriminated unions for interfaces and polymorphic types (via `__typename`)
|
|
34
|
+
- Enum definitions (`str, Enum`)
|
|
35
|
+
- Input models with camelCase -> snake_case aliasing for correct wire serialization
|
|
36
|
+
- Async client methods with typed variables and response deserialization
|
|
37
|
+
- Custom scalar mappings via config
|
|
38
|
+
- Async httpx transport with configurable timeout and persistent connection
|
|
39
|
+
- Compiler-style diagnostics with file/line/column and hints
|
|
40
|
+
|
|
41
|
+
### Example generated usage
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from generated_client import Client
|
|
45
|
+
|
|
46
|
+
# basic usage
|
|
47
|
+
client = Client(url="https://example.com/graphql", headers={"Authorization": "Bearer token"})
|
|
48
|
+
result = await client.get_student(id="123")
|
|
49
|
+
print(result.student.id, result.student.status)
|
|
50
|
+
|
|
51
|
+
# async context manager for clean connection teardown
|
|
52
|
+
async with Client(url="https://example.com/graphql") as client:
|
|
53
|
+
result = await client.get_student(id="123")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install nabu-forge
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Or for development:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
cd nabu-forge
|
|
66
|
+
pip install -e .
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Usage
|
|
70
|
+
|
|
71
|
+
### 1. Write a `nabu.toml`
|
|
72
|
+
|
|
73
|
+
```toml
|
|
74
|
+
schema = "schema.graphqls"
|
|
75
|
+
operations = "operations/"
|
|
76
|
+
output = "generated_client"
|
|
77
|
+
|
|
78
|
+
[scalars]
|
|
79
|
+
DateTime = "datetime.datetime"
|
|
80
|
+
JSON = "typing.Any"
|
|
81
|
+
Void = "None"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 2. Run
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
nabu validate --config nabu.toml # validate without writing files
|
|
88
|
+
nabu inspect --schema schema.graphqls # show schema structure
|
|
89
|
+
nabu generate --config nabu.toml # generate the client package
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Configuration
|
|
93
|
+
|
|
94
|
+
| Field | Description |
|
|
95
|
+
|--------------|-------------------------------------------------|
|
|
96
|
+
| `schema` | Path to the GraphQL SDL file |
|
|
97
|
+
| `operations` | Directory containing `.graphql` operation files |
|
|
98
|
+
| `output` | Output directory for the generated package |
|
|
99
|
+
| `[scalars]` | Python type mappings for custom scalars |
|
|
100
|
+
|
|
101
|
+
All paths are relative to `nabu.toml`.
|
|
102
|
+
|
|
103
|
+
## GraphQL → Python type mapping
|
|
104
|
+
|
|
105
|
+
| GraphQL | Python |
|
|
106
|
+
|---------------|-----------------------------|
|
|
107
|
+
| `String!` | `str` |
|
|
108
|
+
| `String` | `str \| None` |
|
|
109
|
+
| `[String!]!` | `list[str]` |
|
|
110
|
+
| `[String]` | `list[str \| None] \| None` |
|
|
111
|
+
| `Int!` | `int` |
|
|
112
|
+
| `Float!` | `float` |
|
|
113
|
+
| `Boolean!` | `bool` |
|
|
114
|
+
| `ID!` | `str` |
|
|
115
|
+
| Custom scalar | configured via `[scalars]` |
|
|
116
|
+
|
|
117
|
+
camelCase GraphQL field names are converted to `snake_case`. A `Field(alias="camelCase")` is emitted automatically so
|
|
118
|
+
generated models deserialize real GraphQL responses correctly.
|
|
119
|
+
|
|
120
|
+
## Polymorphic types (interfaces and unions)
|
|
121
|
+
|
|
122
|
+
Inline fragments generate discriminated union aliases using `__typename` as the discriminator. Shared interface-level
|
|
123
|
+
fields appear in every member class — you do not need to repeat them in each fragment.
|
|
124
|
+
|
|
125
|
+
```graphql
|
|
126
|
+
query GetJob($id: String!) {
|
|
127
|
+
job(id: $id) {
|
|
128
|
+
jobId # shared — emitted in ALL member classes
|
|
129
|
+
state
|
|
130
|
+
... on ExecutionJob { task { name } }
|
|
131
|
+
... on CollectionJob { }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Generated:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
class GetJobJobExecutionJob(BaseModel):
|
|
140
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
141
|
+
typename: Literal["ExecutionJob"] = Field(alias="__typename")
|
|
142
|
+
job_id: str = Field(..., alias="jobId") # shared
|
|
143
|
+
state: JobState # shared
|
|
144
|
+
task: GetJobJobExecutionJobTask | None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
GetJobJob = Annotated[
|
|
148
|
+
GetJobJobExecutionJob | GetJobJobCollectionJob,
|
|
149
|
+
Field(discriminator="typename"),
|
|
150
|
+
]
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`__typename` is injected automatically in the sent document — no need to add it to your `.graphql` files.
|
|
154
|
+
|
|
155
|
+
## Diagnostics
|
|
156
|
+
|
|
157
|
+
Nabu Forge emits compiler-style diagnostics. Errors abort generation; warnings print and continue.
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
error[E023]: Field 'ghost' does not exist on type 'Query'.
|
|
161
|
+
operations/bad.graphql:2:5
|
|
162
|
+
|
|
163
|
+
warning[E029]: Field 'job' returns polymorphic type 'Job' but has no inline fragments.
|
|
164
|
+
operations/get_job.graphql:3:5
|
|
165
|
+
hint: Add '... on ConcreteType { fields }' for each possible type.
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
| Code | Severity | Meaning |
|
|
169
|
+
|------|----------|--------------------------------------------|
|
|
170
|
+
| E001 | error | Config file not found |
|
|
171
|
+
| E002 | error | Missing required config fields |
|
|
172
|
+
| E010 | error | GraphQL syntax error |
|
|
173
|
+
| E020 | error | Unknown type reference |
|
|
174
|
+
| E021 | error | Unmapped custom scalar |
|
|
175
|
+
| E022 | error | Unknown variable type |
|
|
176
|
+
| E023 | error | Unknown field / direct field on union type |
|
|
177
|
+
| E024 | error | Unknown fragment spread |
|
|
178
|
+
| E025 | error | Bad inline fragment target |
|
|
179
|
+
| E026 | error | Generated name collision |
|
|
180
|
+
| E028 | error | Unsupported feature (subscriptions) |
|
|
181
|
+
| E030 | error | File write error |
|
|
182
|
+
|
|
183
|
+
## Compiler pipeline
|
|
184
|
+
|
|
185
|
+
<img src="architecture/assets/compiler-pipeline.drawio.png" alt="Nabu Forge compiler pipeline"/>
|
|
186
|
+
|
|
187
|
+
## Supported / not supported
|
|
188
|
+
|
|
189
|
+
**Supported:**
|
|
190
|
+
|
|
191
|
+
- Object types, input types, enums, interfaces, unions
|
|
192
|
+
- Lists and nullability (all 8 combinations)
|
|
193
|
+
- Custom scalars via `[scalars]` config
|
|
194
|
+
- Queries and mutations
|
|
195
|
+
- Named fragments (inlined at codegen time)
|
|
196
|
+
- Inline fragments → discriminated unions via `__typename`
|
|
197
|
+
- Shared interface fields alongside inline fragments
|
|
198
|
+
- Field aliases including two aliases of the same underlying field
|
|
199
|
+
- camelCase → snake_case with `Field(alias=...)`
|
|
200
|
+
- Async httpx transport with `timeout`, persistent `AsyncClient`, `async with` support
|
|
201
|
+
|
|
202
|
+
**Not supported:**
|
|
203
|
+
|
|
204
|
+
- Subscriptions (E028 at semantic analysis)
|
|
205
|
+
- File uploads (multipart)
|
|
206
|
+
- Query batching
|
|
207
|
+
- `@skip` / `@include` directives
|
|
208
|
+
- Variable default values
|
|
209
|
+
- Introspection-based generation (SDL file required)
|
|
210
|
+
|
|
211
|
+
## Development
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
pip install -e .
|
|
215
|
+
|
|
216
|
+
ruff check src/ # lint
|
|
217
|
+
pytest # 144 tests
|
|
218
|
+
nabu generate --config samples/university/nabu.toml
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Architecture docs
|
|
222
|
+
|
|
223
|
+
- [`architecture/overview.md`](architecture/overview.md) — project summary and goals
|
|
224
|
+
- [`architecture/compiler-pipeline.md`](architecture/compiler-pipeline.md) — all pipeline stages
|
|
225
|
+
- [`architecture/generated-package.md`](architecture/generated-package.md) — generated package layout
|
|
226
|
+
- [`architecture/diagnostics-and-cli.md`](architecture/diagnostics-and-cli.md) — CLI and diagnostics design
|
|
227
|
+
- [`architecture/development-plan.md`](architecture/development-plan.md) — implementation phases
|
|
228
|
+
- [`architecture/product-requirements.md`](architecture/product-requirements.md) — scope and definition of done
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# Nabu Forge
|
|
2
|
+
|
|
3
|
+
A schema-driven compiler that transforms GraphQL SDL and operation definitions into type-safe Python client packages.
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
GraphQL SDL + operations -> Nabu Forge -> Typed Python client package
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## What it does
|
|
10
|
+
|
|
11
|
+
Given a GraphQL schema and operation documents, Nabu Forge generates a complete Python package with:
|
|
12
|
+
|
|
13
|
+
- Pydantic v2 models for every response shape, with correct list/nullability handling
|
|
14
|
+
- Discriminated unions for interfaces and polymorphic types (via `__typename`)
|
|
15
|
+
- Enum definitions (`str, Enum`)
|
|
16
|
+
- Input models with camelCase -> snake_case aliasing for correct wire serialization
|
|
17
|
+
- Async client methods with typed variables and response deserialization
|
|
18
|
+
- Custom scalar mappings via config
|
|
19
|
+
- Async httpx transport with configurable timeout and persistent connection
|
|
20
|
+
- Compiler-style diagnostics with file/line/column and hints
|
|
21
|
+
|
|
22
|
+
### Example generated usage
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from generated_client import Client
|
|
26
|
+
|
|
27
|
+
# basic usage
|
|
28
|
+
client = Client(url="https://example.com/graphql", headers={"Authorization": "Bearer token"})
|
|
29
|
+
result = await client.get_student(id="123")
|
|
30
|
+
print(result.student.id, result.student.status)
|
|
31
|
+
|
|
32
|
+
# async context manager for clean connection teardown
|
|
33
|
+
async with Client(url="https://example.com/graphql") as client:
|
|
34
|
+
result = await client.get_student(id="123")
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install nabu-forge
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or for development:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
cd nabu-forge
|
|
47
|
+
pip install -e .
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
### 1. Write a `nabu.toml`
|
|
53
|
+
|
|
54
|
+
```toml
|
|
55
|
+
schema = "schema.graphqls"
|
|
56
|
+
operations = "operations/"
|
|
57
|
+
output = "generated_client"
|
|
58
|
+
|
|
59
|
+
[scalars]
|
|
60
|
+
DateTime = "datetime.datetime"
|
|
61
|
+
JSON = "typing.Any"
|
|
62
|
+
Void = "None"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 2. Run
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
nabu validate --config nabu.toml # validate without writing files
|
|
69
|
+
nabu inspect --schema schema.graphqls # show schema structure
|
|
70
|
+
nabu generate --config nabu.toml # generate the client package
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Configuration
|
|
74
|
+
|
|
75
|
+
| Field | Description |
|
|
76
|
+
|--------------|-------------------------------------------------|
|
|
77
|
+
| `schema` | Path to the GraphQL SDL file |
|
|
78
|
+
| `operations` | Directory containing `.graphql` operation files |
|
|
79
|
+
| `output` | Output directory for the generated package |
|
|
80
|
+
| `[scalars]` | Python type mappings for custom scalars |
|
|
81
|
+
|
|
82
|
+
All paths are relative to `nabu.toml`.
|
|
83
|
+
|
|
84
|
+
## GraphQL → Python type mapping
|
|
85
|
+
|
|
86
|
+
| GraphQL | Python |
|
|
87
|
+
|---------------|-----------------------------|
|
|
88
|
+
| `String!` | `str` |
|
|
89
|
+
| `String` | `str \| None` |
|
|
90
|
+
| `[String!]!` | `list[str]` |
|
|
91
|
+
| `[String]` | `list[str \| None] \| None` |
|
|
92
|
+
| `Int!` | `int` |
|
|
93
|
+
| `Float!` | `float` |
|
|
94
|
+
| `Boolean!` | `bool` |
|
|
95
|
+
| `ID!` | `str` |
|
|
96
|
+
| Custom scalar | configured via `[scalars]` |
|
|
97
|
+
|
|
98
|
+
camelCase GraphQL field names are converted to `snake_case`. A `Field(alias="camelCase")` is emitted automatically so
|
|
99
|
+
generated models deserialize real GraphQL responses correctly.
|
|
100
|
+
|
|
101
|
+
## Polymorphic types (interfaces and unions)
|
|
102
|
+
|
|
103
|
+
Inline fragments generate discriminated union aliases using `__typename` as the discriminator. Shared interface-level
|
|
104
|
+
fields appear in every member class — you do not need to repeat them in each fragment.
|
|
105
|
+
|
|
106
|
+
```graphql
|
|
107
|
+
query GetJob($id: String!) {
|
|
108
|
+
job(id: $id) {
|
|
109
|
+
jobId # shared — emitted in ALL member classes
|
|
110
|
+
state
|
|
111
|
+
... on ExecutionJob { task { name } }
|
|
112
|
+
... on CollectionJob { }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Generated:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
class GetJobJobExecutionJob(BaseModel):
|
|
121
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
122
|
+
typename: Literal["ExecutionJob"] = Field(alias="__typename")
|
|
123
|
+
job_id: str = Field(..., alias="jobId") # shared
|
|
124
|
+
state: JobState # shared
|
|
125
|
+
task: GetJobJobExecutionJobTask | None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
GetJobJob = Annotated[
|
|
129
|
+
GetJobJobExecutionJob | GetJobJobCollectionJob,
|
|
130
|
+
Field(discriminator="typename"),
|
|
131
|
+
]
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`__typename` is injected automatically in the sent document — no need to add it to your `.graphql` files.
|
|
135
|
+
|
|
136
|
+
## Diagnostics
|
|
137
|
+
|
|
138
|
+
Nabu Forge emits compiler-style diagnostics. Errors abort generation; warnings print and continue.
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
error[E023]: Field 'ghost' does not exist on type 'Query'.
|
|
142
|
+
operations/bad.graphql:2:5
|
|
143
|
+
|
|
144
|
+
warning[E029]: Field 'job' returns polymorphic type 'Job' but has no inline fragments.
|
|
145
|
+
operations/get_job.graphql:3:5
|
|
146
|
+
hint: Add '... on ConcreteType { fields }' for each possible type.
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
| Code | Severity | Meaning |
|
|
150
|
+
|------|----------|--------------------------------------------|
|
|
151
|
+
| E001 | error | Config file not found |
|
|
152
|
+
| E002 | error | Missing required config fields |
|
|
153
|
+
| E010 | error | GraphQL syntax error |
|
|
154
|
+
| E020 | error | Unknown type reference |
|
|
155
|
+
| E021 | error | Unmapped custom scalar |
|
|
156
|
+
| E022 | error | Unknown variable type |
|
|
157
|
+
| E023 | error | Unknown field / direct field on union type |
|
|
158
|
+
| E024 | error | Unknown fragment spread |
|
|
159
|
+
| E025 | error | Bad inline fragment target |
|
|
160
|
+
| E026 | error | Generated name collision |
|
|
161
|
+
| E028 | error | Unsupported feature (subscriptions) |
|
|
162
|
+
| E030 | error | File write error |
|
|
163
|
+
|
|
164
|
+
## Compiler pipeline
|
|
165
|
+
|
|
166
|
+
<img src="architecture/assets/compiler-pipeline.drawio.png" alt="Nabu Forge compiler pipeline"/>
|
|
167
|
+
|
|
168
|
+
## Supported / not supported
|
|
169
|
+
|
|
170
|
+
**Supported:**
|
|
171
|
+
|
|
172
|
+
- Object types, input types, enums, interfaces, unions
|
|
173
|
+
- Lists and nullability (all 8 combinations)
|
|
174
|
+
- Custom scalars via `[scalars]` config
|
|
175
|
+
- Queries and mutations
|
|
176
|
+
- Named fragments (inlined at codegen time)
|
|
177
|
+
- Inline fragments → discriminated unions via `__typename`
|
|
178
|
+
- Shared interface fields alongside inline fragments
|
|
179
|
+
- Field aliases including two aliases of the same underlying field
|
|
180
|
+
- camelCase → snake_case with `Field(alias=...)`
|
|
181
|
+
- Async httpx transport with `timeout`, persistent `AsyncClient`, `async with` support
|
|
182
|
+
|
|
183
|
+
**Not supported:**
|
|
184
|
+
|
|
185
|
+
- Subscriptions (E028 at semantic analysis)
|
|
186
|
+
- File uploads (multipart)
|
|
187
|
+
- Query batching
|
|
188
|
+
- `@skip` / `@include` directives
|
|
189
|
+
- Variable default values
|
|
190
|
+
- Introspection-based generation (SDL file required)
|
|
191
|
+
|
|
192
|
+
## Development
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
pip install -e .
|
|
196
|
+
|
|
197
|
+
ruff check src/ # lint
|
|
198
|
+
pytest # 144 tests
|
|
199
|
+
nabu generate --config samples/university/nabu.toml
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Architecture docs
|
|
203
|
+
|
|
204
|
+
- [`architecture/overview.md`](architecture/overview.md) — project summary and goals
|
|
205
|
+
- [`architecture/compiler-pipeline.md`](architecture/compiler-pipeline.md) — all pipeline stages
|
|
206
|
+
- [`architecture/generated-package.md`](architecture/generated-package.md) — generated package layout
|
|
207
|
+
- [`architecture/diagnostics-and-cli.md`](architecture/diagnostics-and-cli.md) — CLI and diagnostics design
|
|
208
|
+
- [`architecture/development-plan.md`](architecture/development-plan.md) — implementation phases
|
|
209
|
+
- [`architecture/product-requirements.md`](architecture/product-requirements.md) — scope and definition of done
|
|
Binary file
|