graphql-api 1.6.5__tar.gz → 1.6.7__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.
- graphql_api-1.6.7/AGENTS.md +46 -0
- graphql_api-1.6.7/CLAUDE.md +46 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/PKG-INFO +4 -7
- {graphql_api-1.6.5 → graphql_api-1.6.7}/README.md +4 -7
- graphql_api-1.6.7/VERSION +1 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/graphql_api/pydantic.py +39 -5
- {graphql_api-1.6.5 → graphql_api-1.6.7}/graphql_api/utils.py +11 -4
- {graphql_api-1.6.5 → graphql_api-1.6.7}/graphql_api.egg-info/PKG-INFO +4 -7
- {graphql_api-1.6.5 → graphql_api-1.6.7}/graphql_api.egg-info/SOURCES.txt +2 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/tests/test_pydantic.py +98 -0
- graphql_api-1.6.5/VERSION +0 -1
- {graphql_api-1.6.5 → graphql_api-1.6.7}/.github/workflows/pages.yml +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/.github/workflows/python-package.yml +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/.gitignore +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/.vscode/settings.json +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/LICENSE +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/docs/README.md +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/_index.md +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/_index.md +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/advanced-features/_index.md +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/advanced-features/directives.md +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/advanced-features/performance.md +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/advanced-features/schema-filtering.md +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/building-applications/_index.md +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/building-applications/documentation.md +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/building-applications/error-handling.md +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/distributed-systems/_index.md +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/distributed-systems/federation.md +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/distributed-systems/pagination-relay.md +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/distributed-systems/remote-graphql.md +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/fundamentals/_index.md +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/fundamentals/async-and-subscriptions.md +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/fundamentals/defining-schemas.md +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/fundamentals/enums-interfaces.md +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/fundamentals/field-types.md +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/fundamentals/getting-started.md +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/fundamentals/mutations.md +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/fundamentals/object-types.md +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/fundamentals/pydantic-and-dataclasses.md +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/production/_index.md +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/production/context-metadata.md +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/production/middleware.md +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/reference/_index.md +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/reference/api-reference.md +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/reference/contributing.md +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/reference/examples.md +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/docs/hugo.toml +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/docs/index.md +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/graphql_api/__init__.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/graphql_api/api.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/graphql_api/context.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/graphql_api/dataclass_mapping.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/graphql_api/decorators.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/graphql_api/directives.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/graphql_api/error.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/graphql_api/exception.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/graphql_api/executor.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/graphql_api/federation/__init__.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/graphql_api/federation/directives.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/graphql_api/federation/federation.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/graphql_api/federation/types.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/graphql_api/mapper.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/graphql_api/middleware.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/graphql_api/mutation_schema.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/graphql_api/pydantic_utils.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/graphql_api/reduce.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/graphql_api/relay.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/graphql_api/remote.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/graphql_api/schema.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/graphql_api/schema_filter.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/graphql_api/type_utils.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/graphql_api/types.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/graphql_api.egg-info/dependency_links.txt +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/graphql_api.egg-info/requires.txt +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/graphql_api.egg-info/top_level.txt +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/pyproject.toml +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/setup.cfg +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/tests/__init__.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/tests/test_async.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/tests/test_calculator.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/tests/test_custom_suffixes.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/tests/test_custom_types.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/tests/test_dataclass.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/tests/test_dataclass_relationships.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/tests/test_decorator_patterns.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/tests/test_directives.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/tests/test_docs_advanced.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/tests/test_docs_async_subscriptions.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/tests/test_docs_defining_schemas.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/tests/test_docs_field_types.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/tests/test_docs_getting_started.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/tests/test_docs_relay.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/tests/test_docstrings.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/tests/test_error.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/tests/test_federation.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/tests/test_federation_example.graphql +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/tests/test_federation_example.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/tests/test_filtering.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/tests/test_graphql.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/tests/test_middleware.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/tests/test_performance.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/tests/test_relay.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/tests/test_remote.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/tests/test_schema.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/tests/test_subscriptions.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/tests/test_unified_vs_explicit_schema.py +0 -0
- {graphql_api-1.6.5 → graphql_api-1.6.7}/uv.lock +0 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# GraphQL-API
|
|
2
|
+
|
|
3
|
+
> Compatibility note: `AGENTS.md` and `CLAUDE.md` are both supported in this repo.
|
|
4
|
+
> Keep these files identical. Any change in one must be mirrored in the other.
|
|
5
|
+
|
|
6
|
+
Code-first, decorator-based GraphQL framework for Python. Published on [PyPI](https://pypi.org/project/graphql-api/).
|
|
7
|
+
|
|
8
|
+
## Project Structure
|
|
9
|
+
|
|
10
|
+
| Directory | Description |
|
|
11
|
+
|-----------|-------------|
|
|
12
|
+
| `graphql_api/` | Main package source |
|
|
13
|
+
| `tests/` | Test suite (34 files, pytest) |
|
|
14
|
+
| `docs/` | Sphinx documentation |
|
|
15
|
+
|
|
16
|
+
## Development
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Install dependencies
|
|
20
|
+
uv sync
|
|
21
|
+
|
|
22
|
+
# Run tests
|
|
23
|
+
uv run pytest
|
|
24
|
+
|
|
25
|
+
# Run linter
|
|
26
|
+
uv run flake8 graphql_api tests
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Key Patterns
|
|
30
|
+
|
|
31
|
+
- **Automatic snake_case to camelCase**: All Python field names, arguments, and dataclass/Pydantic fields are auto-converted to camelCase in GraphQL. Input arguments are converted back to snake_case. Conversion lives in `graphql_api/utils.py` (`to_camel_case()`, `to_snake_case()`)
|
|
32
|
+
- `@field` marks a method as a query; `@field(mutable=True)` makes it a mutation; `AsyncGenerator` return type or `subscription=True` makes it a subscription
|
|
33
|
+
- Python types auto-map: `str`→String, `int`→Int, `bool`→Boolean, `float`→Float, `UUID`→UUID scalar, `datetime`→DateTime, `Optional[T]`→nullable, `List[T]`→list, dataclasses/Pydantic models→object types, enums→enum types
|
|
34
|
+
- Schema setup: `GraphQLAPI(query_type=Q, mutation_type=M, subscription_type=S)` or legacy `root_type=Root`
|
|
35
|
+
|
|
36
|
+
## Releasing
|
|
37
|
+
|
|
38
|
+
See the ecosystem-level `CLAUDE.md` in the parent workspace for the full release process. In short:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Ensure CI is green on main, then:
|
|
42
|
+
git tag X.Y.Z
|
|
43
|
+
git push origin X.Y.Z
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
CI publishes to PyPI and creates a GitHub Release automatically.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# GraphQL-API
|
|
2
|
+
|
|
3
|
+
> Compatibility note: `AGENTS.md` and `CLAUDE.md` are both supported in this repo.
|
|
4
|
+
> Keep these files identical. Any change in one must be mirrored in the other.
|
|
5
|
+
|
|
6
|
+
Code-first, decorator-based GraphQL framework for Python. Published on [PyPI](https://pypi.org/project/graphql-api/).
|
|
7
|
+
|
|
8
|
+
## Project Structure
|
|
9
|
+
|
|
10
|
+
| Directory | Description |
|
|
11
|
+
|-----------|-------------|
|
|
12
|
+
| `graphql_api/` | Main package source |
|
|
13
|
+
| `tests/` | Test suite (34 files, pytest) |
|
|
14
|
+
| `docs/` | Sphinx documentation |
|
|
15
|
+
|
|
16
|
+
## Development
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Install dependencies
|
|
20
|
+
uv sync
|
|
21
|
+
|
|
22
|
+
# Run tests
|
|
23
|
+
uv run pytest
|
|
24
|
+
|
|
25
|
+
# Run linter
|
|
26
|
+
uv run flake8 graphql_api tests
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Key Patterns
|
|
30
|
+
|
|
31
|
+
- **Automatic snake_case to camelCase**: All Python field names, arguments, and dataclass/Pydantic fields are auto-converted to camelCase in GraphQL. Input arguments are converted back to snake_case. Conversion lives in `graphql_api/utils.py` (`to_camel_case()`, `to_snake_case()`)
|
|
32
|
+
- `@field` marks a method as a query; `@field(mutable=True)` makes it a mutation; `AsyncGenerator` return type or `subscription=True` makes it a subscription
|
|
33
|
+
- Python types auto-map: `str`→String, `int`→Int, `bool`→Boolean, `float`→Float, `UUID`→UUID scalar, `datetime`→DateTime, `Optional[T]`→nullable, `List[T]`→list, dataclasses/Pydantic models→object types, enums→enum types
|
|
34
|
+
- Schema setup: `GraphQLAPI(query_type=Q, mutation_type=M, subscription_type=S)` or legacy `root_type=Root`
|
|
35
|
+
|
|
36
|
+
## Releasing
|
|
37
|
+
|
|
38
|
+
See the ecosystem-level `CLAUDE.md` in the parent workspace for the full release process. In short:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Ensure CI is green on main, then:
|
|
42
|
+
git tag X.Y.Z
|
|
43
|
+
git push origin X.Y.Z
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
CI publishes to PyPI and creates a GitHub Release automatically.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: graphql-api
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.7
|
|
4
4
|
Summary: A framework for building Python GraphQL APIs.
|
|
5
5
|
Author-email: Robert Parker <rob@parob.com>
|
|
6
6
|
License: MIT
|
|
@@ -28,10 +28,8 @@ Dynamic: license-file
|
|
|
28
28
|
[](https://badge.fury.io/py/graphql-api)
|
|
29
29
|
[](https://pypi.org/project/graphql-api/)
|
|
30
30
|
[](https://opensource.org/licenses/MIT)
|
|
31
|
-
[](https://gitlab.com/parob/graphql-api/commits/master)
|
|
32
|
-
[](https://gitlab.com/parob/graphql-api/commits/master)
|
|
33
31
|
|
|
34
|
-
**[
|
|
32
|
+
**[Documentation](https://graphql-api.parob.com/)** | **[PyPI](https://pypi.org/project/graphql-api/)** | **[GitHub](https://github.com/parob/graphql-api)**
|
|
35
33
|
|
|
36
34
|
---
|
|
37
35
|
|
|
@@ -234,9 +232,8 @@ To contribute or run the test suite locally:
|
|
|
234
232
|
|
|
235
233
|
```bash
|
|
236
234
|
# Install dependencies
|
|
237
|
-
|
|
238
|
-
pipenv install --dev
|
|
235
|
+
uv sync
|
|
239
236
|
|
|
240
237
|
# Run tests
|
|
241
|
-
|
|
238
|
+
uv run pytest
|
|
242
239
|
```
|
|
@@ -3,10 +3,8 @@
|
|
|
3
3
|
[](https://badge.fury.io/py/graphql-api)
|
|
4
4
|
[](https://pypi.org/project/graphql-api/)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
|
-
[](https://gitlab.com/parob/graphql-api/commits/master)
|
|
7
|
-
[](https://gitlab.com/parob/graphql-api/commits/master)
|
|
8
6
|
|
|
9
|
-
**[
|
|
7
|
+
**[Documentation](https://graphql-api.parob.com/)** | **[PyPI](https://pypi.org/project/graphql-api/)** | **[GitHub](https://github.com/parob/graphql-api)**
|
|
10
8
|
|
|
11
9
|
---
|
|
12
10
|
|
|
@@ -209,9 +207,8 @@ To contribute or run the test suite locally:
|
|
|
209
207
|
|
|
210
208
|
```bash
|
|
211
209
|
# Install dependencies
|
|
212
|
-
|
|
213
|
-
pipenv install --dev
|
|
210
|
+
uv sync
|
|
214
211
|
|
|
215
212
|
# Run tests
|
|
216
|
-
|
|
217
|
-
```
|
|
213
|
+
uv run pytest
|
|
214
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.6.7
|
|
@@ -3,7 +3,7 @@ import typing
|
|
|
3
3
|
|
|
4
4
|
from typing import Any, Type, cast, Optional
|
|
5
5
|
|
|
6
|
-
from graphql import GraphQLField, GraphQLObjectType, GraphQLOutputType, GraphQLInputField, GraphQLInputObjectType, GraphQLInputType
|
|
6
|
+
from graphql import GraphQLField, GraphQLObjectType, GraphQLOutputType, GraphQLInputField, GraphQLInputObjectType, GraphQLInputType, GraphQLNonNull
|
|
7
7
|
from graphql.type.definition import is_output_type, is_input_type
|
|
8
8
|
from pydantic import BaseModel
|
|
9
9
|
|
|
@@ -20,6 +20,19 @@ def type_is_pydantic_model(type_: Any) -> bool:
|
|
|
20
20
|
return False
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
def _annotation_allows_none(annotation: Any) -> bool:
|
|
24
|
+
"""True if the annotation explicitly permits ``None`` (e.g. ``Optional[X]``).
|
|
25
|
+
|
|
26
|
+
Pydantic v2 distinguishes "required" (must be supplied) from
|
|
27
|
+
"non-nullable" (cannot be None). ``email: Optional[str]`` with no default
|
|
28
|
+
is required but nullable, so it must NOT become a NonNull GraphQL field.
|
|
29
|
+
"""
|
|
30
|
+
if annotation is type(None):
|
|
31
|
+
return True
|
|
32
|
+
args = typing.get_args(annotation)
|
|
33
|
+
return type(None) in args
|
|
34
|
+
|
|
35
|
+
|
|
23
36
|
def _get_pydantic_model_description(pydantic_model: Type[BaseModel], max_docstring_length: Optional[int] = None) -> str:
|
|
24
37
|
"""
|
|
25
38
|
Get description for a Pydantic model, filtering out default BaseModel docstring.
|
|
@@ -72,9 +85,19 @@ def type_from_pydantic_model(
|
|
|
72
85
|
f"Mapped type for pydantic field '{name}' is not a valid GraphQL Input Type."
|
|
73
86
|
)
|
|
74
87
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
88
|
+
gql_type = cast(GraphQLInputType, graphql_type)
|
|
89
|
+
# Required, non-nullable Pydantic fields become NonNull GraphQL
|
|
90
|
+
# inputs so the GraphQL layer enforces the same contract.
|
|
91
|
+
# ``Optional[X]`` (no default) stays nullable: it's required to
|
|
92
|
+
# supply but allowed to be None.
|
|
93
|
+
if (
|
|
94
|
+
field.is_required()
|
|
95
|
+
and not _annotation_allows_none(field.annotation)
|
|
96
|
+
and not isinstance(gql_type, GraphQLNonNull)
|
|
97
|
+
):
|
|
98
|
+
gql_type = GraphQLNonNull(gql_type)
|
|
99
|
+
|
|
100
|
+
fields[to_camel_case(name)] = GraphQLInputField(gql_type)
|
|
78
101
|
return fields
|
|
79
102
|
|
|
80
103
|
def _make_out_type(model_cls: Type[BaseModel]):
|
|
@@ -128,8 +151,19 @@ def type_from_pydantic_model(
|
|
|
128
151
|
|
|
129
152
|
return resolver
|
|
130
153
|
|
|
154
|
+
gql_type = cast(GraphQLOutputType, graphql_type)
|
|
155
|
+
# Required, non-nullable Pydantic fields become NonNull GraphQL
|
|
156
|
+
# outputs so the schema accurately advertises which fields are
|
|
157
|
+
# guaranteed. ``Optional[X]`` (no default) stays nullable.
|
|
158
|
+
if (
|
|
159
|
+
field.is_required()
|
|
160
|
+
and not _annotation_allows_none(field.annotation)
|
|
161
|
+
and not isinstance(gql_type, GraphQLNonNull)
|
|
162
|
+
):
|
|
163
|
+
gql_type = GraphQLNonNull(gql_type)
|
|
164
|
+
|
|
131
165
|
fields[to_camel_case(name)] = GraphQLField(
|
|
132
|
-
|
|
166
|
+
gql_type, resolve=create_resolver(name)
|
|
133
167
|
)
|
|
134
168
|
return fields
|
|
135
169
|
|
|
@@ -20,6 +20,15 @@ from graphql.type.definition import GraphQLInterfaceType, GraphQLType
|
|
|
20
20
|
|
|
21
21
|
# From this response in Stackoverflow
|
|
22
22
|
# http://stackoverflow.com/a/19053800/1072990
|
|
23
|
+
def _capitalize_first(s):
|
|
24
|
+
"""Capitalize only the first character, preserving the rest.
|
|
25
|
+
|
|
26
|
+
Unlike str.title(), this doesn't capitalize letters after digits.
|
|
27
|
+
e.g. "30d" stays "30d" instead of becoming "30D".
|
|
28
|
+
"""
|
|
29
|
+
return s[0].upper() + s[1:] if s else s
|
|
30
|
+
|
|
31
|
+
|
|
23
32
|
def to_camel_case(snake_str, title=False):
|
|
24
33
|
underscore_prefix = False
|
|
25
34
|
if snake_str.startswith("_"):
|
|
@@ -27,12 +36,10 @@ def to_camel_case(snake_str, title=False):
|
|
|
27
36
|
snake_str = snake_str[1:]
|
|
28
37
|
|
|
29
38
|
components = snake_str.split("_")
|
|
30
|
-
# We capitalize the first letter of each component except the first one
|
|
31
|
-
# with the 'title' method and join them together.
|
|
32
39
|
if not snake_str:
|
|
33
40
|
return ""
|
|
34
|
-
prefix = components[0]
|
|
35
|
-
value = prefix + "".join(x
|
|
41
|
+
prefix = _capitalize_first(components[0]) if title else components[0]
|
|
42
|
+
value = prefix + "".join(_capitalize_first(x) if x else "_" for x in components[1:])
|
|
36
43
|
return ("_" if underscore_prefix else "") + value
|
|
37
44
|
|
|
38
45
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: graphql-api
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.7
|
|
4
4
|
Summary: A framework for building Python GraphQL APIs.
|
|
5
5
|
Author-email: Robert Parker <rob@parob.com>
|
|
6
6
|
License: MIT
|
|
@@ -28,10 +28,8 @@ Dynamic: license-file
|
|
|
28
28
|
[](https://badge.fury.io/py/graphql-api)
|
|
29
29
|
[](https://pypi.org/project/graphql-api/)
|
|
30
30
|
[](https://opensource.org/licenses/MIT)
|
|
31
|
-
[](https://gitlab.com/parob/graphql-api/commits/master)
|
|
32
|
-
[](https://gitlab.com/parob/graphql-api/commits/master)
|
|
33
31
|
|
|
34
|
-
**[
|
|
32
|
+
**[Documentation](https://graphql-api.parob.com/)** | **[PyPI](https://pypi.org/project/graphql-api/)** | **[GitHub](https://github.com/parob/graphql-api)**
|
|
35
33
|
|
|
36
34
|
---
|
|
37
35
|
|
|
@@ -234,9 +232,8 @@ To contribute or run the test suite locally:
|
|
|
234
232
|
|
|
235
233
|
```bash
|
|
236
234
|
# Install dependencies
|
|
237
|
-
|
|
238
|
-
pipenv install --dev
|
|
235
|
+
uv sync
|
|
239
236
|
|
|
240
237
|
# Run tests
|
|
241
|
-
|
|
238
|
+
uv run pytest
|
|
242
239
|
```
|
|
@@ -1229,6 +1229,104 @@ class TestPydantic:
|
|
|
1229
1229
|
}
|
|
1230
1230
|
}
|
|
1231
1231
|
|
|
1232
|
+
def test_pydantic_required_input_fields_become_nonnull(self) -> None:
|
|
1233
|
+
"""Required Pydantic fields on input types must be GraphQLNonNull.
|
|
1234
|
+
|
|
1235
|
+
Without this, the GraphQL API silently accepts ``null`` for fields the
|
|
1236
|
+
Pydantic model considers required, and downstream JSON Schema (e.g.
|
|
1237
|
+
graphql-mcp) loses required-ness for LLM tool calls.
|
|
1238
|
+
"""
|
|
1239
|
+
from graphql import GraphQLNonNull, GraphQLInputObjectType
|
|
1240
|
+
|
|
1241
|
+
class CreateUserInput(BaseModel):
|
|
1242
|
+
name: str # required
|
|
1243
|
+
email: str # required
|
|
1244
|
+
age: Optional[int] = None # optional
|
|
1245
|
+
|
|
1246
|
+
class API:
|
|
1247
|
+
@field(mutable=True)
|
|
1248
|
+
def create_user(self, input: CreateUserInput) -> str:
|
|
1249
|
+
return f"created:{input.name}"
|
|
1250
|
+
|
|
1251
|
+
api = GraphQLAPI(root_type=API)
|
|
1252
|
+
schema = api.schema()
|
|
1253
|
+
|
|
1254
|
+
input_arg_type = schema.mutation_type.fields["createUser"].args["input"].type
|
|
1255
|
+
# `input` itself is non-null (method arg with no default).
|
|
1256
|
+
assert isinstance(input_arg_type, GraphQLNonNull)
|
|
1257
|
+
input_object: GraphQLInputObjectType = input_arg_type.of_type
|
|
1258
|
+
assert isinstance(input_object, GraphQLInputObjectType)
|
|
1259
|
+
|
|
1260
|
+
fields = input_object.fields
|
|
1261
|
+
assert isinstance(fields["name"].type, GraphQLNonNull), (
|
|
1262
|
+
"Required Pydantic field 'name' should map to a NonNull GraphQL type"
|
|
1263
|
+
)
|
|
1264
|
+
assert isinstance(fields["email"].type, GraphQLNonNull), (
|
|
1265
|
+
"Required Pydantic field 'email' should map to a NonNull GraphQL type"
|
|
1266
|
+
)
|
|
1267
|
+
assert not isinstance(fields["age"].type, GraphQLNonNull), (
|
|
1268
|
+
"Optional Pydantic field 'age' should remain nullable"
|
|
1269
|
+
)
|
|
1270
|
+
|
|
1271
|
+
def test_pydantic_required_input_rejects_null_at_graphql_layer(self) -> None:
|
|
1272
|
+
"""Once required fields are NonNull, the GraphQL layer should reject
|
|
1273
|
+
a mutation that omits or nulls a required input field."""
|
|
1274
|
+
|
|
1275
|
+
class CreateUserInput(BaseModel):
|
|
1276
|
+
name: str
|
|
1277
|
+
email: str
|
|
1278
|
+
|
|
1279
|
+
class API:
|
|
1280
|
+
@field(mutable=True)
|
|
1281
|
+
def create_user(self, input: CreateUserInput) -> str:
|
|
1282
|
+
return f"created:{input.name}"
|
|
1283
|
+
|
|
1284
|
+
api = GraphQLAPI(root_type=API)
|
|
1285
|
+
|
|
1286
|
+
# Omitting a required field should be a GraphQL validation error,
|
|
1287
|
+
# not a silent success or a runtime Pydantic error.
|
|
1288
|
+
result = api.execute(
|
|
1289
|
+
'mutation { createUser(input: {name: "Alice"}) }'
|
|
1290
|
+
)
|
|
1291
|
+
assert result.errors, "GraphQL should reject omission of required input field"
|
|
1292
|
+
assert any(
|
|
1293
|
+
"email" in str(err) for err in result.errors
|
|
1294
|
+
), f"Error should mention the missing 'email' field, got: {result.errors}"
|
|
1295
|
+
|
|
1296
|
+
def test_pydantic_required_output_fields_become_nonnull(self) -> None:
|
|
1297
|
+
"""Required Pydantic fields on output types must be GraphQLNonNull,
|
|
1298
|
+
so the schema correctly advertises which fields are guaranteed."""
|
|
1299
|
+
from graphql import GraphQLNonNull, GraphQLObjectType
|
|
1300
|
+
|
|
1301
|
+
class User(BaseModel):
|
|
1302
|
+
id: str # required
|
|
1303
|
+
name: str # required
|
|
1304
|
+
nickname: Optional[str] = None # optional
|
|
1305
|
+
|
|
1306
|
+
class API:
|
|
1307
|
+
@field
|
|
1308
|
+
def get_user(self) -> User:
|
|
1309
|
+
return User(id="1", name="Alice")
|
|
1310
|
+
|
|
1311
|
+
api = GraphQLAPI(root_type=API)
|
|
1312
|
+
schema = api.schema()
|
|
1313
|
+
|
|
1314
|
+
return_type = schema.query_type.fields["getUser"].type
|
|
1315
|
+
assert isinstance(return_type, GraphQLNonNull)
|
|
1316
|
+
user_type: GraphQLObjectType = return_type.of_type
|
|
1317
|
+
assert isinstance(user_type, GraphQLObjectType)
|
|
1318
|
+
|
|
1319
|
+
fields = user_type.fields
|
|
1320
|
+
assert isinstance(fields["id"].type, GraphQLNonNull), (
|
|
1321
|
+
"Required Pydantic output field 'id' should map to NonNull"
|
|
1322
|
+
)
|
|
1323
|
+
assert isinstance(fields["name"].type, GraphQLNonNull), (
|
|
1324
|
+
"Required Pydantic output field 'name' should map to NonNull"
|
|
1325
|
+
)
|
|
1326
|
+
assert not isinstance(fields["nickname"].type, GraphQLNonNull), (
|
|
1327
|
+
"Optional Pydantic output field 'nickname' should remain nullable"
|
|
1328
|
+
)
|
|
1329
|
+
|
|
1232
1330
|
def test_pydantic_input_out_type_converts_to_model(self) -> None:
|
|
1233
1331
|
"""Test that Pydantic input types have out_type that produces model instances."""
|
|
1234
1332
|
class Item(BaseModel):
|
graphql_api-1.6.5/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1.6.5
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/advanced-features/schema-filtering.md
RENAMED
|
File without changes
|
|
File without changes
|
{graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/building-applications/documentation.md
RENAMED
|
File without changes
|
{graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/building-applications/error-handling.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/distributed-systems/pagination-relay.md
RENAMED
|
File without changes
|
{graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/distributed-systems/remote-graphql.md
RENAMED
|
File without changes
|
|
File without changes
|
{graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/fundamentals/async-and-subscriptions.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{graphql_api-1.6.5 → graphql_api-1.6.7}/docs/content/docs/fundamentals/pydantic-and-dataclasses.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|