UncountablePythonSDK 0.0.24__py3-none-any.whl → 0.0.131__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.
Potentially problematic release.
This version of UncountablePythonSDK might be problematic. Click here for more details.
- docs/conf.py +60 -8
- docs/index.md +107 -4
- docs/integration_examples/create_ingredient.md +43 -0
- docs/integration_examples/create_output.md +56 -0
- docs/integration_examples/index.md +6 -0
- docs/justfile +2 -2
- docs/requirements.txt +7 -5
- examples/async_batch.py +5 -6
- examples/basic_auth.py +7 -0
- examples/create_entity.py +4 -6
- examples/create_ingredient_sdk.py +34 -0
- examples/download_files.py +26 -0
- examples/edit_recipe_inputs.py +50 -0
- examples/integration-server/jobs/materials_auto/concurrent_cron.py +11 -0
- examples/integration-server/jobs/materials_auto/example_cron.py +21 -0
- examples/integration-server/jobs/materials_auto/example_http.py +47 -0
- examples/integration-server/jobs/materials_auto/example_instrument.py +100 -0
- examples/integration-server/jobs/materials_auto/example_parse.py +140 -0
- examples/integration-server/jobs/materials_auto/example_predictions.py +61 -0
- examples/integration-server/jobs/materials_auto/example_runsheet_wh.py +39 -0
- examples/integration-server/jobs/materials_auto/example_wh.py +23 -0
- examples/integration-server/jobs/materials_auto/profile.yaml +104 -0
- examples/integration-server/pyproject.toml +224 -0
- examples/invoke_uploader.py +26 -0
- examples/oauth.py +7 -0
- examples/set_recipe_metadata_file.py +40 -0
- examples/set_recipe_output_file_sdk.py +26 -0
- examples/upload_files.py +2 -3
- pkgs/argument_parser/__init__.py +9 -0
- pkgs/argument_parser/_is_namedtuple.py +3 -0
- pkgs/argument_parser/argument_parser.py +295 -74
- pkgs/argument_parser/case_convert.py +4 -3
- pkgs/filesystem_utils/__init__.py +20 -0
- pkgs/filesystem_utils/_blob_session.py +144 -0
- pkgs/filesystem_utils/_gdrive_session.py +309 -0
- pkgs/filesystem_utils/_local_session.py +69 -0
- pkgs/filesystem_utils/_s3_session.py +118 -0
- pkgs/filesystem_utils/_sftp_session.py +151 -0
- pkgs/filesystem_utils/file_type_utils.py +91 -0
- pkgs/filesystem_utils/filesystem_session.py +39 -0
- pkgs/py.typed +0 -0
- pkgs/serialization/__init__.py +8 -1
- pkgs/serialization/annotation.py +64 -0
- pkgs/serialization/missing_sentry.py +1 -1
- pkgs/serialization/opaque_key.py +1 -1
- pkgs/serialization/serial_alias.py +47 -0
- pkgs/serialization/serial_class.py +69 -54
- pkgs/serialization/serial_generic.py +16 -0
- pkgs/serialization/serial_union.py +84 -0
- pkgs/serialization/yaml.py +57 -0
- pkgs/serialization_util/__init__.py +7 -7
- pkgs/serialization_util/convert_to_snakecase.py +27 -0
- pkgs/serialization_util/dataclasses.py +14 -0
- pkgs/serialization_util/serialization_helpers.py +117 -71
- pkgs/type_spec/actions_registry/__main__.py +0 -4
- pkgs/type_spec/actions_registry/emit_typescript.py +5 -5
- pkgs/type_spec/builder.py +438 -109
- pkgs/type_spec/builder_types.py +9 -0
- pkgs/type_spec/config.py +52 -24
- pkgs/type_spec/cross_output_links.py +99 -0
- pkgs/type_spec/emit_io_ts.py +1 -1
- pkgs/type_spec/emit_open_api.py +160 -41
- pkgs/type_spec/emit_open_api_util.py +13 -7
- pkgs/type_spec/emit_python.py +450 -136
- pkgs/type_spec/emit_typescript.py +117 -250
- pkgs/type_spec/emit_typescript_util.py +293 -4
- pkgs/type_spec/load_types.py +20 -5
- pkgs/type_spec/non_discriminated_union_exceptions.py +14 -0
- pkgs/type_spec/open_api_util.py +29 -4
- pkgs/type_spec/parts/base.py.prepart +13 -10
- pkgs/type_spec/parts/base.ts.prepart +4 -0
- pkgs/type_spec/type_info/__main__.py +3 -1
- pkgs/type_spec/type_info/emit_type_info.py +161 -32
- pkgs/type_spec/ui_entry_actions/__init__.py +4 -0
- pkgs/type_spec/ui_entry_actions/generate_ui_entry_actions.py +308 -0
- pkgs/type_spec/util.py +4 -4
- pkgs/type_spec/value_spec/__main__.py +27 -10
- pkgs/type_spec/value_spec/convert_type.py +21 -1
- pkgs/type_spec/value_spec/emit_python.py +25 -7
- pkgs/type_spec/value_spec/types.py +1 -1
- uncountable/__init__.py +1 -2
- uncountable/core/__init__.py +11 -3
- uncountable/core/async_batch.py +16 -1
- uncountable/core/client.py +247 -52
- uncountable/core/environment.py +41 -0
- uncountable/core/file_upload.py +67 -22
- uncountable/core/types.py +8 -13
- uncountable/integration/cli.py +142 -0
- uncountable/integration/construct_client.py +43 -27
- uncountable/integration/cron.py +12 -11
- uncountable/integration/db/connect.py +12 -2
- uncountable/integration/db/session.py +25 -0
- uncountable/integration/entrypoint.py +4 -34
- uncountable/integration/executors/executors.py +147 -0
- uncountable/integration/executors/generic_upload_executor.py +336 -0
- uncountable/integration/executors/script_executor.py +15 -9
- uncountable/integration/http_server/__init__.py +5 -0
- uncountable/integration/http_server/types.py +69 -0
- uncountable/integration/job.py +246 -19
- uncountable/integration/queue_runner/__init__.py +0 -0
- uncountable/integration/queue_runner/command_server/__init__.py +28 -0
- uncountable/integration/queue_runner/command_server/command_client.py +133 -0
- uncountable/integration/queue_runner/command_server/command_server.py +142 -0
- uncountable/integration/queue_runner/command_server/constants.py +4 -0
- uncountable/integration/queue_runner/command_server/protocol/__init__.py +0 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server.proto +58 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +57 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +114 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2_grpc.py +264 -0
- uncountable/integration/queue_runner/command_server/types.py +75 -0
- uncountable/integration/queue_runner/datastore/__init__.py +3 -0
- uncountable/integration/queue_runner/datastore/datastore_sqlite.py +250 -0
- uncountable/integration/queue_runner/datastore/interface.py +29 -0
- uncountable/integration/queue_runner/datastore/model.py +24 -0
- uncountable/integration/queue_runner/job_scheduler.py +200 -0
- uncountable/integration/queue_runner/queue_runner.py +34 -0
- uncountable/integration/queue_runner/types.py +7 -0
- uncountable/integration/queue_runner/worker.py +116 -0
- uncountable/integration/scan_profiles.py +67 -0
- uncountable/integration/scheduler.py +199 -0
- uncountable/integration/secret_retrieval/__init__.py +3 -0
- uncountable/integration/secret_retrieval/retrieve_secret.py +93 -0
- uncountable/integration/server.py +103 -54
- uncountable/integration/telemetry.py +251 -0
- uncountable/integration/webhook_server/entrypoint.py +97 -0
- uncountable/types/__init__.py +149 -30
- uncountable/types/api/batch/execute_batch.py +16 -9
- uncountable/types/api/batch/execute_batch_load_async.py +13 -7
- uncountable/types/api/chemical/convert_chemical_formats.py +20 -8
- uncountable/types/api/condition_parameters/__init__.py +1 -0
- uncountable/types/api/condition_parameters/upsert_condition_match.py +72 -0
- uncountable/types/api/entity/create_entities.py +24 -12
- uncountable/types/api/entity/create_entity.py +22 -13
- uncountable/types/api/entity/create_or_update_entity.py +48 -0
- uncountable/types/api/entity/export_entities.py +59 -0
- uncountable/types/api/entity/get_entities_data.py +18 -9
- uncountable/types/api/entity/grant_entity_permissions.py +48 -0
- uncountable/types/api/entity/list_aggregate.py +79 -0
- uncountable/types/api/entity/list_entities.py +53 -14
- uncountable/types/api/entity/lock_entity.py +45 -0
- uncountable/types/api/entity/lookup_entity.py +116 -0
- uncountable/types/api/entity/resolve_entity_ids.py +19 -10
- uncountable/types/api/entity/set_entity_field_values.py +44 -0
- uncountable/types/api/entity/set_values.py +15 -8
- uncountable/types/api/entity/transition_entity_phase.py +27 -12
- uncountable/types/api/entity/unlock_entity.py +44 -0
- uncountable/types/api/equipment/__init__.py +1 -0
- uncountable/types/api/equipment/associate_equipment_input.py +43 -0
- uncountable/types/api/field_options/__init__.py +1 -0
- uncountable/types/api/field_options/upsert_field_options.py +55 -0
- uncountable/types/api/files/__init__.py +1 -0
- uncountable/types/api/files/download_file.py +77 -0
- uncountable/types/api/id_source/list_id_source.py +20 -11
- uncountable/types/api/id_source/match_id_source.py +15 -10
- uncountable/types/api/input_groups/get_input_group_names.py +16 -7
- uncountable/types/api/inputs/create_inputs.py +28 -14
- uncountable/types/api/inputs/get_input_data.py +34 -16
- uncountable/types/api/inputs/get_input_names.py +19 -10
- uncountable/types/api/inputs/get_inputs_data.py +29 -11
- uncountable/types/api/inputs/set_input_attribute_values.py +16 -10
- uncountable/types/api/inputs/set_input_category.py +44 -0
- uncountable/types/api/inputs/set_input_subcategories.py +45 -0
- uncountable/types/api/inputs/set_intermediate_type.py +50 -0
- uncountable/types/api/integrations/__init__.py +1 -0
- uncountable/types/api/integrations/publish_realtime_data.py +41 -0
- uncountable/types/api/integrations/push_notification.py +49 -0
- uncountable/types/api/integrations/register_sockets_token.py +41 -0
- uncountable/types/api/listing/__init__.py +1 -0
- uncountable/types/api/listing/fetch_listing.py +58 -0
- uncountable/types/api/material_families/__init__.py +1 -0
- uncountable/types/api/material_families/update_entity_material_families.py +47 -0
- uncountable/types/api/notebooks/__init__.py +1 -0
- uncountable/types/api/notebooks/add_notebook_content.py +119 -0
- uncountable/types/api/outputs/get_output_data.py +32 -17
- uncountable/types/api/outputs/get_output_names.py +18 -9
- uncountable/types/api/outputs/get_output_organization.py +173 -0
- uncountable/types/api/outputs/resolve_output_conditions.py +23 -11
- uncountable/types/api/permissions/set_core_permissions.py +31 -15
- uncountable/types/api/project/get_projects.py +20 -11
- uncountable/types/api/project/get_projects_data.py +23 -14
- uncountable/types/api/recipe_links/create_recipe_link.py +17 -10
- uncountable/types/api/recipe_links/remove_recipe_link.py +45 -0
- uncountable/types/api/recipe_metadata/get_recipe_metadata_data.py +19 -10
- uncountable/types/api/recipes/add_recipe_to_project.py +42 -0
- uncountable/types/api/recipes/add_time_series_data.py +64 -0
- uncountable/types/api/recipes/archive_recipes.py +14 -7
- uncountable/types/api/recipes/associate_recipe_as_input.py +16 -8
- uncountable/types/api/recipes/associate_recipe_as_lot.py +14 -7
- uncountable/types/api/recipes/clear_recipe_outputs.py +42 -0
- uncountable/types/api/recipes/create_mix_order.py +44 -0
- uncountable/types/api/recipes/create_recipe.py +21 -14
- uncountable/types/api/recipes/create_recipes.py +25 -13
- uncountable/types/api/recipes/disassociate_recipe_as_input.py +14 -7
- uncountable/types/api/recipes/edit_recipe_inputs.py +208 -19
- uncountable/types/api/recipes/get_column_calculation_values.py +57 -0
- uncountable/types/api/recipes/get_curve.py +15 -9
- uncountable/types/api/recipes/get_recipe_calculations.py +17 -11
- uncountable/types/api/recipes/get_recipe_links.py +14 -8
- uncountable/types/api/recipes/get_recipe_names.py +16 -7
- uncountable/types/api/recipes/get_recipe_output_metadata.py +16 -10
- uncountable/types/api/recipes/get_recipes_data.py +96 -45
- uncountable/types/api/recipes/lock_recipes.py +64 -0
- uncountable/types/api/recipes/remove_recipe_from_project.py +42 -0
- uncountable/types/api/recipes/set_recipe_inputs.py +19 -13
- uncountable/types/api/recipes/set_recipe_metadata.py +14 -7
- uncountable/types/api/recipes/set_recipe_output_annotations.py +114 -0
- uncountable/types/api/recipes/set_recipe_output_file.py +55 -0
- uncountable/types/api/recipes/set_recipe_outputs.py +40 -15
- uncountable/types/api/recipes/set_recipe_tags.py +30 -13
- uncountable/types/api/recipes/set_recipe_total.py +59 -0
- uncountable/types/api/recipes/unarchive_recipes.py +41 -0
- uncountable/types/api/recipes/unlock_recipes.py +51 -0
- uncountable/types/api/runsheet/__init__.py +1 -0
- uncountable/types/api/runsheet/complete_async_upload.py +41 -0
- uncountable/types/api/triggers/run_trigger.py +15 -8
- uncountable/types/api/uploader/__init__.py +1 -0
- uncountable/types/api/uploader/complete_async_parse.py +46 -0
- uncountable/types/api/uploader/invoke_uploader.py +46 -0
- uncountable/types/api/user/__init__.py +1 -0
- uncountable/types/api/user/get_current_user_info.py +40 -0
- uncountable/types/async_batch.py +8 -52
- uncountable/types/async_batch_processor.py +694 -18
- uncountable/types/async_batch_t.py +108 -0
- uncountable/types/async_jobs.py +8 -0
- uncountable/types/async_jobs_t.py +52 -0
- uncountable/types/auth_retrieval.py +11 -0
- uncountable/types/auth_retrieval_t.py +75 -0
- uncountable/types/base.py +5 -80
- uncountable/types/base_t.py +87 -0
- uncountable/types/calculations.py +3 -19
- uncountable/types/calculations_t.py +26 -0
- uncountable/types/chemical_structure.py +3 -23
- uncountable/types/chemical_structure_t.py +28 -0
- uncountable/types/client_base.py +1170 -88
- uncountable/types/client_config.py +8 -0
- uncountable/types/client_config_t.py +36 -0
- uncountable/types/curves.py +5 -43
- uncountable/types/curves_t.py +50 -0
- uncountable/types/data.py +12 -0
- uncountable/types/data_t.py +103 -0
- uncountable/types/entity.py +8 -270
- uncountable/types/entity_t.py +446 -0
- uncountable/types/experiment_groups.py +3 -19
- uncountable/types/experiment_groups_t.py +26 -0
- uncountable/types/exports.py +8 -0
- uncountable/types/exports_t.py +34 -0
- uncountable/types/field_values.py +25 -61
- uncountable/types/field_values_t.py +302 -0
- uncountable/types/fields.py +3 -20
- uncountable/types/fields_t.py +27 -0
- uncountable/types/generic_upload.py +14 -0
- uncountable/types/generic_upload_t.py +119 -0
- uncountable/types/id_source.py +7 -45
- uncountable/types/id_source_t.py +68 -0
- uncountable/types/identifier.py +6 -50
- uncountable/types/identifier_t.py +62 -0
- uncountable/types/input_attributes.py +3 -25
- uncountable/types/input_attributes_t.py +29 -0
- uncountable/types/inputs.py +6 -57
- uncountable/types/inputs_t.py +82 -0
- uncountable/types/integration_server.py +8 -0
- uncountable/types/integration_server_t.py +46 -0
- uncountable/types/integration_session.py +10 -0
- uncountable/types/integration_session_t.py +60 -0
- uncountable/types/integrations.py +10 -0
- uncountable/types/integrations_t.py +62 -0
- uncountable/types/job_definition.py +28 -0
- uncountable/types/job_definition_t.py +285 -0
- uncountable/types/listing.py +9 -0
- uncountable/types/listing_t.py +51 -0
- uncountable/types/notices.py +8 -0
- uncountable/types/notices_t.py +37 -0
- uncountable/types/notifications.py +11 -0
- uncountable/types/notifications_t.py +74 -0
- uncountable/types/outputs.py +3 -22
- uncountable/types/outputs_t.py +29 -0
- uncountable/types/overrides.py +9 -0
- uncountable/types/overrides_t.py +49 -0
- uncountable/types/permissions.py +3 -42
- uncountable/types/permissions_t.py +45 -0
- uncountable/types/phases.py +3 -19
- uncountable/types/phases_t.py +26 -0
- uncountable/types/post_base.py +3 -26
- uncountable/types/post_base_t.py +29 -0
- uncountable/types/queued_job.py +17 -0
- uncountable/types/queued_job_t.py +140 -0
- uncountable/types/recipe_identifiers.py +7 -58
- uncountable/types/recipe_identifiers_t.py +75 -0
- uncountable/types/recipe_inputs.py +4 -26
- uncountable/types/recipe_inputs_t.py +29 -0
- uncountable/types/recipe_links.py +4 -46
- uncountable/types/recipe_links_t.py +53 -0
- uncountable/types/recipe_metadata.py +5 -48
- uncountable/types/recipe_metadata_t.py +57 -0
- uncountable/types/recipe_output_metadata.py +3 -20
- uncountable/types/recipe_output_metadata_t.py +27 -0
- uncountable/types/recipe_tags.py +3 -19
- uncountable/types/recipe_tags_t.py +26 -0
- uncountable/types/recipe_workflow_steps.py +9 -73
- uncountable/types/recipe_workflow_steps_t.py +95 -0
- uncountable/types/recipes.py +7 -0
- uncountable/types/recipes_t.py +25 -0
- uncountable/types/response.py +3 -21
- uncountable/types/response_t.py +26 -0
- uncountable/types/secret_retrieval.py +11 -0
- uncountable/types/secret_retrieval_t.py +75 -0
- uncountable/types/sockets.py +20 -0
- uncountable/types/sockets_t.py +169 -0
- uncountable/types/structured_filters.py +25 -0
- uncountable/types/structured_filters_t.py +248 -0
- uncountable/types/units.py +3 -19
- uncountable/types/units_t.py +26 -0
- uncountable/types/uploader.py +24 -0
- uncountable/types/uploader_t.py +222 -0
- uncountable/types/users.py +3 -20
- uncountable/types/users_t.py +27 -0
- uncountable/types/webhook_job.py +9 -0
- uncountable/types/webhook_job_t.py +48 -0
- uncountable/types/workflows.py +4 -28
- uncountable/types/workflows_t.py +38 -0
- uncountablepythonsdk-0.0.131.dist-info/METADATA +64 -0
- uncountablepythonsdk-0.0.131.dist-info/RECORD +363 -0
- {UncountablePythonSDK-0.0.24.dist-info → uncountablepythonsdk-0.0.131.dist-info}/WHEEL +1 -1
- {UncountablePythonSDK-0.0.24.dist-info → uncountablepythonsdk-0.0.131.dist-info}/top_level.txt +0 -1
- UncountablePythonSDK-0.0.24.dist-info/METADATA +0 -47
- UncountablePythonSDK-0.0.24.dist-info/RECORD +0 -216
- docs/quickstart.md +0 -19
- examples/recipe-import/importer.py +0 -39
- type_spec/external/api/batch/execute_batch.yaml +0 -56
- type_spec/external/api/batch/execute_batch_load_async.yaml +0 -18
- type_spec/external/api/chemical/convert_chemical_formats.yaml +0 -33
- type_spec/external/api/entity/create_entities.yaml +0 -45
- type_spec/external/api/entity/create_entity.yaml +0 -51
- type_spec/external/api/entity/get_entities_data.yaml +0 -29
- type_spec/external/api/entity/list_entities.yaml +0 -52
- type_spec/external/api/entity/resolve_entity_ids.yaml +0 -29
- type_spec/external/api/entity/set_values.yaml +0 -18
- type_spec/external/api/entity/transition_entity_phase.yaml +0 -44
- type_spec/external/api/id_source/list_id_source.yaml +0 -35
- type_spec/external/api/id_source/match_id_source.yaml +0 -32
- type_spec/external/api/input_groups/get_input_group_names.yaml +0 -29
- type_spec/external/api/inputs/create_inputs.yaml +0 -48
- type_spec/external/api/inputs/get_input_data.yaml +0 -95
- type_spec/external/api/inputs/get_input_names.yaml +0 -38
- type_spec/external/api/inputs/get_inputs_data.yaml +0 -82
- type_spec/external/api/inputs/set_input_attribute_values.yaml +0 -33
- type_spec/external/api/outputs/get_output_data.yaml +0 -92
- type_spec/external/api/outputs/get_output_names.yaml +0 -35
- type_spec/external/api/outputs/resolve_output_conditions.yaml +0 -50
- type_spec/external/api/permissions/set_core_permissions.yaml +0 -69
- type_spec/external/api/project/get_projects.yaml +0 -42
- type_spec/external/api/project/get_projects_data.yaml +0 -50
- type_spec/external/api/recipe_links/create_recipe_link.yaml +0 -25
- type_spec/external/api/recipe_metadata/get_recipe_metadata_data.yaml +0 -41
- type_spec/external/api/recipes/archive_recipes.yaml +0 -20
- type_spec/external/api/recipes/associate_recipe_as_input.yaml +0 -19
- type_spec/external/api/recipes/associate_recipe_as_lot.yaml +0 -19
- type_spec/external/api/recipes/create_recipe.yaml +0 -39
- type_spec/external/api/recipes/create_recipes.yaml +0 -47
- type_spec/external/api/recipes/disassociate_recipe_as_input.yaml +0 -16
- type_spec/external/api/recipes/edit_recipe_inputs.yaml +0 -85
- type_spec/external/api/recipes/get_curve.yaml +0 -21
- type_spec/external/api/recipes/get_recipe_calculations.yaml +0 -39
- type_spec/external/api/recipes/get_recipe_links.yaml +0 -26
- type_spec/external/api/recipes/get_recipe_names.yaml +0 -29
- type_spec/external/api/recipes/get_recipe_output_metadata.yaml +0 -36
- type_spec/external/api/recipes/get_recipes_data.yaml +0 -244
- type_spec/external/api/recipes/set_recipe_inputs.yaml +0 -42
- type_spec/external/api/recipes/set_recipe_metadata.yaml +0 -20
- type_spec/external/api/recipes/set_recipe_outputs.yaml +0 -52
- type_spec/external/api/recipes/set_recipe_tags.yaml +0 -62
- type_spec/external/api/triggers/run_trigger.yaml +0 -18
- uncountable/integration/types.py +0 -89
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from collections.abc import Iterable
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
|
|
5
|
+
import paramiko
|
|
6
|
+
import pysftp
|
|
7
|
+
|
|
8
|
+
from pkgs.filesystem_utils.file_type_utils import (
|
|
9
|
+
FileObjectData,
|
|
10
|
+
FileSystemFileReference,
|
|
11
|
+
FileSystemObject,
|
|
12
|
+
FileSystemSFTPConfig,
|
|
13
|
+
FileTransfer,
|
|
14
|
+
IncompatibleFileReference,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from .filesystem_session import FileSystemSession
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def move_sftp_files(
|
|
21
|
+
connection: pysftp.Connection,
|
|
22
|
+
src_filepath: str,
|
|
23
|
+
dest_filepath: str,
|
|
24
|
+
) -> None:
|
|
25
|
+
connection.rename(src_filepath, dest_filepath)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def list_sftp_files(
|
|
29
|
+
connection: pysftp.Connection,
|
|
30
|
+
dir_path: str,
|
|
31
|
+
*,
|
|
32
|
+
valid_extensions: Iterable[str] | None = None,
|
|
33
|
+
parent_dir_path: str | None = None,
|
|
34
|
+
recursive: bool = True,
|
|
35
|
+
) -> list[str]:
|
|
36
|
+
file_paths: list[str] = []
|
|
37
|
+
if recursive:
|
|
38
|
+
|
|
39
|
+
def _skip(name: str) -> None:
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
def _add_file(path: str) -> None:
|
|
43
|
+
if (
|
|
44
|
+
valid_extensions is None
|
|
45
|
+
or os.path.splitext(path)[1] in valid_extensions
|
|
46
|
+
) and (parent_dir_path is None or os.path.dirname(path) == parent_dir_path):
|
|
47
|
+
file_paths.append(path)
|
|
48
|
+
|
|
49
|
+
connection.walktree(
|
|
50
|
+
dir_path, fcallback=_add_file, dcallback=_skip, ucallback=_skip
|
|
51
|
+
)
|
|
52
|
+
else:
|
|
53
|
+
file_paths.extend([
|
|
54
|
+
os.path.join(dir_path, file)
|
|
55
|
+
for file in connection.listdir(dir_path)
|
|
56
|
+
if connection.isfile(os.path.join(dir_path, file))
|
|
57
|
+
and (
|
|
58
|
+
valid_extensions is None
|
|
59
|
+
or os.path.splitext(file)[1] in valid_extensions
|
|
60
|
+
)
|
|
61
|
+
])
|
|
62
|
+
return file_paths
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class SFTPSession(FileSystemSession):
|
|
66
|
+
def __init__(self, sftp_config: FileSystemSFTPConfig) -> None:
|
|
67
|
+
super().__init__()
|
|
68
|
+
self.host: str = sftp_config.ip
|
|
69
|
+
self.username: str = sftp_config.username
|
|
70
|
+
self.key_file: str | paramiko.RSAKey | None = (
|
|
71
|
+
sftp_config.pem_path
|
|
72
|
+
if sftp_config.pem_path is not None
|
|
73
|
+
else sftp_config.pem_key
|
|
74
|
+
)
|
|
75
|
+
self.password: str | None = sftp_config.password
|
|
76
|
+
|
|
77
|
+
def start(self) -> None:
|
|
78
|
+
cnopts = pysftp.CnOpts()
|
|
79
|
+
cnopts.hostkeys = None
|
|
80
|
+
if self.key_file is not None:
|
|
81
|
+
self.connection = pysftp.Connection(
|
|
82
|
+
self.host,
|
|
83
|
+
username=self.username,
|
|
84
|
+
private_key=self.key_file,
|
|
85
|
+
cnopts=cnopts,
|
|
86
|
+
)
|
|
87
|
+
elif self.password is not None:
|
|
88
|
+
self.connection = pysftp.Connection(
|
|
89
|
+
self.host,
|
|
90
|
+
username=self.username,
|
|
91
|
+
password=self.password,
|
|
92
|
+
cnopts=cnopts,
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
raise pysftp.CredentialException(
|
|
96
|
+
"Must specify either a private key path or a password."
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def __enter__(self) -> "SFTPSession":
|
|
100
|
+
self.start()
|
|
101
|
+
return self
|
|
102
|
+
|
|
103
|
+
def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None:
|
|
104
|
+
self.connection.close()
|
|
105
|
+
|
|
106
|
+
def list_files(
|
|
107
|
+
self,
|
|
108
|
+
dir_path: FileSystemObject,
|
|
109
|
+
*,
|
|
110
|
+
recursive: bool = True,
|
|
111
|
+
valid_extensions: list[str] | None = None,
|
|
112
|
+
) -> list[FileSystemObject]:
|
|
113
|
+
if not isinstance(
|
|
114
|
+
dir_path, FileSystemFileReference
|
|
115
|
+
) or not self.connection.isdir(dir_path.filepath):
|
|
116
|
+
raise IncompatibleFileReference()
|
|
117
|
+
|
|
118
|
+
return [
|
|
119
|
+
FileSystemFileReference(file_path)
|
|
120
|
+
for file_path in list_sftp_files(
|
|
121
|
+
self.connection,
|
|
122
|
+
dir_path.filepath,
|
|
123
|
+
recursive=recursive,
|
|
124
|
+
valid_extensions=valid_extensions,
|
|
125
|
+
)
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
def download_files(self, filepaths: list[FileSystemObject]) -> list[FileObjectData]:
|
|
129
|
+
downloaded_files: list[FileObjectData] = []
|
|
130
|
+
for file_object in filepaths:
|
|
131
|
+
if (
|
|
132
|
+
not isinstance(file_object, FileSystemFileReference)
|
|
133
|
+
or file_object.filename is None
|
|
134
|
+
):
|
|
135
|
+
raise IncompatibleFileReference()
|
|
136
|
+
filepath = file_object.filepath
|
|
137
|
+
file_data = self.connection.open(filepath).read()
|
|
138
|
+
downloaded_file = FileObjectData(
|
|
139
|
+
file_data, BytesIO(file_data), file_object.filename, filepath=filepath
|
|
140
|
+
)
|
|
141
|
+
if downloaded_file is not None:
|
|
142
|
+
downloaded_files.append(downloaded_file)
|
|
143
|
+
return downloaded_files
|
|
144
|
+
|
|
145
|
+
def move_files(self, file_mappings: list[FileTransfer]) -> None:
|
|
146
|
+
for src_file, dest_file in file_mappings:
|
|
147
|
+
if not isinstance(src_file, FileSystemFileReference) or not isinstance(
|
|
148
|
+
dest_file, FileSystemFileReference
|
|
149
|
+
):
|
|
150
|
+
raise IncompatibleFileReference()
|
|
151
|
+
move_sftp_files(self.connection, src_file.filepath, dest_file.filepath)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
from typing import Union
|
|
5
|
+
|
|
6
|
+
import paramiko
|
|
7
|
+
from azure.core.credentials import (
|
|
8
|
+
AzureNamedKeyCredential,
|
|
9
|
+
AzureSasCredential,
|
|
10
|
+
TokenCredential,
|
|
11
|
+
)
|
|
12
|
+
from azure.storage.blob import ContainerProperties
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class FileObjectData:
|
|
17
|
+
file_data: bytes
|
|
18
|
+
file_IO: BytesIO
|
|
19
|
+
filename: str
|
|
20
|
+
filepath: str | None = None
|
|
21
|
+
mime_type: str | None = None
|
|
22
|
+
metadata: dict[str, str] | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class FileSystemFileReference:
|
|
27
|
+
filepath: str
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def filename(self) -> str:
|
|
31
|
+
return os.path.basename(self.filepath)
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def dirname(self) -> str:
|
|
35
|
+
return os.path.dirname(self.filepath)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class RemoteObjectReference:
|
|
40
|
+
file_id: str
|
|
41
|
+
mime_type: str
|
|
42
|
+
filename: str | None = None
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def is_dir(self) -> bool:
|
|
46
|
+
return "folder" in self.mime_type
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
FileSystemObject = Union[FileSystemFileReference, RemoteObjectReference, FileObjectData]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
FileTransfer = tuple[FileSystemObject, FileSystemObject]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class IncompatibleFileReference(Exception):
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True, kw_only=True)
|
|
60
|
+
class FileSystemSFTPConfig:
|
|
61
|
+
ip: str
|
|
62
|
+
username: str
|
|
63
|
+
pem_path: str | None
|
|
64
|
+
pem_key: paramiko.RSAKey | None = None
|
|
65
|
+
password: str | None = None
|
|
66
|
+
valid_extensions: tuple[str] | None = None
|
|
67
|
+
recursive: bool = True
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(kw_only=True)
|
|
71
|
+
class FileSystemS3Config:
|
|
72
|
+
endpoint_url: str
|
|
73
|
+
bucket_name: str
|
|
74
|
+
region_name: str | None
|
|
75
|
+
access_key_id: str | None
|
|
76
|
+
secret_access_key: str | None
|
|
77
|
+
session_token: str | None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(kw_only=True)
|
|
81
|
+
class FileSystemBlobConfig:
|
|
82
|
+
account_url: str
|
|
83
|
+
credential: (
|
|
84
|
+
str
|
|
85
|
+
| dict[str, str]
|
|
86
|
+
| AzureNamedKeyCredential
|
|
87
|
+
| AzureSasCredential
|
|
88
|
+
| TokenCredential
|
|
89
|
+
| None
|
|
90
|
+
)
|
|
91
|
+
container: ContainerProperties | str
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
from pkgs.filesystem_utils.file_type_utils import (
|
|
4
|
+
FileObjectData,
|
|
5
|
+
FileSystemObject,
|
|
6
|
+
FileTransfer,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FileSystemSession(ABC):
|
|
11
|
+
def __init__(self) -> None:
|
|
12
|
+
return
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def start(self) -> None:
|
|
16
|
+
raise NotImplementedError
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def list_files(
|
|
20
|
+
self, dir_path: FileSystemObject, *, recursive: bool = True
|
|
21
|
+
) -> list[FileSystemObject]:
|
|
22
|
+
raise NotImplementedError
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def move_files(self, file_mappings: list[FileTransfer]) -> None:
|
|
26
|
+
raise NotImplementedError
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def download_files(self, filepaths: list[FileSystemObject]) -> list[FileObjectData]:
|
|
30
|
+
raise NotImplementedError
|
|
31
|
+
|
|
32
|
+
def delete_files(self, filepaths: list[FileSystemObject]) -> None:
|
|
33
|
+
raise NotImplementedError
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def __enter__(self) -> "FileSystemSession": ...
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None: ...
|
pkgs/py.typed
ADDED
|
File without changes
|
pkgs/serialization/__init__.py
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
from .annotation import unwrap_annotated as unwrap_annotated
|
|
2
2
|
from .missing_sentry import MISSING_SENTRY as MISSING_SENTRY
|
|
3
3
|
from .missing_sentry import MissingSentryType as MissingSentryType
|
|
4
4
|
from .missing_sentry import MissingType as MissingType
|
|
5
5
|
from .missing_sentry import coalesce_missing_sentry as coalesce_missing_sentry
|
|
6
6
|
from .opaque_key import OpaqueKey as OpaqueKey
|
|
7
|
+
from .serial_alias import SerialAliasInspector as SerialAliasInspector
|
|
8
|
+
from .serial_alias import get_serial_alias_data as get_serial_alias_data
|
|
9
|
+
from .serial_alias import serial_alias_annotation as serial_alias_annotation
|
|
10
|
+
from .serial_class import SerialClassDataInspector as SerialClassDataInspector
|
|
7
11
|
from .serial_class import get_serial_class_data as get_serial_class_data
|
|
8
12
|
from .serial_class import get_serial_string_enum_data as get_serial_string_enum_data
|
|
9
13
|
from .serial_class import serial_class as serial_class
|
|
10
14
|
from .serial_class import serial_string_enum as serial_string_enum
|
|
15
|
+
from .serial_generic import get_serial_data as get_serial_data
|
|
16
|
+
from .serial_union import get_serial_union_data as get_serial_union_data
|
|
17
|
+
from .serial_union import serial_union_annotation as serial_union_annotation
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
T = typing.TypeVar("T")
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclasses.dataclass(kw_only=True, frozen=True, eq=True)
|
|
8
|
+
class SerialBase:
|
|
9
|
+
named_type_path: str | None = None
|
|
10
|
+
# Indicates this type is allowed in dynamic lookups, such as via a named_type_path
|
|
11
|
+
# This isn't meant to be limting, but to catalog all the types where we need it
|
|
12
|
+
is_dynamic_allowed: bool = False
|
|
13
|
+
# Tracks if this data was provided as a decorator to the type.
|
|
14
|
+
# This is used to track "proper types" which are appropriate
|
|
15
|
+
# for serialization and/or dynamic discovery
|
|
16
|
+
from_decorator: bool = False
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_serial_annotation(parsed_type: type[T]) -> SerialBase | None:
|
|
20
|
+
if not hasattr(parsed_type, "__metadata__"):
|
|
21
|
+
return None
|
|
22
|
+
metadata = parsed_type.__metadata__ # type:ignore[attr-defined]
|
|
23
|
+
if not isinstance(metadata, tuple) or len(metadata) != 1:
|
|
24
|
+
return None
|
|
25
|
+
serial = metadata[0]
|
|
26
|
+
if not isinstance(serial, SerialBase):
|
|
27
|
+
return None
|
|
28
|
+
return serial
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SerialInspector(typing.Generic[T]):
|
|
32
|
+
def __init__(self, parsed_type: type[T], serial_base: SerialBase) -> None:
|
|
33
|
+
self._parsed_type = parsed_type
|
|
34
|
+
self._serial_base = serial_base
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def named_type_path(self) -> str | None:
|
|
38
|
+
return self._serial_base.named_type_path
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def from_decorator(self) -> bool:
|
|
42
|
+
return self._serial_base.from_decorator
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def is_field_proper(self) -> bool:
|
|
46
|
+
return (
|
|
47
|
+
self._serial_base.from_decorator
|
|
48
|
+
and self._serial_base.named_type_path is not None
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def is_dynamic_allowed(self) -> bool:
|
|
53
|
+
return self._serial_base.is_dynamic_allowed
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def unwrap_annotated(parsed_type: type[T]) -> type[T]:
|
|
57
|
+
"""
|
|
58
|
+
If the type is an annotated type then return the origin of it.
|
|
59
|
+
Otherwise return the original type.
|
|
60
|
+
"""
|
|
61
|
+
if typing.get_origin(parsed_type) is typing.Annotated:
|
|
62
|
+
# It's unclear if there's anyway to type this correctly
|
|
63
|
+
return parsed_type.__origin__ # type:ignore[attr-defined, no-any-return]
|
|
64
|
+
return parsed_type
|
|
@@ -26,5 +26,5 @@ MISSING_SENTRY = MissingSentryType()
|
|
|
26
26
|
MissingType = Union[MissingSentryType, ClassT]
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
def coalesce_missing_sentry(value: MissingType[ClassT]) ->
|
|
29
|
+
def coalesce_missing_sentry(value: MissingType[ClassT]) -> ClassT | None:
|
|
30
30
|
return None if isinstance(value, MissingSentryType) else value
|
pkgs/serialization/opaque_key.py
CHANGED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
from .annotation import SerialBase, SerialInspector, get_serial_annotation
|
|
5
|
+
|
|
6
|
+
T = typing.TypeVar("T")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclasses.dataclass(kw_only=True, frozen=True, eq=True)
|
|
10
|
+
class _SerialAlias(SerialBase):
|
|
11
|
+
"""
|
|
12
|
+
This class is to be kept private, to provide flexibility in registration/lookup.
|
|
13
|
+
Places that need the data should access it via help classes/methods.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def serial_alias_annotation(
|
|
18
|
+
*,
|
|
19
|
+
named_type_path: str | None = None,
|
|
20
|
+
is_dynamic_allowed: bool = False,
|
|
21
|
+
) -> _SerialAlias:
|
|
22
|
+
return _SerialAlias(
|
|
23
|
+
named_type_path=named_type_path,
|
|
24
|
+
from_decorator=True,
|
|
25
|
+
is_dynamic_allowed=is_dynamic_allowed,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _get_serial_alias(parsed_type: type[T]) -> _SerialAlias | None:
|
|
30
|
+
serial = get_serial_annotation(parsed_type)
|
|
31
|
+
if not isinstance(serial, _SerialAlias):
|
|
32
|
+
return None
|
|
33
|
+
return serial
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SerialAliasInspector(SerialInspector[T]):
|
|
37
|
+
def __init__(self, parsed_type: type[T], serial_alias: _SerialAlias) -> None:
|
|
38
|
+
super().__init__(parsed_type, serial_alias)
|
|
39
|
+
self._serial_alias = serial_alias
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_serial_alias_data(parsed_type: type[T]) -> SerialAliasInspector[T] | None:
|
|
43
|
+
serial = _get_serial_alias(parsed_type)
|
|
44
|
+
if serial is None:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
return SerialAliasInspector(parsed_type, serial)
|
|
@@ -3,26 +3,34 @@ from __future__ import annotations
|
|
|
3
3
|
import dataclasses
|
|
4
4
|
from collections.abc import Callable
|
|
5
5
|
from enum import StrEnum
|
|
6
|
-
from typing import Any,
|
|
6
|
+
from typing import Any, TypeVar, cast
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
from .annotation import SerialBase, SerialInspector
|
|
9
9
|
|
|
10
|
+
ClassT = TypeVar("ClassT")
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
|
|
13
|
+
@dataclasses.dataclass(kw_only=True, frozen=True, eq=True)
|
|
14
|
+
class _SerialClassData(SerialBase):
|
|
13
15
|
unconverted_keys: set[str] = dataclasses.field(default_factory=set)
|
|
14
16
|
unconverted_values: set[str] = dataclasses.field(default_factory=set)
|
|
15
17
|
to_string_values: set[str] = dataclasses.field(default_factory=set)
|
|
16
18
|
parse_require: set[str] = dataclasses.field(default_factory=set)
|
|
19
|
+
named_type_path: str | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
EMPTY_SERIAL_CLASS_DATA = _SerialClassData()
|
|
17
23
|
|
|
18
24
|
|
|
19
25
|
def serial_class(
|
|
20
26
|
*,
|
|
21
|
-
unconverted_keys:
|
|
22
|
-
unconverted_values:
|
|
23
|
-
to_string_values:
|
|
24
|
-
parse_require:
|
|
25
|
-
|
|
27
|
+
unconverted_keys: set[str] | None = None,
|
|
28
|
+
unconverted_values: set[str] | None = None,
|
|
29
|
+
to_string_values: set[str] | None = None,
|
|
30
|
+
parse_require: set[str] | None = None,
|
|
31
|
+
named_type_path: str | None = None,
|
|
32
|
+
is_dynamic_allowed: bool = False,
|
|
33
|
+
) -> Callable[[ClassT], ClassT]:
|
|
26
34
|
"""
|
|
27
35
|
An additional decorator to a dataclass that specifies serialization options.
|
|
28
36
|
|
|
@@ -42,73 +50,80 @@ def serial_class(
|
|
|
42
50
|
This field is always required while parsing, even if it has a default in the definition.
|
|
43
51
|
This allows supporting literal type defaults for Python instantiation, but
|
|
44
52
|
requiring them for the API input.
|
|
53
|
+
@param named_type_path
|
|
54
|
+
The type_spec type-path to this type. This applies only to named types.
|
|
45
55
|
"""
|
|
46
56
|
|
|
47
|
-
def decorate(orig_class:
|
|
57
|
+
def decorate(orig_class: ClassT) -> ClassT:
|
|
48
58
|
cast(Any, orig_class).__unc_serial_data = _SerialClassData(
|
|
49
59
|
unconverted_keys=unconverted_keys or set(),
|
|
50
60
|
unconverted_values=unconverted_values or set(),
|
|
51
61
|
to_string_values=to_string_values or set(),
|
|
52
62
|
parse_require=parse_require or set(),
|
|
63
|
+
named_type_path=named_type_path,
|
|
64
|
+
from_decorator=True,
|
|
65
|
+
is_dynamic_allowed=is_dynamic_allowed,
|
|
53
66
|
)
|
|
54
67
|
return orig_class
|
|
55
68
|
|
|
56
69
|
return decorate
|
|
57
70
|
|
|
58
71
|
|
|
59
|
-
class SerialClassDataInspector:
|
|
60
|
-
bases: list[SerialClassDataInspector]
|
|
61
|
-
|
|
72
|
+
class SerialClassDataInspector(SerialInspector[ClassT]):
|
|
62
73
|
def __init__(
|
|
63
|
-
self,
|
|
74
|
+
self,
|
|
75
|
+
parsed_type: type[ClassT],
|
|
76
|
+
current: _SerialClassData,
|
|
64
77
|
) -> None:
|
|
65
|
-
|
|
78
|
+
super().__init__(parsed_type, current)
|
|
66
79
|
self.current = current
|
|
67
80
|
|
|
68
81
|
def has_unconverted_key(self, key: str) -> bool:
|
|
69
|
-
|
|
70
|
-
return True
|
|
71
|
-
for base in self.bases:
|
|
72
|
-
if base.has_unconverted_key(key):
|
|
73
|
-
return True
|
|
74
|
-
return False
|
|
82
|
+
return key in self.current.unconverted_keys
|
|
75
83
|
|
|
76
84
|
def has_unconverted_value(self, key: str) -> bool:
|
|
77
|
-
|
|
78
|
-
return True
|
|
79
|
-
for base in self.bases:
|
|
80
|
-
if base.has_unconverted_value(key):
|
|
81
|
-
return True
|
|
82
|
-
return False
|
|
85
|
+
return key in self.current.unconverted_values
|
|
83
86
|
|
|
84
87
|
def has_to_string_value(self, key: str) -> bool:
|
|
85
|
-
|
|
86
|
-
return True
|
|
87
|
-
for base in self.bases:
|
|
88
|
-
if base.has_to_string_value(key):
|
|
89
|
-
return True
|
|
90
|
-
return False
|
|
88
|
+
return key in self.current.to_string_values
|
|
91
89
|
|
|
92
90
|
def has_parse_require(self, key: str) -> bool:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return False
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def get_serial_class_data(type_class: type[Any]) -> SerialClassDataInspector:
|
|
102
|
-
bases = (
|
|
103
|
-
[get_serial_class_data(base) for base in type_class.__bases__]
|
|
104
|
-
if type_class.__bases__ is not None
|
|
105
|
-
else []
|
|
106
|
-
)
|
|
107
|
-
return SerialClassDataInspector(
|
|
108
|
-
bases,
|
|
91
|
+
return key in self.current.parse_require
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_merged_serial_class_data(type_class: type[Any]) -> _SerialClassData | None:
|
|
95
|
+
base_class_data = (
|
|
109
96
|
cast(_SerialClassData, type_class.__unc_serial_data)
|
|
110
97
|
if hasattr(type_class, "__unc_serial_data")
|
|
111
|
-
else
|
|
98
|
+
else None
|
|
99
|
+
)
|
|
100
|
+
if base_class_data is None:
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
# IMPROVE: We should cache this result on the type
|
|
104
|
+
if type_class.__bases__ is not None:
|
|
105
|
+
for base in type_class.__bases__:
|
|
106
|
+
curr_base_class_data = get_merged_serial_class_data(base)
|
|
107
|
+
if curr_base_class_data is not None:
|
|
108
|
+
base_class_data = dataclasses.replace(
|
|
109
|
+
base_class_data,
|
|
110
|
+
unconverted_keys=base_class_data.unconverted_keys
|
|
111
|
+
| curr_base_class_data.unconverted_keys,
|
|
112
|
+
unconverted_values=base_class_data.unconverted_values
|
|
113
|
+
| curr_base_class_data.unconverted_values,
|
|
114
|
+
to_string_values=base_class_data.to_string_values
|
|
115
|
+
| curr_base_class_data.to_string_values,
|
|
116
|
+
parse_require=base_class_data.parse_require
|
|
117
|
+
| curr_base_class_data.parse_require,
|
|
118
|
+
)
|
|
119
|
+
return base_class_data
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def get_serial_class_data(
|
|
123
|
+
type_class: type[ClassT],
|
|
124
|
+
) -> SerialClassDataInspector[ClassT]:
|
|
125
|
+
return SerialClassDataInspector(
|
|
126
|
+
type_class, get_merged_serial_class_data(type_class) or EMPTY_SERIAL_CLASS_DATA
|
|
112
127
|
)
|
|
113
128
|
|
|
114
129
|
|
|
@@ -119,13 +134,13 @@ class _SerialStringEnumData:
|
|
|
119
134
|
|
|
120
135
|
|
|
121
136
|
def serial_string_enum(
|
|
122
|
-
*, labels:
|
|
123
|
-
) -> Callable[[
|
|
137
|
+
*, labels: dict[str, str] | None = None, deprecated: set[str] | None = None
|
|
138
|
+
) -> Callable[[ClassT], ClassT]:
|
|
124
139
|
"""
|
|
125
140
|
A decorator for enums to provide serialization data, including labels.
|
|
126
141
|
"""
|
|
127
142
|
|
|
128
|
-
def decorate(orig_class:
|
|
143
|
+
def decorate(orig_class: ClassT) -> ClassT:
|
|
129
144
|
cast(Any, orig_class).__unc_serial_string_enum_data = _SerialStringEnumData(
|
|
130
145
|
labels=labels or {}, deprecated=deprecated or set()
|
|
131
146
|
)
|
|
@@ -138,7 +153,7 @@ class SerialStringEnumInspector:
|
|
|
138
153
|
def __init__(self, current: _SerialStringEnumData) -> None:
|
|
139
154
|
self.current = current
|
|
140
155
|
|
|
141
|
-
def get_label(self, value: str) ->
|
|
156
|
+
def get_label(self, value: str) -> str | None:
|
|
142
157
|
return self.current.labels.get(value)
|
|
143
158
|
|
|
144
159
|
def get_deprecated(self, value: str) -> bool:
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
from .annotation import SerialInspector, get_serial_annotation
|
|
4
|
+
from .serial_class import get_merged_serial_class_data
|
|
5
|
+
|
|
6
|
+
T = typing.TypeVar("T")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_serial_data(parsed_type: type[T]) -> SerialInspector[T] | None:
|
|
10
|
+
serial = get_serial_annotation(parsed_type)
|
|
11
|
+
if serial is None:
|
|
12
|
+
serial = get_merged_serial_class_data(parsed_type)
|
|
13
|
+
|
|
14
|
+
if serial is not None:
|
|
15
|
+
return SerialInspector(parsed_type, serial)
|
|
16
|
+
return None
|