graphql-api 1.4.11__tar.gz → 1.4.13__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.
Files changed (53) hide show
  1. {graphql_api-1.4.11/graphql_api.egg-info → graphql_api-1.4.13}/PKG-INFO +2 -2
  2. graphql_api-1.4.13/VERSION +1 -0
  3. {graphql_api-1.4.11 → graphql_api-1.4.13}/graphql_api/__init__.py +1 -1
  4. {graphql_api-1.4.11 → graphql_api-1.4.13}/graphql_api/mapper.py +13 -14
  5. {graphql_api-1.4.11 → graphql_api-1.4.13}/graphql_api/reduce.py +57 -15
  6. {graphql_api-1.4.11 → graphql_api-1.4.13}/graphql_api/types.py +26 -3
  7. {graphql_api-1.4.11 → graphql_api-1.4.13/graphql_api.egg-info}/PKG-INFO +2 -2
  8. {graphql_api-1.4.11 → graphql_api-1.4.13}/tests/test_dataclass.py +2 -2
  9. {graphql_api-1.4.11 → graphql_api-1.4.13}/tests/test_filtering.py +60 -20
  10. {graphql_api-1.4.11 → graphql_api-1.4.13}/tests/test_graphql.py +328 -0
  11. {graphql_api-1.4.11 → graphql_api-1.4.13}/tests/test_schema.py +23 -21
  12. graphql_api-1.4.11/VERSION +0 -1
  13. {graphql_api-1.4.11 → graphql_api-1.4.13}/LICENSE +0 -0
  14. {graphql_api-1.4.11 → graphql_api-1.4.13}/MANIFEST.in +0 -0
  15. {graphql_api-1.4.11 → graphql_api-1.4.13}/README.md +0 -0
  16. {graphql_api-1.4.11 → graphql_api-1.4.13}/graphql_api/api.py +0 -0
  17. {graphql_api-1.4.11 → graphql_api-1.4.13}/graphql_api/context.py +0 -0
  18. {graphql_api-1.4.11 → graphql_api-1.4.13}/graphql_api/dataclass_mapping.py +0 -0
  19. {graphql_api-1.4.11 → graphql_api-1.4.13}/graphql_api/decorators.py +0 -0
  20. {graphql_api-1.4.11 → graphql_api-1.4.13}/graphql_api/directives.py +0 -0
  21. {graphql_api-1.4.11 → graphql_api-1.4.13}/graphql_api/error.py +0 -0
  22. {graphql_api-1.4.11 → graphql_api-1.4.13}/graphql_api/exception.py +0 -0
  23. {graphql_api-1.4.11 → graphql_api-1.4.13}/graphql_api/executor.py +0 -0
  24. {graphql_api-1.4.11 → graphql_api-1.4.13}/graphql_api/federation/__init__.py +0 -0
  25. {graphql_api-1.4.11 → graphql_api-1.4.13}/graphql_api/federation/directives.py +0 -0
  26. {graphql_api-1.4.11 → graphql_api-1.4.13}/graphql_api/federation/federation.py +0 -0
  27. {graphql_api-1.4.11 → graphql_api-1.4.13}/graphql_api/federation/types.py +0 -0
  28. {graphql_api-1.4.11 → graphql_api-1.4.13}/graphql_api/middleware.py +0 -0
  29. {graphql_api-1.4.11 → graphql_api-1.4.13}/graphql_api/pydantic.py +0 -0
  30. {graphql_api-1.4.11 → graphql_api-1.4.13}/graphql_api/relay.py +0 -0
  31. {graphql_api-1.4.11 → graphql_api-1.4.13}/graphql_api/remote.py +0 -0
  32. {graphql_api-1.4.11 → graphql_api-1.4.13}/graphql_api/schema.py +0 -0
  33. {graphql_api-1.4.11 → graphql_api-1.4.13}/graphql_api/utils.py +0 -0
  34. {graphql_api-1.4.11 → graphql_api-1.4.13}/graphql_api.egg-info/SOURCES.txt +0 -0
  35. {graphql_api-1.4.11 → graphql_api-1.4.13}/graphql_api.egg-info/dependency_links.txt +0 -0
  36. {graphql_api-1.4.11 → graphql_api-1.4.13}/graphql_api.egg-info/requires.txt +0 -0
  37. {graphql_api-1.4.11 → graphql_api-1.4.13}/graphql_api.egg-info/top_level.txt +0 -0
  38. {graphql_api-1.4.11 → graphql_api-1.4.13}/setup.cfg +0 -0
  39. {graphql_api-1.4.11 → graphql_api-1.4.13}/setup.py +0 -0
  40. {graphql_api-1.4.11 → graphql_api-1.4.13}/tests/__init__.py +0 -0
  41. {graphql_api-1.4.11 → graphql_api-1.4.13}/tests/test_async.py +0 -0
  42. {graphql_api-1.4.11 → graphql_api-1.4.13}/tests/test_calculator.py +0 -0
  43. {graphql_api-1.4.11 → graphql_api-1.4.13}/tests/test_custom_types.py +0 -0
  44. {graphql_api-1.4.11 → graphql_api-1.4.13}/tests/test_directives.py +0 -0
  45. {graphql_api-1.4.11 → graphql_api-1.4.13}/tests/test_docstrings.py +0 -0
  46. {graphql_api-1.4.11 → graphql_api-1.4.13}/tests/test_error.py +0 -0
  47. {graphql_api-1.4.11 → graphql_api-1.4.13}/tests/test_federation.py +0 -0
  48. {graphql_api-1.4.11 → graphql_api-1.4.13}/tests/test_federation_example.py +0 -0
  49. {graphql_api-1.4.11 → graphql_api-1.4.13}/tests/test_middleware.py +0 -0
  50. {graphql_api-1.4.11 → graphql_api-1.4.13}/tests/test_pydantic.py +0 -0
  51. {graphql_api-1.4.11 → graphql_api-1.4.13}/tests/test_relay.py +0 -0
  52. {graphql_api-1.4.11 → graphql_api-1.4.13}/tests/test_remote.py +0 -0
  53. {graphql_api-1.4.11 → graphql_api-1.4.13}/tests/test_subscriptions.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphql_api
3
- Version: 1.4.11
3
+ Version: 1.4.13
4
4
  Summary: A framework for building Python GraphQL APIs.
5
5
  Home-page: https://gitlab.com/parob/graphql-api
6
- Download-URL: https://gitlab.com/parob/graphql/-/archive/v1.4.11/graphql_api-v1.4.11.tar.gz
6
+ Download-URL: https://gitlab.com/parob/graphql/-/archive/v1.4.13/graphql_api-v1.4.13.tar.gz
7
7
  Author: Robert Parker
8
8
  Author-email: rob@parob.com
9
9
  License: MIT
@@ -0,0 +1 @@
1
+ 1.4.13
@@ -5,5 +5,5 @@ from graphql_api.api import GraphQLAPI, GraphQLRootTypeDelegate
5
5
  from graphql_api.decorators import field, type
6
6
  from graphql_api.error import GraphQLError
7
7
  from graphql_api.executor import GraphQLExecutor
8
- from graphql_api.reduce import GraphQLFilter, TagFilter
8
+ from graphql_api.reduce import GraphQLFilter, TagFilter, FilterResponse
9
9
  from graphql_api.schema import AppliedDirective
@@ -60,7 +60,6 @@ from graphql_api.utils import (
60
60
  has_single_type_union_return,
61
61
  to_camel_case,
62
62
  to_camel_case_text,
63
- to_input_value,
64
63
  to_snake_case,
65
64
  )
66
65
 
@@ -406,11 +405,15 @@ class GraphQLTypeMapper:
406
405
 
407
406
  def serialize(_self, value) -> Union[str, None, UndefinedType]:
408
407
  if value and isinstance(value, collections.abc.Hashable):
408
+ # For both string and regular enums, we need to extract the .value attribute
409
409
  if isinstance(value, enum.Enum):
410
- value = value.value
410
+ lookup_key = value.value
411
+ else:
412
+ # If it's not an enum instance, use the value directly
413
+ lookup_key = value
411
414
 
412
415
  # noinspection PyProtectedMember
413
- lookup_value = _self._value_lookup.get(value)
416
+ lookup_value = _self._value_lookup.get(lookup_key)
414
417
  if lookup_value:
415
418
  return lookup_value
416
419
  else:
@@ -566,16 +569,10 @@ class GraphQLTypeMapper:
566
569
  # noinspection PyTypeChecker
567
570
  input_arg_type = GraphQLNonNull(input_arg_type) # type: ignore
568
571
 
572
+ # Use the raw Python default for input coercion.
573
+ # Do not convert to GraphQL query literals here.
569
574
  default_value = local_default_args.get(key, None)
570
575
 
571
- if default_value is not None:
572
- try:
573
- default_value = to_input_value(default_value)
574
- except ValueError as _err:
575
- raise ValueError(
576
- f"Unable to map {local_name}.{key}, {_err}."
577
- )
578
-
579
576
  arguments[to_camel_case(key)] = GraphQLInputField(
580
577
  type_=input_arg_type, default_value=default_value # type: ignore
581
578
  )
@@ -751,12 +748,14 @@ class GraphQLTypeMapper:
751
748
  if issubclass(type__, GraphQLType):
752
749
  return type__()
753
750
 
754
- if issubclass(type__, tuple(self.scalar_classes())):
755
- return self.map_to_scalar(type__)
756
-
751
+ # Check for enum.Enum before scalar types to handle string enums correctly
752
+ # String enums inherit from both str and Enum, so we need to check Enum first
757
753
  if issubclass(type__, enum.Enum):
758
754
  return self.map_to_enum(type__)
759
755
 
756
+ if issubclass(type__, tuple(self.scalar_classes())):
757
+ return self.map_to_scalar(type__)
758
+
760
759
  if is_interface(type__, self.schema):
761
760
  return self.map_to_interface(type__)
762
761
 
@@ -1,4 +1,4 @@
1
- from typing import List, Optional
1
+ from typing import List, Optional, Union
2
2
  from enum import Enum
3
3
 
4
4
  from graphql import GraphQLList, GraphQLNonNull, GraphQLObjectType
@@ -12,14 +12,15 @@ class FilterResponse(Enum):
12
12
  """
13
13
  Response from a GraphQL filter indicating how to handle a field and transitive types.
14
14
 
15
- ALLOW - Keep the field, don't preserve transitive object types (should_filter=False, preserve_transitive=False)
16
- ALLOW_TRANSITIVE - Keep the field and preserve transitive object types (should_filter=False, preserve_transitive=True)
17
- REMOVE - Remove the field but preserve types referenced by unfiltered fields (should_filter=True, preserve_transitive=True)
18
- REMOVE_STRICT - Remove the field and don't preserve unreferenced types (should_filter=True, preserve_transitive=False)
15
+ KEEP - Keep the field, don't preserve transitive object types
16
+ KEEP_TRANSITIVE - Keep the field and preserve transitive object types
17
+ REMOVE - Remove the field but preserve types referenced by unfiltered fields
18
+ REMOVE_STRICT - Always remove the field even if it is referenced by an unfiltered field that preserves transitive types.
19
+
19
20
  """
20
21
 
21
- ALLOW = "allow"
22
- ALLOW_TRANSITIVE = "allow_transitive"
22
+ KEEP = "keep"
23
+ KEEP_TRANSITIVE = "allow_transitive"
23
24
  REMOVE = "remove"
24
25
  REMOVE_STRICT = "remove_strict"
25
26
 
@@ -31,18 +32,32 @@ class FilterResponse(Enum):
31
32
  @property
32
33
  def preserve_transitive(self) -> bool:
33
34
  """True if types referenced by unfiltered fields should be preserved"""
34
- return self in (FilterResponse.ALLOW_TRANSITIVE, FilterResponse.REMOVE)
35
+ return self in (FilterResponse.KEEP_TRANSITIVE, FilterResponse.REMOVE)
35
36
 
36
37
 
37
38
  class GraphQLFilter:
38
- def filter_field(self, name, meta: dict) -> FilterResponse:
39
+
40
+ def filter_field(self, name: str, meta: dict) -> Union[bool, FilterResponse]:
39
41
  """
40
- Return FilterResponse indicating how to handle the field
42
+ Return either FilterReponse or a bool value indicating how to handle the field.
43
+
44
+ There are two options for what is returned;
45
+
46
+ Basic usage:
47
+ True: This field should be removed.
48
+ False: This field should be kept.
49
+
50
+ Advanced usage:
51
+ FilterResponse.KEEP_TRANSITIVE: This field should be kept and any transitive types should be kept.
52
+ FilterResponse.KEEP: This field should be kept but any transitive types set to REMOVE or REMOVE_STRICT will be removed.
53
+ FilterResponse.REMOVE: This field should generally be removed, but will be kept if it is referenced by an unfiltered field that is set to KEEP_TRANSITIVE.
54
+ FilterResponse.REMOVE_STRICT: This field should always be removed, even if it is referenced by an unfiltered field that is set to KEEP_TRANSITIVE.
41
55
  """
42
56
  raise NotImplementedError()
43
57
 
44
58
 
45
59
  class TagFilter(GraphQLFilter):
60
+
46
61
  def __init__(
47
62
  self, tags: Optional[List[str]] = None, preserve_transitive: bool = True
48
63
  ):
@@ -62,9 +77,9 @@ class TagFilter(GraphQLFilter):
62
77
  if not should_filter:
63
78
  # Field is allowed - use ALLOW_TRANSITIVE only if preserve_transitive is enabled
64
79
  if self.preserve_transitive:
65
- return FilterResponse.ALLOW_TRANSITIVE
80
+ return FilterResponse.KEEP_TRANSITIVE
66
81
  else:
67
- return FilterResponse.ALLOW
82
+ return FilterResponse.KEEP
68
83
  elif self.preserve_transitive:
69
84
  # Field is filtered but preserve transitive dependencies
70
85
  return FilterResponse.REMOVE
@@ -448,8 +463,20 @@ class GraphQLSchemaReducer:
448
463
  filter_response = field_filter.filter_field(
449
464
  field_name, field_meta
450
465
  )
466
+
467
+ # Handle backwards compatibility with boolean returns
468
+ if isinstance(filter_response, bool):
469
+ # Boolean True means filter the field (old API)
470
+ # Convert to FilterResponse for consistent handling
471
+ if filter_response:
472
+ # Field should be filtered - use REMOVE_STRICT as default
473
+ filter_response = FilterResponse.REMOVE_STRICT
474
+ else:
475
+ # Field should be kept - use KEEP as default
476
+ filter_response = FilterResponse.KEEP
477
+
451
478
  # If this field uses ALLOW_TRANSITIVE, preserve its referenced type
452
- if isinstance(filter_response, FilterResponse) and filter_response == FilterResponse.ALLOW_TRANSITIVE:
479
+ if isinstance(filter_response, FilterResponse) and filter_response == FilterResponse.KEEP_TRANSITIVE:
453
480
  if isinstance(type_, (GraphQLInterfaceType, GraphQLObjectType)):
454
481
  allow_transitive_types.add(type_)
455
482
  # If any filter response wants to preserve transitive,
@@ -559,7 +586,13 @@ class GraphQLSchemaReducer:
559
586
  filter_response = field_filter.filter_field(
560
587
  field_name, field_meta
561
588
  )
562
- if isinstance(filter_response, FilterResponse) and filter_response.should_filter:
589
+
590
+ # Handle backwards compatibility with boolean returns
591
+ if isinstance(filter_response, bool):
592
+ # Boolean True means filter the field (old API)
593
+ if filter_response:
594
+ invalid_fields.add((root_type, key))
595
+ elif isinstance(filter_response, FilterResponse) and filter_response.should_filter:
563
596
  invalid_fields.add((root_type, key))
564
597
 
565
598
  if isinstance(type_, (GraphQLInterfaceType, GraphQLObjectType)):
@@ -667,7 +700,16 @@ class GraphQLSchemaReducer:
667
700
  filter_response = field_filter.filter_field(
668
701
  field_name, field_meta
669
702
  )
670
- if isinstance(filter_response, FilterResponse) and filter_response.should_filter:
703
+
704
+ # Handle backwards compatibility with boolean returns
705
+ should_filter_field = False
706
+ if isinstance(filter_response, bool):
707
+ # Boolean True means filter the field (old API)
708
+ should_filter_field = filter_response
709
+ elif isinstance(filter_response, FilterResponse):
710
+ should_filter_field = filter_response.should_filter
711
+
712
+ if should_filter_field:
671
713
  invalid_fields.add((current_type, key))
672
714
  field_is_filtered = True
673
715
  break
@@ -4,7 +4,7 @@ import json
4
4
  import uuid
5
5
  import enum
6
6
 
7
- from typing import Any, Dict, List, Optional, Type, Union
7
+ from typing import Any, Dict, List, Optional, Type, Union, cast
8
8
 
9
9
  from graphql import (
10
10
  GraphQLEnumType,
@@ -24,8 +24,31 @@ class GraphQLMappedEnumType(GraphQLEnumType):
24
24
 
25
25
  def parse_literal(self, *args, **kwargs):
26
26
  result = super().parse_literal(*args, **kwargs)
27
- if result and hasattr(self, "enum_type") and self.enum_type:
28
- return self.enum_type(result)
27
+ if result is not Undefined and result is not None and getattr(self, "enum_type", None):
28
+ try:
29
+ enum_type: Optional[Type[enum.Enum]] = self.enum_type
30
+ if enum_type is None:
31
+ return result
32
+ return cast(Type[enum.Enum], enum_type)(result)
33
+ except Exception:
34
+ return Undefined
35
+ return result
36
+
37
+ def parse_value(self, value):
38
+ """Coerce incoming variable values to the mapped Python Enum instance.
39
+
40
+ GraphQL variables use `parse_value`, while inline literals use
41
+ `parse_literal`. Ensure both paths produce the Python Enum.
42
+ """
43
+ result = super().parse_value(value)
44
+ if result is not Undefined and result is not None and getattr(self, "enum_type", None):
45
+ try:
46
+ enum_type: Optional[Type[enum.Enum]] = self.enum_type
47
+ if enum_type is None:
48
+ return result
49
+ return cast(Type[enum.Enum], enum_type)(result)
50
+ except Exception:
51
+ return Undefined
29
52
  return result
30
53
 
31
54
 
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphql_api
3
- Version: 1.4.11
3
+ Version: 1.4.13
4
4
  Summary: A framework for building Python GraphQL APIs.
5
5
  Home-page: https://gitlab.com/parob/graphql-api
6
- Download-URL: https://gitlab.com/parob/graphql/-/archive/v1.4.11/graphql_api-v1.4.11.tar.gz
6
+ Download-URL: https://gitlab.com/parob/graphql/-/archive/v1.4.13/graphql_api-v1.4.13.tar.gz
7
7
  Author: Robert Parker
8
8
  Author-email: rob@parob.com
9
9
  License: MIT
@@ -111,11 +111,11 @@ class TestDataclass:
111
111
  if "admin" in tags:
112
112
  if name == "get_stats":
113
113
  # Use ALLOW_TRANSITIVE to preserve the Stats type
114
- return FilterResponse.ALLOW_TRANSITIVE
114
+ return FilterResponse.KEEP_TRANSITIVE
115
115
  else:
116
116
  # Other admin fields should be removed
117
117
  return FilterResponse.REMOVE_STRICT
118
- return FilterResponse.ALLOW
118
+ return FilterResponse.KEEP
119
119
 
120
120
  # Build the API with the custom filter
121
121
  api = GraphQLAPI(root_type=Root, filters=[TestFilter()])
@@ -1,5 +1,6 @@
1
1
  from graphql_api.api import GraphQLAPI
2
2
  from graphql_api.mapper import GraphQLMetaKey
3
+ from graphql_api.directives import print_schema
3
4
 
4
5
 
5
6
  class TestSchemaFiltering:
@@ -515,12 +516,12 @@ class TestSchemaFiltering:
515
516
  from graphql_api.reduce import FilterResponse
516
517
 
517
518
  # Test ALLOW - keep field, don't preserve transitive
518
- assert not FilterResponse.ALLOW.should_filter
519
- assert not FilterResponse.ALLOW.preserve_transitive
519
+ assert not FilterResponse.KEEP.should_filter
520
+ assert not FilterResponse.KEEP.preserve_transitive
520
521
 
521
522
  # Test ALLOW_TRANSITIVE - keep field, preserve transitive
522
- assert not FilterResponse.ALLOW_TRANSITIVE.should_filter
523
- assert FilterResponse.ALLOW_TRANSITIVE.preserve_transitive
523
+ assert not FilterResponse.KEEP_TRANSITIVE.should_filter
524
+ assert FilterResponse.KEEP_TRANSITIVE.preserve_transitive
524
525
 
525
526
  # Test REMOVE - remove field, preserve transitive
526
527
  assert FilterResponse.REMOVE.should_filter
@@ -545,10 +546,10 @@ class TestSchemaFiltering:
545
546
  tags = meta.get("tags", [])
546
547
 
547
548
  if "allow" in tags:
548
- return FilterResponse.ALLOW # Keep field, don't preserve transitive
549
+ return FilterResponse.KEEP # Keep field, don't preserve transitive
549
550
  elif "allow_transitive" in tags:
550
551
  return (
551
- FilterResponse.ALLOW_TRANSITIVE
552
+ FilterResponse.KEEP_TRANSITIVE
552
553
  ) # Keep field, preserve transitive
553
554
  elif "remove" in tags:
554
555
  return FilterResponse.REMOVE # Remove field, preserve transitive
@@ -557,7 +558,7 @@ class TestSchemaFiltering:
557
558
  FilterResponse.REMOVE_STRICT
558
559
  ) # Remove field, don't preserve transitive
559
560
  else:
560
- return FilterResponse.ALLOW_TRANSITIVE # Default behavior
561
+ return FilterResponse.KEEP_TRANSITIVE # Default behavior
561
562
 
562
563
  class ReferencedType:
563
564
  @field({"tags": ["allow"]})
@@ -673,7 +674,7 @@ class TestSchemaFiltering:
673
674
  elif "admin" in tags:
674
675
  return FilterResponse.REMOVE # Remove with preservation
675
676
  else:
676
- return FilterResponse.ALLOW_TRANSITIVE # Keep with preservation
677
+ return FilterResponse.KEEP_TRANSITIVE # Keep with preservation
677
678
 
678
679
  class DataType:
679
680
  @field
@@ -975,11 +976,11 @@ class TestSchemaFiltering:
975
976
  if "admin" in tags:
976
977
  if name == "admin_user":
977
978
  # Use ALLOW_TRANSITIVE to preserve the AdminUser type
978
- return FilterResponse.ALLOW_TRANSITIVE
979
+ return FilterResponse.KEEP_TRANSITIVE
979
980
  else:
980
981
  # Other admin fields should be removed
981
982
  return FilterResponse.REMOVE_STRICT
982
- return FilterResponse.ALLOW
983
+ return FilterResponse.KEEP
983
984
 
984
985
  # Build the API with the custom filter
985
986
  api = GraphQLAPI(root_type=Root, filters=[TestFilter()])
@@ -1087,10 +1088,10 @@ class TestSchemaFiltering:
1087
1088
  tags = meta.get("tags", [])
1088
1089
  if "admin" in tags:
1089
1090
  if name == "admin_user":
1090
- return FilterResponse.ALLOW_TRANSITIVE
1091
+ return FilterResponse.KEEP_TRANSITIVE
1091
1092
  else:
1092
1093
  return FilterResponse.REMOVE_STRICT
1093
- return FilterResponse.ALLOW
1094
+ return FilterResponse.KEEP
1094
1095
 
1095
1096
  api_strict = GraphQLAPI(root_type=Root, filters=[TestFilterStrict()])
1096
1097
  schema_strict, _ = api_strict.build_schema()
@@ -1101,10 +1102,10 @@ class TestSchemaFiltering:
1101
1102
  tags = meta.get("tags", [])
1102
1103
  if "admin" in tags:
1103
1104
  if name == "admin_user":
1104
- return FilterResponse.ALLOW_TRANSITIVE
1105
+ return FilterResponse.KEEP_TRANSITIVE
1105
1106
  else:
1106
1107
  return FilterResponse.REMOVE # Use REMOVE instead of REMOVE_STRICT
1107
- return FilterResponse.ALLOW
1108
+ return FilterResponse.KEEP
1108
1109
 
1109
1110
  api_remove = GraphQLAPI(root_type=Root, filters=[TestFilterRemove()])
1110
1111
  schema_remove, _ = api_remove.build_schema()
@@ -1198,7 +1199,7 @@ class TestSchemaFiltering:
1198
1199
 
1199
1200
  if name == "secret_data" and "admin" in tags:
1200
1201
  # Use ALLOW_TRANSITIVE to preserve SecretData type
1201
- return FilterResponse.ALLOW_TRANSITIVE
1202
+ return FilterResponse.KEEP_TRANSITIVE
1202
1203
  elif "secret" in tags or "classified" in tags:
1203
1204
  # These fields should ALWAYS be removed, even if type is preserved
1204
1205
  return FilterResponse.REMOVE_STRICT
@@ -1207,7 +1208,7 @@ class TestSchemaFiltering:
1207
1208
  return FilterResponse.REMOVE
1208
1209
  else:
1209
1210
  # Allow all other fields
1210
- return FilterResponse.ALLOW
1211
+ return FilterResponse.KEEP
1211
1212
 
1212
1213
  # Build the API with the test filter
1213
1214
  api = GraphQLAPI(root_type=Root, filters=[TestFilter()])
@@ -1524,11 +1525,11 @@ class TestSchemaFiltering:
1524
1525
  class Animal:
1525
1526
  @field
1526
1527
  def name(self) -> str:
1527
- pass
1528
+ ...
1528
1529
 
1529
1530
  @field({"tags": ["vet"]})
1530
1531
  def medical_history(self) -> str:
1531
- pass
1532
+ ...
1532
1533
 
1533
1534
  class Dog(Animal):
1534
1535
  def __init__(self):
@@ -1913,7 +1914,7 @@ class TestSchemaFiltering:
1913
1914
  else FilterResponse.REMOVE_STRICT
1914
1915
  )
1915
1916
  else:
1916
- return FilterResponse.ALLOW_TRANSITIVE
1917
+ return FilterResponse.KEEP_TRANSITIVE
1917
1918
 
1918
1919
  class Data:
1919
1920
  @field
@@ -2058,6 +2059,45 @@ class TestSchemaFiltering:
2058
2059
  assert result.errors
2059
2060
  # Should fail because updateName field is filtered out
2060
2061
 
2062
+ def test_custom_public_filter(self):
2063
+ """
2064
+ Test that we can use a custom filter to keep public fields and remove non-public ones
2065
+ """
2066
+ from graphql_api.reduce import GraphQLFilter
2067
+
2068
+ class PublicFilter(GraphQLFilter):
2069
+
2070
+ def filter_field(self, name: str, meta: dict) -> bool:
2071
+ # Return True to remove the field, False to keep it
2072
+ # We want to keep public fields and remove non-public ones
2073
+
2074
+ is_public = meta.get("public")
2075
+ should_filter = not is_public
2076
+ if should_filter:
2077
+ print(f"Filtering field {name} with meta {meta}: {should_filter}")
2078
+ else:
2079
+ print(f"Keeping field {name} with meta {meta}: {should_filter}")
2080
+
2081
+ return should_filter
2082
+
2083
+ api = GraphQLAPI(filters=[PublicFilter()])
2084
+
2085
+ @api.type(is_root_type=True)
2086
+ class Root:
2087
+
2088
+ @api.field({"public": True})
2089
+ def public_field(self) -> str:
2090
+ return "public"
2091
+
2092
+ @api.field
2093
+ def non_public_field(self) -> str:
2094
+ return "non-public"
2095
+
2096
+ schema, _ = api.build_schema()
2097
+ printed_schema = print_schema(schema)
2098
+ assert "publicField" in printed_schema
2099
+ assert "nonPublicField" not in printed_schema
2100
+
2061
2101
  def test_recursive_object_type_preservation(self):
2062
2102
  """
2063
2103
  Test that object types on fields of unfiltered objects (recursive)
@@ -2195,7 +2235,7 @@ class TestSchemaFiltering:
2195
2235
  elif self.return_type == "int":
2196
2236
  return 42 # Should be FilterResponse
2197
2237
  else:
2198
- return FilterResponse.ALLOW # Valid response
2238
+ return FilterResponse.KEEP # Valid response
2199
2239
 
2200
2240
  api = GraphQLAPI()
2201
2241
 
@@ -900,6 +900,334 @@ class TestGraphQL:
900
900
 
901
901
  assert result.data == expected
902
902
 
903
+ def test_string_enum(self):
904
+ """Test string enum functionality"""
905
+ api = GraphQLAPI()
906
+
907
+ class Timeframe(str, enum.Enum):
908
+ """A timeframe."""
909
+ LAST_30_DAYS = "last_30_days"
910
+ ALL_TIME = "all_time"
911
+
912
+ @api.type(is_root_type=True)
913
+ class Root:
914
+ @api.field
915
+ def get_timeframe(self) -> Timeframe:
916
+ return Timeframe.LAST_30_DAYS
917
+
918
+ @api.field
919
+ def echo_timeframe(self, timeframe: Timeframe) -> Timeframe:
920
+ return timeframe
921
+
922
+ executor = api.executor()
923
+
924
+ # Test return value
925
+ query1 = """
926
+ query {
927
+ getTimeframe
928
+ }
929
+ """
930
+ result1 = executor.execute(query1)
931
+ assert result1.data == {'getTimeframe': 'LAST_30_DAYS'}
932
+
933
+ # Test input argument
934
+ query2 = """
935
+ query {
936
+ echoTimeframe(timeframe: ALL_TIME)
937
+ }
938
+ """
939
+ result2 = executor.execute(query2)
940
+ assert result2.data == {'echoTimeframe': 'ALL_TIME'}
941
+
942
+ def test_string_enum_with_regular_enum(self):
943
+ """Test that both string enums and regular enums work together"""
944
+ api = GraphQLAPI()
945
+
946
+ class StringStatus(str, enum.Enum):
947
+ ACTIVE = "active"
948
+ INACTIVE = "inactive"
949
+
950
+ class RegularPriority(enum.Enum):
951
+ LOW = 1
952
+ MEDIUM = 2
953
+ HIGH = 3
954
+
955
+ @api.type(is_root_type=True)
956
+ class Root:
957
+ @api.field
958
+ def get_status(self) -> StringStatus:
959
+ return StringStatus.ACTIVE
960
+
961
+ @api.field
962
+ def get_priority(self) -> RegularPriority:
963
+ return RegularPriority.HIGH
964
+
965
+ @api.field
966
+ def combine(self, status: StringStatus, priority: RegularPriority) -> str:
967
+ return f"{status.value}:{priority.value}"
968
+
969
+ executor = api.executor()
970
+
971
+ query = """
972
+ query {
973
+ getStatus
974
+ getPriority
975
+ combine(status: ACTIVE, priority: HIGH)
976
+ }
977
+ """
978
+
979
+ result = executor.execute(query)
980
+ assert result.data == {
981
+ 'getStatus': 'ACTIVE',
982
+ 'getPriority': 'HIGH',
983
+ 'combine': 'active:3'
984
+ }
985
+
986
+ def test_string_enum_optional(self):
987
+ """Test optional string enum"""
988
+ api = GraphQLAPI()
989
+
990
+ class Color(str, enum.Enum):
991
+ RED = "red"
992
+ GREEN = "green"
993
+ BLUE = "blue"
994
+
995
+ @api.type(is_root_type=True)
996
+ class Root:
997
+ @api.field
998
+ def get_color(self, color: Optional[Color] = None) -> str:
999
+ if color:
1000
+ return f"Color is {color.value}"
1001
+ return "No color specified"
1002
+
1003
+ executor = api.executor()
1004
+
1005
+ # Test with value
1006
+ query1 = """
1007
+ query {
1008
+ getColor(color: RED)
1009
+ }
1010
+ """
1011
+ result1 = executor.execute(query1)
1012
+ assert result1.data == {'getColor': 'Color is red'}
1013
+
1014
+ # Test without value
1015
+ query2 = """
1016
+ query {
1017
+ getColor
1018
+ }
1019
+ """
1020
+ result2 = executor.execute(query2)
1021
+ assert result2.data == {'getColor': 'No color specified'}
1022
+
1023
+ def test_string_enum_list(self):
1024
+ """Test list of string enums"""
1025
+ api = GraphQLAPI()
1026
+
1027
+ class Tag(str, enum.Enum):
1028
+ PYTHON = "python"
1029
+ JAVASCRIPT = "javascript"
1030
+ RUST = "rust"
1031
+ GO = "go"
1032
+
1033
+ @api.type(is_root_type=True)
1034
+ class Root:
1035
+ @api.field
1036
+ def get_tags(self) -> List[Tag]:
1037
+ return [Tag.PYTHON, Tag.RUST]
1038
+
1039
+ @api.field
1040
+ def filter_tags(self, tags: List[Tag]) -> List[Tag]:
1041
+ # Return only tags that start with 'P' or 'R'
1042
+ return [t for t in tags if t.value[0] in ('p', 'r')]
1043
+
1044
+ executor = api.executor()
1045
+
1046
+ # Test returning list
1047
+ query1 = """
1048
+ query {
1049
+ getTags
1050
+ }
1051
+ """
1052
+ result1 = executor.execute(query1)
1053
+ assert result1.data == {'getTags': ['PYTHON', 'RUST']}
1054
+
1055
+ # Test list as input
1056
+ query2 = """
1057
+ query {
1058
+ filterTags(tags: [PYTHON, JAVASCRIPT, RUST, GO])
1059
+ }
1060
+ """
1061
+ result2 = executor.execute(query2)
1062
+ assert result2.data == {'filterTags': ['PYTHON', 'RUST']}
1063
+
1064
+ def test_string_enum_argument(self):
1065
+ api = GraphQLAPI()
1066
+
1067
+ class Tag(str, enum.Enum):
1068
+ PYTHON = "python"
1069
+ JAVASCRIPT = "javascript"
1070
+ RUST = "rust"
1071
+
1072
+ @api.type(is_root_type=True)
1073
+ class Root:
1074
+ @api.field
1075
+ def check_tag(self, tag: Tag) -> bool:
1076
+ if isinstance(tag, Tag):
1077
+ return True
1078
+ return False
1079
+
1080
+ executor = api.executor()
1081
+
1082
+ query = """
1083
+ query CheckTag($tag: TagEnum!) {
1084
+ checkTag(tag: $tag)
1085
+ }
1086
+ """
1087
+ result = executor.execute(query, variables={'tag': Tag.PYTHON.name})
1088
+ assert result.data == {'checkTag': True}
1089
+
1090
+ def test_string_enum_variable_rejects_value_literal(self):
1091
+ api = GraphQLAPI()
1092
+
1093
+ class Tag(str, enum.Enum):
1094
+ PYTHON = "python"
1095
+
1096
+ @api.type(is_root_type=True)
1097
+ class Root:
1098
+ @api.field
1099
+ def check(self, tag: Tag) -> bool:
1100
+ return isinstance(tag, Tag)
1101
+
1102
+ executor = api.executor()
1103
+ query = """
1104
+ query($tag: TagEnum!){
1105
+ check(tag: $tag)
1106
+ }
1107
+ """
1108
+ # Variable must be the NAME ("PYTHON"), not the underlying value ("python")
1109
+ result = executor.execute(query, variables={"tag": "python"})
1110
+ assert result.errors and "does not exist in 'TagEnum'" in result.errors[0].message
1111
+
1112
+ def test_regular_enum_variable(self):
1113
+ api = GraphQLAPI()
1114
+
1115
+ class Priority(enum.Enum):
1116
+ LOW = 1
1117
+ HIGH = 3
1118
+
1119
+ @api.type(is_root_type=True)
1120
+ class Root:
1121
+ @api.field
1122
+ def is_high(self, p: Priority) -> bool:
1123
+ return p is Priority.HIGH
1124
+
1125
+ executor = api.executor()
1126
+ q = "query($p: PriorityEnum!){ isHigh(p: $p) }"
1127
+ result = executor.execute(q, variables={"p": "HIGH"})
1128
+ assert result.data == {"isHigh": True}
1129
+
1130
+ def test_input_object_enum_default_and_override(self):
1131
+ api = GraphQLAPI()
1132
+
1133
+ class Status(str, enum.Enum):
1134
+ ACTIVE = "active"
1135
+ INACTIVE = "inactive"
1136
+
1137
+ class Filter:
1138
+ def __init__(self, status: Status = Status.ACTIVE):
1139
+ self.status = status
1140
+
1141
+ @api.type(is_root_type=True)
1142
+ class Root:
1143
+ @api.field
1144
+ def get_status(self, flt: Filter) -> Status:
1145
+ return flt.status
1146
+
1147
+ executor = api.executor()
1148
+ # Uses default from InputObject field
1149
+ r1 = executor.execute("query{ getStatus(flt: {}) }")
1150
+ assert not r1.errors and r1.data == {"getStatus": "ACTIVE"}
1151
+
1152
+ # Override via variable with enum NAME
1153
+ q = "query($s: StatusEnum!){ getStatus(flt: {status: $s}) }"
1154
+ r2 = executor.execute(q, variables={"s": "INACTIVE"})
1155
+ assert r2.data == {"getStatus": "INACTIVE"}
1156
+
1157
+ def test_string_enum_list_variable(self):
1158
+ api = GraphQLAPI()
1159
+
1160
+ class Tag(str, enum.Enum):
1161
+ PYTHON = "python"
1162
+ RUST = "rust"
1163
+
1164
+ @api.type(is_root_type=True)
1165
+ class Root:
1166
+ @api.field
1167
+ def echo(self, tags: List[Tag]) -> List[Tag]:
1168
+ assert all(isinstance(t, Tag) for t in tags)
1169
+ return tags
1170
+
1171
+ executor = api.executor()
1172
+ q = "query($tags: [TagEnum!]!){ echo(tags: $tags) }"
1173
+ r = executor.execute(q, variables={"tags": ["PYTHON", "RUST"]})
1174
+ assert r.data == {"echo": ["PYTHON", "RUST"]}
1175
+
1176
+ def test_optional_enum_undefined_vs_null(self):
1177
+ api = GraphQLAPI()
1178
+
1179
+ class Color(str, enum.Enum):
1180
+ RED = "red"
1181
+
1182
+ @api.type(is_root_type=True)
1183
+ class Root:
1184
+ @api.field
1185
+ def show(self, color: Optional[Color] = None) -> str:
1186
+ return "none" if color is None else color.value
1187
+
1188
+ executor = api.executor()
1189
+ q = "query($c: ColorEnum){ show(color: $c) }"
1190
+ # Undefined variable
1191
+ r1 = executor.execute(q, variables={})
1192
+ assert r1.data == {"show": "none"}
1193
+ # Explicit null
1194
+ r2 = executor.execute(q, variables={"c": None})
1195
+ assert r2.data == {"show": "none"}
1196
+
1197
+ def test_returning_underlying_value_maps_to_enum_name(self):
1198
+ api = GraphQLAPI()
1199
+
1200
+ class Color(str, enum.Enum):
1201
+ RED = "red"
1202
+
1203
+ @api.type(is_root_type=True)
1204
+ class Root:
1205
+ @api.field
1206
+ def color(self) -> Color: # type: ignore[override]
1207
+ # Returning the underlying value should still serialize to NAME
1208
+ return "red" # type: ignore[return-value]
1209
+
1210
+ executor = api.executor()
1211
+ r = executor.execute("query{ color }")
1212
+ assert r.data == {"color": "RED"}
1213
+
1214
+ def test_enum_variable_mixed_case_invalid(self):
1215
+ api = GraphQLAPI()
1216
+
1217
+ class Tag(str, enum.Enum):
1218
+ PYTHON = "python"
1219
+
1220
+ @api.type(is_root_type=True)
1221
+ class Root:
1222
+ @api.field
1223
+ def ok(self, tag: Tag) -> bool:
1224
+ return True
1225
+
1226
+ executor = api.executor()
1227
+ q = "query($t: TagEnum!){ ok(tag: $t) }"
1228
+ res = executor.execute(q, variables={"t": "Python"})
1229
+ assert res.errors and "does not exist in 'TagEnum'" in res.errors[0].message
1230
+
903
1231
  # noinspection PyUnusedLocal
904
1232
  def test_literal(self):
905
1233
  api = GraphQLAPI()
@@ -10,44 +10,44 @@ class TestGraphQLSchema:
10
10
  class ObjectNoSchema:
11
11
  @field
12
12
  def test_query_no_schema(self, a: int) -> int:
13
- pass
13
+ ...
14
14
 
15
15
  @field(mutable=True)
16
16
  def test_mutation_no_schema(self, a: int) -> int:
17
- pass
17
+ ...
18
18
 
19
19
  @type(abstract=True)
20
20
  class AbstractNoSchema:
21
21
  @field
22
22
  def test_abstract_query_no_schema(self, a: int) -> int:
23
- pass
23
+ ...
24
24
 
25
25
  @field(mutable=True)
26
26
  def test_abstract_mutation_no_schema(self, a: int) -> int:
27
- pass
27
+ ...
28
28
 
29
29
  @type(interface=True)
30
30
  class InterfaceNoSchema:
31
31
  @field
32
32
  def test_interface_query_no_schema(self, a: int) -> int:
33
- pass
33
+ ...
34
34
 
35
35
  @field(mutable=True)
36
36
  def test_interface_mutation_no_schema(self, a: int) -> int:
37
- pass
37
+ ...
38
38
 
39
39
  # noinspection PyUnresolvedReferences
40
- assert ObjectNoSchema._graphql
40
+ assert ObjectNoSchema._graphql # type: ignore[reportAttributeAccess]
41
41
  assert ObjectNoSchema.test_query_no_schema._graphql
42
42
  assert ObjectNoSchema.test_mutation_no_schema._graphql
43
43
 
44
44
  # noinspection PyUnresolvedReferences
45
- assert AbstractNoSchema._graphql
45
+ assert AbstractNoSchema._graphql # type: ignore[reportAttributeAccess]
46
46
  assert AbstractNoSchema.test_abstract_query_no_schema._graphql
47
47
  assert AbstractNoSchema.test_abstract_mutation_no_schema._graphql
48
48
 
49
49
  # noinspection PyUnresolvedReferences
50
- assert InterfaceNoSchema._graphql
50
+ assert InterfaceNoSchema._graphql # type: ignore[reportAttributeAccess]
51
51
  assert InterfaceNoSchema.test_interface_query_no_schema._graphql
52
52
  assert InterfaceNoSchema.test_interface_mutation_no_schema._graphql
53
53
 
@@ -58,14 +58,14 @@ class TestGraphQLSchema:
58
58
  class ObjectSchema:
59
59
  @api_1.field
60
60
  def test_query_schema(self, a: int) -> int:
61
- pass
61
+ ...
62
62
 
63
63
  @api_1.field(mutable=True)
64
64
  def test_mutation_schema(self, a: int) -> int:
65
- pass
65
+ ...
66
66
 
67
67
  # noinspection PyUnresolvedReferences
68
- assert ObjectSchema._graphql
68
+ assert ObjectSchema._graphql # type: ignore[reportAttributeAccess]
69
69
  assert ObjectSchema.test_query_schema._graphql
70
70
  assert ObjectSchema.test_mutation_schema._graphql
71
71
 
@@ -74,14 +74,14 @@ class TestGraphQLSchema:
74
74
  class ObjectNoSchemaMeta:
75
75
  @field(meta={"test": "test"})
76
76
  def test_query_no_schema_meta(self, a: int) -> int:
77
- pass
77
+ ...
78
78
 
79
79
  @field(meta={"test": "test"}, mutable=True)
80
80
  def test_mutation_no_schema_meta(self, a: int) -> int:
81
- pass
81
+ ...
82
82
 
83
83
  # noinspection PyUnresolvedReferences
84
- assert ObjectNoSchemaMeta._graphql
84
+ assert ObjectNoSchemaMeta._graphql # type: ignore[reportAttributeAccess]
85
85
  assert ObjectNoSchemaMeta.test_query_no_schema_meta._graphql
86
86
  assert ObjectNoSchemaMeta.test_mutation_no_schema_meta._graphql
87
87
 
@@ -92,20 +92,21 @@ class TestGraphQLSchema:
92
92
  class ObjectSchemaMeta:
93
93
  @api_1.field(meta={"test3": "test4"})
94
94
  def test_query_schema_meta(self, a: int) -> int:
95
- pass
95
+ ...
96
96
 
97
97
  @api_1.field(meta={"test5": "test6"}, mutable=True)
98
98
  def test_mutation_schema_meta(self, a: int) -> int:
99
- pass
99
+ ...
100
100
 
101
101
  # noinspection PyUnresolvedReferences
102
- assert ObjectSchemaMeta._graphql
102
+ assert ObjectSchemaMeta._graphql # type: ignore[reportAttributeAccess]
103
103
  assert ObjectSchemaMeta.test_query_schema_meta._graphql
104
104
  assert ObjectSchemaMeta.test_mutation_schema_meta._graphql
105
105
 
106
- schema = api_1.build_schema()
106
+ schema, _ = api_1.build_schema()
107
107
 
108
- assert schema
108
+ assert schema is not None
109
+ assert schema.query_type is not None
109
110
 
110
111
  def test_schema_with_no_root_type(self):
111
112
  """
@@ -115,7 +116,8 @@ class TestGraphQLSchema:
115
116
  api = GraphQLAPI()
116
117
  schema, _ = api.build_schema()
117
118
 
118
- assert schema
119
+ assert schema is not None
120
+ assert schema.query_type is not None
119
121
  assert schema.query_type.name == "PlaceholderQuery"
120
122
  assert "placeholder" in schema.query_type.fields
121
123
  assert schema.mutation_type is None
@@ -1 +0,0 @@
1
- 1.4.11
File without changes
File without changes
File without changes
File without changes
File without changes