redhat-datalayer-graphql 0.1.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.
@@ -0,0 +1,7 @@
1
+ # Supergraph integration test (copy to .env and fill in)
2
+ SUPERGRAPH_URL=https://vpn.graphql.stage.redhat.com
3
+ SUPERGRAPH_TOKEN=your-bearer-token-here
4
+ SUPERGRAPH_CLIENT_NAME=datalayer-sdk-test
5
+ SUPERGRAPH_VERIFY_SSL=false
6
+ SUPERGRAPH_SMOKE_OPERATION=Cves
7
+ SUPERGRAPH_SMOKE_VARIABLES={"first": 10}
@@ -0,0 +1,54 @@
1
+ # --- Secrets & local env ---
2
+ .env
3
+ .env.*
4
+ !.env.example
5
+ # --- Python ---
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+ *.so
10
+ .Python
11
+ # --- Packaging / build ---
12
+ build/
13
+ dist/
14
+ wheels/
15
+ *.egg-info/
16
+ *.egg
17
+ pip-wheel-metadata/
18
+ # --- Virtual environments ---
19
+ .venv/
20
+ venv/
21
+ env/
22
+ # --- uv (keep uv.lock — commit it) ---
23
+ # --- Testing ---
24
+ .pytest_cache/
25
+ .coverage
26
+ .coverage.*
27
+ htmlcov/
28
+ .tox/
29
+ .nox/
30
+ coverage.xml
31
+ *.cover
32
+ .hypothesis/
33
+ # --- Type checkers / linters ---
34
+ .mypy_cache/
35
+ .ruff_cache/
36
+ .pytype/
37
+ # --- IDE / editors ---
38
+ .idea/
39
+ .vscode/
40
+ *.swp
41
+ *.swo
42
+ *~
43
+ .project
44
+ .pydevproject
45
+ .settings/
46
+ # --- OS ---
47
+ .DS_Store
48
+ Thumbs.db
49
+ # --- Logs & temp ---
50
+ *.log
51
+ *.tmp
52
+ .cache/
53
+
54
+ .cursor
@@ -0,0 +1,382 @@
1
+ Metadata-Version: 2.5
2
+ Name: redhat-datalayer-graphql
3
+ Version: 0.1.0
4
+ Summary: Async Python client for Red Hat's GraphQL supergraph (Apollo Router)
5
+ Author: Red Hat DataLayer Team
6
+ Maintainer-email: Mayur Deshmukh <mdeshmuk@redhat.com>, Pranav Advani <padvani@redhat.com>
7
+ License-Expression: Apache-2.0
8
+ Requires-Python: >=3.13
9
+ Requires-Dist: gql[httpx]>=3.5.0
10
+ Requires-Dist: graphql-core>=3.2.0
11
+ Requires-Dist: httpx>=0.28.1
12
+ Requires-Dist: pydantic>=2.13.4
13
+ Description-Content-Type: text/markdown
14
+
15
+ # datalayer-graphql
16
+
17
+ Async Python client for Red Hat's GraphQL **supergraph** (Apollo Router). Provides a single connection surface — auth, Apollo client headers, and operation execution — so every consumer (MCP servers, scripts, tests) connects the same way.
18
+
19
+ Built on [gql](https://gql.readthedocs.io/) with an [httpx](https://www.python-httpx.org/) transport, adding org-specific auth patterns, Apollo headers, and federation-aware error handling.
20
+
21
+ ## Features
22
+
23
+ - Async `SupergraphClient` built on `gql` + `httpx`
24
+ - Typed `SupergraphConfig` via [Pydantic](https://docs.pydantic.dev/)
25
+ - Bearer token auth from environment variables
26
+ - Standard Apollo client headers (`apollographql-client-name`, `apollographql-client-version`)
27
+ - Operation registry — call `execute(operation_name=...)` without passing the query each time
28
+ - `.graphql` file loading via `load_operations()`
29
+ - Structured exception hierarchy with federation error support
30
+ - Response headers captured for router inspection
31
+
32
+ ## Requirements
33
+
34
+ - Python 3.13+
35
+ - [uv](https://docs.astral.sh/uv/) (recommended) or pip
36
+ - VPN access for internal endpoints (e.g. `vpn.graphql.stage.redhat.com`)
37
+ - Valid GraphQL bearer token with `api.graphql` scope
38
+
39
+ ## Installation
40
+
41
+ From the monorepo root:
42
+
43
+ ```bash
44
+ uv sync --package datalayer-graphql
45
+ ```
46
+
47
+ As a workspace dependency (already wired in `datalayer-mcp`):
48
+
49
+ ```toml
50
+ dependencies = ["datalayer-graphql"]
51
+
52
+ [tool.uv.sources]
53
+ datalayer-graphql = { workspace = true }
54
+ ```
55
+
56
+ ## Quick start
57
+
58
+ ```python
59
+ import asyncio
60
+
61
+ from datalayer_graphql import BearerTokenAuth, SupergraphClient, SupergraphConfig
62
+
63
+
64
+ async def main() -> None:
65
+ client = SupergraphClient(
66
+ SupergraphConfig(
67
+ url="https://vpn.graphql.stage.redhat.com",
68
+ auth=BearerTokenAuth(token_env="SUPERGRAPH_TOKEN"),
69
+ client_name="rh-graphql-studio-explorer",
70
+ operations={
71
+ "Cves": """
72
+ query Cves($first: Int!) {
73
+ cves(first: $first) {
74
+ totalCount
75
+ edges { node { title url } }
76
+ }
77
+ }
78
+ """,
79
+ },
80
+ )
81
+ )
82
+
83
+ async with client:
84
+ result = await client.execute(
85
+ operation_name="Cves",
86
+ variables={"first": 10},
87
+ )
88
+ print(result.data)
89
+
90
+
91
+ asyncio.run(main())
92
+ ```
93
+
94
+ Set the token before running:
95
+
96
+ ```bash
97
+ export SUPERGRAPH_TOKEN="eyJ..." # client-credentials JWT with api.graphql scope
98
+ ```
99
+
100
+ See [`examples/graphql/`](../../examples/graphql/) for more complete examples including
101
+ operations registries, variable passing, and `.graphql` file loading.
102
+
103
+ ### Loading operations from `.graphql` files
104
+
105
+ ```python
106
+ from pathlib import Path
107
+ from datalayer_graphql import load_operations, SupergraphConfig, BearerTokenAuth
108
+
109
+ operations = load_operations(Path("operations/"))
110
+
111
+ config = SupergraphConfig(
112
+ url="https://vpn.graphql.stage.redhat.com",
113
+ auth=BearerTokenAuth(token_env="SUPERGRAPH_TOKEN"),
114
+ operations=operations,
115
+ )
116
+ ```
117
+
118
+ Each `.graphql` file must contain exactly one named operation. The operation name becomes the key in the returned dict.
119
+
120
+ ## Configuration
121
+
122
+ ### `SupergraphConfig`
123
+
124
+ | Field | Type | Default | Description |
125
+ |-------|------|---------|-------------|
126
+ | `url` | `HttpUrl` | — | Supergraph endpoint URL |
127
+ | `auth` | `BearerTokenAuth` | — | Authentication configuration |
128
+ | `client_name` | `str` | `"rh-graphql-studio-explorer"` | Value for `apollographql-client-name` header |
129
+ | `client_version` | `str` | `"latest"` | Value for `apollographql-client-version` header |
130
+ | `timeout` | `float` | `30.0` | HTTP request timeout in seconds |
131
+ | `verify_ssl` | `bool` | `True` | TLS certificate verification. Set `False` on VPN/corporate networks with internal CAs |
132
+ | `operations` | `dict[str, str]` | `{}` | Maps operation name -> GraphQL query string |
133
+ | `extra_headers` | `dict[str, str]` | `{}` | Additional HTTP headers merged into every request |
134
+
135
+ ### `BearerTokenAuth`
136
+
137
+ Reads a bearer token from an environment variable at request time (not stored in config):
138
+
139
+ ```python
140
+ BearerTokenAuth(token_env="SUPERGRAPH_TOKEN")
141
+ ```
142
+
143
+ - Looks up `os.environ["SUPERGRAPH_TOKEN"]`
144
+ - Strips whitespace and a leading `Bearer ` prefix if present
145
+ - Sets `Authorization: Bearer <token>` on every HTTP request via httpx auth hooks
146
+
147
+ ### Operation registry
148
+
149
+ GraphQL POST bodies require a `query` string. To support an API that only passes `operation_name`, register queries on config:
150
+
151
+ ```python
152
+ SupergraphConfig(
153
+ url="...",
154
+ auth=BearerTokenAuth(token_env="SUPERGRAPH_TOKEN"),
155
+ operations={
156
+ "GetCustomerById": "query GetCustomerById($id: ID!) { customer(id: $id) { id name } }",
157
+ "Cves": "query Cves($first: Int!) { cves(first: $first) { totalCount } }",
158
+ },
159
+ )
160
+ ```
161
+
162
+ You can also pass `query=` directly to `execute()` to bypass the registry:
163
+
164
+ ```python
165
+ await client.execute(
166
+ operation_name="AdHoc",
167
+ query="query AdHoc { __typename }",
168
+ )
169
+ ```
170
+
171
+ ## API reference
172
+
173
+ ### `SupergraphClient`
174
+
175
+ ```python
176
+ client = SupergraphClient(config: SupergraphConfig)
177
+ ```
178
+
179
+ Creates a `gql.Client` with an `HTTPXAsyncTransport`, configured with default headers, auth, and connection pooling.
180
+
181
+ #### `execute`
182
+
183
+ ```python
184
+ result = await client.execute(
185
+ operation_name: str,
186
+ variables: dict | None = None,
187
+ query: str | None = None,
188
+ ) -> ExecuteResult
189
+ ```
190
+
191
+ Sends a GraphQL POST request via the `gql` transport.
192
+
193
+ **Returns:** `ExecuteResult(data=..., extensions=..., response_headers=...)`
194
+
195
+ #### Context manager
196
+
197
+ Always close the client when done (or use `async with`):
198
+
199
+ ```python
200
+ async with SupergraphClient(config) as client:
201
+ result = await client.execute(operation_name="Cves", variables={"first": 10})
202
+ ```
203
+
204
+ ### `ExecuteResult`
205
+
206
+ ```python
207
+ @dataclass(frozen=True)
208
+ class ExecuteResult:
209
+ data: dict[str, Any] | None
210
+ extensions: dict[str, Any] | None = None
211
+ response_headers: Mapping[str, str] | None = None
212
+ ```
213
+
214
+ ### `load_operations`
215
+
216
+ ```python
217
+ from pathlib import Path
218
+
219
+ operations: dict[str, str] = load_operations(Path("operations/"))
220
+ ```
221
+
222
+ Scans a directory for `.graphql` files. Each file must contain exactly one named operation. Returns `{operation_name: query_string}`.
223
+
224
+ ## Error handling
225
+
226
+ ### Exception hierarchy
227
+
228
+ ```
229
+ SupergraphError # Base for all supergraph errors
230
+ ├── SupergraphConnectionError # Network / connection failures
231
+ ├── SupergraphHTTPError # HTTP 4xx/5xx (status_code attribute)
232
+ └── GraphQLError # GraphQL-level errors in response body
233
+ ├── errors: list[GraphQLErrorDetail] # Parsed error details
234
+ ├── data: dict | None # Partial data (federation)
235
+ └── extensions: dict | None
236
+ ```
237
+
238
+ ### `GraphQLErrorDetail`
239
+
240
+ ```python
241
+ @dataclass(frozen=True)
242
+ class GraphQLErrorDetail:
243
+ message: str
244
+ path: list[str | int] | None
245
+ locations: list[dict[str, int]] | None
246
+ extensions: dict[str, Any] | None
247
+
248
+ @property
249
+ def code(self) -> str | None: ... # e.g. "SUBREQUEST_HTTP_ERROR"
250
+ @property
251
+ def service(self) -> str | None: ... # e.g. "rhg-docs-aem"
252
+ ```
253
+
254
+ ### Error mapping
255
+
256
+ | Situation | Exception |
257
+ |-----------|-----------|
258
+ | Missing env var for token | `ValueError` from `BearerTokenAuth.resolve_token()` |
259
+ | Missing query for operation | `ValueError` from `execute()` |
260
+ | HTTP 401/403/503 | `SupergraphHTTPError` (`.status_code` attribute) |
261
+ | GraphQL errors in body | `GraphQLError` with parsed `GraphQLErrorDetail` list |
262
+ | Network / connection failure | `SupergraphConnectionError` |
263
+ | Malformed response | `SupergraphError` |
264
+ | Timeout | `TimeoutError` (standard Python) |
265
+
266
+ Federation error example:
267
+
268
+ ```python
269
+ try:
270
+ result = await client.execute(operation_name="GetDocs")
271
+ except GraphQLError as e:
272
+ for detail in e.errors:
273
+ print(detail.code) # "SUBREQUEST_HTTP_ERROR"
274
+ print(detail.service) # "rhg-docs-aem"
275
+ if e.data:
276
+ print("Partial data:", e.data)
277
+ ```
278
+
279
+ ## Package layout
280
+
281
+ ```
282
+ src/datalayer_graphql/
283
+ ├── __init__.py # Public exports
284
+ ├── auth/
285
+ │ └── bearer.py # BearerTokenAuth + httpx auth flow
286
+ ├── client/
287
+ │ └── supergraph.py # SupergraphClient (wraps gql.Client)
288
+ ├── config/
289
+ │ └── supergraph.py # SupergraphConfig + header builder
290
+ ├── exceptions.py # Exception hierarchy
291
+ ├── models.py # ExecuteResult
292
+ └── operations.py # load_operations()
293
+ ```
294
+
295
+ ## Tests
296
+
297
+ ### Unit tests
298
+
299
+ ```bash
300
+ uv run pytest packages/datalayer-graphql/tests/test_client.py tests/test_operations.py -v
301
+ ```
302
+
303
+ ### Integration smoke tests
304
+
305
+ Live smoke tests hit the stage VPN supergraph. They are **skipped** automatically when `.env` is missing or incomplete.
306
+
307
+ #### Setup
308
+
309
+ ```bash
310
+ cd packages/datalayer-graphql
311
+ cp .env.example .env
312
+ ```
313
+
314
+ Edit `.env`:
315
+
316
+ ```env
317
+ SUPERGRAPH_URL=https://vpn.graphql.stage.redhat.com
318
+ SUPERGRAPH_TOKEN=your-bearer-token-here
319
+ SUPERGRAPH_CLIENT_NAME=rh-graphql-studio-explorer
320
+ SUPERGRAPH_VERIFY_SSL=false
321
+ SUPERGRAPH_SMOKE_OPERATION=Cves
322
+ SUPERGRAPH_SMOKE_VARIABLES={"first": 10}
323
+ ```
324
+
325
+ #### Run
326
+
327
+ ```bash
328
+ uv run pytest packages/datalayer-graphql/tests/test_smoke_supergraph.py -v -s
329
+ ```
330
+
331
+ ### Obtaining a token
332
+
333
+ Tokens expire after ~15 minutes. Use the included token script to fetch a fresh
334
+ client-credentials token from Red Hat SSO and write it to your `.env` file:
335
+
336
+ ```bash
337
+ uv run scripts/fetch_token.py \
338
+ --client-id my-client-id \
339
+ --client-secret YOUR_SECRET \
340
+ --env-file packages/datalayer-graphql/.env
341
+ ```
342
+
343
+ Or fetch manually with curl:
344
+
345
+ ```bash
346
+ curl -X POST \
347
+ "https://sso.stage.redhat.com/auth/realms/redhat-external/protocol/openid-connect/token" \
348
+ -H "Content-Type: application/x-www-form-urlencoded" \
349
+ -d "grant_type=client_credentials" \
350
+ -d "client_id=my-client-id" \
351
+ -d "client_secret=YOUR_SECRET" \
352
+ -d "scope=api.graphql"
353
+ ```
354
+
355
+ Copy `access_token` into `SUPERGRAPH_TOKEN` in `.env`.
356
+
357
+ ## Development
358
+
359
+ ### Install dev dependencies
360
+
361
+ ```bash
362
+ uv sync --all-packages
363
+ ```
364
+
365
+ ### Lint and type check
366
+
367
+ ```bash
368
+ uv run ruff check .
369
+ uv run mypy packages/datalayer-graphql/src/
370
+ ```
371
+
372
+ ### Roadmap
373
+
374
+ - [ ] `ClientCredentialsAuth` — auto-fetch and refresh tokens from Red Hat SSO
375
+ - [ ] Router response header validation (`rhg-auth`, `cache-control`)
376
+ - [x] Federation-aware error types (`GraphQLErrorDetail` with `code`, `service`)
377
+ - [x] Unit tests with mock transport
378
+ - [x] `.graphql` file loading utility
379
+
380
+ ## License
381
+
382
+ Internal Red Hat / Datalayer project.