hexastack-graphql 0.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.
- hexastack_graphql-0.0.0/PKG-INFO +190 -0
- hexastack_graphql-0.0.0/README.md +175 -0
- hexastack_graphql-0.0.0/pyproject.toml +58 -0
- hexastack_graphql-0.0.0/pyproject.toml.orig +53 -0
- hexastack_graphql-0.0.0/src/hexastack_graphql/__init__.py +7 -0
- hexastack_graphql-0.0.0/src/hexastack_graphql/adapters/__init__.py +0 -0
- hexastack_graphql-0.0.0/src/hexastack_graphql/adapters/fastapi.py +122 -0
- hexastack_graphql-0.0.0/src/hexastack_graphql/domain/__init__.py +0 -0
- hexastack_graphql-0.0.0/src/hexastack_graphql/domain/context.py +27 -0
- hexastack_graphql-0.0.0/src/hexastack_graphql/domain/exceptions.py +25 -0
- hexastack_graphql-0.0.0/src/hexastack_graphql/infra/__init__.py +0 -0
- hexastack_graphql-0.0.0/src/hexastack_graphql/infra/autodiscovery.py +106 -0
- hexastack_graphql-0.0.0/src/hexastack_graphql/infra/bootstrap.py +113 -0
- hexastack_graphql-0.0.0/src/hexastack_graphql/infra/config.py +60 -0
- hexastack_graphql-0.0.0/src/hexastack_graphql/infra/decorators.py +212 -0
- hexastack_graphql-0.0.0/src/hexastack_graphql/infra/extensions.py +26 -0
- hexastack_graphql-0.0.0/src/hexastack_graphql/infra/registries/__init__.py +0 -0
- hexastack_graphql-0.0.0/src/hexastack_graphql/infra/registries/schema.py +151 -0
- hexastack_graphql-0.0.0/src/hexastack_graphql/infra/resolvers.py +69 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: hexastack-graphql
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Strawberry GraphQL presentation adapter and CQRS integration for Hexastack
|
|
5
|
+
Author: Richard West
|
|
6
|
+
Author-email: Richard West <dopplereffect.us@gmail.com>
|
|
7
|
+
Requires-Dist: hexastack-core
|
|
8
|
+
Requires-Dist: hexastack-cqrs
|
|
9
|
+
Requires-Dist: strawberry-graphql>=0.260.0
|
|
10
|
+
Requires-Dist: fastapi>=0.141.1 ; extra == 'fastapi'
|
|
11
|
+
Requires-Dist: hexastack-fastapi ; extra == 'fastapi'
|
|
12
|
+
Requires-Python: >=3.13
|
|
13
|
+
Provides-Extra: fastapi
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# hexastack-graphql
|
|
17
|
+
|
|
18
|
+
> Strawberry GraphQL presentation adapter and CQRS integration for Hexastack.
|
|
19
|
+
|
|
20
|
+
[](https://www.python.org/downloads/)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 1. Overview & Capabilities
|
|
25
|
+
|
|
26
|
+
`hexastack-graphql` brings the power and type-safety of [Strawberry GraphQL](https://strawberry.rocks/) into the Hexastack architecture:
|
|
27
|
+
|
|
28
|
+
- **Type-Safe GraphQL Schemas**: Native Python dataclass-based GraphQL schema definition via Strawberry.
|
|
29
|
+
- **CQRS Integration via Context**: Injects the `rodi.Container`, `CommandBusPort`, and `QueryBusPort` directly into Strawberry's `Info.context` (`GraphQLContext`).
|
|
30
|
+
- **Declarative Query & Mutation Registries**:
|
|
31
|
+
- `@graphql_query_type` and `@graphql_mutation_type`: Register whole type classes to be merged into root Query and Mutation types.
|
|
32
|
+
- `@graphql_query` and `@graphql_mutation`: Register standalone resolver functions as top-level fields.
|
|
33
|
+
- **Dynamic Field Resolver Feature Flagging**:
|
|
34
|
+
- `@feature_flag_field("flag_key", raise_error=True, fallback=...)`: Evaluates feature flags dynamically before executing field resolvers, raising a `GraphQLError` or returning a safe fallback value.
|
|
35
|
+
- **FastAPI Mount & GraphiQL Playground**: Seamless mounting as a `GraphQLRouter` into FastAPI applications with interactive GraphiQL playground enabled.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 2. Package Anatomy & Key Components
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
hexastack_graphql/
|
|
43
|
+
├── domain/ # GraphQLContext, GraphQLError, SchemaBuildingError
|
|
44
|
+
├── ports/ # GraphQLContextFactoryPort
|
|
45
|
+
├── adapters/ # create_graphql_router, mount_graphql_router (FastAPI integration)
|
|
46
|
+
└── infra/
|
|
47
|
+
├── bootstrap.py # GraphQLBootstrapper (order=35)
|
|
48
|
+
├── config.py # HexastackGraphQLConfig
|
|
49
|
+
├── decorators.py# @graphql_query, @graphql_mutation, @graphql_query_type, @graphql_mutation_type, @feature_flag_field
|
|
50
|
+
└── registries/ # schema.py (GraphQLSchemaRegistry)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Key Exports
|
|
54
|
+
|
|
55
|
+
| Category | Exports |
|
|
56
|
+
|---|---|
|
|
57
|
+
| **Bootstrap** | `GraphQLBootstrapper` (order=35), `HexastackGraphQLConfig` |
|
|
58
|
+
| **Context & Domain** | `GraphQLContext`, `GraphQLError`, `SchemaBuildingError` |
|
|
59
|
+
| **Decorators** | `@graphql_query`, `@graphql_mutation`, `@graphql_query_type`, `@graphql_mutation_type`, `@feature_flag_field` |
|
|
60
|
+
| **FastAPI Adapters** | `create_graphql_router`, `mount_graphql_router` |
|
|
61
|
+
| **Registries** | `GraphQLSchemaRegistry`, `get_schema_registry` |
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 3. Monorepo & Sibling Relationships
|
|
66
|
+
|
|
67
|
+
```mermaid
|
|
68
|
+
graph TD
|
|
69
|
+
subgraph ClientRequests ["GraphQL Client Requests"]
|
|
70
|
+
CLIENT["Web / Mobile GraphQL Clients"]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
subgraph GraphQLAdapter ["hexastack-graphql"]
|
|
74
|
+
SCHEMA["strawberry.Schema"]
|
|
75
|
+
CTX["GraphQLContext (Container + Buses)"]
|
|
76
|
+
ROUTER["GraphQLRouter (FastAPI integration)"]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
subgraph ApplicationLayer ["hexastack-cqrs"]
|
|
80
|
+
CBUS["CommandBusPort"]
|
|
81
|
+
QBUS["QueryBusPort"]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
subgraph WebServer ["hexastack-fastapi"]
|
|
85
|
+
FASTAPI_APP["FastAPI Application"]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
CLIENT --> ROUTER
|
|
89
|
+
FASTAPI_APP --> ROUTER
|
|
90
|
+
ROUTER --> SCHEMA
|
|
91
|
+
SCHEMA --> CTX
|
|
92
|
+
CTX -->|dispatches commands/queries to| CBUS
|
|
93
|
+
CTX -->|dispatches commands/queries to| QBUS
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Explicit Dependencies (Direct)
|
|
97
|
+
- `hexastack-core`: DI container, configuration registry, base exceptions.
|
|
98
|
+
- `hexastack-cqrs`: `CommandBusPort` and `QueryBusPort` for message dispatching.
|
|
99
|
+
- `strawberry-graphql>=0.260.0`: Core GraphQL engine and schema generator.
|
|
100
|
+
|
|
101
|
+
### Implied / Behavioral Relationships (DI-Mediated)
|
|
102
|
+
- **FastAPI Auto-Mounting**: `GraphQLBootstrapper` (order=35) discovers the `FastAPI` instance created by `FastApiBootstrapper` (order=30) and attaches the `GraphQLRouter` automatically if `auto_mount_fastapi=true`.
|
|
103
|
+
- **CQRS Dispatching**: Field resolvers receive `info.context.query_bus` and `info.context.command_bus` to delegate execution into the CQRS pipeline.
|
|
104
|
+
|
|
105
|
+
### Optional Integrations (Extras)
|
|
106
|
+
- `[fastapi]`: Installs `hexastack-fastapi` and `fastapi>=0.141.1` for HTTP routing and GraphiQL playground.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 4. Installation
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# Standalone install
|
|
114
|
+
pip install hexastack-graphql
|
|
115
|
+
|
|
116
|
+
# With FastAPI integration
|
|
117
|
+
pip install "hexastack-graphql[fastapi]"
|
|
118
|
+
|
|
119
|
+
# Via umbrella package
|
|
120
|
+
pip install "hexastack[graphql]"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## 5. Configuration Reference
|
|
126
|
+
|
|
127
|
+
```toml
|
|
128
|
+
[hexastack.graphql]
|
|
129
|
+
path = "/graphql" # Route prefix for GraphQL endpoint
|
|
130
|
+
graphiql = true # Enable interactive GraphiQL web UI
|
|
131
|
+
allow_queries = true
|
|
132
|
+
allow_mutations = true
|
|
133
|
+
auto_mount_fastapi = true # Auto mount onto FastAPI application on bootstrap
|
|
134
|
+
title = "Hexastack GraphQL API"
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## 6. Quickstart Example
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from dataclasses import dataclass
|
|
143
|
+
import strawberry
|
|
144
|
+
from strawberry.types import Info
|
|
145
|
+
from hexastack_core.infra.bootstrap import bootstrap
|
|
146
|
+
from hexastack_cqrs.domain.query import Query
|
|
147
|
+
from hexastack_cqrs.infra.decorators import query_handler
|
|
148
|
+
from hexastack_graphql.domain.context import GraphQLContext
|
|
149
|
+
from hexastack_graphql.infra.decorators import graphql_query_type
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# 1. Define CQRS Query & Handler
|
|
153
|
+
@dataclass(frozen=True)
|
|
154
|
+
class GetItemQuery(Query):
|
|
155
|
+
item_id: str
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@query_handler(GetItemQuery)
|
|
159
|
+
class GetItemHandler:
|
|
160
|
+
def __call__(self, qry: GetItemQuery) -> dict:
|
|
161
|
+
return {"id": qry.item_id, "name": f"Item {qry.item_id}"}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# 2. Define Strawberry GraphQL Type
|
|
165
|
+
@strawberry.type
|
|
166
|
+
class ItemType:
|
|
167
|
+
id: str
|
|
168
|
+
name: str
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@graphql_query_type
|
|
172
|
+
class Query:
|
|
173
|
+
@strawberry.field
|
|
174
|
+
def item(self, info: Info[GraphQLContext, None], item_id: str) -> ItemType:
|
|
175
|
+
res = info.context.query_bus.dispatch(GetItemQuery(item_id=item_id))
|
|
176
|
+
return ItemType(id=res["id"], name=res["name"])
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# 3. Bootstrap Runtime with GraphQL
|
|
180
|
+
runtime = bootstrap(packages_to_scan=[__name__])
|
|
181
|
+
schema = runtime.get("graphql_schema")
|
|
182
|
+
|
|
183
|
+
result = schema.execute_sync(
|
|
184
|
+
'{ item(itemId: "123") { id name } }',
|
|
185
|
+
context_value=GraphQLContext(
|
|
186
|
+
container=runtime.container, query_bus=runtime.get("query_bus")
|
|
187
|
+
),
|
|
188
|
+
)
|
|
189
|
+
print(result.data) # {'item': {'id': '123', 'name': 'Item 123'}}
|
|
190
|
+
```
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# hexastack-graphql
|
|
2
|
+
|
|
3
|
+
> Strawberry GraphQL presentation adapter and CQRS integration for Hexastack.
|
|
4
|
+
|
|
5
|
+
[](https://www.python.org/downloads/)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Overview & Capabilities
|
|
10
|
+
|
|
11
|
+
`hexastack-graphql` brings the power and type-safety of [Strawberry GraphQL](https://strawberry.rocks/) into the Hexastack architecture:
|
|
12
|
+
|
|
13
|
+
- **Type-Safe GraphQL Schemas**: Native Python dataclass-based GraphQL schema definition via Strawberry.
|
|
14
|
+
- **CQRS Integration via Context**: Injects the `rodi.Container`, `CommandBusPort`, and `QueryBusPort` directly into Strawberry's `Info.context` (`GraphQLContext`).
|
|
15
|
+
- **Declarative Query & Mutation Registries**:
|
|
16
|
+
- `@graphql_query_type` and `@graphql_mutation_type`: Register whole type classes to be merged into root Query and Mutation types.
|
|
17
|
+
- `@graphql_query` and `@graphql_mutation`: Register standalone resolver functions as top-level fields.
|
|
18
|
+
- **Dynamic Field Resolver Feature Flagging**:
|
|
19
|
+
- `@feature_flag_field("flag_key", raise_error=True, fallback=...)`: Evaluates feature flags dynamically before executing field resolvers, raising a `GraphQLError` or returning a safe fallback value.
|
|
20
|
+
- **FastAPI Mount & GraphiQL Playground**: Seamless mounting as a `GraphQLRouter` into FastAPI applications with interactive GraphiQL playground enabled.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 2. Package Anatomy & Key Components
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
hexastack_graphql/
|
|
28
|
+
├── domain/ # GraphQLContext, GraphQLError, SchemaBuildingError
|
|
29
|
+
├── ports/ # GraphQLContextFactoryPort
|
|
30
|
+
├── adapters/ # create_graphql_router, mount_graphql_router (FastAPI integration)
|
|
31
|
+
└── infra/
|
|
32
|
+
├── bootstrap.py # GraphQLBootstrapper (order=35)
|
|
33
|
+
├── config.py # HexastackGraphQLConfig
|
|
34
|
+
├── decorators.py# @graphql_query, @graphql_mutation, @graphql_query_type, @graphql_mutation_type, @feature_flag_field
|
|
35
|
+
└── registries/ # schema.py (GraphQLSchemaRegistry)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Key Exports
|
|
39
|
+
|
|
40
|
+
| Category | Exports |
|
|
41
|
+
|---|---|
|
|
42
|
+
| **Bootstrap** | `GraphQLBootstrapper` (order=35), `HexastackGraphQLConfig` |
|
|
43
|
+
| **Context & Domain** | `GraphQLContext`, `GraphQLError`, `SchemaBuildingError` |
|
|
44
|
+
| **Decorators** | `@graphql_query`, `@graphql_mutation`, `@graphql_query_type`, `@graphql_mutation_type`, `@feature_flag_field` |
|
|
45
|
+
| **FastAPI Adapters** | `create_graphql_router`, `mount_graphql_router` |
|
|
46
|
+
| **Registries** | `GraphQLSchemaRegistry`, `get_schema_registry` |
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## 3. Monorepo & Sibling Relationships
|
|
51
|
+
|
|
52
|
+
```mermaid
|
|
53
|
+
graph TD
|
|
54
|
+
subgraph ClientRequests ["GraphQL Client Requests"]
|
|
55
|
+
CLIENT["Web / Mobile GraphQL Clients"]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
subgraph GraphQLAdapter ["hexastack-graphql"]
|
|
59
|
+
SCHEMA["strawberry.Schema"]
|
|
60
|
+
CTX["GraphQLContext (Container + Buses)"]
|
|
61
|
+
ROUTER["GraphQLRouter (FastAPI integration)"]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
subgraph ApplicationLayer ["hexastack-cqrs"]
|
|
65
|
+
CBUS["CommandBusPort"]
|
|
66
|
+
QBUS["QueryBusPort"]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
subgraph WebServer ["hexastack-fastapi"]
|
|
70
|
+
FASTAPI_APP["FastAPI Application"]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
CLIENT --> ROUTER
|
|
74
|
+
FASTAPI_APP --> ROUTER
|
|
75
|
+
ROUTER --> SCHEMA
|
|
76
|
+
SCHEMA --> CTX
|
|
77
|
+
CTX -->|dispatches commands/queries to| CBUS
|
|
78
|
+
CTX -->|dispatches commands/queries to| QBUS
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Explicit Dependencies (Direct)
|
|
82
|
+
- `hexastack-core`: DI container, configuration registry, base exceptions.
|
|
83
|
+
- `hexastack-cqrs`: `CommandBusPort` and `QueryBusPort` for message dispatching.
|
|
84
|
+
- `strawberry-graphql>=0.260.0`: Core GraphQL engine and schema generator.
|
|
85
|
+
|
|
86
|
+
### Implied / Behavioral Relationships (DI-Mediated)
|
|
87
|
+
- **FastAPI Auto-Mounting**: `GraphQLBootstrapper` (order=35) discovers the `FastAPI` instance created by `FastApiBootstrapper` (order=30) and attaches the `GraphQLRouter` automatically if `auto_mount_fastapi=true`.
|
|
88
|
+
- **CQRS Dispatching**: Field resolvers receive `info.context.query_bus` and `info.context.command_bus` to delegate execution into the CQRS pipeline.
|
|
89
|
+
|
|
90
|
+
### Optional Integrations (Extras)
|
|
91
|
+
- `[fastapi]`: Installs `hexastack-fastapi` and `fastapi>=0.141.1` for HTTP routing and GraphiQL playground.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 4. Installation
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# Standalone install
|
|
99
|
+
pip install hexastack-graphql
|
|
100
|
+
|
|
101
|
+
# With FastAPI integration
|
|
102
|
+
pip install "hexastack-graphql[fastapi]"
|
|
103
|
+
|
|
104
|
+
# Via umbrella package
|
|
105
|
+
pip install "hexastack[graphql]"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 5. Configuration Reference
|
|
111
|
+
|
|
112
|
+
```toml
|
|
113
|
+
[hexastack.graphql]
|
|
114
|
+
path = "/graphql" # Route prefix for GraphQL endpoint
|
|
115
|
+
graphiql = true # Enable interactive GraphiQL web UI
|
|
116
|
+
allow_queries = true
|
|
117
|
+
allow_mutations = true
|
|
118
|
+
auto_mount_fastapi = true # Auto mount onto FastAPI application on bootstrap
|
|
119
|
+
title = "Hexastack GraphQL API"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## 6. Quickstart Example
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from dataclasses import dataclass
|
|
128
|
+
import strawberry
|
|
129
|
+
from strawberry.types import Info
|
|
130
|
+
from hexastack_core.infra.bootstrap import bootstrap
|
|
131
|
+
from hexastack_cqrs.domain.query import Query
|
|
132
|
+
from hexastack_cqrs.infra.decorators import query_handler
|
|
133
|
+
from hexastack_graphql.domain.context import GraphQLContext
|
|
134
|
+
from hexastack_graphql.infra.decorators import graphql_query_type
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# 1. Define CQRS Query & Handler
|
|
138
|
+
@dataclass(frozen=True)
|
|
139
|
+
class GetItemQuery(Query):
|
|
140
|
+
item_id: str
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@query_handler(GetItemQuery)
|
|
144
|
+
class GetItemHandler:
|
|
145
|
+
def __call__(self, qry: GetItemQuery) -> dict:
|
|
146
|
+
return {"id": qry.item_id, "name": f"Item {qry.item_id}"}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# 2. Define Strawberry GraphQL Type
|
|
150
|
+
@strawberry.type
|
|
151
|
+
class ItemType:
|
|
152
|
+
id: str
|
|
153
|
+
name: str
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@graphql_query_type
|
|
157
|
+
class Query:
|
|
158
|
+
@strawberry.field
|
|
159
|
+
def item(self, info: Info[GraphQLContext, None], item_id: str) -> ItemType:
|
|
160
|
+
res = info.context.query_bus.dispatch(GetItemQuery(item_id=item_id))
|
|
161
|
+
return ItemType(id=res["id"], name=res["name"])
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# 3. Bootstrap Runtime with GraphQL
|
|
165
|
+
runtime = bootstrap(packages_to_scan=[__name__])
|
|
166
|
+
schema = runtime.get("graphql_schema")
|
|
167
|
+
|
|
168
|
+
result = schema.execute_sync(
|
|
169
|
+
'{ item(itemId: "123") { id name } }',
|
|
170
|
+
context_value=GraphQLContext(
|
|
171
|
+
container=runtime.container, query_bus=runtime.get("query_bus")
|
|
172
|
+
),
|
|
173
|
+
)
|
|
174
|
+
print(result.data) # {'item': {'id': '123', 'name': 'Item 123'}}
|
|
175
|
+
```
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hexastack-graphql"
|
|
3
|
+
version = "0.0.0"
|
|
4
|
+
description = "Strawberry GraphQL presentation adapter and CQRS integration for Hexastack"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"hexastack-core",
|
|
9
|
+
"hexastack-cqrs",
|
|
10
|
+
"strawberry-graphql>=0.260.0",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[[project.authors]]
|
|
14
|
+
name = "Richard West"
|
|
15
|
+
email = "dopplereffect.us@gmail.com"
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
fastapi = [
|
|
19
|
+
"fastapi>=0.141.1",
|
|
20
|
+
"hexastack-fastapi",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.entry-points."hexastack.bootstrappers"]
|
|
24
|
+
graphql = "hexastack_graphql.infra.bootstrap:GraphQLBootstrapper"
|
|
25
|
+
|
|
26
|
+
[build-system]
|
|
27
|
+
requires = ["uv_build>=0.12.3,<0.13.0"]
|
|
28
|
+
build-backend = "uv_build"
|
|
29
|
+
|
|
30
|
+
[tool.uv.sources.hexastack-core]
|
|
31
|
+
workspace = true
|
|
32
|
+
|
|
33
|
+
[tool.uv.sources.hexastack-cqrs]
|
|
34
|
+
workspace = true
|
|
35
|
+
|
|
36
|
+
[tool.uv.sources.hexastack-fastapi]
|
|
37
|
+
workspace = true
|
|
38
|
+
|
|
39
|
+
[tool.importlinter]
|
|
40
|
+
root_packages = ["hexastack_graphql"]
|
|
41
|
+
|
|
42
|
+
[[tool.importlinter.contracts]]
|
|
43
|
+
name = "Hexagonal architecture layer hierarchy"
|
|
44
|
+
type = "layers"
|
|
45
|
+
containers = ["hexastack_graphql"]
|
|
46
|
+
layers = [
|
|
47
|
+
"adapters",
|
|
48
|
+
"domain",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[[tool.importlinter.contracts]]
|
|
52
|
+
name = "Forbidden imports for domain"
|
|
53
|
+
type = "forbidden"
|
|
54
|
+
source_modules = ["hexastack_graphql.domain"]
|
|
55
|
+
forbidden_modules = [
|
|
56
|
+
"hexastack_graphql.adapters",
|
|
57
|
+
"hexastack_graphql.infra",
|
|
58
|
+
]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hexastack-graphql"
|
|
3
|
+
version = "0.0.0"
|
|
4
|
+
description = "Strawberry GraphQL presentation adapter and CQRS integration for Hexastack"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Richard West", email = "dopplereffect.us@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.13"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"hexastack-core",
|
|
12
|
+
"hexastack-cqrs",
|
|
13
|
+
"strawberry-graphql>=0.260.0",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
fastapi = [
|
|
18
|
+
"fastapi>=0.141.1",
|
|
19
|
+
"hexastack-fastapi",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.entry-points."hexastack.bootstrappers"]
|
|
23
|
+
graphql = "hexastack_graphql.infra.bootstrap:GraphQLBootstrapper"
|
|
24
|
+
|
|
25
|
+
[build-system]
|
|
26
|
+
requires = ["uv_build>=0.12.3,<0.13.0"]
|
|
27
|
+
build-backend = "uv_build"
|
|
28
|
+
|
|
29
|
+
[tool.uv.sources]
|
|
30
|
+
hexastack-core = { workspace = true }
|
|
31
|
+
hexastack-cqrs = { workspace = true }
|
|
32
|
+
hexastack-fastapi = { workspace = true }
|
|
33
|
+
|
|
34
|
+
[tool.importlinter]
|
|
35
|
+
root_packages = ["hexastack_graphql"]
|
|
36
|
+
|
|
37
|
+
[[tool.importlinter.contracts]]
|
|
38
|
+
name = "Hexagonal architecture layer hierarchy"
|
|
39
|
+
type = "layers"
|
|
40
|
+
containers = ["hexastack_graphql"]
|
|
41
|
+
layers = [
|
|
42
|
+
"adapters",
|
|
43
|
+
"domain",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[[tool.importlinter.contracts]]
|
|
47
|
+
name = "Forbidden imports for domain"
|
|
48
|
+
type = "forbidden"
|
|
49
|
+
source_modules = ["hexastack_graphql.domain"]
|
|
50
|
+
forbidden_modules = [
|
|
51
|
+
"hexastack_graphql.adapters",
|
|
52
|
+
"hexastack_graphql.infra",
|
|
53
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import importlib.util
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import strawberry
|
|
5
|
+
from rodi import Container
|
|
6
|
+
|
|
7
|
+
from hexastack_core.domain.exceptions import MissingDependencyError
|
|
8
|
+
from hexastack_cqrs.ports.buses import CommandBusPort, QueryBusPort
|
|
9
|
+
from hexastack_graphql.domain.context import GraphQLContext
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"create_graphql_router",
|
|
13
|
+
"mount_graphql_router",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _require_fastapi() -> None:
|
|
18
|
+
"""Guard against missing FastAPI installation.
|
|
19
|
+
|
|
20
|
+
Raises:
|
|
21
|
+
MissingDependencyError: If fastapi is not installed.
|
|
22
|
+
"""
|
|
23
|
+
if importlib.util.find_spec("fastapi") is None:
|
|
24
|
+
raise MissingDependencyError(
|
|
25
|
+
"fastapi is required for FastAPI GraphQL integration. "
|
|
26
|
+
"Install via 'pip install hexastack-graphql[fastapi]'."
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def create_graphql_router(
|
|
31
|
+
schema: strawberry.Schema,
|
|
32
|
+
container: Container,
|
|
33
|
+
*,
|
|
34
|
+
command_bus: CommandBusPort | None = None,
|
|
35
|
+
query_bus: QueryBusPort | None = None,
|
|
36
|
+
graphiql: bool = True,
|
|
37
|
+
) -> Any:
|
|
38
|
+
"""Construct a Strawberry GraphQLRouter for FastAPI.
|
|
39
|
+
|
|
40
|
+
Notes/Architectural Intent:
|
|
41
|
+
Creates a FastAPI APIRouter subclass (GraphQLRouter) that dynamically
|
|
42
|
+
injects the rodi Container and CQRS message buses into Info.context for
|
|
43
|
+
every HTTP execution.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
schema: Compiled strawberry.Schema instance.
|
|
47
|
+
container: rodi DI Container.
|
|
48
|
+
command_bus: Optional CommandBusPort instance.
|
|
49
|
+
query_bus: Optional QueryBusPort instance.
|
|
50
|
+
graphiql: If True, enables the GraphiQL interactive playground.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Configured strawberry.fastapi.GraphQLRouter instance.
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
MissingDependencyError: If fastapi is not installed.
|
|
57
|
+
"""
|
|
58
|
+
_require_fastapi()
|
|
59
|
+
from starlette.requests import Request
|
|
60
|
+
from strawberry.fastapi import GraphQLRouter
|
|
61
|
+
|
|
62
|
+
async def get_context(request: Request) -> GraphQLContext:
|
|
63
|
+
# Dynamically resolve buses from container if not passed explicitly
|
|
64
|
+
c_bus = command_bus
|
|
65
|
+
if c_bus is None and container is not None:
|
|
66
|
+
try:
|
|
67
|
+
c_bus = container.resolve(CommandBusPort)
|
|
68
|
+
except Exception: # noqa: BLE001
|
|
69
|
+
c_bus = None
|
|
70
|
+
|
|
71
|
+
q_bus = query_bus
|
|
72
|
+
if q_bus is None and container is not None:
|
|
73
|
+
try:
|
|
74
|
+
q_bus = container.resolve(QueryBusPort)
|
|
75
|
+
except Exception: # noqa: BLE001
|
|
76
|
+
q_bus = None
|
|
77
|
+
|
|
78
|
+
ctx = GraphQLContext(
|
|
79
|
+
container=container,
|
|
80
|
+
command_bus=c_bus,
|
|
81
|
+
query_bus=q_bus,
|
|
82
|
+
)
|
|
83
|
+
ctx.request = request
|
|
84
|
+
return ctx
|
|
85
|
+
|
|
86
|
+
return GraphQLRouter(
|
|
87
|
+
schema=schema,
|
|
88
|
+
context_getter=get_context,
|
|
89
|
+
graphql_ide="graphiql" if graphiql else None,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def mount_graphql_router(
|
|
94
|
+
app: Any,
|
|
95
|
+
schema: strawberry.Schema,
|
|
96
|
+
container: Container,
|
|
97
|
+
*,
|
|
98
|
+
path: str = "/graphql",
|
|
99
|
+
graphiql: bool = True,
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Mount Strawberry GraphQL router directly onto a FastAPI application instance.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
app: Target FastAPI application instance.
|
|
105
|
+
schema: Compiled strawberry.Schema.
|
|
106
|
+
container: rodi DI Container.
|
|
107
|
+
path: Route prefix for GraphQL endpoints. Defaults to "/graphql".
|
|
108
|
+
graphiql: If True, enables GraphiQL playground.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
None.
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
MissingDependencyError: If fastapi is not installed.
|
|
115
|
+
"""
|
|
116
|
+
_require_fastapi()
|
|
117
|
+
router = create_graphql_router(
|
|
118
|
+
schema=schema,
|
|
119
|
+
container=container,
|
|
120
|
+
graphiql=graphiql,
|
|
121
|
+
)
|
|
122
|
+
app.include_router(router, prefix=path)
|
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from strawberry.fastapi.context import BaseContext
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class GraphQLContext(BaseContext):
|
|
9
|
+
"""Execution context injected into Strawberry GraphQL resolvers.
|
|
10
|
+
|
|
11
|
+
Notes/Architectural Intent:
|
|
12
|
+
Carries the rodi DI container and CQRS command/query buses into
|
|
13
|
+
field resolvers, enabling clean dispatching from GraphQL queries
|
|
14
|
+
and mutations. Inherits from Strawberry's BaseContext for full
|
|
15
|
+
FastAPI router integration.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
container: Any | None = None
|
|
19
|
+
command_bus: Any | None = None
|
|
20
|
+
query_bus: Any | None = None
|
|
21
|
+
request: Any | None = None
|
|
22
|
+
properties: dict[str, Any] = field(default_factory=dict)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"GraphQLContext",
|
|
27
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from hexastack_core.domain.exceptions import HexastackError
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class GraphQLError(HexastackError):
|
|
5
|
+
"""Base exception for all GraphQL adapter errors.
|
|
6
|
+
|
|
7
|
+
Notes/Architectural Intent:
|
|
8
|
+
Inherits from HexastackError to maintain unified exception hierarchy
|
|
9
|
+
across core and adapter layers.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SchemaBuildingError(GraphQLError):
|
|
14
|
+
"""Exception raised when GraphQL schema construction or validation fails.
|
|
15
|
+
|
|
16
|
+
Notes/Architectural Intent:
|
|
17
|
+
Raised when no queries or mutations are registered, or when invalid
|
|
18
|
+
field definitions conflict during schema compilation.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"GraphQLError",
|
|
24
|
+
"SchemaBuildingError",
|
|
25
|
+
]
|
|
File without changes
|