mygenx-fastql 0.0.1__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.
- mygenx_fastql-0.0.1/.gitignore +13 -0
- mygenx_fastql-0.0.1/CHANGELOG.md +14 -0
- mygenx_fastql-0.0.1/PKG-INFO +214 -0
- mygenx_fastql-0.0.1/README.md +170 -0
- mygenx_fastql-0.0.1/fastql/__init__.py +143 -0
- mygenx_fastql-0.0.1/fastql/__main__.py +6 -0
- mygenx_fastql-0.0.1/fastql/cli.py +130 -0
- mygenx_fastql-0.0.1/fastql/context.py +211 -0
- mygenx_fastql-0.0.1/fastql/dataloader.py +229 -0
- mygenx_fastql-0.0.1/fastql/decorators/__init__.py +41 -0
- mygenx_fastql-0.0.1/fastql/decorators/annotations.py +215 -0
- mygenx_fastql-0.0.1/fastql/decorators/construct.py +92 -0
- mygenx_fastql-0.0.1/fastql/decorators/definition.py +294 -0
- mygenx_fastql-0.0.1/fastql/decorators/enum.py +38 -0
- mygenx_fastql-0.0.1/fastql/decorators/field.py +164 -0
- mygenx_fastql-0.0.1/fastql/decorators/object.py +31 -0
- mygenx_fastql-0.0.1/fastql/decorators/operations.py +39 -0
- mygenx_fastql-0.0.1/fastql/decorators/registry.py +76 -0
- mygenx_fastql-0.0.1/fastql/decorators/scalar.py +45 -0
- mygenx_fastql-0.0.1/fastql/decorators/union.py +41 -0
- mygenx_fastql-0.0.1/fastql/errors.py +76 -0
- mygenx_fastql-0.0.1/fastql/execution/__init__.py +11 -0
- mygenx_fastql-0.0.1/fastql/execution/collect_fields.py +107 -0
- mygenx_fastql-0.0.1/fastql/execution/execute.py +796 -0
- mygenx_fastql-0.0.1/fastql/execution/values.py +224 -0
- mygenx_fastql-0.0.1/fastql/extensions.py +157 -0
- mygenx_fastql-0.0.1/fastql/integrations/__init__.py +26 -0
- mygenx_fastql-0.0.1/fastql/integrations/_sync.py +23 -0
- mygenx_fastql-0.0.1/fastql/integrations/asgi.py +87 -0
- mygenx_fastql-0.0.1/fastql/integrations/django.py +101 -0
- mygenx_fastql-0.0.1/fastql/integrations/fastapi.py +68 -0
- mygenx_fastql-0.0.1/fastql/integrations/flask.py +62 -0
- mygenx_fastql-0.0.1/fastql/integrations/http.py +421 -0
- mygenx_fastql-0.0.1/fastql/integrations/starlette.py +56 -0
- mygenx_fastql-0.0.1/fastql/introspection.py +320 -0
- mygenx_fastql-0.0.1/fastql/language/__init__.py +18 -0
- mygenx_fastql-0.0.1/fastql/language/ast.py +235 -0
- mygenx_fastql-0.0.1/fastql/language/lexer.py +314 -0
- mygenx_fastql-0.0.1/fastql/language/parser.py +393 -0
- mygenx_fastql-0.0.1/fastql/language/printer.py +172 -0
- mygenx_fastql-0.0.1/fastql/language/source.py +63 -0
- mygenx_fastql-0.0.1/fastql/playground.py +63 -0
- mygenx_fastql-0.0.1/fastql/registry.py +30 -0
- mygenx_fastql-0.0.1/fastql/schema_builder.py +370 -0
- mygenx_fastql-0.0.1/fastql/sdl.py +149 -0
- mygenx_fastql-0.0.1/fastql/server.py +221 -0
- mygenx_fastql-0.0.1/fastql/testing.py +93 -0
- mygenx_fastql-0.0.1/fastql/types/__init__.py +47 -0
- mygenx_fastql-0.0.1/fastql/types/definition.py +168 -0
- mygenx_fastql-0.0.1/fastql/types/scalars.py +113 -0
- mygenx_fastql-0.0.1/fastql/types/schema.py +164 -0
- mygenx_fastql-0.0.1/fastql/types/wrappers.py +33 -0
- mygenx_fastql-0.0.1/fastql/validation/__init__.py +5 -0
- mygenx_fastql-0.0.1/fastql/validation/rules.py +696 -0
- mygenx_fastql-0.0.1/pyproject.toml +77 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- A shared dependency-free GraphQL-over-HTTP handler and request-aware `HTTPContext`.
|
|
8
|
+
- Generic ASGI, Starlette, FastAPI, Flask, and Django integrations.
|
|
9
|
+
- Independent `asgi`, `starlette`, `fastapi`, `flask`, and `django` installation extras, plus `all`.
|
|
10
|
+
- Configurable GraphiQL, SDL, and introspection routes for production adapters.
|
|
11
|
+
|
|
12
|
+
### Deferred
|
|
13
|
+
|
|
14
|
+
- WebSocket subscriptions, multipart uploads, batching, persisted queries, and incremental delivery remain outside the initial integration contract.
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mygenx-fastql
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A code-first, decorator-driven GraphQL framework for Python with a hand-built engine and a web-framework-agnostic core.
|
|
5
|
+
Project-URL: Homepage, https://github.com/MyGenX/FastQL
|
|
6
|
+
Project-URL: Documentation, https://github.com/MyGenX/FastQL/tree/main/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/MyGenX/FastQL
|
|
8
|
+
Project-URL: Issues, https://github.com/MyGenX/FastQL/issues
|
|
9
|
+
Author: FastQL
|
|
10
|
+
License: MIT
|
|
11
|
+
Keywords: async,decorators,framework-agnostic,graphql,schema
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Provides-Extra: all
|
|
24
|
+
Requires-Dist: django<6,>=5.2; extra == 'all'
|
|
25
|
+
Requires-Dist: fastapi<1,>=0.129; extra == 'all'
|
|
26
|
+
Requires-Dist: flask<4,>=3.1; extra == 'all'
|
|
27
|
+
Requires-Dist: starlette<2,>=0.50; extra == 'all'
|
|
28
|
+
Provides-Extra: asgi
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
31
|
+
Requires-Dist: hatchling>=1.26; extra == 'dev'
|
|
32
|
+
Requires-Dist: httpx>=0.28; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
35
|
+
Provides-Extra: django
|
|
36
|
+
Requires-Dist: django<6,>=5.2; extra == 'django'
|
|
37
|
+
Provides-Extra: fastapi
|
|
38
|
+
Requires-Dist: fastapi<1,>=0.129; extra == 'fastapi'
|
|
39
|
+
Provides-Extra: flask
|
|
40
|
+
Requires-Dist: flask<4,>=3.1; extra == 'flask'
|
|
41
|
+
Provides-Extra: starlette
|
|
42
|
+
Requires-Dist: starlette<2,>=0.50; extra == 'starlette'
|
|
43
|
+
Description-Content-Type: text/markdown
|
|
44
|
+
|
|
45
|
+
# FastQL
|
|
46
|
+
|
|
47
|
+
A code-first, decorator-driven GraphQL framework for Python with a **hand-built engine**
|
|
48
|
+
(lexer → parser → validator → executor), a built-in **dependency-injection / context** layer,
|
|
49
|
+
and a **web-framework-agnostic core** — zero runtime dependencies, Python 3.11+.
|
|
50
|
+
|
|
51
|
+
> Status: early development. The core engine (parse → build → validate → execute, with
|
|
52
|
+
> dependency injection and introspection) is in place.
|
|
53
|
+
|
|
54
|
+
**Documentation:** Start with the [FastQL documentation](docs/index.mdx), then use the
|
|
55
|
+
[capability catalog](docs/specifications/capability-catalog.mdx) to trace documented
|
|
56
|
+
behavior to its canonical OpenSpec requirements.
|
|
57
|
+
|
|
58
|
+
## Quickstart
|
|
59
|
+
|
|
60
|
+
Define types and operations with decorators — field and argument types come from your
|
|
61
|
+
Python type hints. Resolvers are plain functions; the executor injects arguments, the
|
|
62
|
+
parent object, resolve `info`, and the `Context` based on each resolver's signature.
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import asyncio
|
|
66
|
+
from typing import Annotated
|
|
67
|
+
from fastql import Argument, Context, Field, Query, Schema, Type, execute
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@Type
|
|
71
|
+
class User: # just fields — constructor/repr/eq are auto-generated
|
|
72
|
+
id: int
|
|
73
|
+
full_name: str
|
|
74
|
+
|
|
75
|
+
@Field
|
|
76
|
+
def loud_name(self) -> str: # exposed as loudName by default
|
|
77
|
+
return self.full_name.upper()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class AppContext(Context):
|
|
81
|
+
def __init__(self, users):
|
|
82
|
+
self.users = users
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@Query # group related queries on a class…
|
|
86
|
+
class Queries:
|
|
87
|
+
@Field
|
|
88
|
+
async def user(
|
|
89
|
+
self,
|
|
90
|
+
user_id: Annotated[int, Argument(name="id")],
|
|
91
|
+
ctx: Context,
|
|
92
|
+
) -> "User | None":
|
|
93
|
+
return ctx.users.get(user_id)
|
|
94
|
+
|
|
95
|
+
@Field
|
|
96
|
+
def ping(self) -> str: # sync resolvers work alongside async ones
|
|
97
|
+
return "pong"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
schema = Schema(query=Queries)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
async def main():
|
|
104
|
+
ctx = AppContext(users={1: User(1, "Ada Lovelace")})
|
|
105
|
+
result = await execute(schema, "{ user(id: 1) { id fullName loudName } ping }", context=ctx)
|
|
106
|
+
print(result.data) # {'user': {'id': 1, 'fullName': 'Ada Lovelace', 'loudName': 'ADA LOVELACE'}, 'ping': 'pong'}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
asyncio.run(main())
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`@Type` and `@Input` classes get generated constructors, `repr`, and equality unless they define
|
|
113
|
+
their own methods. Python snake_case fields and arguments become GraphQL camelCase by default;
|
|
114
|
+
pass `SchemaConfig(auto_camel_case=False)` to `Schema` to preserve Python names. Explicit `name=`
|
|
115
|
+
metadata always wins. `build_schema()` remains available for applications that intentionally merge
|
|
116
|
+
multiple globally decorated root classes.
|
|
117
|
+
|
|
118
|
+
`execute` returns an `ExecutionResult` with `data`, `errors`, and `extensions`, and a
|
|
119
|
+
`.formatted()` helper that produces the GraphQL-over-HTTP response shape. Introspection
|
|
120
|
+
(`__schema`, `__type`, `__typename`) is built in.
|
|
121
|
+
|
|
122
|
+
A fuller, runnable version — covering every type kind, mutations, subscriptions,
|
|
123
|
+
DataLoaders, permissions, and extensions — lives in [`examples/app`](examples/app), the
|
|
124
|
+
showcase schema that the per-framework projects under [`examples/projects`](examples/projects)
|
|
125
|
+
all reuse.
|
|
126
|
+
|
|
127
|
+
The documentation quickstart and first-schema examples are also executed by the test
|
|
128
|
+
suite from [`docs/snippets`](docs/snippets).
|
|
129
|
+
|
|
130
|
+
## Dev server & playground
|
|
131
|
+
|
|
132
|
+
Try a schema in the browser with the built-in, zero-dependency dev server:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
python -m fastql serve examples.app:schema # http://127.0.0.1:7691
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
It serves, on the default port **7691**:
|
|
139
|
+
|
|
140
|
+
| Route | Description |
|
|
141
|
+
| ----------------- | -------------------------------------------- |
|
|
142
|
+
| `GET /` | GraphiQL IDE (loaded from CDN) |
|
|
143
|
+
| `POST/GET /graphql` | the GraphQL endpoint (`{data, errors}` JSON) |
|
|
144
|
+
| `GET /schema.graphql` | the schema as SDL |
|
|
145
|
+
| `GET /schema.json` | the schema as an introspection result |
|
|
146
|
+
|
|
147
|
+
Override the binding with `--host` / `--port`. If your resolvers need a `Context`,
|
|
148
|
+
point `--context` at a value or zero-arg factory:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
python -m fastql serve examples.app:schema --context examples.app:make_context
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Or call it programmatically:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
import fastql
|
|
158
|
+
from examples.app import schema, make_context
|
|
159
|
+
|
|
160
|
+
fastql.serve(schema, port=7691, context_factory=make_context) # blocking; Ctrl-C to stop
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The dev server lives outside the agnostic core (in `fastql.server`) and only
|
|
164
|
+
consumes `build_schema` / `execute` — it is a developer convenience, not a
|
|
165
|
+
production transport (no TLS, auth, or subscriptions).
|
|
166
|
+
|
|
167
|
+
## Design at a glance
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
@Type / @Input / root class decorators ← unified type-hint-driven authoring
|
|
171
|
+
▼
|
|
172
|
+
Schema(query=...) / build_schema() ← compiles decorators into the type-system IR
|
|
173
|
+
▼
|
|
174
|
+
Type-system IR (Schema, ObjectType, Field, scalars, wrappers)
|
|
175
|
+
▼
|
|
176
|
+
execute() ── validation ── coercion ── async resolution + DI/context
|
|
177
|
+
▲
|
|
178
|
+
language: Source → Lexer → Parser → AST ← hand-built front-end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The core never imports an HTTP framework. Transports (an optional built-in dev server,
|
|
182
|
+
plus FastAPI/Django/Flask/ASGI adapters) plug in on top and consume `build_schema` / `execute`.
|
|
183
|
+
|
|
184
|
+
## Web framework integrations
|
|
185
|
+
|
|
186
|
+
Install only the framework adapter an application uses:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
pip install mygenx-fastql[fastapi] # or starlette, flask, django
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
from fastapi import FastAPI
|
|
194
|
+
from fastql.integrations.fastapi import create_fastapi_router
|
|
195
|
+
|
|
196
|
+
app = FastAPI()
|
|
197
|
+
app.include_router(create_fastapi_router(schema, graphiql=True))
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The base installation includes the dependency-free `GraphQLASGI` adapter. See
|
|
201
|
+
the [integration documentation](docs/integrations/overview.mdx) for mounting,
|
|
202
|
+
request context, endpoint configuration, and supported versions.
|
|
203
|
+
|
|
204
|
+
Runnable per-framework projects — FastAPI, Starlette, Flask, Django, and raw ASGI, each
|
|
205
|
+
mounting the **same** [`examples/app`](examples/app) schema — live under
|
|
206
|
+
[`examples/projects`](examples/projects). Reusing one schema across every adapter is the
|
|
207
|
+
proof that the core is framework-agnostic; each project differs only in a few lines of glue.
|
|
208
|
+
|
|
209
|
+
## Development
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
pip install -e ".[dev]"
|
|
213
|
+
pytest
|
|
214
|
+
```
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# FastQL
|
|
2
|
+
|
|
3
|
+
A code-first, decorator-driven GraphQL framework for Python with a **hand-built engine**
|
|
4
|
+
(lexer → parser → validator → executor), a built-in **dependency-injection / context** layer,
|
|
5
|
+
and a **web-framework-agnostic core** — zero runtime dependencies, Python 3.11+.
|
|
6
|
+
|
|
7
|
+
> Status: early development. The core engine (parse → build → validate → execute, with
|
|
8
|
+
> dependency injection and introspection) is in place.
|
|
9
|
+
|
|
10
|
+
**Documentation:** Start with the [FastQL documentation](docs/index.mdx), then use the
|
|
11
|
+
[capability catalog](docs/specifications/capability-catalog.mdx) to trace documented
|
|
12
|
+
behavior to its canonical OpenSpec requirements.
|
|
13
|
+
|
|
14
|
+
## Quickstart
|
|
15
|
+
|
|
16
|
+
Define types and operations with decorators — field and argument types come from your
|
|
17
|
+
Python type hints. Resolvers are plain functions; the executor injects arguments, the
|
|
18
|
+
parent object, resolve `info`, and the `Context` based on each resolver's signature.
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
import asyncio
|
|
22
|
+
from typing import Annotated
|
|
23
|
+
from fastql import Argument, Context, Field, Query, Schema, Type, execute
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@Type
|
|
27
|
+
class User: # just fields — constructor/repr/eq are auto-generated
|
|
28
|
+
id: int
|
|
29
|
+
full_name: str
|
|
30
|
+
|
|
31
|
+
@Field
|
|
32
|
+
def loud_name(self) -> str: # exposed as loudName by default
|
|
33
|
+
return self.full_name.upper()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AppContext(Context):
|
|
37
|
+
def __init__(self, users):
|
|
38
|
+
self.users = users
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@Query # group related queries on a class…
|
|
42
|
+
class Queries:
|
|
43
|
+
@Field
|
|
44
|
+
async def user(
|
|
45
|
+
self,
|
|
46
|
+
user_id: Annotated[int, Argument(name="id")],
|
|
47
|
+
ctx: Context,
|
|
48
|
+
) -> "User | None":
|
|
49
|
+
return ctx.users.get(user_id)
|
|
50
|
+
|
|
51
|
+
@Field
|
|
52
|
+
def ping(self) -> str: # sync resolvers work alongside async ones
|
|
53
|
+
return "pong"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
schema = Schema(query=Queries)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def main():
|
|
60
|
+
ctx = AppContext(users={1: User(1, "Ada Lovelace")})
|
|
61
|
+
result = await execute(schema, "{ user(id: 1) { id fullName loudName } ping }", context=ctx)
|
|
62
|
+
print(result.data) # {'user': {'id': 1, 'fullName': 'Ada Lovelace', 'loudName': 'ADA LOVELACE'}, 'ping': 'pong'}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
asyncio.run(main())
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`@Type` and `@Input` classes get generated constructors, `repr`, and equality unless they define
|
|
69
|
+
their own methods. Python snake_case fields and arguments become GraphQL camelCase by default;
|
|
70
|
+
pass `SchemaConfig(auto_camel_case=False)` to `Schema` to preserve Python names. Explicit `name=`
|
|
71
|
+
metadata always wins. `build_schema()` remains available for applications that intentionally merge
|
|
72
|
+
multiple globally decorated root classes.
|
|
73
|
+
|
|
74
|
+
`execute` returns an `ExecutionResult` with `data`, `errors`, and `extensions`, and a
|
|
75
|
+
`.formatted()` helper that produces the GraphQL-over-HTTP response shape. Introspection
|
|
76
|
+
(`__schema`, `__type`, `__typename`) is built in.
|
|
77
|
+
|
|
78
|
+
A fuller, runnable version — covering every type kind, mutations, subscriptions,
|
|
79
|
+
DataLoaders, permissions, and extensions — lives in [`examples/app`](examples/app), the
|
|
80
|
+
showcase schema that the per-framework projects under [`examples/projects`](examples/projects)
|
|
81
|
+
all reuse.
|
|
82
|
+
|
|
83
|
+
The documentation quickstart and first-schema examples are also executed by the test
|
|
84
|
+
suite from [`docs/snippets`](docs/snippets).
|
|
85
|
+
|
|
86
|
+
## Dev server & playground
|
|
87
|
+
|
|
88
|
+
Try a schema in the browser with the built-in, zero-dependency dev server:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
python -m fastql serve examples.app:schema # http://127.0.0.1:7691
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
It serves, on the default port **7691**:
|
|
95
|
+
|
|
96
|
+
| Route | Description |
|
|
97
|
+
| ----------------- | -------------------------------------------- |
|
|
98
|
+
| `GET /` | GraphiQL IDE (loaded from CDN) |
|
|
99
|
+
| `POST/GET /graphql` | the GraphQL endpoint (`{data, errors}` JSON) |
|
|
100
|
+
| `GET /schema.graphql` | the schema as SDL |
|
|
101
|
+
| `GET /schema.json` | the schema as an introspection result |
|
|
102
|
+
|
|
103
|
+
Override the binding with `--host` / `--port`. If your resolvers need a `Context`,
|
|
104
|
+
point `--context` at a value or zero-arg factory:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
python -m fastql serve examples.app:schema --context examples.app:make_context
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Or call it programmatically:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
import fastql
|
|
114
|
+
from examples.app import schema, make_context
|
|
115
|
+
|
|
116
|
+
fastql.serve(schema, port=7691, context_factory=make_context) # blocking; Ctrl-C to stop
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The dev server lives outside the agnostic core (in `fastql.server`) and only
|
|
120
|
+
consumes `build_schema` / `execute` — it is a developer convenience, not a
|
|
121
|
+
production transport (no TLS, auth, or subscriptions).
|
|
122
|
+
|
|
123
|
+
## Design at a glance
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
@Type / @Input / root class decorators ← unified type-hint-driven authoring
|
|
127
|
+
▼
|
|
128
|
+
Schema(query=...) / build_schema() ← compiles decorators into the type-system IR
|
|
129
|
+
▼
|
|
130
|
+
Type-system IR (Schema, ObjectType, Field, scalars, wrappers)
|
|
131
|
+
▼
|
|
132
|
+
execute() ── validation ── coercion ── async resolution + DI/context
|
|
133
|
+
▲
|
|
134
|
+
language: Source → Lexer → Parser → AST ← hand-built front-end
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The core never imports an HTTP framework. Transports (an optional built-in dev server,
|
|
138
|
+
plus FastAPI/Django/Flask/ASGI adapters) plug in on top and consume `build_schema` / `execute`.
|
|
139
|
+
|
|
140
|
+
## Web framework integrations
|
|
141
|
+
|
|
142
|
+
Install only the framework adapter an application uses:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
pip install mygenx-fastql[fastapi] # or starlette, flask, django
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from fastapi import FastAPI
|
|
150
|
+
from fastql.integrations.fastapi import create_fastapi_router
|
|
151
|
+
|
|
152
|
+
app = FastAPI()
|
|
153
|
+
app.include_router(create_fastapi_router(schema, graphiql=True))
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The base installation includes the dependency-free `GraphQLASGI` adapter. See
|
|
157
|
+
the [integration documentation](docs/integrations/overview.mdx) for mounting,
|
|
158
|
+
request context, endpoint configuration, and supported versions.
|
|
159
|
+
|
|
160
|
+
Runnable per-framework projects — FastAPI, Starlette, Flask, Django, and raw ASGI, each
|
|
161
|
+
mounting the **same** [`examples/app`](examples/app) schema — live under
|
|
162
|
+
[`examples/projects`](examples/projects). Reusing one schema across every adapter is the
|
|
163
|
+
proof that the core is framework-agnostic; each project differs only in a few lines of glue.
|
|
164
|
+
|
|
165
|
+
## Development
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
pip install -e ".[dev]"
|
|
169
|
+
pytest
|
|
170
|
+
```
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""FastQL — a code-first, decorator-driven GraphQL framework for Python.
|
|
2
|
+
|
|
3
|
+
Define types and operations with decorators, build a schema, and execute
|
|
4
|
+
operations — all without any web framework. The core turns Python values into a
|
|
5
|
+
:class:`~fastql.types.schema.Schema` and runs operations through a hand-built,
|
|
6
|
+
async-first engine.
|
|
7
|
+
|
|
8
|
+
from fastql import Type, Query, Schema, Context, execute
|
|
9
|
+
|
|
10
|
+
@Type
|
|
11
|
+
class User:
|
|
12
|
+
id: int
|
|
13
|
+
name: str
|
|
14
|
+
|
|
15
|
+
@Query
|
|
16
|
+
class QueryRoot:
|
|
17
|
+
@Field
|
|
18
|
+
async def user(self, id: int, ctx: Context) -> "User":
|
|
19
|
+
return ctx.users[id]
|
|
20
|
+
|
|
21
|
+
schema = Schema(query=QueryRoot)
|
|
22
|
+
result = await execute(schema, "{ user(id: 1) { id name } }", context=ctx)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from fastql.context import (
|
|
26
|
+
Context,
|
|
27
|
+
Info,
|
|
28
|
+
ResolveInfo,
|
|
29
|
+
provides,
|
|
30
|
+
register_dependency,
|
|
31
|
+
)
|
|
32
|
+
from fastql.dataloader import DataLoader, DataLoaderError, get_loader
|
|
33
|
+
from fastql.decorators import (
|
|
34
|
+
Arg,
|
|
35
|
+
Argument,
|
|
36
|
+
BasePermission,
|
|
37
|
+
Enum,
|
|
38
|
+
Field,
|
|
39
|
+
FieldExtension,
|
|
40
|
+
Input,
|
|
41
|
+
Interface,
|
|
42
|
+
Mutation,
|
|
43
|
+
Query,
|
|
44
|
+
Scalar,
|
|
45
|
+
Subscription,
|
|
46
|
+
Type,
|
|
47
|
+
Union,
|
|
48
|
+
)
|
|
49
|
+
from fastql.errors import GraphQLError, GraphQLSyntaxError, ValidationError
|
|
50
|
+
from fastql.execution import ExecutionResult, execute, subscribe
|
|
51
|
+
from fastql.extensions import SchemaExtension
|
|
52
|
+
from fastql.language import parse
|
|
53
|
+
from fastql.registry import TypeRegistry, default_registry
|
|
54
|
+
from fastql.schema_builder import SchemaBuildError, build_schema
|
|
55
|
+
from fastql.sdl import print_schema
|
|
56
|
+
from fastql.testing import GraphQLTestClient
|
|
57
|
+
from fastql.types import (
|
|
58
|
+
AppliedDirective,
|
|
59
|
+
Boolean,
|
|
60
|
+
Float,
|
|
61
|
+
ID,
|
|
62
|
+
Int,
|
|
63
|
+
ListType,
|
|
64
|
+
NonNull,
|
|
65
|
+
Schema,
|
|
66
|
+
SchemaConfig,
|
|
67
|
+
String,
|
|
68
|
+
)
|
|
69
|
+
from fastql.validation import validate
|
|
70
|
+
|
|
71
|
+
__version__ = "0.0.1"
|
|
72
|
+
|
|
73
|
+
__all__ = [
|
|
74
|
+
"__version__",
|
|
75
|
+
# Decorators
|
|
76
|
+
"Type",
|
|
77
|
+
"Input",
|
|
78
|
+
"Interface",
|
|
79
|
+
"Enum",
|
|
80
|
+
"Union",
|
|
81
|
+
"Scalar",
|
|
82
|
+
"Query",
|
|
83
|
+
"Mutation",
|
|
84
|
+
"Subscription",
|
|
85
|
+
"Field",
|
|
86
|
+
"Arg",
|
|
87
|
+
"Argument",
|
|
88
|
+
"AppliedDirective",
|
|
89
|
+
"BasePermission",
|
|
90
|
+
"FieldExtension",
|
|
91
|
+
"SchemaExtension",
|
|
92
|
+
# Context / DI
|
|
93
|
+
"Context",
|
|
94
|
+
"Info",
|
|
95
|
+
"ResolveInfo",
|
|
96
|
+
"register_dependency",
|
|
97
|
+
"provides",
|
|
98
|
+
# DataLoader (request-scoped batch loading)
|
|
99
|
+
"DataLoader",
|
|
100
|
+
"DataLoaderError",
|
|
101
|
+
"get_loader",
|
|
102
|
+
# Built-in scalars and wrappers
|
|
103
|
+
"Int",
|
|
104
|
+
"Float",
|
|
105
|
+
"String",
|
|
106
|
+
"Boolean",
|
|
107
|
+
"ID",
|
|
108
|
+
"NonNull",
|
|
109
|
+
"ListType",
|
|
110
|
+
# Schema building and execution
|
|
111
|
+
"Schema",
|
|
112
|
+
"SchemaConfig",
|
|
113
|
+
"build_schema",
|
|
114
|
+
"SchemaBuildError",
|
|
115
|
+
"execute",
|
|
116
|
+
"subscribe",
|
|
117
|
+
"ExecutionResult",
|
|
118
|
+
"validate",
|
|
119
|
+
"parse",
|
|
120
|
+
"print_schema",
|
|
121
|
+
"GraphQLTestClient",
|
|
122
|
+
# Dev server (lazy — keeps the agnostic core free of the HTTP layer)
|
|
123
|
+
"serve",
|
|
124
|
+
"start_server",
|
|
125
|
+
# Registry
|
|
126
|
+
"TypeRegistry",
|
|
127
|
+
"default_registry",
|
|
128
|
+
# Errors
|
|
129
|
+
"GraphQLError",
|
|
130
|
+
"GraphQLSyntaxError",
|
|
131
|
+
"ValidationError",
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def __getattr__(name: str):
|
|
136
|
+
# `serve` / `start_server` live in fastql.server, which imports asyncio HTTP
|
|
137
|
+
# machinery. Import them lazily so the agnostic core never pulls in the
|
|
138
|
+
# transport layer just by `import fastql`.
|
|
139
|
+
if name in ("serve", "start_server"):
|
|
140
|
+
from fastql import server
|
|
141
|
+
|
|
142
|
+
return getattr(server, name)
|
|
143
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Command-line entry point: ``python -m fastql serve module:attr``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import asyncio
|
|
7
|
+
import importlib
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from fastql.server import DEFAULT_HOST, DEFAULT_PORT, serve
|
|
13
|
+
from fastql.types.schema import Schema
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
17
|
+
parser = argparse.ArgumentParser(prog="fastql", description="FastQL CLI")
|
|
18
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
19
|
+
|
|
20
|
+
serve_parser = sub.add_parser("serve", help="Run the dev server for a schema.")
|
|
21
|
+
serve_parser.add_argument(
|
|
22
|
+
"target",
|
|
23
|
+
help="Dotted path to the schema object, e.g. 'myapp.schema:schema'.",
|
|
24
|
+
)
|
|
25
|
+
serve_parser.add_argument("--host", default=DEFAULT_HOST)
|
|
26
|
+
serve_parser.add_argument("--port", type=int, default=DEFAULT_PORT)
|
|
27
|
+
serve_parser.add_argument(
|
|
28
|
+
"--context",
|
|
29
|
+
default=None,
|
|
30
|
+
help=(
|
|
31
|
+
"Dotted path to a per-request context value or zero-arg factory, "
|
|
32
|
+
"e.g. 'myapp:make_context'."
|
|
33
|
+
),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
export_parser = sub.add_parser(
|
|
37
|
+
"export-schema", help="Write a schema's SDL or introspection JSON."
|
|
38
|
+
)
|
|
39
|
+
export_parser.add_argument(
|
|
40
|
+
"target",
|
|
41
|
+
help="Dotted path to the schema object, e.g. 'myapp.schema:schema'.",
|
|
42
|
+
)
|
|
43
|
+
export_parser.add_argument(
|
|
44
|
+
"--output",
|
|
45
|
+
"-o",
|
|
46
|
+
default=None,
|
|
47
|
+
help="File to write to. Writes to stdout when omitted.",
|
|
48
|
+
)
|
|
49
|
+
export_parser.add_argument(
|
|
50
|
+
"--format",
|
|
51
|
+
choices=("sdl", "json"),
|
|
52
|
+
default="sdl",
|
|
53
|
+
help="Output format: 'sdl' (default) or 'json' (introspection result).",
|
|
54
|
+
)
|
|
55
|
+
return parser
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _load_schema(target: str) -> Any:
|
|
59
|
+
"""Import and return the schema object named by a dotted ``module:attr`` path."""
|
|
60
|
+
if ":" in target:
|
|
61
|
+
module_name, attr = target.split(":", 1)
|
|
62
|
+
else:
|
|
63
|
+
module_name, _, attr = target.rpartition(".")
|
|
64
|
+
if not module_name or not attr:
|
|
65
|
+
raise SystemExit(
|
|
66
|
+
f"Invalid target {target!r}; expected 'module:attr' or 'module.attr'."
|
|
67
|
+
)
|
|
68
|
+
try:
|
|
69
|
+
module = importlib.import_module(module_name)
|
|
70
|
+
except ImportError as error:
|
|
71
|
+
raise SystemExit(f"Could not import module {module_name!r}: {error}")
|
|
72
|
+
try:
|
|
73
|
+
return getattr(module, attr)
|
|
74
|
+
except AttributeError:
|
|
75
|
+
raise SystemExit(f"Module {module_name!r} has no attribute {attr!r}.")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _render_schema(schema: Any, fmt: str) -> str:
|
|
79
|
+
"""Render ``schema`` as SDL or as an introspection-result JSON document."""
|
|
80
|
+
if not isinstance(schema, Schema):
|
|
81
|
+
raise SystemExit(
|
|
82
|
+
f"Loaded object is not a fastql Schema (got {type(schema).__name__})."
|
|
83
|
+
)
|
|
84
|
+
if fmt == "json":
|
|
85
|
+
from fastql.execution import execute
|
|
86
|
+
from fastql.integrations.http import INTROSPECTION_QUERY
|
|
87
|
+
|
|
88
|
+
result = asyncio.run(execute(schema, INTROSPECTION_QUERY))
|
|
89
|
+
if result.errors:
|
|
90
|
+
raise SystemExit(
|
|
91
|
+
f"Introspection failed: {result.errors[0].message}"
|
|
92
|
+
)
|
|
93
|
+
return json.dumps(result.data, indent=2)
|
|
94
|
+
|
|
95
|
+
from fastql.sdl import print_schema
|
|
96
|
+
|
|
97
|
+
return print_schema(schema)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _export_schema(target: str, output: str | None, fmt: str) -> None:
|
|
101
|
+
schema = _load_schema(target)
|
|
102
|
+
rendered = _render_schema(schema, fmt)
|
|
103
|
+
if output:
|
|
104
|
+
with open(output, "w", encoding="utf-8") as handle:
|
|
105
|
+
handle.write(rendered + ("\n" if not rendered.endswith("\n") else ""))
|
|
106
|
+
else:
|
|
107
|
+
print(rendered)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def main(argv: list[str] | None = None) -> None:
|
|
111
|
+
parser = _build_parser()
|
|
112
|
+
args = parser.parse_args(argv)
|
|
113
|
+
if args.command == "serve":
|
|
114
|
+
schema = _load_schema(args.target)
|
|
115
|
+
context_factory = None
|
|
116
|
+
if args.context:
|
|
117
|
+
obj = _load_schema(args.context)
|
|
118
|
+
context_factory = obj if callable(obj) else (lambda value=obj: value)
|
|
119
|
+
serve(
|
|
120
|
+
schema,
|
|
121
|
+
host=args.host,
|
|
122
|
+
port=args.port,
|
|
123
|
+
context_factory=context_factory,
|
|
124
|
+
)
|
|
125
|
+
elif args.command == "export-schema":
|
|
126
|
+
_export_schema(args.target, args.output, args.format)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
if __name__ == "__main__": # pragma: no cover
|
|
130
|
+
main()
|