Coverage for src / lexigram / contracts / graphql / protocols.py: 100%

63 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-19 05:41 +0800

1"""GraphQL protocol definitions. 

2 

3Protocols for GraphQL execution, schema building, data loading, 

4resolvers, and subscriptions. 

5""" 

6 

7from __future__ import annotations 

8 

9from typing import ( 

10 TYPE_CHECKING, 

11 Any, 

12 Protocol, 

13 runtime_checkable, 

14) 

15 

16# constant used by web layer when registering subscription routes 

17DEFAULT_SUBSCRIPTIONS_PATH: str = "/graphql/ws" 

18 

19 

20if TYPE_CHECKING: 

21 from collections.abc import AsyncIterator 

22 

23 from lexigram.contracts.core.result import Result 

24 from lexigram.contracts.exceptions.base import LexigramError 

25 from lexigram.contracts.graphql.types import GraphQLPrincipal 

26 

27 

28@runtime_checkable 

29class GraphQLExecutorProtocol(Protocol): 

30 """Protocol for GraphQL query execution.""" 

31 

32 async def execute( 

33 self, 

34 query: str, 

35 variables: dict[str, Any] | None = None, 

36 context: Any | None = None, 

37 operation_name: str | None = None, 

38 ) -> Result[dict[str, Any], LexigramError]: 

39 """Execute a GraphQL query. 

40 

41 Args: 

42 query: GraphQL query string 

43 variables: Query variables 

44 context: Execution context 

45 operation_name: Operation name for multi-operation queries 

46 

47 Returns: 

48 Ok(result_dict) on success; Err(error) on transport-level failure 

49 (e.g. timeout, schema misconfiguration). GraphQL field-level errors 

50 are carried inside the Ok value via the ``errors`` key. 

51 """ 

52 ... 

53 

54 

55@runtime_checkable 

56class GraphQLControllerProtocol(Protocol): 

57 """Marker protocol representing the HTTP controller for GraphQL. 

58 

59 The web integration only needs to be able to resolve an instance from the 

60 container; no specific methods are required. 

61 """ 

62 

63 

64@runtime_checkable 

65class SchemaBuilderProtocol(Protocol): 

66 """Protocol for building GraphQL schemas via a fluent interface.""" 

67 

68 def add_type(self, type_class: Any) -> SchemaBuilderProtocol: 

69 """Add an additional type to the schema. 

70 

71 Args: 

72 type_class: The type class to add. 

73 

74 Returns: 

75 Self for chaining. 

76 """ 

77 ... 

78 

79 def query(self, query_type: Any) -> SchemaBuilderProtocol: 

80 """Set the root query type. 

81 

82 Args: 

83 query_type: The query type class. 

84 

85 Returns: 

86 Self for chaining. 

87 """ 

88 ... 

89 

90 def mutation(self, mutation_type: Any) -> SchemaBuilderProtocol: 

91 """Set the root mutation type. 

92 

93 Args: 

94 mutation_type: The mutation type class. 

95 

96 Returns: 

97 Self for chaining. 

98 """ 

99 ... 

100 

101 def subscription(self, subscription_type: Any) -> SchemaBuilderProtocol: 

102 """Set the root subscription type. 

103 

104 Args: 

105 subscription_type: The subscription type class. 

106 

107 Returns: 

108 Self for chaining. 

109 """ 

110 ... 

111 

112 def add_extension(self, extension: Any) -> SchemaBuilderProtocol: 

113 """Register a schema extension. 

114 

115 Args: 

116 extension: The extension instance (e.g. a Strawberry SchemaExtension). 

117 

118 Returns: 

119 Self for chaining. 

120 """ 

121 ... 

122 

123 def add_dataloader( 

124 self, 

125 name: str, 

126 factory: Any, 

127 ) -> SchemaBuilderProtocol: 

128 """Register a DataLoaderProtocol factory for per-request loader initialisation. 

129 

130 The factory is called once per request with the current 

131 :class:`GraphQLContext` as its sole argument and must return a 

132 DataLoaderProtocol instance. All registered factories are forwarded to the 

133 :class:`ContextFactory` so loaders are available at resolve time via 

134 ``context.get_dataloader(name)``. 

135 

136 Args: 

137 name: Unique loader name (used as the key in context). 

138 factory: Callable ``(context) -> DataLoaderProtocol`` that creates the 

139 loader for each request. 

140 

141 Returns: 

142 Self for chaining. 

143 """ 

144 ... 

145 

146 def build(self) -> Any: 

147 """Build and return the configured GraphQL schema.""" 

148 ... 

149 

150 

151@runtime_checkable 

152class DataLoaderProtocol(Protocol): 

153 """Protocol for GraphQL data loading (N+1 problem solution).""" 

154 

155 async def load(self, key: Any) -> Any: 

156 """Load a single item by key.""" 

157 ... 

158 

159 async def load_many(self, keys: list[Any]) -> list[Any]: 

160 """Load multiple items by keys.""" 

161 ... 

162 

163 def prime(self, key: Any, value: Any) -> None: 

164 """Prime the cache with a key-value pair.""" 

165 ... 

166 

167 

168@runtime_checkable 

169class ResolverProtocol(Protocol): 

170 """Protocol for GraphQL field resolvers.""" 

171 

172 async def resolve( 

173 self, 

174 parent: Any, 

175 args: dict[str, Any], 

176 context: Any, 

177 info: Any, 

178 ) -> Any: 

179 """Resolve a GraphQL field. 

180 

181 Args: 

182 parent: Parent object 

183 args: Field arguments 

184 context: Execution context 

185 info: Field resolution info 

186 

187 Returns: 

188 Resolved field value 

189 """ 

190 ... 

191 

192 

193@runtime_checkable 

194class EntityResolverProtocol(Protocol): 

195 """Protocol for resolving entities in GraphQL federation.""" 

196 

197 async def resolve_reference( 

198 self, 

199 reference: dict[str, Any], 

200 context: Any, 

201 info: Any, 

202 ) -> Any | None: 

203 """Resolve an entity by reference. 

204 

205 Args: 

206 reference: Entity reference 

207 context: Execution context 

208 info: Resolution info 

209 

210 Returns: 

211 Resolved entity or None 

212 """ 

213 ... 

214 

215 

216@runtime_checkable 

217class ValidationRuleProtocol(Protocol): 

218 """Protocol for GraphQL query validators (depth, complexity, alias, etc.). 

219 

220 Implementations perform a single focused check on a parsed document 

221 and raise a :class:`~lexigram.graphql.exceptions.GraphQLError` 

222 subclass on failure. A no-op return indicates the document passed. 

223 

224 All three built-in validators — :class:`DepthLimitValidator`, 

225 :class:`ComplexityAnalyzer`, and :class:`AliasLimitValidator` — 

226 satisfy this protocol. 

227 """ 

228 

229 def validate(self, document: Any) -> None: 

230 """Validate a GraphQL document; raise on failure. 

231 

232 Args: 

233 document: Parsed GraphQL document (a ``graphql-core`` 

234 ``DocumentNode`` in practice, typed as ``Any`` to avoid 

235 a hard compile-time dependency on ``graphql-core``). 

236 

237 Raises: 

238 GraphQLBaseError: If the document violates this rule. 

239 """ 

240 ... 

241 

242 

243@runtime_checkable 

244class SubscriptionHandlerProtocol(Protocol): 

245 """Protocol for field-level GraphQL subscription resolvers. 

246 

247 Implementations produce an async iterable of events for a single 

248 subscription field. This is the *resolver-level* contract; for the 

249 WebSocket transport protocol see :class:`WebSocketTransportProtocol`. 

250 """ 

251 

252 async def subscribe( 

253 self, 

254 field_name: str, 

255 args: dict[str, Any], 

256 context: Any, 

257 info: Any, 

258 ) -> AsyncIterator[Any]: 

259 """Yield events for the named subscription field. 

260 

261 Args: 

262 field_name: Subscription field name 

263 args: Subscription arguments 

264 context: Execution context 

265 info: Field resolution info 

266 

267 Yields: 

268 Subscription events 

269 """ 

270 ... 

271 

272 

273@runtime_checkable 

274class SubscriptionAuthHandlerProtocol(Protocol): 

275 """Protocol for per-subscription authorization checks. 

276 

277 Invoked during ``_handle_subscribe`` before the subscription is 

278 established. Return ``False`` to reject the subscription. 

279 

280 This is a *per-subscription* check distinct from the connection-level 

281 authentication performed by ``SubscriptionAuth.authenticate`` during 

282 ``connection_init``. 

283 """ 

284 

285 async def authorize( 

286 self, 

287 user: Any, 

288 operation_name: str | None, 

289 query: str | None, 

290 ) -> bool: 

291 """Authorize a subscription request. 

292 

293 Args: 

294 user: The authenticated user from the connection context, 

295 or ``None`` when no authentication handler is configured. 

296 operation_name: The GraphQL operation name, if provided. 

297 query: The raw GraphQL query string. 

298 

299 Returns: 

300 ``True`` if the subscription is authorized, ``False`` to reject. 

301 """ 

302 ... 

303 

304 

305@runtime_checkable 

306class WebSocketTransportProtocol(Protocol): 

307 """Protocol for WebSocket-level GraphQL subscription transport. 

308 

309 Describes the connection-handler contract for classes that manage 

310 the full lifecycle of a WebSocket subscription session (e.g. 

311 the ``graphql-transport-ws`` protocol). It is satisfied structurally 

312 by :class:`~lexigram.graphql.subscriptions.transport.GraphQLWSTransport`. 

313 

314 This is deliberately separate from :class:`SubscriptionHandler` which 

315 operates at the field-resolver level. 

316 """ 

317 

318 async def handle(self, websocket: Any, app: Any | None = None) -> None: 

319 """Handle a single WebSocket connection for the entire session. 

320 

321 Implementations should accept the WebSocket, drive the 

322 protocol handshake, stream subscription events, and clean up 

323 on disconnect. 

324 

325 Args: 

326 websocket: A ``WebSocketProtocol``-compatible connection 

327 (typed as ``Any`` to avoid a hard Starlette dependency 

328 in the contracts layer). 

329 app: Optional application instance for DI / container 

330 resolution. 

331 """ 

332 ... 

333 

334 

335@runtime_checkable 

336class MutationHandlerProtocol(Protocol): 

337 """Protocol for GraphQL mutation handling.""" 

338 

339 async def mutate( 

340 self, 

341 field_name: str, 

342 args: dict[str, Any], 

343 context: Any, 

344 info: Any, 

345 ) -> Any: 

346 """Handle a GraphQL mutation. 

347 

348 Args: 

349 field_name: Mutation field name 

350 args: Mutation arguments 

351 context: Execution context 

352 info: Field info 

353 

354 Returns: 

355 Mutation result 

356 """ 

357 ... 

358 

359 

360@runtime_checkable 

361class DirectiveHandlerProtocol(Protocol): 

362 """Protocol for GraphQL directive handling.""" 

363 

364 def apply_directive( 

365 self, 

366 directive_name: str, 

367 args: dict[str, Any], 

368 target: Any, 

369 ) -> Any: 

370 """Apply a GraphQL directive. 

371 

372 Args: 

373 directive_name: Directive name 

374 args: Directive arguments 

375 target: Target object to apply directive to 

376 

377 Returns: 

378 Modified target object 

379 """ 

380 ... 

381 

382 

383@runtime_checkable 

384class ErrorFormatterProtocol(Protocol): 

385 """Protocol for formatting GraphQL errors. 

386 

387 Implementations receive the error only; any request-scoped data 

388 (e.g. request_id, user) must be accessed via the shared execution context 

389 rather than as a method parameter. 

390 """ 

391 

392 def format_error( 

393 self, 

394 error: Any, 

395 ) -> dict[str, Any]: 

396 """Format a GraphQL error for response. 

397 

398 Args: 

399 error: GraphQL error object. 

400 

401 Returns: 

402 Formatted error dictionary conforming to the GraphQL error spec. 

403 """ 

404 ... 

405 

406 

407@runtime_checkable 

408class GraphQLRequestProtocol(Protocol): 

409 """Protocol for HTTP requests processed by the GraphQL controller. 

410 

411 Decouples ``GraphQLController`` from concrete web-framework types 

412 (e.g. Starlette ``Request``) so that the graphql package does not 

413 create a cross-extension import dependency on ``lexigram-web``. 

414 

415 Any framework request object that exposes these members satisfies 

416 the protocol at runtime. 

417 """ 

418 

419 state: Any 

420 """Mutable per-request state bag (e.g. ``request.state``).""" 

421 

422 scope: dict[str, Any] 

423 """ASGI connection scope dictionary.""" 

424 

425 async def json(self) -> Any: 

426 """Deserialise the request body as JSON. 

427 

428 Returns: 

429 Parsed JSON value (typically a ``dict``). 

430 """ 

431 ... 

432 

433 

434@runtime_checkable 

435class IntrospectionHandlerProtocol(Protocol): 

436 """Protocol for GraphQL introspection. 

437 

438 Implementations perform full schema introspection and return the 

439 standard GraphQL introspection result. 

440 """ 

441 

442 async def introspect(self, context: Any) -> dict[str, Any]: 

443 """Perform GraphQL introspection. 

444 

445 Args: 

446 context: Introspection context 

447 

448 Returns: 

449 Introspection result 

450 """ 

451 ... 

452 

453 

454@runtime_checkable 

455class GraphQLPrincipalResolverProtocol(Protocol): 

456 """Protocol for resolving GraphQL principals from authentication sources. 

457 

458 Implementations transform raw authentication data (e.g., decoded JWT, 

459 OAuth2 user object) into a framework-standard :class:`GraphQLPrincipal` 

460 for use in the GraphQL execution context. 

461 

462 This decouples the GraphQL layer from authentication implementation 

463 details and enables consistent principal access across all resolvers. 

464 """ 

465 

466 async def resolve_principal( 

467 self, 

468 user: Any, 

469 request: Any = None, 

470 ) -> GraphQLPrincipal: 

471 """Resolve a GraphQLPrincipal from the authenticated user. 

472 

473 Args: 

474 user: Raw authenticated user object from the authentication 

475 layer (e.g., decoded JWT payload, OAuth2 user dict). 

476 request: Optional HTTP request object for additional context 

477 (e.g., extracting headers, client IP). 

478 

479 Returns: 

480 A :class:`GraphQLPrincipal` with identity fields populated 

481 from the authentication source. 

482 """ 

483 ... 

484 

485 

486__all__ = [ 

487 "DataLoaderProtocol", 

488 "DirectiveHandlerProtocol", 

489 "EntityResolverProtocol", 

490 "ErrorFormatterProtocol", 

491 "GraphQLControllerProtocol", 

492 "GraphQLExecutorProtocol", 

493 "GraphQLPrincipalResolverProtocol", 

494 "GraphQLRequestProtocol", 

495 "IntrospectionHandlerProtocol", 

496 "MutationHandlerProtocol", 

497 "ResolverProtocol", 

498 "SchemaBuilderProtocol", 

499 "SubscriptionAuthHandlerProtocol", 

500 "SubscriptionHandlerProtocol", 

501 "ValidationRuleProtocol", 

502 "WebSocketTransportProtocol", 

503]