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,26 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from uncountable.core import AuthDetailsApiKey, Client, MediaFileUpload
|
|
4
|
+
from uncountable.types import generic_upload_t
|
|
5
|
+
from uncountable.types.identifier_t import IdentifierKeyId
|
|
6
|
+
|
|
7
|
+
client = Client(
|
|
8
|
+
base_url="http://localhost:5000",
|
|
9
|
+
auth_details=AuthDetailsApiKey(
|
|
10
|
+
api_id=os.environ["UNC_API_ID"],
|
|
11
|
+
api_secret_key=os.environ["UNC_API_SECRET_KEY"],
|
|
12
|
+
),
|
|
13
|
+
)
|
|
14
|
+
uploaded_file = client.upload_files(
|
|
15
|
+
file_uploads=[
|
|
16
|
+
MediaFileUpload(path="~/Downloads/my_file_to_upload.csv"),
|
|
17
|
+
]
|
|
18
|
+
)[0]
|
|
19
|
+
|
|
20
|
+
client.invoke_uploader(
|
|
21
|
+
file_id=uploaded_file.file_id,
|
|
22
|
+
uploader_key=IdentifierKeyId(id=48),
|
|
23
|
+
destination=generic_upload_t.UploadDestinationMaterialFamily(
|
|
24
|
+
material_family_key=IdentifierKeyId(id=7)
|
|
25
|
+
),
|
|
26
|
+
)
|
examples/oauth.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from uncountable.core import (
|
|
4
|
+
AsyncBatchProcessor,
|
|
5
|
+
AuthDetailsApiKey,
|
|
6
|
+
Client,
|
|
7
|
+
MediaFileUpload,
|
|
8
|
+
)
|
|
9
|
+
from uncountable.types import recipe_metadata_t
|
|
10
|
+
from uncountable.types.identifier_t import IdentifierKeyBatchReference
|
|
11
|
+
|
|
12
|
+
client = Client(
|
|
13
|
+
base_url="http://localhost:5000",
|
|
14
|
+
auth_details=AuthDetailsApiKey(
|
|
15
|
+
api_id=os.environ["UNC_API_ID"],
|
|
16
|
+
api_secret_key=os.environ["UNC_API_SECRET_KEY"],
|
|
17
|
+
),
|
|
18
|
+
)
|
|
19
|
+
uploaded_file = client.upload_files(
|
|
20
|
+
file_uploads=[
|
|
21
|
+
MediaFileUpload(path="Downloads/my_file_to_upload.csv"),
|
|
22
|
+
]
|
|
23
|
+
)[0]
|
|
24
|
+
|
|
25
|
+
batch_processor = AsyncBatchProcessor(client=client)
|
|
26
|
+
|
|
27
|
+
recipe_batch_identifier = batch_processor.create_recipe(
|
|
28
|
+
material_family_id=1, workflow_id=1
|
|
29
|
+
).batch_reference
|
|
30
|
+
|
|
31
|
+
batch_processor.set_recipe_metadata(
|
|
32
|
+
recipe_key=IdentifierKeyBatchReference(reference=recipe_batch_identifier),
|
|
33
|
+
recipe_metadata=[
|
|
34
|
+
recipe_metadata_t.MetadataValue(
|
|
35
|
+
metadata_id=102, value_file_ids=[uploaded_file.file_id]
|
|
36
|
+
)
|
|
37
|
+
],
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
batch_processor.send()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import uncountable.types.api.recipes.set_recipe_output_file as set_recipe_output_file_t
|
|
5
|
+
from uncountable.core import AuthDetailsApiKey, Client, MediaFileUpload
|
|
6
|
+
|
|
7
|
+
client = Client(
|
|
8
|
+
base_url="http://localhost:5000",
|
|
9
|
+
auth_details=AuthDetailsApiKey(
|
|
10
|
+
api_id=os.environ["UNC_API_ID"],
|
|
11
|
+
api_secret_key=os.environ["UNC_API_SECRET_KEY"],
|
|
12
|
+
),
|
|
13
|
+
)
|
|
14
|
+
uploaded_file = client.upload_files(
|
|
15
|
+
file_uploads=[
|
|
16
|
+
MediaFileUpload(
|
|
17
|
+
path=str((Path.home() / "Downloads" / "my_file_to_upload.csv").absolute())
|
|
18
|
+
),
|
|
19
|
+
]
|
|
20
|
+
)[0]
|
|
21
|
+
|
|
22
|
+
client.set_recipe_output_file(
|
|
23
|
+
output_file_data=set_recipe_output_file_t.RecipeOutputFileValue(
|
|
24
|
+
recipe_id=58070, output_id=148, file_id=uploaded_file.file_id, experiment_num=1
|
|
25
|
+
)
|
|
26
|
+
)
|
examples/upload_files.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from pprint import pprint
|
|
3
3
|
|
|
4
|
-
from uncountable import AuthDetailsApiKey, Client, MediaFileUpload
|
|
4
|
+
from uncountable.core import AuthDetailsApiKey, Client, MediaFileUpload
|
|
5
5
|
|
|
6
6
|
client = Client(
|
|
7
7
|
base_url="http://localhost:5000",
|
|
@@ -12,8 +12,7 @@ client = Client(
|
|
|
12
12
|
)
|
|
13
13
|
uploaded = client.upload_files(
|
|
14
14
|
file_uploads=[
|
|
15
|
-
MediaFileUpload(path="/
|
|
16
|
-
MediaFileUpload(path="/my/file/path2.doc"),
|
|
15
|
+
MediaFileUpload(path="Downloads/file"),
|
|
17
16
|
]
|
|
18
17
|
)
|
|
19
18
|
pprint(uploaded)
|
pkgs/argument_parser/__init__.py
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
|
+
from ._is_enum import is_string_enum_class as is_string_enum_class
|
|
1
2
|
from .argument_parser import CachedParser as CachedParser
|
|
3
|
+
from .argument_parser import ParserBase as ParserBase
|
|
4
|
+
from .argument_parser import ParserError as ParserError
|
|
5
|
+
from .argument_parser import ParserExtraFieldsError as ParserExtraFieldsError
|
|
6
|
+
from .argument_parser import ParserFunction as ParserFunction
|
|
2
7
|
from .argument_parser import ParserOptions as ParserOptions
|
|
8
|
+
from .argument_parser import SourceEncoding as SourceEncoding
|
|
3
9
|
from .argument_parser import build_parser as build_parser
|
|
10
|
+
from .argument_parser import is_missing as is_missing
|
|
11
|
+
from .argument_parser import is_optional as is_optional
|
|
12
|
+
from .argument_parser import is_union as is_union
|
|
4
13
|
from .case_convert import camel_to_snake_case as camel_to_snake_case
|
|
5
14
|
from .case_convert import kebab_to_pascal_case as kebab_to_pascal_case
|
|
6
15
|
from .case_convert import snake_to_camel_case as snake_to_camel_case
|
|
@@ -1,16 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import dataclasses
|
|
4
|
+
import datetime
|
|
5
|
+
import math
|
|
2
6
|
import types
|
|
3
7
|
import typing
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
4
9
|
from collections import defaultdict
|
|
5
|
-
from
|
|
6
|
-
from datetime import date, datetime
|
|
10
|
+
from datetime import date
|
|
7
11
|
from decimal import Decimal
|
|
12
|
+
from enum import Enum, auto
|
|
8
13
|
from importlib import resources
|
|
9
14
|
|
|
10
15
|
import dateutil.parser
|
|
11
|
-
import yaml
|
|
16
|
+
import msgspec.yaml
|
|
12
17
|
|
|
13
|
-
from pkgs.serialization import
|
|
18
|
+
from pkgs.serialization import (
|
|
19
|
+
MissingSentryType,
|
|
20
|
+
OpaqueKey,
|
|
21
|
+
get_serial_class_data,
|
|
22
|
+
get_serial_union_data,
|
|
23
|
+
)
|
|
14
24
|
|
|
15
25
|
from ._is_enum import is_string_enum_class
|
|
16
26
|
from ._is_namedtuple import is_namedtuple_type
|
|
@@ -21,31 +31,91 @@ ParserFunction = typing.Callable[[typing.Any], T]
|
|
|
21
31
|
ParserCache = dict[type[typing.Any], ParserFunction[typing.Any]]
|
|
22
32
|
|
|
23
33
|
|
|
24
|
-
|
|
34
|
+
class SourceEncoding(Enum):
|
|
35
|
+
API = auto()
|
|
36
|
+
STORAGE = auto()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclasses.dataclass(frozen=True, eq=True)
|
|
25
40
|
class ParserOptions:
|
|
26
|
-
|
|
41
|
+
encoding: SourceEncoding
|
|
42
|
+
strict_property_parsing: bool = False
|
|
27
43
|
|
|
44
|
+
@staticmethod
|
|
45
|
+
def Api(*, strict_property_parsing: bool = False) -> ParserOptions:
|
|
46
|
+
return ParserOptions(
|
|
47
|
+
encoding=SourceEncoding.API, strict_property_parsing=strict_property_parsing
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def Storage(*, strict_property_parsing: bool = False) -> ParserOptions:
|
|
52
|
+
return ParserOptions(
|
|
53
|
+
encoding=SourceEncoding.STORAGE,
|
|
54
|
+
strict_property_parsing=strict_property_parsing,
|
|
55
|
+
)
|
|
28
56
|
|
|
29
|
-
@
|
|
57
|
+
@property
|
|
58
|
+
def from_camel_case(self) -> bool:
|
|
59
|
+
return self.encoding == SourceEncoding.API
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def allow_direct_type(self) -> bool:
|
|
63
|
+
"""This allows parsing from a DB column without having to check whether it's
|
|
64
|
+
the native format of the type, a JSON column, or a string encoding."""
|
|
65
|
+
return self.encoding == SourceEncoding.STORAGE
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclasses.dataclass(frozen=True)
|
|
30
69
|
class ParserContext:
|
|
31
70
|
options: ParserOptions
|
|
32
71
|
cache: ParserCache
|
|
33
72
|
|
|
34
73
|
|
|
74
|
+
class ParserError(BaseException): ...
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ParserExtraFieldsError(ParserError):
|
|
78
|
+
extra_fields: set[str]
|
|
79
|
+
|
|
80
|
+
def __init__(self, extra_fields: set[str]) -> None:
|
|
81
|
+
self.extra_fields = extra_fields
|
|
82
|
+
|
|
83
|
+
def __str__(self) -> str:
|
|
84
|
+
return f"extra fields were provided: {', '.join(self.extra_fields)}"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def is_union(field_type: typing.Any) -> bool:
|
|
88
|
+
origin = typing.get_origin(field_type)
|
|
89
|
+
return origin is typing.Union or origin is types.UnionType
|
|
90
|
+
|
|
91
|
+
|
|
35
92
|
def is_optional(field_type: typing.Any) -> bool:
|
|
36
|
-
return
|
|
37
|
-
None
|
|
38
|
-
) in typing.get_args(field_type)
|
|
93
|
+
return is_union(field_type) and type(None) in typing.get_args(field_type)
|
|
39
94
|
|
|
40
95
|
|
|
41
96
|
def is_missing(field_type: typing.Any) -> bool:
|
|
42
|
-
|
|
43
|
-
if origin is not typing.Union:
|
|
97
|
+
if not is_union(field_type):
|
|
44
98
|
return False
|
|
45
99
|
args = typing.get_args(field_type)
|
|
46
100
|
return not (len(args) == 0 or args[0] is not MissingSentryType)
|
|
47
101
|
|
|
48
102
|
|
|
103
|
+
def _has_field_default(field: dataclasses.Field[typing.Any]) -> bool:
|
|
104
|
+
return (
|
|
105
|
+
field.default != dataclasses.MISSING
|
|
106
|
+
and not isinstance(field.default, MissingSentryType)
|
|
107
|
+
) or field.default_factory != dataclasses.MISSING
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _get_field_default(
|
|
111
|
+
field: dataclasses.Field[typing.Any],
|
|
112
|
+
) -> typing.Any:
|
|
113
|
+
if field.default != dataclasses.MISSING:
|
|
114
|
+
return field.default
|
|
115
|
+
assert field.default_factory != dataclasses.MISSING
|
|
116
|
+
return field.default_factory()
|
|
117
|
+
|
|
118
|
+
|
|
49
119
|
def _invoke_tuple_parsers(
|
|
50
120
|
tuple_type: type[T],
|
|
51
121
|
arg_parsers: typing.Sequence[typing.Callable[[typing.Any], object]],
|
|
@@ -98,26 +168,79 @@ def _invoke_membership_parser(
|
|
|
98
168
|
raise ValueError(f"Expected value from {expected_values} but got value {value}")
|
|
99
169
|
|
|
100
170
|
|
|
171
|
+
# Uses `is` to compare
|
|
172
|
+
def _build_identity_parser(
|
|
173
|
+
identity_value: T,
|
|
174
|
+
) -> ParserFunction[T]:
|
|
175
|
+
def parse(value: typing.Any) -> T:
|
|
176
|
+
if value is identity_value:
|
|
177
|
+
return identity_value
|
|
178
|
+
raise ValueError(
|
|
179
|
+
f"Expected value {identity_value} (type: {type(identity_value)}) but got value {value} (type: {type(value)})"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
return parse
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
NONE_IDENTITY_PARSER = _build_identity_parser(None)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _build_parser_discriminated_union(
|
|
189
|
+
context: ParserContext,
|
|
190
|
+
discriminator_raw: str,
|
|
191
|
+
discriminator_map: dict[str, ParserFunction[T]],
|
|
192
|
+
) -> ParserFunction[T]:
|
|
193
|
+
discriminator = (
|
|
194
|
+
snake_to_camel_case(discriminator_raw)
|
|
195
|
+
if context.options.from_camel_case
|
|
196
|
+
else discriminator_raw
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
def parse(value: typing.Any) -> typing.Any:
|
|
200
|
+
if context.options.allow_direct_type and dataclasses.is_dataclass(value):
|
|
201
|
+
discriminant = getattr(value, discriminator)
|
|
202
|
+
else:
|
|
203
|
+
discriminant = value.get(discriminator)
|
|
204
|
+
if discriminant is None:
|
|
205
|
+
raise ValueError("missing-union-discriminant")
|
|
206
|
+
if not isinstance(discriminant, str):
|
|
207
|
+
raise ValueError("union-discriminant-is-not-string")
|
|
208
|
+
parser = discriminator_map.get(discriminant)
|
|
209
|
+
if parser is None:
|
|
210
|
+
raise ValueError("missing-type-for-union-discriminant", discriminant)
|
|
211
|
+
return parser(value)
|
|
212
|
+
|
|
213
|
+
return parse
|
|
214
|
+
|
|
215
|
+
|
|
101
216
|
def _build_parser_inner(
|
|
102
217
|
parsed_type: type[T],
|
|
103
218
|
context: ParserContext,
|
|
104
|
-
*,
|
|
105
|
-
convert_string_to_snake_case: bool = False,
|
|
106
219
|
) -> ParserFunction[T]:
|
|
107
220
|
"""
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
then the generated parser will convert camel to snake case case
|
|
111
|
-
should only be True for cases like dictionary keys
|
|
112
|
-
should only be True if options.convert_to_snake_case is True
|
|
113
|
-
|
|
114
|
-
NOTE: This argument makes caching at this level difficult, as the cache-map
|
|
115
|
-
would need to vary based on this argument. For this reason only dataclasses
|
|
116
|
-
are cached now, as they don't use the argument, and they're known to be safe.
|
|
117
|
-
This is also enough to support some recursion.
|
|
221
|
+
IMPROVE: We can now cache at this level, to avoid producing redundant
|
|
222
|
+
internal parsers.
|
|
118
223
|
"""
|
|
224
|
+
|
|
225
|
+
serial_union = get_serial_union_data(parsed_type)
|
|
226
|
+
if serial_union is not None:
|
|
227
|
+
discriminator = serial_union.discriminator
|
|
228
|
+
discriminator_map = serial_union.discriminator_map
|
|
229
|
+
if discriminator is None or discriminator_map is None:
|
|
230
|
+
# fallback to standard union parsing
|
|
231
|
+
parsed_type = serial_union.get_union_underlying()
|
|
232
|
+
else:
|
|
233
|
+
return _build_parser_discriminated_union(
|
|
234
|
+
context,
|
|
235
|
+
discriminator,
|
|
236
|
+
{
|
|
237
|
+
key: _build_parser_inner(value, context)
|
|
238
|
+
for key, value in discriminator_map.items()
|
|
239
|
+
},
|
|
240
|
+
)
|
|
241
|
+
|
|
119
242
|
if dataclasses.is_dataclass(parsed_type):
|
|
120
|
-
return _build_parser_dataclass(parsed_type, context)
|
|
243
|
+
return _build_parser_dataclass(parsed_type, context)
|
|
121
244
|
|
|
122
245
|
# namedtuple support
|
|
123
246
|
if is_namedtuple_type(parsed_type):
|
|
@@ -130,15 +253,17 @@ def _build_parser_inner(
|
|
|
130
253
|
field_name: field_parser(
|
|
131
254
|
value.get(
|
|
132
255
|
snake_to_camel_case(field_name)
|
|
133
|
-
if context.options.
|
|
256
|
+
if context.options.from_camel_case
|
|
134
257
|
else field_name
|
|
135
258
|
)
|
|
136
259
|
)
|
|
137
260
|
for field_name, field_parser in field_parsers
|
|
138
261
|
})
|
|
139
262
|
|
|
263
|
+
# IMPROVE: unclear why we need == here
|
|
140
264
|
if parsed_type == type(None): # noqa: E721
|
|
141
|
-
|
|
265
|
+
# Need to convince type checker that parsed_type is type(None)
|
|
266
|
+
return typing.cast(ParserFunction[T], NONE_IDENTITY_PARSER)
|
|
142
267
|
|
|
143
268
|
origin = typing.get_origin(parsed_type)
|
|
144
269
|
if origin is tuple:
|
|
@@ -191,45 +316,70 @@ def _build_parser_inner(
|
|
|
191
316
|
args = typing.get_args(parsed_type)
|
|
192
317
|
if len(args) != 2:
|
|
193
318
|
raise ValueError("Dict types only support two arguments for now")
|
|
194
|
-
|
|
319
|
+
k_inner_parser = _build_parser_inner(
|
|
195
320
|
args[0],
|
|
196
321
|
context,
|
|
197
|
-
convert_string_to_snake_case=context.options.convert_to_snake_case,
|
|
198
322
|
)
|
|
323
|
+
|
|
324
|
+
def key_parser(value: typing.Any) -> object:
|
|
325
|
+
inner = k_inner_parser(value)
|
|
326
|
+
if (
|
|
327
|
+
isinstance(inner, str)
|
|
328
|
+
# enum keys and OpaqueData's would also have string value types,
|
|
329
|
+
# but their explicit type is not a string, thus shouldn't be converted
|
|
330
|
+
and args[0] is str
|
|
331
|
+
and context.options.from_camel_case
|
|
332
|
+
):
|
|
333
|
+
return camel_to_snake_case(value)
|
|
334
|
+
return inner
|
|
335
|
+
|
|
199
336
|
v_parser = _build_parser_inner(args[1], context)
|
|
200
|
-
return lambda value: origin(
|
|
337
|
+
return lambda value: origin(
|
|
338
|
+
(key_parser(k), v_parser(v)) for k, v in value.items()
|
|
339
|
+
)
|
|
201
340
|
|
|
202
341
|
if origin == typing.Literal:
|
|
203
342
|
valid_values: set[T] = set(typing.get_args(parsed_type))
|
|
204
343
|
return lambda value: _invoke_membership_parser(valid_values, value)
|
|
205
344
|
|
|
206
|
-
if parsed_type is str and convert_string_to_snake_case:
|
|
207
|
-
return lambda value: camel_to_snake_case(value) # type: ignore
|
|
208
|
-
|
|
209
345
|
if parsed_type is int:
|
|
210
346
|
# first parse ints to decimal to allow scientific notation and decimals
|
|
211
347
|
# e.g. (1) 1e4 => 1000, (2) 3.0 => 3
|
|
212
348
|
|
|
213
349
|
def parse_int(value: typing.Any) -> T:
|
|
214
350
|
if isinstance(value, str):
|
|
215
|
-
assert (
|
|
216
|
-
"
|
|
217
|
-
)
|
|
351
|
+
assert "_" not in value, (
|
|
352
|
+
"numbers with underscores not considered integers"
|
|
353
|
+
)
|
|
218
354
|
|
|
219
355
|
dec_value = Decimal(value)
|
|
220
356
|
int_value = int(dec_value)
|
|
221
|
-
assert (
|
|
222
|
-
|
|
223
|
-
)
|
|
357
|
+
assert int_value == dec_value, (
|
|
358
|
+
f"value ({value}) cannot be parsed to int without discarding precision"
|
|
359
|
+
)
|
|
224
360
|
return int_value # type: ignore
|
|
225
361
|
|
|
226
362
|
return parse_int
|
|
227
363
|
|
|
228
|
-
if parsed_type is datetime:
|
|
229
|
-
|
|
364
|
+
if parsed_type is datetime.datetime:
|
|
365
|
+
|
|
366
|
+
def parse_datetime(value: typing.Any) -> T:
|
|
367
|
+
if context.options.allow_direct_type and isinstance(
|
|
368
|
+
value, datetime.datetime
|
|
369
|
+
):
|
|
370
|
+
return value # type: ignore
|
|
371
|
+
return dateutil.parser.isoparse(value) # type:ignore
|
|
372
|
+
|
|
373
|
+
return parse_datetime
|
|
230
374
|
|
|
231
375
|
if parsed_type is date:
|
|
232
|
-
|
|
376
|
+
|
|
377
|
+
def parse_date(value: typing.Any) -> T:
|
|
378
|
+
if context.options.allow_direct_type and isinstance(value, date):
|
|
379
|
+
return value # type:ignore
|
|
380
|
+
return date.fromisoformat(value) # type:ignore
|
|
381
|
+
|
|
382
|
+
return parse_date
|
|
233
383
|
|
|
234
384
|
# MyPy: It's unclear why `parsed_type in (str, OpaqueKey)` is flagged as invalid
|
|
235
385
|
# Thus an or statement is used instead, which isn't flagged as invalid.
|
|
@@ -244,7 +394,18 @@ def _build_parser_inner(
|
|
|
244
394
|
|
|
245
395
|
return parse_str
|
|
246
396
|
|
|
247
|
-
if parsed_type in (float,
|
|
397
|
+
if parsed_type in (float, Decimal):
|
|
398
|
+
|
|
399
|
+
def parse_as_numeric_type(value: typing.Any) -> T:
|
|
400
|
+
numeric_value: Decimal | float = parsed_type(value) # type: ignore
|
|
401
|
+
if math.isnan(numeric_value):
|
|
402
|
+
raise ValueError(f"Invalid numeric value: {numeric_value}")
|
|
403
|
+
|
|
404
|
+
return numeric_value # type: ignore
|
|
405
|
+
|
|
406
|
+
return parse_as_numeric_type
|
|
407
|
+
|
|
408
|
+
if parsed_type in (dict, bool) or is_string_enum_class(parsed_type):
|
|
248
409
|
return lambda value: parsed_type(value) # type: ignore
|
|
249
410
|
|
|
250
411
|
if parsed_type is MissingSentryType:
|
|
@@ -253,7 +414,17 @@ def _build_parser_inner(
|
|
|
253
414
|
raise ValueError("Missing type cannot be parsed directly")
|
|
254
415
|
|
|
255
416
|
return error
|
|
256
|
-
|
|
417
|
+
|
|
418
|
+
# Check last for generic annotated types and process them unwrapped
|
|
419
|
+
# this must be last, since some of the expected types, like Unions,
|
|
420
|
+
# will also be annotated, but have a special form
|
|
421
|
+
if typing.get_origin(parsed_type) is typing.Annotated:
|
|
422
|
+
return _build_parser_inner(
|
|
423
|
+
parsed_type.__origin__, # type: ignore[attr-defined]
|
|
424
|
+
context,
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
raise ValueError(f"Unhandled type {parsed_type}/{origin}")
|
|
257
428
|
|
|
258
429
|
|
|
259
430
|
def _build_parser_dataclass(
|
|
@@ -268,8 +439,7 @@ def _build_parser_dataclass(
|
|
|
268
439
|
cur_parser = context.cache.get(parsed_type)
|
|
269
440
|
if cur_parser is not None:
|
|
270
441
|
return cur_parser
|
|
271
|
-
|
|
272
|
-
type_hints = typing.get_type_hints(parsed_type)
|
|
442
|
+
type_hints = typing.get_type_hints(parsed_type, include_extras=True)
|
|
273
443
|
dc_field_parsers: list[
|
|
274
444
|
tuple[
|
|
275
445
|
dataclasses.Field[typing.Any],
|
|
@@ -280,28 +450,35 @@ def _build_parser_dataclass(
|
|
|
280
450
|
|
|
281
451
|
serial_class_data = get_serial_class_data(parsed_type)
|
|
282
452
|
|
|
453
|
+
def resolve_serialized_field_name(*, field_name: str) -> str:
|
|
454
|
+
return (
|
|
455
|
+
snake_to_camel_case(field_name)
|
|
456
|
+
if (
|
|
457
|
+
context.options.from_camel_case
|
|
458
|
+
and not serial_class_data.has_unconverted_key(field_name)
|
|
459
|
+
)
|
|
460
|
+
else field_name
|
|
461
|
+
)
|
|
462
|
+
|
|
283
463
|
def parse(value: typing.Any) -> typing.Any:
|
|
464
|
+
# Use an exact type match to prevent base/derived class mismatches
|
|
465
|
+
if context.options.allow_direct_type and type(value) is parsed_type:
|
|
466
|
+
return value
|
|
467
|
+
|
|
284
468
|
data: dict[typing.Any, typing.Any] = {}
|
|
285
469
|
for field, field_type, field_parser in dc_field_parsers:
|
|
286
470
|
field_raw_value = None
|
|
287
471
|
try:
|
|
288
472
|
field_raw_value = value.get(
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
context.options.convert_to_snake_case
|
|
292
|
-
and not serial_class_data.has_unconverted_key(field.name)
|
|
293
|
-
)
|
|
294
|
-
else field.name,
|
|
295
|
-
MISSING,
|
|
473
|
+
resolve_serialized_field_name(field_name=field.name),
|
|
474
|
+
dataclasses.MISSING,
|
|
296
475
|
)
|
|
297
476
|
field_value: typing.Any
|
|
298
|
-
if field_raw_value == MISSING:
|
|
477
|
+
if field_raw_value == dataclasses.MISSING:
|
|
299
478
|
if serial_class_data.has_parse_require(field.name):
|
|
300
479
|
raise ValueError("missing-required-field", field.name)
|
|
301
|
-
if field
|
|
302
|
-
field_value = field
|
|
303
|
-
elif field.default_factory != MISSING:
|
|
304
|
-
field_value = field.default_factory()
|
|
480
|
+
if _has_field_default(field):
|
|
481
|
+
field_value = _get_field_default(field)
|
|
305
482
|
elif is_missing(field_type):
|
|
306
483
|
field_value = MissingSentryType()
|
|
307
484
|
elif is_optional(field_type):
|
|
@@ -313,6 +490,13 @@ def _build_parser_dataclass(
|
|
|
313
490
|
field_value = False
|
|
314
491
|
else:
|
|
315
492
|
raise ValueError("missing-value-for-field", field.name)
|
|
493
|
+
elif (
|
|
494
|
+
field_raw_value is None
|
|
495
|
+
and not is_optional(field_type)
|
|
496
|
+
and _has_field_default(field)
|
|
497
|
+
and not serial_class_data.has_parse_require(field.name)
|
|
498
|
+
):
|
|
499
|
+
field_value = _get_field_default(field)
|
|
316
500
|
elif serial_class_data.has_unconverted_value(field.name):
|
|
317
501
|
field_value = field_raw_value
|
|
318
502
|
else:
|
|
@@ -322,9 +506,21 @@ def _build_parser_dataclass(
|
|
|
322
506
|
|
|
323
507
|
except Exception as e:
|
|
324
508
|
raise ValueError(
|
|
325
|
-
f"unable
|
|
509
|
+
f"unable-to-parse-field:{field.name}", field_raw_value
|
|
326
510
|
) from e
|
|
327
511
|
|
|
512
|
+
if context.options.strict_property_parsing:
|
|
513
|
+
all_allowed_field_names = set(
|
|
514
|
+
resolve_serialized_field_name(field_name=field.name)
|
|
515
|
+
for (field, _, _) in dc_field_parsers
|
|
516
|
+
)
|
|
517
|
+
passed_field_names = set(value.keys())
|
|
518
|
+
disallowed_field_names = passed_field_names.difference(
|
|
519
|
+
all_allowed_field_names
|
|
520
|
+
)
|
|
521
|
+
if len(disallowed_field_names) > 0:
|
|
522
|
+
raise ParserExtraFieldsError(disallowed_field_names)
|
|
523
|
+
|
|
328
524
|
return parsed_type(**data)
|
|
329
525
|
|
|
330
526
|
# Add to cache before building inner types, to support recursion
|
|
@@ -368,14 +564,47 @@ def build_parser(
|
|
|
368
564
|
return built_parser
|
|
369
565
|
|
|
370
566
|
|
|
371
|
-
class
|
|
567
|
+
class ParserBase(ABC, typing.Generic[T]):
|
|
568
|
+
def parse_from_encoding(
|
|
569
|
+
self,
|
|
570
|
+
args: typing.Any,
|
|
571
|
+
*,
|
|
572
|
+
source_encoding: SourceEncoding,
|
|
573
|
+
) -> T:
|
|
574
|
+
match source_encoding:
|
|
575
|
+
case SourceEncoding.API:
|
|
576
|
+
return self.parse_api(args)
|
|
577
|
+
case SourceEncoding.STORAGE:
|
|
578
|
+
return self.parse_storage(args)
|
|
579
|
+
case _:
|
|
580
|
+
typing.assert_never(source_encoding)
|
|
581
|
+
|
|
582
|
+
# IMPROVE: Args would be better typed as "object"
|
|
583
|
+
@abstractmethod
|
|
584
|
+
def parse_storage(self, args: typing.Any) -> T: ...
|
|
585
|
+
|
|
586
|
+
@abstractmethod
|
|
587
|
+
def parse_api(self, args: typing.Any) -> T: ...
|
|
588
|
+
|
|
589
|
+
def parse_yaml_file(self, path: str) -> T:
|
|
590
|
+
with open(path, encoding="utf-8") as data_in:
|
|
591
|
+
return self.parse_storage(msgspec.yaml.decode(data_in.read()))
|
|
592
|
+
|
|
593
|
+
def parse_yaml_resource(self, package: resources.Package, resource: str) -> T:
|
|
594
|
+
with resources.open_text(package, resource) as fp:
|
|
595
|
+
return self.parse_storage(msgspec.yaml.decode(fp.read()))
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
class CachedParser(ParserBase[T], typing.Generic[T]):
|
|
372
599
|
def __init__(
|
|
373
600
|
self,
|
|
374
601
|
args: type[T],
|
|
602
|
+
strict_property_parsing: bool = False,
|
|
375
603
|
):
|
|
376
604
|
self.arguments = args
|
|
377
|
-
self.parser_api:
|
|
378
|
-
self.parser_storage:
|
|
605
|
+
self.parser_api: ParserFunction[T] | None = None
|
|
606
|
+
self.parser_storage: ParserFunction[T] | None = None
|
|
607
|
+
self.strict_property_parsing = strict_property_parsing
|
|
379
608
|
|
|
380
609
|
def parse_api(self, args: typing.Any) -> T:
|
|
381
610
|
"""
|
|
@@ -388,8 +617,8 @@ class CachedParser(typing.Generic[T]):
|
|
|
388
617
|
if self.parser_api is None:
|
|
389
618
|
self.parser_api = build_parser(
|
|
390
619
|
self.arguments,
|
|
391
|
-
ParserOptions(
|
|
392
|
-
|
|
620
|
+
ParserOptions.Api(
|
|
621
|
+
strict_property_parsing=self.strict_property_parsing,
|
|
393
622
|
),
|
|
394
623
|
)
|
|
395
624
|
assert self.parser_api is not None
|
|
@@ -402,17 +631,9 @@ class CachedParser(typing.Generic[T]):
|
|
|
402
631
|
if self.parser_storage is None:
|
|
403
632
|
self.parser_storage = build_parser(
|
|
404
633
|
self.arguments,
|
|
405
|
-
ParserOptions(
|
|
406
|
-
|
|
634
|
+
ParserOptions.Storage(
|
|
635
|
+
strict_property_parsing=self.strict_property_parsing,
|
|
407
636
|
),
|
|
408
637
|
)
|
|
409
638
|
assert self.parser_storage is not None
|
|
410
639
|
return self.parser_storage(args)
|
|
411
|
-
|
|
412
|
-
def parse_yaml_file(self, path: str) -> T:
|
|
413
|
-
with open(path, encoding="utf-8") as data_in:
|
|
414
|
-
return self.parse_storage(yaml.safe_load(data_in))
|
|
415
|
-
|
|
416
|
-
def parse_yaml_resource(self, package: resources.Package, resource: str) -> T:
|
|
417
|
-
with resources.open_text(package, resource) as fp:
|
|
418
|
-
return self.parse_storage(yaml.safe_load(fp))
|