UncountablePythonSDK 0.0.170__py3-none-any.whl → 0.0.172__py3-none-any.whl

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 (150) hide show
  1. examples/integration-server/jobs/materials_auto/example_instrument.py +1 -1
  2. examples/integration-server/pyproject.toml +3 -4
  3. pkgs/argument_parser/__init__.py +47 -17
  4. pkgs/argument_parser/cached_parser.py +56 -0
  5. pkgs/argument_parser/parser_base.py +42 -0
  6. pkgs/argument_parser/parser_builder.py +52 -0
  7. pkgs/argument_parser/parser_cache.py +30 -0
  8. pkgs/argument_parser/parser_function_type.py +6 -0
  9. pkgs/argument_parser/{argument_parser.py → parser_inner.py} +69 -183
  10. pkgs/argument_parser/parser_options.py +49 -0
  11. pkgs/argument_parser/type_predicates.py +45 -0
  12. pkgs/filesystem_utils/__init__.py +29 -19
  13. pkgs/filesystem_utils/_sftp_connection.py +168 -0
  14. pkgs/filesystem_utils/_sftp_session.py +9 -10
  15. pkgs/serialization/__init__.py +23 -15
  16. pkgs/serialization/annotation.py +40 -9
  17. pkgs/serialization/serial_class.py +6 -2
  18. pkgs/serialization/serial_generic.py +2 -2
  19. pkgs/serialization/serial_union.py +18 -5
  20. pkgs/serialization/yaml.py +1 -6
  21. pkgs/serialization_util/__init__.py +1 -2
  22. pkgs/serialization_util/serialization_helpers.py +2 -2
  23. pkgs/type_spec/actions_registry/__main__.py +10 -1
  24. pkgs/type_spec/actions_registry/emit_python.py +92 -0
  25. pkgs/type_spec/builder.py +24 -8
  26. pkgs/type_spec/emit_open_api.py +5 -0
  27. pkgs/type_spec/emit_python.py +11 -1
  28. pkgs/type_spec/emit_typescript_util.py +5 -1
  29. pkgs/type_spec/load_types.py +35 -25
  30. pkgs/type_spec/non_discriminated_union_exceptions.py +2 -1
  31. pkgs/type_spec/open_api_util.py +5 -5
  32. pkgs/type_spec/type_info/emit_type_info.py +4 -2
  33. pkgs/type_spec/value_spec/__main__.py +53 -21
  34. pkgs/type_spec/value_spec/emit_python.py +6 -1
  35. uncountable/core/client.py +34 -1
  36. uncountable/integration/cli.py +1 -2
  37. uncountable/integration/http_server/__init__.py +5 -3
  38. uncountable/integration/queue_runner/job_scheduler.py +4 -1
  39. uncountable/integration/telemetry.py +29 -11
  40. uncountable/types/__init__.py +6 -0
  41. uncountable/types/api/batch/execute_batch.py +2 -0
  42. uncountable/types/api/batch/execute_batch_load_async.py +2 -0
  43. uncountable/types/api/chemical/convert_chemical_formats.py +2 -0
  44. uncountable/types/api/condition_parameters/upsert_condition_match.py +2 -0
  45. uncountable/types/api/condition_parameters/upsert_condition_matches.py +2 -0
  46. uncountable/types/api/entity/create_entities.py +2 -0
  47. uncountable/types/api/entity/create_entity.py +2 -0
  48. uncountable/types/api/entity/create_or_update_entities.py +59 -0
  49. uncountable/types/api/entity/create_or_update_entity.py +2 -0
  50. uncountable/types/api/entity/export_entities.py +2 -0
  51. uncountable/types/api/entity/get_entities_data.py +2 -0
  52. uncountable/types/api/entity/grant_entity_permissions.py +2 -0
  53. uncountable/types/api/entity/list_aggregate.py +2 -0
  54. uncountable/types/api/entity/list_entities.py +2 -0
  55. uncountable/types/api/entity/lock_entity.py +2 -0
  56. uncountable/types/api/entity/lookup_entity.py +2 -0
  57. uncountable/types/api/entity/resolve_entity_ids.py +2 -0
  58. uncountable/types/api/entity/set_barcode.py +2 -0
  59. uncountable/types/api/entity/set_entities_field_values.py +57 -0
  60. uncountable/types/api/entity/set_entity_field_values.py +2 -0
  61. uncountable/types/api/entity/set_values.py +2 -0
  62. uncountable/types/api/entity/transition_entity_phase.py +2 -0
  63. uncountable/types/api/entity/unlock_entity.py +2 -0
  64. uncountable/types/api/equipment/associate_equipment_input.py +2 -0
  65. uncountable/types/api/field_options/upsert_field_options.py +2 -0
  66. uncountable/types/api/file_folders/add_file_to_folder.py +2 -0
  67. uncountable/types/api/file_folders/modify_file_system.py +2 -0
  68. uncountable/types/api/files/download_file.py +2 -0
  69. uncountable/types/api/id_source/list_id_source.py +2 -0
  70. uncountable/types/api/id_source/match_id_source.py +2 -0
  71. uncountable/types/api/input_groups/get_input_group_names.py +2 -0
  72. uncountable/types/api/inputs/create_inputs.py +2 -0
  73. uncountable/types/api/inputs/get_input_data.py +2 -0
  74. uncountable/types/api/inputs/get_input_names.py +2 -0
  75. uncountable/types/api/inputs/get_inputs_data.py +2 -0
  76. uncountable/types/api/inputs/set_input_attribute_values.py +2 -0
  77. uncountable/types/api/inputs/set_input_category.py +2 -0
  78. uncountable/types/api/inputs/set_input_subcategories.py +2 -0
  79. uncountable/types/api/inputs/set_intermediate_type.py +2 -0
  80. uncountable/types/api/integrations/publish_realtime_data.py +2 -0
  81. uncountable/types/api/integrations/push_notification.py +2 -0
  82. uncountable/types/api/integrations/register_sockets_token.py +2 -0
  83. uncountable/types/api/listing/export_listing.py +2 -0
  84. uncountable/types/api/listing/fetch_listing.py +2 -0
  85. uncountable/types/api/material_families/update_entity_material_families.py +2 -0
  86. uncountable/types/api/notebooks/add_notebook_content.py +4 -0
  87. uncountable/types/api/notebooks/get_notebook_content.py +2 -0
  88. uncountable/types/api/output_parameters/swap_output_condition_parameters.py +2 -0
  89. uncountable/types/api/outputs/get_output_data.py +2 -0
  90. uncountable/types/api/outputs/get_output_names.py +2 -0
  91. uncountable/types/api/outputs/get_output_organization.py +2 -0
  92. uncountable/types/api/outputs/resolve_output_conditions.py +2 -0
  93. uncountable/types/api/outputs/update_output_condition_parameter.py +2 -0
  94. uncountable/types/api/permissions/set_core_permissions.py +2 -0
  95. uncountable/types/api/permissions/set_entity_permission.py +2 -0
  96. uncountable/types/api/project/get_projects.py +2 -0
  97. uncountable/types/api/project/get_projects_data.py +2 -0
  98. uncountable/types/api/recipe_links/create_recipe_link.py +2 -0
  99. uncountable/types/api/recipe_links/create_recipe_links.py +57 -0
  100. uncountable/types/api/recipe_links/remove_recipe_link.py +2 -0
  101. uncountable/types/api/recipe_metadata/get_recipe_metadata_data.py +2 -0
  102. uncountable/types/api/recipes/add_recipe_to_project.py +2 -0
  103. uncountable/types/api/recipes/add_time_series_data.py +2 -0
  104. uncountable/types/api/recipes/archive_recipes.py +2 -0
  105. uncountable/types/api/recipes/associate_recipe_as_input.py +2 -0
  106. uncountable/types/api/recipes/associate_recipe_as_lot.py +2 -0
  107. uncountable/types/api/recipes/clear_recipe_outputs.py +2 -0
  108. uncountable/types/api/recipes/create_mix_order.py +2 -0
  109. uncountable/types/api/recipes/create_recipe.py +2 -0
  110. uncountable/types/api/recipes/create_recipes.py +2 -0
  111. uncountable/types/api/recipes/disassociate_recipe_as_input.py +2 -0
  112. uncountable/types/api/recipes/edit_recipe_inputs.py +61 -2
  113. uncountable/types/api/recipes/get_column_calculation_values.py +2 -0
  114. uncountable/types/api/recipes/get_curve.py +2 -0
  115. uncountable/types/api/recipes/get_recipe_calculations.py +2 -0
  116. uncountable/types/api/recipes/get_recipe_links.py +2 -0
  117. uncountable/types/api/recipes/get_recipe_names.py +2 -0
  118. uncountable/types/api/recipes/get_recipe_output_metadata.py +2 -0
  119. uncountable/types/api/recipes/get_recipes_data.py +16 -0
  120. uncountable/types/api/recipes/lock_recipes.py +2 -0
  121. uncountable/types/api/recipes/remove_recipe_from_project.py +2 -0
  122. uncountable/types/api/recipes/set_recipe_inputs.py +2 -0
  123. uncountable/types/api/recipes/set_recipe_metadata.py +2 -0
  124. uncountable/types/api/recipes/set_recipe_output_annotations.py +2 -0
  125. uncountable/types/api/recipes/set_recipe_output_file.py +2 -0
  126. uncountable/types/api/recipes/set_recipe_outputs.py +2 -0
  127. uncountable/types/api/recipes/set_recipe_tags.py +2 -0
  128. uncountable/types/api/recipes/set_recipe_total.py +2 -0
  129. uncountable/types/api/recipes/unarchive_recipes.py +2 -0
  130. uncountable/types/api/recipes/unlock_recipes.py +2 -0
  131. uncountable/types/api/recipes/upsert_recipe_workflow_step.py +2 -0
  132. uncountable/types/api/recipes/upsert_step_relationships.py +2 -0
  133. uncountable/types/api/runsheet/complete_async_upload.py +2 -0
  134. uncountable/types/api/runsheet/export_default_runsheet.py +2 -0
  135. uncountable/types/api/triggers/run_trigger.py +2 -0
  136. uncountable/types/api/uploader/complete_async_parse.py +2 -0
  137. uncountable/types/api/uploader/invoke_uploader.py +2 -0
  138. uncountable/types/api/user/get_current_user_info.py +2 -0
  139. uncountable/types/async_batch_processor.py +1 -1
  140. uncountable/types/client_base.py +77 -2
  141. uncountable/types/entity_t.py +18 -0
  142. uncountable/types/recipe_workflow_step_types_t.py +2 -2
  143. uncountable/types/request_headers.py +1 -0
  144. uncountable/types/request_headers_t.py +6 -0
  145. {uncountablepythonsdk-0.0.170.dist-info → uncountablepythonsdk-0.0.172.dist-info}/METADATA +6 -8
  146. {uncountablepythonsdk-0.0.170.dist-info → uncountablepythonsdk-0.0.172.dist-info}/RECORD +148 -138
  147. pkgs/argument_parser/_is_enum.py +0 -11
  148. pkgs/argument_parser/_is_namedtuple.py +0 -14
  149. {uncountablepythonsdk-0.0.170.dist-info → uncountablepythonsdk-0.0.172.dist-info}/WHEEL +0 -0
  150. {uncountablepythonsdk-0.0.170.dist-info → uncountablepythonsdk-0.0.172.dist-info}/top_level.txt +0 -0
@@ -14,7 +14,7 @@ from uncountable.types.integration_session_t import IntegrationSessionInstrument
14
14
  from websockets.sync.client import connect
15
15
  from websockets.typing import Data
16
16
 
17
- from pkgs.argument_parser.argument_parser import CachedParser
17
+ from pkgs.argument_parser import CachedParser
18
18
  from pkgs.serialization_util import serialize_for_api
19
19
 
20
20
 
@@ -5,19 +5,18 @@ name = "integration-server-testing"
5
5
  # end_project_name
6
6
  dynamic = ["version"]
7
7
  dependencies = [
8
- "mypy == 1.*",
8
+ "mypy >= 2.1.0, < 3",
9
9
  "ruff == 0.*",
10
10
  "openpyxl == 3.*",
11
11
  "more_itertools ==11.*",
12
- "types-paramiko ==4.0.0.20260402",
12
+ "types-paramiko ==4.0.0.20260518",
13
13
  "types-openpyxl == 3.*",
14
- "types-pysftp == 0.*",
15
14
  "types-pytz ==2026.*",
16
15
  "types-requests == 2.*",
17
16
  "types-simplejson == 3.*",
18
17
  "pandas-stubs",
19
18
  "xlrd == 2.*",
20
- "msgspec ==0.20.*",
19
+ "msgspec ==0.21.*",
21
20
  "websockets==16.0",
22
21
  ]
23
22
 
@@ -1,17 +1,47 @@
1
- from ._is_enum import is_string_enum_class as is_string_enum_class
2
- from .argument_parser import CachedParser as CachedParser
3
- from .argument_parser import ParserBase as ParserBase
4
- from .argument_parser import ParserFunction as ParserFunction
5
- from .argument_parser import ParserOptions as ParserOptions
6
- from .argument_parser import SourceEncoding as SourceEncoding
7
- from .argument_parser import build_parser as build_parser
8
- from .argument_parser import is_missing as is_missing
9
- from .argument_parser import is_optional as is_optional
10
- from .argument_parser import is_union as is_union
11
- from .case_convert import camel_to_snake_case as camel_to_snake_case
12
- from .case_convert import kebab_to_pascal_case as kebab_to_pascal_case
13
- from .case_convert import snake_to_camel_case as snake_to_camel_case
14
- from .parser_error import ParserEnumError as ParserEnumError
15
- from .parser_error import ParserError as ParserError
16
- from .parser_error import ParserExtraFieldsError as ParserExtraFieldsError
17
- from .parser_error import ParserTypeError as ParserTypeError
1
+ # CLOSED MODULE
2
+
3
+ __all__ = [
4
+ "CachedParser",
5
+ "ParserBase",
6
+ "ParserEnumError",
7
+ "ParserError",
8
+ "ParserExtraFieldsError",
9
+ "ParserFunction",
10
+ "ParserOptions",
11
+ "ParserTypeError",
12
+ "SourceEncoding",
13
+ "build_parser",
14
+ "camel_to_snake_case",
15
+ "is_missing",
16
+ "is_optional",
17
+ "is_string_enum_class",
18
+ "is_union",
19
+ "kebab_to_pascal_case",
20
+ "snake_to_camel_case",
21
+ ]
22
+
23
+ from .cached_parser import CachedParser as CachedParser
24
+ from .case_convert import (
25
+ camel_to_snake_case as camel_to_snake_case,
26
+ kebab_to_pascal_case as kebab_to_pascal_case,
27
+ snake_to_camel_case as snake_to_camel_case,
28
+ )
29
+ from .parser_base import ParserBase as ParserBase
30
+ from .parser_builder import build_parser as build_parser
31
+ from .parser_error import (
32
+ ParserEnumError as ParserEnumError,
33
+ ParserError as ParserError,
34
+ ParserExtraFieldsError as ParserExtraFieldsError,
35
+ ParserTypeError as ParserTypeError,
36
+ )
37
+ from .parser_function_type import ParserFunction as ParserFunction
38
+ from .parser_options import (
39
+ ParserOptions as ParserOptions,
40
+ SourceEncoding as SourceEncoding,
41
+ )
42
+ from .type_predicates import (
43
+ is_missing as is_missing,
44
+ is_optional as is_optional,
45
+ is_string_enum_class as is_string_enum_class,
46
+ is_union as is_union,
47
+ )
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ import typing
4
+
5
+ from pkgs.serialization import SerialType
6
+
7
+ from .parser_base import ParserBase
8
+ from .parser_builder import build_parser
9
+ from .parser_function_type import ParserFunction
10
+ from .parser_options import ParserOptions
11
+
12
+ T = typing.TypeVar("T")
13
+
14
+
15
+ class CachedParser(ParserBase[T], typing.Generic[T]):
16
+ def __init__(
17
+ self,
18
+ args: SerialType[T],
19
+ strict_property_parsing: bool = False,
20
+ ):
21
+ self.arguments = args
22
+ self.parser_api: ParserFunction[T] | None = None
23
+ self.parser_storage: ParserFunction[T] | None = None
24
+ self.strict_property_parsing = strict_property_parsing
25
+
26
+ def parse_api(self, args: typing.Any) -> T:
27
+ """
28
+ Parses data coming from an API/Endpoint
29
+
30
+ NOTE: Some places use this to parse storage data due to backwards
31
+ compatibility. If your data is coming from the DB or a file, it is
32
+ preferred to use parse_storage.
33
+ """
34
+ if self.parser_api is None:
35
+ self.parser_api = build_parser(
36
+ self.arguments,
37
+ ParserOptions.Api(
38
+ strict_property_parsing=self.strict_property_parsing,
39
+ ),
40
+ )
41
+ assert self.parser_api is not None
42
+ return self.parser_api(args)
43
+
44
+ def parse_storage(self, args: typing.Any) -> T:
45
+ """
46
+ Parses data coming from the database or file.
47
+ """
48
+ if self.parser_storage is None:
49
+ self.parser_storage = build_parser(
50
+ self.arguments,
51
+ ParserOptions.Storage(
52
+ strict_property_parsing=self.strict_property_parsing,
53
+ ),
54
+ )
55
+ assert self.parser_storage is not None
56
+ return self.parser_storage(args)
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ import typing
4
+ from abc import ABC, abstractmethod
5
+ from importlib import resources
6
+
7
+ import msgspec.yaml
8
+
9
+ from .parser_options import SourceEncoding
10
+
11
+ T = typing.TypeVar("T")
12
+
13
+
14
+ class ParserBase(ABC, typing.Generic[T]):
15
+ def parse_from_encoding(
16
+ self,
17
+ args: typing.Any,
18
+ *,
19
+ source_encoding: SourceEncoding,
20
+ ) -> T:
21
+ match source_encoding:
22
+ case SourceEncoding.API:
23
+ return self.parse_api(args)
24
+ case SourceEncoding.STORAGE:
25
+ return self.parse_storage(args)
26
+ case _:
27
+ typing.assert_never(source_encoding)
28
+
29
+ # IMPROVE: Args would be better typed as "object"
30
+ @abstractmethod
31
+ def parse_storage(self, args: typing.Any) -> T: ...
32
+
33
+ @abstractmethod
34
+ def parse_api(self, args: typing.Any) -> T: ...
35
+
36
+ def parse_yaml_file(self, path: str) -> T:
37
+ with open(path, encoding="utf-8") as data_in:
38
+ return self.parse_storage(msgspec.yaml.decode(data_in.read()))
39
+
40
+ def parse_yaml_resource(self, package: resources.Package, resource: str) -> T:
41
+ with resources.open_text(package, resource) as fp:
42
+ return self.parse_storage(msgspec.yaml.decode(fp.read()))
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ import typing
4
+ from collections import defaultdict
5
+
6
+ from pkgs.serialization import SerialType
7
+
8
+ from .parser_cache import ParserCache, ParserCacheManager
9
+ from .parser_function_type import ParserFunction
10
+ from .parser_inner import build_parser_inner
11
+ from .parser_options import ParserContext, ParserOptions
12
+
13
+ T = typing.TypeVar("T")
14
+
15
+ _CACHE_MAP: dict[ParserOptions, ParserCache] = defaultdict(ParserCache)
16
+
17
+
18
+ def build_parser(
19
+ parsed_type: SerialType[T],
20
+ options: ParserOptions,
21
+ ) -> ParserFunction[T]:
22
+ """
23
+ Consider using CachedParser to provide a cleaner API for storage and API
24
+ data parsing.
25
+ """
26
+
27
+ # Keep a cache per ParserOptions type, as they produce distinct parsers
28
+ global_cache = _CACHE_MAP[options]
29
+
30
+ context = ParserContext(options=options, cache=ParserCacheManager(global_cache))
31
+
32
+ # PEP 695 ``type X[T] = ...`` aliases (and their subscripted forms) are not
33
+ # ``type`` instances, so they can't share the top-level cache keyed on
34
+ # ``type[Any]``. Skip the top-level cache for them; ``build_parser_inner``
35
+ # caches parameterized dataclasses internally by ``(type, type_var_map)``.
36
+ is_alias_type = isinstance(parsed_type, typing.TypeAliasType) or isinstance(
37
+ typing.get_origin(parsed_type), typing.TypeAliasType
38
+ )
39
+
40
+ if not is_alias_type:
41
+ cur_parser = global_cache.get(parsed_type)
42
+ if cur_parser is not None:
43
+ return cur_parser
44
+
45
+ built_parser = build_parser_inner(parsed_type, context, {})
46
+
47
+ if not is_alias_type:
48
+ context.cache.put(parsed_type, built_parser)
49
+
50
+ global_cache.update(context.cache.local_cache())
51
+
52
+ return built_parser
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ import types
4
+ import typing
5
+
6
+ from .parser_function_type import ParserFunction
7
+
8
+ T = typing.TypeVar("T")
9
+ ParserCacheKey = type[typing.Any] | typing.TypeAliasType | types.UnionType
10
+ ParserCache = dict[
11
+ ParserCacheKey,
12
+ ParserFunction[typing.Any],
13
+ ]
14
+
15
+
16
+ class ParserCacheManager:
17
+ def __init__(self, global_cache: ParserCache) -> None:
18
+ self._global_cache = global_cache
19
+ self._local_cache: ParserCache = dict()
20
+
21
+ def get(self, key: ParserCacheKey) -> ParserFunction[typing.Any] | None:
22
+ if key in self._local_cache:
23
+ return self._local_cache[key]
24
+ return self._global_cache.get(key)
25
+
26
+ def put(self, key: ParserCacheKey, value: ParserFunction[typing.Any]) -> None:
27
+ self._local_cache[key] = value
28
+
29
+ def local_cache(self) -> ParserCache:
30
+ return self._local_cache
@@ -0,0 +1,6 @@
1
+ from __future__ import annotations
2
+
3
+ import typing
4
+
5
+ T = typing.TypeVar("T")
6
+ ParserFunction = typing.Callable[[typing.Any], T]
@@ -5,15 +5,11 @@ import datetime
5
5
  import math
6
6
  import types
7
7
  import typing
8
- from abc import ABC, abstractmethod
9
- from collections import defaultdict
10
8
  from datetime import date
11
9
  from decimal import Decimal
12
- from enum import Enum, auto
13
- from importlib import resources
10
+ from enum import Enum
14
11
 
15
12
  import dateutil.parser
16
- import msgspec.yaml
17
13
 
18
14
  from pkgs.serialization import (
19
15
  MissingSentryType,
@@ -23,8 +19,6 @@ from pkgs.serialization import (
23
19
  get_serial_union_data,
24
20
  )
25
21
 
26
- from ._is_enum import is_string_enum_class
27
- from ._is_namedtuple import is_namedtuple_type
28
22
  from .case_convert import camel_to_snake_case, snake_to_camel_case
29
23
  from .parser_error import (
30
24
  ParserEnumError,
@@ -32,66 +26,16 @@ from .parser_error import (
32
26
  ParserExtraFieldsError,
33
27
  ParserTypeError,
34
28
  )
29
+ from .parser_function_type import ParserFunction
30
+ from .parser_options import ParserContext
31
+ from .type_predicates import (
32
+ is_missing,
33
+ is_namedtuple_type,
34
+ is_optional,
35
+ is_string_enum_class,
36
+ )
35
37
 
36
38
  T = typing.TypeVar("T")
37
- ParserFunction = typing.Callable[[typing.Any], T]
38
- ParserCache = dict[type[typing.Any], ParserFunction[typing.Any]]
39
-
40
-
41
- class SourceEncoding(Enum):
42
- API = auto()
43
- STORAGE = auto()
44
-
45
-
46
- @dataclasses.dataclass(frozen=True, eq=True)
47
- class ParserOptions:
48
- encoding: SourceEncoding
49
- strict_property_parsing: bool = False
50
-
51
- @staticmethod
52
- def Api(*, strict_property_parsing: bool = False) -> ParserOptions:
53
- return ParserOptions(
54
- encoding=SourceEncoding.API, strict_property_parsing=strict_property_parsing
55
- )
56
-
57
- @staticmethod
58
- def Storage(*, strict_property_parsing: bool = False) -> ParserOptions:
59
- return ParserOptions(
60
- encoding=SourceEncoding.STORAGE,
61
- strict_property_parsing=strict_property_parsing,
62
- )
63
-
64
- @property
65
- def from_camel_case(self) -> bool:
66
- return self.encoding == SourceEncoding.API
67
-
68
- @property
69
- def allow_direct_type(self) -> bool:
70
- """This allows parsing from a DB column without having to check whether it's
71
- the native format of the type, a JSON column, or a string encoding."""
72
- return self.encoding == SourceEncoding.STORAGE
73
-
74
-
75
- @dataclasses.dataclass(frozen=True)
76
- class ParserContext:
77
- options: ParserOptions
78
- cache: ParserCache
79
-
80
-
81
- def is_union(field_type: typing.Any) -> bool:
82
- origin = typing.get_origin(field_type)
83
- return origin is typing.Union or origin is types.UnionType
84
-
85
-
86
- def is_optional(field_type: typing.Any) -> bool:
87
- return is_union(field_type) and type(None) in typing.get_args(field_type)
88
-
89
-
90
- def is_missing(field_type: typing.Any) -> bool:
91
- if not is_union(field_type):
92
- return False
93
- args = typing.get_args(field_type)
94
- return not (len(args) == 0 or args[0] is not MissingSentryType)
95
39
 
96
40
 
97
41
  def _has_field_default(field: dataclasses.Field[typing.Any]) -> bool:
@@ -130,7 +74,7 @@ def _invoke_tuple_parsers(
130
74
 
131
75
 
132
76
  def _invoke_fallback_parsers(
133
- original_type: type[T],
77
+ original_type: type[T] | types.UnionType,
134
78
  arg_parsers: typing.Sequence[typing.Callable[[typing.Any], T]],
135
79
  value: typing.Any,
136
80
  ) -> T:
@@ -259,15 +203,40 @@ def _resolve_base_type_vars(
259
203
  type_var_map[base_param] = resolved_new_type
260
204
 
261
205
  # Recurse into base's bases
262
- if dataclasses.is_dataclass(base_origin):
206
+ if isinstance(base_origin, type) and dataclasses.is_dataclass(base_origin):
263
207
  _resolve_base_type_vars(
264
- cls=base_origin, # type: ignore[arg-type]
208
+ cls=base_origin,
265
209
  type_var_map=type_var_map,
266
210
  )
267
211
 
268
212
 
269
- def _build_parser_inner(
270
- parsed_type: type[T] | typing.TypeVar,
213
+ def _build_parser_for_type_alias(
214
+ alias: typing.TypeAliasType,
215
+ alias_args: tuple[typing.Any, ...],
216
+ context: ParserContext,
217
+ type_var_map: dict[typing.TypeVar, type],
218
+ ) -> ParserFunction[T]:
219
+ """
220
+ Resolve a PEP 695 ``type X[T] = ...`` alias into a parser by unwrapping
221
+ ``alias.__value__`` and threading ``alias_args`` through ``type_var_map``
222
+ so TypeVars inside the alias body resolve correctly downstream.
223
+
224
+ ``alias_args`` is empty for the bare alias and the concrete type arguments
225
+ for a subscripted alias (e.g. ``X[int]``).
226
+ """
227
+ merged_alias_map: dict[typing.TypeVar, type] = dict(type_var_map)
228
+ for param, arg in zip(alias.__type_params__, alias_args, strict=True):
229
+ assert isinstance(param, typing.TypeVar), (
230
+ f"PEP 695 alias parameter {param!r} is not a TypeVar"
231
+ )
232
+ merged_alias_map[param] = _resolve_type_var(
233
+ type_arg=arg, type_var_map=type_var_map
234
+ )
235
+ return build_parser_inner(alias.__value__, context, merged_alias_map)
236
+
237
+
238
+ def build_parser_inner(
239
+ parsed_type: type[T] | typing.TypeVar | typing.TypeAliasType | types.UnionType,
271
240
  context: ParserContext,
272
241
  type_var_map: dict[typing.TypeVar, type],
273
242
  ) -> ParserFunction[T]:
@@ -276,7 +245,15 @@ def _build_parser_inner(
276
245
  internal parsers.
277
246
  """
278
247
  if isinstance(parsed_type, typing.TypeVar):
279
- return _build_parser_inner(type_var_map[parsed_type], context, type_var_map)
248
+ return build_parser_inner(type_var_map[parsed_type], context, type_var_map)
249
+
250
+ if isinstance(parsed_type, typing.TypeAliasType):
251
+ return _build_parser_for_type_alias(parsed_type, (), context, type_var_map)
252
+ alias_origin = typing.get_origin(parsed_type)
253
+ if isinstance(alias_origin, typing.TypeAliasType):
254
+ return _build_parser_for_type_alias(
255
+ alias_origin, typing.get_args(parsed_type), context, type_var_map
256
+ )
280
257
 
281
258
  serial_union = get_serial_union_data(parsed_type)
282
259
  if serial_union is not None:
@@ -290,7 +267,7 @@ def _build_parser_inner(
290
267
  context,
291
268
  discriminator,
292
269
  {
293
- key: _build_parser_inner(value, context, type_var_map)
270
+ key: build_parser_inner(value, context, type_var_map)
294
271
  for key, value in discriminator_map.items()
295
272
  },
296
273
  )
@@ -302,7 +279,11 @@ def _build_parser_inner(
302
279
 
303
280
  origin = typing.get_origin(parsed_type)
304
281
  type_args = typing.get_args(parsed_type)
305
- if dataclasses.is_dataclass(origin) and type_args is not None:
282
+ if (
283
+ isinstance(origin, type)
284
+ and dataclasses.is_dataclass(origin)
285
+ and type_args is not None
286
+ ):
306
287
  # Build local TypeVar map
307
288
  merged_map: dict[typing.TypeVar, type] = {
308
289
  **type_var_map,
@@ -316,7 +297,7 @@ def _build_parser_inner(
316
297
 
317
298
  # Resolve base class TypeVars
318
299
  _resolve_base_type_vars(
319
- cls=origin, # type: ignore[arg-type]
300
+ cls=origin,
320
301
  type_var_map=merged_map,
321
302
  )
322
303
 
@@ -324,11 +305,12 @@ def _build_parser_inner(
324
305
 
325
306
  # namedtuple support
326
307
  if is_namedtuple_type(parsed_type):
308
+ assert not isinstance(parsed_type, types.UnionType)
327
309
  type_hints = typing.get_type_hints(parsed_type)
328
310
  field_parsers = [
329
311
  (
330
312
  field_name,
331
- _build_parser_inner(type_hints[field_name], context, type_var_map),
313
+ build_parser_inner(type_hints[field_name], context, type_var_map),
332
314
  )
333
315
  for field_name in parsed_type.__annotations__
334
316
  ]
@@ -349,6 +331,7 @@ def _build_parser_inner(
349
331
  return typing.cast(ParserFunction[T], NONE_IDENTITY_PARSER)
350
332
 
351
333
  if origin is tuple:
334
+ assert not isinstance(parsed_type, types.UnionType)
352
335
  args = typing.get_args(parsed_type)
353
336
  element_parsers: list[typing.Callable[[typing.Any], object]] = []
354
337
  has_ellipsis = False
@@ -358,7 +341,7 @@ def _build_parser_inner(
358
341
  assert len(element_parsers) == 1
359
342
  has_ellipsis = True
360
343
  else:
361
- element_parsers.append(_build_parser_inner(arg, context, type_var_map))
344
+ element_parsers.append(build_parser_inner(arg, context, type_var_map))
362
345
  return lambda value: _invoke_tuple_parsers(
363
346
  parsed_type, element_parsers, has_ellipsis, value
364
347
  )
@@ -370,7 +353,7 @@ def _build_parser_inner(
370
353
  key=lambda subtype: 0 if subtype == type(None) else 1, # noqa: E721
371
354
  )
372
355
  arg_parsers = [
373
- _build_parser_inner(arg, context, type_var_map) for arg in sorted_args
356
+ build_parser_inner(arg, context, type_var_map) for arg in sorted_args
374
357
  ]
375
358
  return lambda value: _invoke_fallback_parsers(parsed_type, arg_parsers, value)
376
359
 
@@ -383,7 +366,7 @@ def _build_parser_inner(
383
366
  raise ParserError(
384
367
  "List types only support one argument", expected_type=parsed_type
385
368
  )
386
- arg_parser = _build_parser_inner(args[0], context, type_var_map)
369
+ arg_parser = build_parser_inner(args[0], context, type_var_map)
387
370
 
388
371
  def parse_element(value: typing.Any) -> typing.Any:
389
372
  try:
@@ -407,7 +390,7 @@ def _build_parser_inner(
407
390
  "Dict types only support two arguments for now",
408
391
  expected_type=parsed_type,
409
392
  )
410
- k_inner_parser = _build_parser_inner(
393
+ k_inner_parser = build_parser_inner(
411
394
  args[0],
412
395
  context,
413
396
  type_var_map,
@@ -425,7 +408,7 @@ def _build_parser_inner(
425
408
  return camel_to_snake_case(value)
426
409
  return inner
427
410
 
428
- v_parser = _build_parser_inner(args[1], context, type_var_map)
411
+ v_parser = build_parser_inner(args[1], context, type_var_map)
429
412
 
430
413
  def parse_dict(value: typing.Any) -> typing.Any:
431
414
  return origin((key_parser(k), v_parser(v)) for k, v in value.items())
@@ -470,6 +453,9 @@ def _build_parser_inner(
470
453
 
471
454
  def parse_date(value: typing.Any) -> T:
472
455
  if context.options.allow_direct_type and isinstance(value, date):
456
+ if isinstance(value, datetime.datetime):
457
+ assert value.time() == datetime.time.min
458
+ return value.date() # type:ignore
473
459
  return value # type:ignore
474
460
  return date.fromisoformat(value) # type:ignore
475
461
 
@@ -541,7 +527,7 @@ def _build_parser_inner(
541
527
  # this must be last, since some of the expected types, like Unions,
542
528
  # will also be annotated, but have a special form
543
529
  if typing.get_origin(parsed_type) is typing.Annotated:
544
- return _build_parser_inner(
530
+ return build_parser_inner(
545
531
  parsed_type.__origin__, # type: ignore[attr-defined]
546
532
  context,
547
533
  type_var_map,
@@ -668,7 +654,7 @@ def _build_parser_dataclass(
668
654
 
669
655
  # Add to cache before building inner types, to support recursion
670
656
  parser_function = parse
671
- context.cache[cache_key] = parser_function
657
+ context.cache.put(cache_key, parser_function)
672
658
 
673
659
  dc_field_parsers = []
674
660
  for field in dataclasses.fields(parsed_type): # type:ignore[arg-type]
@@ -679,107 +665,7 @@ def _build_parser_dataclass(
679
665
  dc_field_parsers.append((
680
666
  field,
681
667
  field_type_hint,
682
- _build_parser_inner(field_type_hint, context, type_var_map),
668
+ build_parser_inner(field_type_hint, context, type_var_map),
683
669
  ))
684
670
 
685
671
  return parser_function
686
-
687
-
688
- _CACHE_MAP: dict[ParserOptions, ParserCache] = defaultdict(ParserCache)
689
-
690
-
691
- def build_parser(
692
- parsed_type: type[T],
693
- options: ParserOptions,
694
- ) -> ParserFunction[T]:
695
- """
696
- Consider using CachedParser to provide a cleaner API for storage and API
697
- data parsing.
698
- """
699
-
700
- # Keep a cache per ParserOptions type, as they produce distinct parsers
701
- cache = _CACHE_MAP[options]
702
-
703
- cur_parser = cache.get(parsed_type)
704
- if cur_parser is not None:
705
- return cur_parser
706
-
707
- context = ParserContext(options=options, cache=cache)
708
- built_parser = _build_parser_inner(parsed_type, context, {})
709
- cache[parsed_type] = built_parser
710
- return built_parser
711
-
712
-
713
- class ParserBase(ABC, typing.Generic[T]):
714
- def parse_from_encoding(
715
- self,
716
- args: typing.Any,
717
- *,
718
- source_encoding: SourceEncoding,
719
- ) -> T:
720
- match source_encoding:
721
- case SourceEncoding.API:
722
- return self.parse_api(args)
723
- case SourceEncoding.STORAGE:
724
- return self.parse_storage(args)
725
- case _:
726
- typing.assert_never(source_encoding)
727
-
728
- # IMPROVE: Args would be better typed as "object"
729
- @abstractmethod
730
- def parse_storage(self, args: typing.Any) -> T: ...
731
-
732
- @abstractmethod
733
- def parse_api(self, args: typing.Any) -> T: ...
734
-
735
- def parse_yaml_file(self, path: str) -> T:
736
- with open(path, encoding="utf-8") as data_in:
737
- return self.parse_storage(msgspec.yaml.decode(data_in.read()))
738
-
739
- def parse_yaml_resource(self, package: resources.Package, resource: str) -> T:
740
- with resources.open_text(package, resource) as fp:
741
- return self.parse_storage(msgspec.yaml.decode(fp.read()))
742
-
743
-
744
- class CachedParser(ParserBase[T], typing.Generic[T]):
745
- def __init__(
746
- self,
747
- args: type[T],
748
- strict_property_parsing: bool = False,
749
- ):
750
- self.arguments = args
751
- self.parser_api: ParserFunction[T] | None = None
752
- self.parser_storage: ParserFunction[T] | None = None
753
- self.strict_property_parsing = strict_property_parsing
754
-
755
- def parse_api(self, args: typing.Any) -> T:
756
- """
757
- Parses data coming from an API/Endpoint
758
-
759
- NOTE: Some places use this to parse storage data due to backwards
760
- compatibility. If your data is coming from the DB or a file, it is
761
- preferred to use parse_storage.
762
- """
763
- if self.parser_api is None:
764
- self.parser_api = build_parser(
765
- self.arguments,
766
- ParserOptions.Api(
767
- strict_property_parsing=self.strict_property_parsing,
768
- ),
769
- )
770
- assert self.parser_api is not None
771
- return self.parser_api(args)
772
-
773
- def parse_storage(self, args: typing.Any) -> T:
774
- """
775
- Parses data coming from the database or file.
776
- """
777
- if self.parser_storage is None:
778
- self.parser_storage = build_parser(
779
- self.arguments,
780
- ParserOptions.Storage(
781
- strict_property_parsing=self.strict_property_parsing,
782
- ),
783
- )
784
- assert self.parser_storage is not None
785
- return self.parser_storage(args)