pyrelay 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.
pyrelay-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 nehz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
pyrelay-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,211 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyrelay
3
+ Version: 0.1.0
4
+ Summary: Relay-spec GraphQL helpers for Python: global object IDs, cursor connections, node registry, and mutation payloads. Framework-agnostic, zero dependencies.
5
+ Author: nehz
6
+ License-Expression: MIT
7
+ Keywords: graphql,relay,pagination,cursor,connection,global-id
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Provides-Extra: test
25
+ Requires-Dist: pytest>=7; extra == "test"
26
+ Dynamic: license-file
27
+
28
+ # pyrelay
29
+
30
+ **Relay-spec GraphQL helpers for Python. Framework-agnostic, with zero dependencies.**
31
+
32
+ Relay's server conventions (opaque global IDs, cursor connections, the `node`
33
+ refetch field, `clientMutationId` on mutations) are small, but they're easy to
34
+ get subtly wrong. Each Python GraphQL library also tends to ship its own
35
+ version. `pyrelay` implements them once as plain Python functions and
36
+ dataclasses. You can use it with Strawberry, Graphene, Ariadne, `graphql-core`,
37
+ or a hand-rolled resolver layer. The ID and cursor formats match the reference
38
+ `graphql-relay-js` implementation, so existing Relay clients and data keep working.
39
+
40
+ ## Features
41
+
42
+ - **Global object IDs**: `to_global_id` / `from_global_id` encode and decode `base64("Type:id")`, with strict validation.
43
+ - **Cursor connections**: `connection_from_sequence` implements the Relay Cursor Connections spec (`first`/`after`/`last`/`before`) and produces correct `PageInfo`.
44
+ - **Database-friendly slicing**: `connection_from_sequence_slice` paginates when only an `OFFSET`/`LIMIT` window is loaded.
45
+ - **Node registry**: `NodeRegistry` maps type names to loaders for the `node(id:)` field. It forwards context and works with async loaders.
46
+ - **Mutation helper**: the `@relay_mutation` decorator strips `clientMutationId` from the input and echoes it back in the payload. It works for sync and async functions.
47
+ - **GraphQL-shaped output**: `.to_dict()` emits camelCase `edges` / `pageInfo` / `totalCount`.
48
+ - Fully type-hinted (`py.typed`), standard library only, Python 3.9+.
49
+
50
+ ## Install
51
+
52
+ ```bash
53
+ pip install pyrelay # once published
54
+ # or, from a checkout:
55
+ pip install -e .
56
+ ```
57
+
58
+ ## Quickstart
59
+
60
+ ```python
61
+ from pyrelay import (
62
+ ConnectionArguments,
63
+ NodeRegistry,
64
+ connection_from_sequence,
65
+ from_global_id,
66
+ relay_mutation,
67
+ to_global_id,
68
+ )
69
+
70
+ # 1. Global object IDs
71
+ gid = to_global_id("User", 42)
72
+ assert gid == "VXNlcjo0Mg=="
73
+ assert from_global_id(gid) == ("User", "42")
74
+
75
+ # 2. Cursor pagination over any sequence
76
+ users = [{"id": i, "name": n} for i, n in enumerate(["Ada", "Grace", "Linus", "Guido"])]
77
+ page1 = connection_from_sequence(users, ConnectionArguments(first=2))
78
+ assert [u["name"] for u in page1.nodes] == ["Ada", "Grace"]
79
+ assert page1.page_info.has_next_page
80
+
81
+ page2 = connection_from_sequence(
82
+ users, ConnectionArguments(first=2, after=page1.page_info.end_cursor)
83
+ )
84
+ assert [u["name"] for u in page2.nodes] == ["Linus", "Guido"]
85
+ assert not page2.page_info.has_next_page
86
+
87
+ # GraphQL-shaped result, e.g. to return from a resolver
88
+ data = page2.to_dict(lambda u: {"id": to_global_id("User", u["id"]), "name": u["name"]})
89
+ assert set(data) == {"edges", "pageInfo"}
90
+
91
+ # 3. The `node(id:)` field
92
+ registry = NodeRegistry()
93
+
94
+ @registry.register("User")
95
+ def load_user(local_id: str, context=None):
96
+ return users[int(local_id)]
97
+
98
+ assert registry.resolve(registry.global_id("User", 1))["name"] == "Grace"
99
+ assert registry.resolve(to_global_id("Unknown", 1)) is None
100
+
101
+ # 4. Input-object mutations with clientMutationId
102
+ @relay_mutation
103
+ def rename_user(input, context=None):
104
+ user = registry.resolve(input["id"], context)
105
+ user["name"] = input["name"]
106
+ return {"user": user}
107
+
108
+ payload = rename_user(
109
+ {"id": to_global_id("User", 0), "name": "Countess Ada", "clientMutationId": "m-1"}
110
+ )
111
+ assert payload == {"user": {"id": 0, "name": "Countess Ada"}, "clientMutationId": "m-1"}
112
+ ```
113
+
114
+ ### Paginating a database query
115
+
116
+ Load only the window you need, then tell pyrelay where that window sits:
117
+
118
+ ```python
119
+ from pyrelay import ConnectionArguments, connection_from_sequence_slice, cursor_to_offset
120
+
121
+ rows = list(range(100)) # pretend this is a table
122
+ args = ConnectionArguments(first=10, after="YXJyYXljb25uZWN0aW9uOjQ=") # offset 4
123
+
124
+ offset = cursor_to_offset(args.after) + 1 if args.after else 0
125
+ window = rows[offset : offset + args.first] # SELECT ... OFFSET :offset LIMIT :first
126
+
127
+ conn = connection_from_sequence_slice(
128
+ window, args, slice_start=offset, sequence_length=len(rows), include_total_count=True
129
+ )
130
+ assert conn.nodes == list(range(5, 15))
131
+ assert conn.page_info.has_next_page and conn.total_count == 100
132
+ ```
133
+
134
+ ## API overview
135
+
136
+ Everything below can be imported from the top-level `pyrelay` package.
137
+
138
+ ### Global IDs (`pyrelay.ids`)
139
+
140
+ | Name | Description |
141
+ | --- | --- |
142
+ | `to_global_id(type_name, local_id) -> str` | Returns `base64("Type:id")`. Raises `ValueError` if `type_name` is empty or contains `:`. |
143
+ | `from_global_id(global_id) -> ResolvedGlobalId` | Decodes an ID. Raises `InvalidGlobalIdError` on malformed input. |
144
+ | `ResolvedGlobalId` | A `NamedTuple` with fields `type: str` and `id: str`. |
145
+
146
+ ### Cursors (`pyrelay.cursors`)
147
+
148
+ | Name | Description |
149
+ | --- | --- |
150
+ | `offset_to_cursor(offset) -> str` | Returns the opaque cursor `base64("arrayconnection:N")`. |
151
+ | `cursor_to_offset(cursor) -> int` | Inverse of `offset_to_cursor`. Raises `InvalidCursorError`. |
152
+ | `cursor_for_object_in_sequence(data, obj) -> str \| None` | Returns the cursor of the first item equal to `obj`. |
153
+
154
+ ### Connections (`pyrelay.connection`)
155
+
156
+ | Name | Description |
157
+ | --- | --- |
158
+ | `ConnectionArguments(first=None, after=None, last=None, before=None)` | Frozen dataclass. Raises `ConnectionArgumentError` if `first`/`last` is negative or not an int. `ConnectionArguments.from_mapping(kwargs)` ignores unknown keys. |
159
+ | `connection_from_sequence(data, args=None, *, include_total_count=False) -> Connection` | Paginates an in-memory sequence. |
160
+ | `connection_from_sequence_slice(slice, args=None, *, slice_start, sequence_length, include_total_count=False) -> Connection` | Paginates a pre-fetched window of a larger sequence. |
161
+ | `Connection` | Frozen dataclass with `edges: list[Edge]`, `page_info: PageInfo` and `total_count: int \| None`. Also has a `.nodes` property and `.to_dict(serialize_node=None)`. |
162
+ | `Edge` | Frozen dataclass with `node` and `cursor: str`, plus `.to_dict(serialize_node=None)`. |
163
+ | `PageInfo` | Frozen dataclass with `has_previous_page`, `has_next_page`, `start_cursor` and `end_cursor`, plus `.to_dict()`. |
164
+
165
+ As in `graphql-relay-js`, `has_previous_page` is computed only when `last` is
166
+ given, and `has_next_page` only when `first` is given. Otherwise they are `False`.
167
+
168
+ ### Node registry (`pyrelay.node`)
169
+
170
+ | Name | Description |
171
+ | --- | --- |
172
+ | `NodeRegistry()` | Registry of `type_name -> resolver(local_id, *args, **kwargs)`. |
173
+ | `.register(type_name, resolver=None)` | Registers a resolver. It can be used as a decorator. Raises `ValueError` on a duplicate or invalid name. |
174
+ | `.unregister(type_name)` | Removes a resolver. Raises `KeyError` if the type is not registered. |
175
+ | `.resolve(global_id, *args, **kwargs)` | Calls the resolver and returns its result. Returns `None` for an unregistered type. Raises `InvalidGlobalIdError` for a malformed ID. |
176
+ | `.global_id(type_name, local_id) -> str` | Like `to_global_id`, but raises `KeyError` for an unregistered type. |
177
+ | `.types`, `type_name in registry` | Introspection. |
178
+
179
+ ### Mutations (`pyrelay.mutation`)
180
+
181
+ | Name | Description |
182
+ | --- | --- |
183
+ | `relay_mutation(func)` | Decorator. The wrapped function is called as `wrapped(input, *args, **kwargs)`. `func` receives `input` without `clientMutationId` and must return a mapping or `None`. The payload dict always includes `clientMutationId`. Coroutine functions stay awaitable. |
184
+ | `CLIENT_MUTATION_ID` | The constant `"clientMutationId"`. |
185
+
186
+ ### Errors (`pyrelay.errors`)
187
+
188
+ | Name | Description |
189
+ | --- | --- |
190
+ | `RelayError` | Base class of all pyrelay exceptions. |
191
+ | `InvalidGlobalIdError` | Subclass of `RelayError` and `ValueError`. |
192
+ | `InvalidCursorError` | Subclass of `RelayError` and `ValueError`. |
193
+ | `ConnectionArgumentError` | Subclass of `RelayError` and `ValueError`. |
194
+
195
+ `__version__` holds the package version string (`"0.1.0"`).
196
+
197
+ ## Development
198
+
199
+ ```bash
200
+ python3 -m venv .venv && . .venv/bin/activate
201
+ pip install -e '.[test]'
202
+ python -m pytest # or, without pytest:
203
+ PYTHONPATH=src python3 -m unittest discover -s tests -v
204
+ ```
205
+
206
+ The test suite also runs every Python block in this README and checks that
207
+ each public name is documented here, so the docs and code stay in sync.
208
+
209
+ ## License
210
+
211
+ MIT
@@ -0,0 +1,184 @@
1
+ # pyrelay
2
+
3
+ **Relay-spec GraphQL helpers for Python. Framework-agnostic, with zero dependencies.**
4
+
5
+ Relay's server conventions (opaque global IDs, cursor connections, the `node`
6
+ refetch field, `clientMutationId` on mutations) are small, but they're easy to
7
+ get subtly wrong. Each Python GraphQL library also tends to ship its own
8
+ version. `pyrelay` implements them once as plain Python functions and
9
+ dataclasses. You can use it with Strawberry, Graphene, Ariadne, `graphql-core`,
10
+ or a hand-rolled resolver layer. The ID and cursor formats match the reference
11
+ `graphql-relay-js` implementation, so existing Relay clients and data keep working.
12
+
13
+ ## Features
14
+
15
+ - **Global object IDs**: `to_global_id` / `from_global_id` encode and decode `base64("Type:id")`, with strict validation.
16
+ - **Cursor connections**: `connection_from_sequence` implements the Relay Cursor Connections spec (`first`/`after`/`last`/`before`) and produces correct `PageInfo`.
17
+ - **Database-friendly slicing**: `connection_from_sequence_slice` paginates when only an `OFFSET`/`LIMIT` window is loaded.
18
+ - **Node registry**: `NodeRegistry` maps type names to loaders for the `node(id:)` field. It forwards context and works with async loaders.
19
+ - **Mutation helper**: the `@relay_mutation` decorator strips `clientMutationId` from the input and echoes it back in the payload. It works for sync and async functions.
20
+ - **GraphQL-shaped output**: `.to_dict()` emits camelCase `edges` / `pageInfo` / `totalCount`.
21
+ - Fully type-hinted (`py.typed`), standard library only, Python 3.9+.
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ pip install pyrelay # once published
27
+ # or, from a checkout:
28
+ pip install -e .
29
+ ```
30
+
31
+ ## Quickstart
32
+
33
+ ```python
34
+ from pyrelay import (
35
+ ConnectionArguments,
36
+ NodeRegistry,
37
+ connection_from_sequence,
38
+ from_global_id,
39
+ relay_mutation,
40
+ to_global_id,
41
+ )
42
+
43
+ # 1. Global object IDs
44
+ gid = to_global_id("User", 42)
45
+ assert gid == "VXNlcjo0Mg=="
46
+ assert from_global_id(gid) == ("User", "42")
47
+
48
+ # 2. Cursor pagination over any sequence
49
+ users = [{"id": i, "name": n} for i, n in enumerate(["Ada", "Grace", "Linus", "Guido"])]
50
+ page1 = connection_from_sequence(users, ConnectionArguments(first=2))
51
+ assert [u["name"] for u in page1.nodes] == ["Ada", "Grace"]
52
+ assert page1.page_info.has_next_page
53
+
54
+ page2 = connection_from_sequence(
55
+ users, ConnectionArguments(first=2, after=page1.page_info.end_cursor)
56
+ )
57
+ assert [u["name"] for u in page2.nodes] == ["Linus", "Guido"]
58
+ assert not page2.page_info.has_next_page
59
+
60
+ # GraphQL-shaped result, e.g. to return from a resolver
61
+ data = page2.to_dict(lambda u: {"id": to_global_id("User", u["id"]), "name": u["name"]})
62
+ assert set(data) == {"edges", "pageInfo"}
63
+
64
+ # 3. The `node(id:)` field
65
+ registry = NodeRegistry()
66
+
67
+ @registry.register("User")
68
+ def load_user(local_id: str, context=None):
69
+ return users[int(local_id)]
70
+
71
+ assert registry.resolve(registry.global_id("User", 1))["name"] == "Grace"
72
+ assert registry.resolve(to_global_id("Unknown", 1)) is None
73
+
74
+ # 4. Input-object mutations with clientMutationId
75
+ @relay_mutation
76
+ def rename_user(input, context=None):
77
+ user = registry.resolve(input["id"], context)
78
+ user["name"] = input["name"]
79
+ return {"user": user}
80
+
81
+ payload = rename_user(
82
+ {"id": to_global_id("User", 0), "name": "Countess Ada", "clientMutationId": "m-1"}
83
+ )
84
+ assert payload == {"user": {"id": 0, "name": "Countess Ada"}, "clientMutationId": "m-1"}
85
+ ```
86
+
87
+ ### Paginating a database query
88
+
89
+ Load only the window you need, then tell pyrelay where that window sits:
90
+
91
+ ```python
92
+ from pyrelay import ConnectionArguments, connection_from_sequence_slice, cursor_to_offset
93
+
94
+ rows = list(range(100)) # pretend this is a table
95
+ args = ConnectionArguments(first=10, after="YXJyYXljb25uZWN0aW9uOjQ=") # offset 4
96
+
97
+ offset = cursor_to_offset(args.after) + 1 if args.after else 0
98
+ window = rows[offset : offset + args.first] # SELECT ... OFFSET :offset LIMIT :first
99
+
100
+ conn = connection_from_sequence_slice(
101
+ window, args, slice_start=offset, sequence_length=len(rows), include_total_count=True
102
+ )
103
+ assert conn.nodes == list(range(5, 15))
104
+ assert conn.page_info.has_next_page and conn.total_count == 100
105
+ ```
106
+
107
+ ## API overview
108
+
109
+ Everything below can be imported from the top-level `pyrelay` package.
110
+
111
+ ### Global IDs (`pyrelay.ids`)
112
+
113
+ | Name | Description |
114
+ | --- | --- |
115
+ | `to_global_id(type_name, local_id) -> str` | Returns `base64("Type:id")`. Raises `ValueError` if `type_name` is empty or contains `:`. |
116
+ | `from_global_id(global_id) -> ResolvedGlobalId` | Decodes an ID. Raises `InvalidGlobalIdError` on malformed input. |
117
+ | `ResolvedGlobalId` | A `NamedTuple` with fields `type: str` and `id: str`. |
118
+
119
+ ### Cursors (`pyrelay.cursors`)
120
+
121
+ | Name | Description |
122
+ | --- | --- |
123
+ | `offset_to_cursor(offset) -> str` | Returns the opaque cursor `base64("arrayconnection:N")`. |
124
+ | `cursor_to_offset(cursor) -> int` | Inverse of `offset_to_cursor`. Raises `InvalidCursorError`. |
125
+ | `cursor_for_object_in_sequence(data, obj) -> str \| None` | Returns the cursor of the first item equal to `obj`. |
126
+
127
+ ### Connections (`pyrelay.connection`)
128
+
129
+ | Name | Description |
130
+ | --- | --- |
131
+ | `ConnectionArguments(first=None, after=None, last=None, before=None)` | Frozen dataclass. Raises `ConnectionArgumentError` if `first`/`last` is negative or not an int. `ConnectionArguments.from_mapping(kwargs)` ignores unknown keys. |
132
+ | `connection_from_sequence(data, args=None, *, include_total_count=False) -> Connection` | Paginates an in-memory sequence. |
133
+ | `connection_from_sequence_slice(slice, args=None, *, slice_start, sequence_length, include_total_count=False) -> Connection` | Paginates a pre-fetched window of a larger sequence. |
134
+ | `Connection` | Frozen dataclass with `edges: list[Edge]`, `page_info: PageInfo` and `total_count: int \| None`. Also has a `.nodes` property and `.to_dict(serialize_node=None)`. |
135
+ | `Edge` | Frozen dataclass with `node` and `cursor: str`, plus `.to_dict(serialize_node=None)`. |
136
+ | `PageInfo` | Frozen dataclass with `has_previous_page`, `has_next_page`, `start_cursor` and `end_cursor`, plus `.to_dict()`. |
137
+
138
+ As in `graphql-relay-js`, `has_previous_page` is computed only when `last` is
139
+ given, and `has_next_page` only when `first` is given. Otherwise they are `False`.
140
+
141
+ ### Node registry (`pyrelay.node`)
142
+
143
+ | Name | Description |
144
+ | --- | --- |
145
+ | `NodeRegistry()` | Registry of `type_name -> resolver(local_id, *args, **kwargs)`. |
146
+ | `.register(type_name, resolver=None)` | Registers a resolver. It can be used as a decorator. Raises `ValueError` on a duplicate or invalid name. |
147
+ | `.unregister(type_name)` | Removes a resolver. Raises `KeyError` if the type is not registered. |
148
+ | `.resolve(global_id, *args, **kwargs)` | Calls the resolver and returns its result. Returns `None` for an unregistered type. Raises `InvalidGlobalIdError` for a malformed ID. |
149
+ | `.global_id(type_name, local_id) -> str` | Like `to_global_id`, but raises `KeyError` for an unregistered type. |
150
+ | `.types`, `type_name in registry` | Introspection. |
151
+
152
+ ### Mutations (`pyrelay.mutation`)
153
+
154
+ | Name | Description |
155
+ | --- | --- |
156
+ | `relay_mutation(func)` | Decorator. The wrapped function is called as `wrapped(input, *args, **kwargs)`. `func` receives `input` without `clientMutationId` and must return a mapping or `None`. The payload dict always includes `clientMutationId`. Coroutine functions stay awaitable. |
157
+ | `CLIENT_MUTATION_ID` | The constant `"clientMutationId"`. |
158
+
159
+ ### Errors (`pyrelay.errors`)
160
+
161
+ | Name | Description |
162
+ | --- | --- |
163
+ | `RelayError` | Base class of all pyrelay exceptions. |
164
+ | `InvalidGlobalIdError` | Subclass of `RelayError` and `ValueError`. |
165
+ | `InvalidCursorError` | Subclass of `RelayError` and `ValueError`. |
166
+ | `ConnectionArgumentError` | Subclass of `RelayError` and `ValueError`. |
167
+
168
+ `__version__` holds the package version string (`"0.1.0"`).
169
+
170
+ ## Development
171
+
172
+ ```bash
173
+ python3 -m venv .venv && . .venv/bin/activate
174
+ pip install -e '.[test]'
175
+ python -m pytest # or, without pytest:
176
+ PYTHONPATH=src python3 -m unittest discover -s tests -v
177
+ ```
178
+
179
+ The test suite also runs every Python block in this README and checks that
180
+ each public name is documented here, so the docs and code stay in sync.
181
+
182
+ ## License
183
+
184
+ MIT
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pyrelay"
7
+ version = "0.1.0"
8
+ description = "Relay-spec GraphQL helpers for Python: global object IDs, cursor connections, node registry, and mutation payloads. Framework-agnostic, zero dependencies."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "nehz" }]
14
+ keywords = ["graphql", "relay", "pagination", "cursor", "connection", "global-id"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
27
+ "Topic :: Software Development :: Libraries :: Python Modules",
28
+ "Typing :: Typed",
29
+ ]
30
+ dependencies = []
31
+
32
+ [project.optional-dependencies]
33
+ test = ["pytest>=7"]
34
+
35
+ [tool.setuptools.packages.find]
36
+ where = ["src"]
37
+
38
+ [tool.setuptools.package-data]
39
+ pyrelay = ["py.typed"]
40
+
41
+ [tool.pytest.ini_options]
42
+ testpaths = ["tests"]
43
+ pythonpath = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,57 @@
1
+ """pyrelay: Relay-spec GraphQL server helpers, independent of any GraphQL library.
2
+
3
+ Provides global object IDs, cursor-based connection pagination, a ``node``
4
+ resolver registry, and ``clientMutationId`` handling for input-object mutations.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from .connection import (
10
+ Connection,
11
+ ConnectionArguments,
12
+ Edge,
13
+ PageInfo,
14
+ connection_from_sequence,
15
+ connection_from_sequence_slice,
16
+ )
17
+ from .cursors import cursor_for_object_in_sequence, cursor_to_offset, offset_to_cursor
18
+ from .errors import (
19
+ ConnectionArgumentError,
20
+ InvalidCursorError,
21
+ InvalidGlobalIdError,
22
+ RelayError,
23
+ )
24
+ from .ids import ResolvedGlobalId, from_global_id, to_global_id
25
+ from .mutation import CLIENT_MUTATION_ID, relay_mutation
26
+ from .node import NodeRegistry
27
+
28
+ __version__ = "0.1.0"
29
+
30
+ __all__ = [
31
+ "__version__",
32
+ # ids
33
+ "to_global_id",
34
+ "from_global_id",
35
+ "ResolvedGlobalId",
36
+ # cursors
37
+ "offset_to_cursor",
38
+ "cursor_to_offset",
39
+ "cursor_for_object_in_sequence",
40
+ # connections
41
+ "PageInfo",
42
+ "Edge",
43
+ "Connection",
44
+ "ConnectionArguments",
45
+ "connection_from_sequence",
46
+ "connection_from_sequence_slice",
47
+ # node
48
+ "NodeRegistry",
49
+ # mutations
50
+ "CLIENT_MUTATION_ID",
51
+ "relay_mutation",
52
+ # errors
53
+ "RelayError",
54
+ "InvalidGlobalIdError",
55
+ "InvalidCursorError",
56
+ "ConnectionArgumentError",
57
+ ]