Coverage for src/lexigram/graphql/core/context/_context.py: 70%
56 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:37 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:37 +0800
1"""Per-request GraphQL context."""
3from __future__ import annotations
5from dataclasses import dataclass, field
6from datetime import UTC, datetime
7from typing import TYPE_CHECKING, Any, Generic, TypeVar
8import uuid
10from lexigram.contracts.auth import AuthenticatorProtocol
11from lexigram.contracts.core import IdGeneratorProtocol
12from lexigram.contracts.graphql import (
13 GraphQLPrincipal,
14 GraphQLPrincipalResolverProtocol,
15)
16from lexigram.domain import DomainModel
17from lexigram.graphql.core.context._models import GraphQLRequest
18from lexigram.logging import get_logger
19from lexigram.primitives import clock as ambient_clock
20from lexigram.validation import Field
22if TYPE_CHECKING:
23 from lexigram.graphql.config import GraphQLConfig
25logger = get_logger(__name__)
28T = TypeVar("T")
31@dataclass
32class GraphQLContext:
33 """GraphQL execution context.
35 Provides context for GraphQL operations including
36 request information, user data, and configuration.
38 Attributes:
39 request_id: Unique request identifier.
40 user: Current user (if authenticated).
41 principal: Resolved principal for identity access across resolvers.
42 request: The GraphQL request.
43 config: GraphQL configuration.
44 started_at: Request start time.
45 metadata: Additional context metadata.
46 dataloaders: DataLoaderProtocol instances by name.
47 """
49 request_id: str = field(default_factory=lambda: str(uuid.uuid4()))
50 user: Any | None = None
51 principal: GraphQLPrincipal | None = None
52 request: GraphQLRequest | None = None
53 config: GraphQLConfig | None = None
54 started_at: datetime = field(default_factory=lambda: datetime.now(UTC))
55 metadata: dict[str, Any] = field(default_factory=dict)
56 dataloaders: dict[str, Any] = field(default_factory=dict)
57 raw_request: Any | None = None
58 #: Per-request DI scope — dispose via ``await context.dispose_scope()``
59 #: after the request completes to release scoped services.
60 scope: Any | None = None
62 def get_dataloader(self, name: str) -> Any | None:
63 """Get a DataLoaderProtocol by name.
65 Args:
66 name: DataLoaderProtocol name.
68 Returns:
69 The DataLoaderProtocol instance or None.
70 """
71 return self.dataloaders.get(name)
73 def set_dataloader(self, name: str, loader: Any) -> None:
74 """Set a DataLoaderProtocol.
76 Args:
77 name: DataLoaderProtocol name.
78 loader: DataLoaderProtocol instance.
79 """
80 self.dataloaders[name] = loader
82 def get_metadata(self, key: str, default: Any = None) -> Any:
83 """Get metadata value.
85 Args:
86 key: Metadata key.
87 default: Default value if not found.
89 Returns:
90 The metadata value.
91 """
92 return self.metadata.get(key, default)
94 def set_metadata(self, key: str, value: Any) -> None:
95 """Set metadata value.
97 Args:
98 key: Metadata key.
99 value: Metadata value.
100 """
101 self.metadata[key] = value
103 @property
104 def operation_name(self) -> str | None:
105 """Get the operation name from request."""
106 if self.request:
107 return self.request.operation_name
108 return None
110 @property
111 def variables(self) -> dict[str, Any]:
112 """Get variables from request."""
113 if self.request:
114 return self.request.variables
115 return {}
117 @property
118 def elapsed_ms(self) -> float:
119 """Get elapsed time in milliseconds."""
120 elapsed = datetime.now(UTC) - self.started_at
121 return elapsed.total_seconds() * 1000
123 async def dispose_scope(self) -> None:
124 """Dispose the per-request DI scope, releasing all scoped services.
126 No-op when no scope was created for this context. The GraphQL
127 executor calls this automatically; application code should not
128 normally need to call it directly.
129 """
130 if self.scope is not None and hasattr(self.scope, "dispose"):
131 import inspect
133 result = self.scope.dispose()
134 if inspect.isawaitable(result):
135 await result
136 self.scope = None