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.
- examples/integration-server/jobs/materials_auto/example_instrument.py +1 -1
- examples/integration-server/pyproject.toml +3 -4
- pkgs/argument_parser/__init__.py +47 -17
- pkgs/argument_parser/cached_parser.py +56 -0
- pkgs/argument_parser/parser_base.py +42 -0
- pkgs/argument_parser/parser_builder.py +52 -0
- pkgs/argument_parser/parser_cache.py +30 -0
- pkgs/argument_parser/parser_function_type.py +6 -0
- pkgs/argument_parser/{argument_parser.py → parser_inner.py} +69 -183
- pkgs/argument_parser/parser_options.py +49 -0
- pkgs/argument_parser/type_predicates.py +45 -0
- pkgs/filesystem_utils/__init__.py +29 -19
- pkgs/filesystem_utils/_sftp_connection.py +168 -0
- pkgs/filesystem_utils/_sftp_session.py +9 -10
- pkgs/serialization/__init__.py +23 -15
- pkgs/serialization/annotation.py +40 -9
- pkgs/serialization/serial_class.py +6 -2
- pkgs/serialization/serial_generic.py +2 -2
- pkgs/serialization/serial_union.py +18 -5
- pkgs/serialization/yaml.py +1 -6
- pkgs/serialization_util/__init__.py +1 -2
- pkgs/serialization_util/serialization_helpers.py +2 -2
- pkgs/type_spec/actions_registry/__main__.py +10 -1
- pkgs/type_spec/actions_registry/emit_python.py +92 -0
- pkgs/type_spec/builder.py +24 -8
- pkgs/type_spec/emit_open_api.py +5 -0
- pkgs/type_spec/emit_python.py +11 -1
- pkgs/type_spec/emit_typescript_util.py +5 -1
- pkgs/type_spec/load_types.py +35 -25
- pkgs/type_spec/non_discriminated_union_exceptions.py +2 -1
- pkgs/type_spec/open_api_util.py +5 -5
- pkgs/type_spec/type_info/emit_type_info.py +4 -2
- pkgs/type_spec/value_spec/__main__.py +53 -21
- pkgs/type_spec/value_spec/emit_python.py +6 -1
- uncountable/core/client.py +34 -1
- uncountable/integration/cli.py +1 -2
- uncountable/integration/http_server/__init__.py +5 -3
- uncountable/integration/queue_runner/job_scheduler.py +4 -1
- uncountable/integration/telemetry.py +29 -11
- uncountable/types/__init__.py +6 -0
- uncountable/types/api/batch/execute_batch.py +2 -0
- uncountable/types/api/batch/execute_batch_load_async.py +2 -0
- uncountable/types/api/chemical/convert_chemical_formats.py +2 -0
- uncountable/types/api/condition_parameters/upsert_condition_match.py +2 -0
- uncountable/types/api/condition_parameters/upsert_condition_matches.py +2 -0
- uncountable/types/api/entity/create_entities.py +2 -0
- uncountable/types/api/entity/create_entity.py +2 -0
- uncountable/types/api/entity/create_or_update_entities.py +59 -0
- uncountable/types/api/entity/create_or_update_entity.py +2 -0
- uncountable/types/api/entity/export_entities.py +2 -0
- uncountable/types/api/entity/get_entities_data.py +2 -0
- uncountable/types/api/entity/grant_entity_permissions.py +2 -0
- uncountable/types/api/entity/list_aggregate.py +2 -0
- uncountable/types/api/entity/list_entities.py +2 -0
- uncountable/types/api/entity/lock_entity.py +2 -0
- uncountable/types/api/entity/lookup_entity.py +2 -0
- uncountable/types/api/entity/resolve_entity_ids.py +2 -0
- uncountable/types/api/entity/set_barcode.py +2 -0
- uncountable/types/api/entity/set_entities_field_values.py +57 -0
- uncountable/types/api/entity/set_entity_field_values.py +2 -0
- uncountable/types/api/entity/set_values.py +2 -0
- uncountable/types/api/entity/transition_entity_phase.py +2 -0
- uncountable/types/api/entity/unlock_entity.py +2 -0
- uncountable/types/api/equipment/associate_equipment_input.py +2 -0
- uncountable/types/api/field_options/upsert_field_options.py +2 -0
- uncountable/types/api/file_folders/add_file_to_folder.py +2 -0
- uncountable/types/api/file_folders/modify_file_system.py +2 -0
- uncountable/types/api/files/download_file.py +2 -0
- uncountable/types/api/id_source/list_id_source.py +2 -0
- uncountable/types/api/id_source/match_id_source.py +2 -0
- uncountable/types/api/input_groups/get_input_group_names.py +2 -0
- uncountable/types/api/inputs/create_inputs.py +2 -0
- uncountable/types/api/inputs/get_input_data.py +2 -0
- uncountable/types/api/inputs/get_input_names.py +2 -0
- uncountable/types/api/inputs/get_inputs_data.py +2 -0
- uncountable/types/api/inputs/set_input_attribute_values.py +2 -0
- uncountable/types/api/inputs/set_input_category.py +2 -0
- uncountable/types/api/inputs/set_input_subcategories.py +2 -0
- uncountable/types/api/inputs/set_intermediate_type.py +2 -0
- uncountable/types/api/integrations/publish_realtime_data.py +2 -0
- uncountable/types/api/integrations/push_notification.py +2 -0
- uncountable/types/api/integrations/register_sockets_token.py +2 -0
- uncountable/types/api/listing/export_listing.py +2 -0
- uncountable/types/api/listing/fetch_listing.py +2 -0
- uncountable/types/api/material_families/update_entity_material_families.py +2 -0
- uncountable/types/api/notebooks/add_notebook_content.py +4 -0
- uncountable/types/api/notebooks/get_notebook_content.py +2 -0
- uncountable/types/api/output_parameters/swap_output_condition_parameters.py +2 -0
- uncountable/types/api/outputs/get_output_data.py +2 -0
- uncountable/types/api/outputs/get_output_names.py +2 -0
- uncountable/types/api/outputs/get_output_organization.py +2 -0
- uncountable/types/api/outputs/resolve_output_conditions.py +2 -0
- uncountable/types/api/outputs/update_output_condition_parameter.py +2 -0
- uncountable/types/api/permissions/set_core_permissions.py +2 -0
- uncountable/types/api/permissions/set_entity_permission.py +2 -0
- uncountable/types/api/project/get_projects.py +2 -0
- uncountable/types/api/project/get_projects_data.py +2 -0
- uncountable/types/api/recipe_links/create_recipe_link.py +2 -0
- uncountable/types/api/recipe_links/create_recipe_links.py +57 -0
- uncountable/types/api/recipe_links/remove_recipe_link.py +2 -0
- uncountable/types/api/recipe_metadata/get_recipe_metadata_data.py +2 -0
- uncountable/types/api/recipes/add_recipe_to_project.py +2 -0
- uncountable/types/api/recipes/add_time_series_data.py +2 -0
- uncountable/types/api/recipes/archive_recipes.py +2 -0
- uncountable/types/api/recipes/associate_recipe_as_input.py +2 -0
- uncountable/types/api/recipes/associate_recipe_as_lot.py +2 -0
- uncountable/types/api/recipes/clear_recipe_outputs.py +2 -0
- uncountable/types/api/recipes/create_mix_order.py +2 -0
- uncountable/types/api/recipes/create_recipe.py +2 -0
- uncountable/types/api/recipes/create_recipes.py +2 -0
- uncountable/types/api/recipes/disassociate_recipe_as_input.py +2 -0
- uncountable/types/api/recipes/edit_recipe_inputs.py +61 -2
- uncountable/types/api/recipes/get_column_calculation_values.py +2 -0
- uncountable/types/api/recipes/get_curve.py +2 -0
- uncountable/types/api/recipes/get_recipe_calculations.py +2 -0
- uncountable/types/api/recipes/get_recipe_links.py +2 -0
- uncountable/types/api/recipes/get_recipe_names.py +2 -0
- uncountable/types/api/recipes/get_recipe_output_metadata.py +2 -0
- uncountable/types/api/recipes/get_recipes_data.py +16 -0
- uncountable/types/api/recipes/lock_recipes.py +2 -0
- uncountable/types/api/recipes/remove_recipe_from_project.py +2 -0
- uncountable/types/api/recipes/set_recipe_inputs.py +2 -0
- uncountable/types/api/recipes/set_recipe_metadata.py +2 -0
- uncountable/types/api/recipes/set_recipe_output_annotations.py +2 -0
- uncountable/types/api/recipes/set_recipe_output_file.py +2 -0
- uncountable/types/api/recipes/set_recipe_outputs.py +2 -0
- uncountable/types/api/recipes/set_recipe_tags.py +2 -0
- uncountable/types/api/recipes/set_recipe_total.py +2 -0
- uncountable/types/api/recipes/unarchive_recipes.py +2 -0
- uncountable/types/api/recipes/unlock_recipes.py +2 -0
- uncountable/types/api/recipes/upsert_recipe_workflow_step.py +2 -0
- uncountable/types/api/recipes/upsert_step_relationships.py +2 -0
- uncountable/types/api/runsheet/complete_async_upload.py +2 -0
- uncountable/types/api/runsheet/export_default_runsheet.py +2 -0
- uncountable/types/api/triggers/run_trigger.py +2 -0
- uncountable/types/api/uploader/complete_async_parse.py +2 -0
- uncountable/types/api/uploader/invoke_uploader.py +2 -0
- uncountable/types/api/user/get_current_user_info.py +2 -0
- uncountable/types/async_batch_processor.py +1 -1
- uncountable/types/client_base.py +77 -2
- uncountable/types/entity_t.py +18 -0
- uncountable/types/recipe_workflow_step_types_t.py +2 -2
- uncountable/types/request_headers.py +1 -0
- uncountable/types/request_headers_t.py +6 -0
- {uncountablepythonsdk-0.0.170.dist-info → uncountablepythonsdk-0.0.172.dist-info}/METADATA +6 -8
- {uncountablepythonsdk-0.0.170.dist-info → uncountablepythonsdk-0.0.172.dist-info}/RECORD +148 -138
- pkgs/argument_parser/_is_enum.py +0 -11
- pkgs/argument_parser/_is_namedtuple.py +0 -14
- {uncountablepythonsdk-0.0.170.dist-info → uncountablepythonsdk-0.0.172.dist-info}/WHEEL +0 -0
- {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
|
|
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
|
|
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.
|
|
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.
|
|
19
|
+
"msgspec ==0.21.*",
|
|
21
20
|
"websockets==16.0",
|
|
22
21
|
]
|
|
23
22
|
|
pkgs/argument_parser/__init__.py
CHANGED
|
@@ -1,17 +1,47 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
@@ -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
|
|
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,
|
|
208
|
+
cls=base_origin,
|
|
265
209
|
type_var_map=type_var_map,
|
|
266
210
|
)
|
|
267
211
|
|
|
268
212
|
|
|
269
|
-
def
|
|
270
|
-
|
|
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
|
|
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:
|
|
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
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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)
|