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.

Files changed (373) hide show
  1. docs/conf.py +60 -8
  2. docs/index.md +107 -4
  3. docs/integration_examples/create_ingredient.md +43 -0
  4. docs/integration_examples/create_output.md +56 -0
  5. docs/integration_examples/index.md +6 -0
  6. docs/justfile +2 -2
  7. docs/requirements.txt +7 -5
  8. examples/async_batch.py +5 -6
  9. examples/basic_auth.py +7 -0
  10. examples/create_entity.py +4 -6
  11. examples/create_ingredient_sdk.py +34 -0
  12. examples/download_files.py +26 -0
  13. examples/edit_recipe_inputs.py +50 -0
  14. examples/integration-server/jobs/materials_auto/concurrent_cron.py +11 -0
  15. examples/integration-server/jobs/materials_auto/example_cron.py +21 -0
  16. examples/integration-server/jobs/materials_auto/example_http.py +47 -0
  17. examples/integration-server/jobs/materials_auto/example_instrument.py +100 -0
  18. examples/integration-server/jobs/materials_auto/example_parse.py +140 -0
  19. examples/integration-server/jobs/materials_auto/example_predictions.py +61 -0
  20. examples/integration-server/jobs/materials_auto/example_runsheet_wh.py +39 -0
  21. examples/integration-server/jobs/materials_auto/example_wh.py +23 -0
  22. examples/integration-server/jobs/materials_auto/profile.yaml +104 -0
  23. examples/integration-server/pyproject.toml +224 -0
  24. examples/invoke_uploader.py +26 -0
  25. examples/oauth.py +7 -0
  26. examples/set_recipe_metadata_file.py +40 -0
  27. examples/set_recipe_output_file_sdk.py +26 -0
  28. examples/upload_files.py +2 -3
  29. pkgs/argument_parser/__init__.py +9 -0
  30. pkgs/argument_parser/_is_namedtuple.py +3 -0
  31. pkgs/argument_parser/argument_parser.py +295 -74
  32. pkgs/argument_parser/case_convert.py +4 -3
  33. pkgs/filesystem_utils/__init__.py +20 -0
  34. pkgs/filesystem_utils/_blob_session.py +144 -0
  35. pkgs/filesystem_utils/_gdrive_session.py +309 -0
  36. pkgs/filesystem_utils/_local_session.py +69 -0
  37. pkgs/filesystem_utils/_s3_session.py +118 -0
  38. pkgs/filesystem_utils/_sftp_session.py +151 -0
  39. pkgs/filesystem_utils/file_type_utils.py +91 -0
  40. pkgs/filesystem_utils/filesystem_session.py +39 -0
  41. pkgs/py.typed +0 -0
  42. pkgs/serialization/__init__.py +8 -1
  43. pkgs/serialization/annotation.py +64 -0
  44. pkgs/serialization/missing_sentry.py +1 -1
  45. pkgs/serialization/opaque_key.py +1 -1
  46. pkgs/serialization/serial_alias.py +47 -0
  47. pkgs/serialization/serial_class.py +69 -54
  48. pkgs/serialization/serial_generic.py +16 -0
  49. pkgs/serialization/serial_union.py +84 -0
  50. pkgs/serialization/yaml.py +57 -0
  51. pkgs/serialization_util/__init__.py +7 -7
  52. pkgs/serialization_util/convert_to_snakecase.py +27 -0
  53. pkgs/serialization_util/dataclasses.py +14 -0
  54. pkgs/serialization_util/serialization_helpers.py +117 -71
  55. pkgs/type_spec/actions_registry/__main__.py +0 -4
  56. pkgs/type_spec/actions_registry/emit_typescript.py +5 -5
  57. pkgs/type_spec/builder.py +438 -109
  58. pkgs/type_spec/builder_types.py +9 -0
  59. pkgs/type_spec/config.py +52 -24
  60. pkgs/type_spec/cross_output_links.py +99 -0
  61. pkgs/type_spec/emit_io_ts.py +1 -1
  62. pkgs/type_spec/emit_open_api.py +160 -41
  63. pkgs/type_spec/emit_open_api_util.py +13 -7
  64. pkgs/type_spec/emit_python.py +450 -136
  65. pkgs/type_spec/emit_typescript.py +117 -250
  66. pkgs/type_spec/emit_typescript_util.py +293 -4
  67. pkgs/type_spec/load_types.py +20 -5
  68. pkgs/type_spec/non_discriminated_union_exceptions.py +14 -0
  69. pkgs/type_spec/open_api_util.py +29 -4
  70. pkgs/type_spec/parts/base.py.prepart +13 -10
  71. pkgs/type_spec/parts/base.ts.prepart +4 -0
  72. pkgs/type_spec/type_info/__main__.py +3 -1
  73. pkgs/type_spec/type_info/emit_type_info.py +161 -32
  74. pkgs/type_spec/ui_entry_actions/__init__.py +4 -0
  75. pkgs/type_spec/ui_entry_actions/generate_ui_entry_actions.py +308 -0
  76. pkgs/type_spec/util.py +4 -4
  77. pkgs/type_spec/value_spec/__main__.py +27 -10
  78. pkgs/type_spec/value_spec/convert_type.py +21 -1
  79. pkgs/type_spec/value_spec/emit_python.py +25 -7
  80. pkgs/type_spec/value_spec/types.py +1 -1
  81. uncountable/__init__.py +1 -2
  82. uncountable/core/__init__.py +11 -3
  83. uncountable/core/async_batch.py +16 -1
  84. uncountable/core/client.py +247 -52
  85. uncountable/core/environment.py +41 -0
  86. uncountable/core/file_upload.py +67 -22
  87. uncountable/core/types.py +8 -13
  88. uncountable/integration/cli.py +142 -0
  89. uncountable/integration/construct_client.py +43 -27
  90. uncountable/integration/cron.py +12 -11
  91. uncountable/integration/db/connect.py +12 -2
  92. uncountable/integration/db/session.py +25 -0
  93. uncountable/integration/entrypoint.py +4 -34
  94. uncountable/integration/executors/executors.py +147 -0
  95. uncountable/integration/executors/generic_upload_executor.py +336 -0
  96. uncountable/integration/executors/script_executor.py +15 -9
  97. uncountable/integration/http_server/__init__.py +5 -0
  98. uncountable/integration/http_server/types.py +69 -0
  99. uncountable/integration/job.py +246 -19
  100. uncountable/integration/queue_runner/__init__.py +0 -0
  101. uncountable/integration/queue_runner/command_server/__init__.py +28 -0
  102. uncountable/integration/queue_runner/command_server/command_client.py +133 -0
  103. uncountable/integration/queue_runner/command_server/command_server.py +142 -0
  104. uncountable/integration/queue_runner/command_server/constants.py +4 -0
  105. uncountable/integration/queue_runner/command_server/protocol/__init__.py +0 -0
  106. uncountable/integration/queue_runner/command_server/protocol/command_server.proto +58 -0
  107. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +57 -0
  108. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +114 -0
  109. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2_grpc.py +264 -0
  110. uncountable/integration/queue_runner/command_server/types.py +75 -0
  111. uncountable/integration/queue_runner/datastore/__init__.py +3 -0
  112. uncountable/integration/queue_runner/datastore/datastore_sqlite.py +250 -0
  113. uncountable/integration/queue_runner/datastore/interface.py +29 -0
  114. uncountable/integration/queue_runner/datastore/model.py +24 -0
  115. uncountable/integration/queue_runner/job_scheduler.py +200 -0
  116. uncountable/integration/queue_runner/queue_runner.py +34 -0
  117. uncountable/integration/queue_runner/types.py +7 -0
  118. uncountable/integration/queue_runner/worker.py +116 -0
  119. uncountable/integration/scan_profiles.py +67 -0
  120. uncountable/integration/scheduler.py +199 -0
  121. uncountable/integration/secret_retrieval/__init__.py +3 -0
  122. uncountable/integration/secret_retrieval/retrieve_secret.py +93 -0
  123. uncountable/integration/server.py +103 -54
  124. uncountable/integration/telemetry.py +251 -0
  125. uncountable/integration/webhook_server/entrypoint.py +97 -0
  126. uncountable/types/__init__.py +149 -30
  127. uncountable/types/api/batch/execute_batch.py +16 -9
  128. uncountable/types/api/batch/execute_batch_load_async.py +13 -7
  129. uncountable/types/api/chemical/convert_chemical_formats.py +20 -8
  130. uncountable/types/api/condition_parameters/__init__.py +1 -0
  131. uncountable/types/api/condition_parameters/upsert_condition_match.py +72 -0
  132. uncountable/types/api/entity/create_entities.py +24 -12
  133. uncountable/types/api/entity/create_entity.py +22 -13
  134. uncountable/types/api/entity/create_or_update_entity.py +48 -0
  135. uncountable/types/api/entity/export_entities.py +59 -0
  136. uncountable/types/api/entity/get_entities_data.py +18 -9
  137. uncountable/types/api/entity/grant_entity_permissions.py +48 -0
  138. uncountable/types/api/entity/list_aggregate.py +79 -0
  139. uncountable/types/api/entity/list_entities.py +53 -14
  140. uncountable/types/api/entity/lock_entity.py +45 -0
  141. uncountable/types/api/entity/lookup_entity.py +116 -0
  142. uncountable/types/api/entity/resolve_entity_ids.py +19 -10
  143. uncountable/types/api/entity/set_entity_field_values.py +44 -0
  144. uncountable/types/api/entity/set_values.py +15 -8
  145. uncountable/types/api/entity/transition_entity_phase.py +27 -12
  146. uncountable/types/api/entity/unlock_entity.py +44 -0
  147. uncountable/types/api/equipment/__init__.py +1 -0
  148. uncountable/types/api/equipment/associate_equipment_input.py +43 -0
  149. uncountable/types/api/field_options/__init__.py +1 -0
  150. uncountable/types/api/field_options/upsert_field_options.py +55 -0
  151. uncountable/types/api/files/__init__.py +1 -0
  152. uncountable/types/api/files/download_file.py +77 -0
  153. uncountable/types/api/id_source/list_id_source.py +20 -11
  154. uncountable/types/api/id_source/match_id_source.py +15 -10
  155. uncountable/types/api/input_groups/get_input_group_names.py +16 -7
  156. uncountable/types/api/inputs/create_inputs.py +28 -14
  157. uncountable/types/api/inputs/get_input_data.py +34 -16
  158. uncountable/types/api/inputs/get_input_names.py +19 -10
  159. uncountable/types/api/inputs/get_inputs_data.py +29 -11
  160. uncountable/types/api/inputs/set_input_attribute_values.py +16 -10
  161. uncountable/types/api/inputs/set_input_category.py +44 -0
  162. uncountable/types/api/inputs/set_input_subcategories.py +45 -0
  163. uncountable/types/api/inputs/set_intermediate_type.py +50 -0
  164. uncountable/types/api/integrations/__init__.py +1 -0
  165. uncountable/types/api/integrations/publish_realtime_data.py +41 -0
  166. uncountable/types/api/integrations/push_notification.py +49 -0
  167. uncountable/types/api/integrations/register_sockets_token.py +41 -0
  168. uncountable/types/api/listing/__init__.py +1 -0
  169. uncountable/types/api/listing/fetch_listing.py +58 -0
  170. uncountable/types/api/material_families/__init__.py +1 -0
  171. uncountable/types/api/material_families/update_entity_material_families.py +47 -0
  172. uncountable/types/api/notebooks/__init__.py +1 -0
  173. uncountable/types/api/notebooks/add_notebook_content.py +119 -0
  174. uncountable/types/api/outputs/get_output_data.py +32 -17
  175. uncountable/types/api/outputs/get_output_names.py +18 -9
  176. uncountable/types/api/outputs/get_output_organization.py +173 -0
  177. uncountable/types/api/outputs/resolve_output_conditions.py +23 -11
  178. uncountable/types/api/permissions/set_core_permissions.py +31 -15
  179. uncountable/types/api/project/get_projects.py +20 -11
  180. uncountable/types/api/project/get_projects_data.py +23 -14
  181. uncountable/types/api/recipe_links/create_recipe_link.py +17 -10
  182. uncountable/types/api/recipe_links/remove_recipe_link.py +45 -0
  183. uncountable/types/api/recipe_metadata/get_recipe_metadata_data.py +19 -10
  184. uncountable/types/api/recipes/add_recipe_to_project.py +42 -0
  185. uncountable/types/api/recipes/add_time_series_data.py +64 -0
  186. uncountable/types/api/recipes/archive_recipes.py +14 -7
  187. uncountable/types/api/recipes/associate_recipe_as_input.py +16 -8
  188. uncountable/types/api/recipes/associate_recipe_as_lot.py +14 -7
  189. uncountable/types/api/recipes/clear_recipe_outputs.py +42 -0
  190. uncountable/types/api/recipes/create_mix_order.py +44 -0
  191. uncountable/types/api/recipes/create_recipe.py +21 -14
  192. uncountable/types/api/recipes/create_recipes.py +25 -13
  193. uncountable/types/api/recipes/disassociate_recipe_as_input.py +14 -7
  194. uncountable/types/api/recipes/edit_recipe_inputs.py +208 -19
  195. uncountable/types/api/recipes/get_column_calculation_values.py +57 -0
  196. uncountable/types/api/recipes/get_curve.py +15 -9
  197. uncountable/types/api/recipes/get_recipe_calculations.py +17 -11
  198. uncountable/types/api/recipes/get_recipe_links.py +14 -8
  199. uncountable/types/api/recipes/get_recipe_names.py +16 -7
  200. uncountable/types/api/recipes/get_recipe_output_metadata.py +16 -10
  201. uncountable/types/api/recipes/get_recipes_data.py +96 -45
  202. uncountable/types/api/recipes/lock_recipes.py +64 -0
  203. uncountable/types/api/recipes/remove_recipe_from_project.py +42 -0
  204. uncountable/types/api/recipes/set_recipe_inputs.py +19 -13
  205. uncountable/types/api/recipes/set_recipe_metadata.py +14 -7
  206. uncountable/types/api/recipes/set_recipe_output_annotations.py +114 -0
  207. uncountable/types/api/recipes/set_recipe_output_file.py +55 -0
  208. uncountable/types/api/recipes/set_recipe_outputs.py +40 -15
  209. uncountable/types/api/recipes/set_recipe_tags.py +30 -13
  210. uncountable/types/api/recipes/set_recipe_total.py +59 -0
  211. uncountable/types/api/recipes/unarchive_recipes.py +41 -0
  212. uncountable/types/api/recipes/unlock_recipes.py +51 -0
  213. uncountable/types/api/runsheet/__init__.py +1 -0
  214. uncountable/types/api/runsheet/complete_async_upload.py +41 -0
  215. uncountable/types/api/triggers/run_trigger.py +15 -8
  216. uncountable/types/api/uploader/__init__.py +1 -0
  217. uncountable/types/api/uploader/complete_async_parse.py +46 -0
  218. uncountable/types/api/uploader/invoke_uploader.py +46 -0
  219. uncountable/types/api/user/__init__.py +1 -0
  220. uncountable/types/api/user/get_current_user_info.py +40 -0
  221. uncountable/types/async_batch.py +8 -52
  222. uncountable/types/async_batch_processor.py +694 -18
  223. uncountable/types/async_batch_t.py +108 -0
  224. uncountable/types/async_jobs.py +8 -0
  225. uncountable/types/async_jobs_t.py +52 -0
  226. uncountable/types/auth_retrieval.py +11 -0
  227. uncountable/types/auth_retrieval_t.py +75 -0
  228. uncountable/types/base.py +5 -80
  229. uncountable/types/base_t.py +87 -0
  230. uncountable/types/calculations.py +3 -19
  231. uncountable/types/calculations_t.py +26 -0
  232. uncountable/types/chemical_structure.py +3 -23
  233. uncountable/types/chemical_structure_t.py +28 -0
  234. uncountable/types/client_base.py +1170 -88
  235. uncountable/types/client_config.py +8 -0
  236. uncountable/types/client_config_t.py +36 -0
  237. uncountable/types/curves.py +5 -43
  238. uncountable/types/curves_t.py +50 -0
  239. uncountable/types/data.py +12 -0
  240. uncountable/types/data_t.py +103 -0
  241. uncountable/types/entity.py +8 -270
  242. uncountable/types/entity_t.py +446 -0
  243. uncountable/types/experiment_groups.py +3 -19
  244. uncountable/types/experiment_groups_t.py +26 -0
  245. uncountable/types/exports.py +8 -0
  246. uncountable/types/exports_t.py +34 -0
  247. uncountable/types/field_values.py +25 -61
  248. uncountable/types/field_values_t.py +302 -0
  249. uncountable/types/fields.py +3 -20
  250. uncountable/types/fields_t.py +27 -0
  251. uncountable/types/generic_upload.py +14 -0
  252. uncountable/types/generic_upload_t.py +119 -0
  253. uncountable/types/id_source.py +7 -45
  254. uncountable/types/id_source_t.py +68 -0
  255. uncountable/types/identifier.py +6 -50
  256. uncountable/types/identifier_t.py +62 -0
  257. uncountable/types/input_attributes.py +3 -25
  258. uncountable/types/input_attributes_t.py +29 -0
  259. uncountable/types/inputs.py +6 -57
  260. uncountable/types/inputs_t.py +82 -0
  261. uncountable/types/integration_server.py +8 -0
  262. uncountable/types/integration_server_t.py +46 -0
  263. uncountable/types/integration_session.py +10 -0
  264. uncountable/types/integration_session_t.py +60 -0
  265. uncountable/types/integrations.py +10 -0
  266. uncountable/types/integrations_t.py +62 -0
  267. uncountable/types/job_definition.py +28 -0
  268. uncountable/types/job_definition_t.py +285 -0
  269. uncountable/types/listing.py +9 -0
  270. uncountable/types/listing_t.py +51 -0
  271. uncountable/types/notices.py +8 -0
  272. uncountable/types/notices_t.py +37 -0
  273. uncountable/types/notifications.py +11 -0
  274. uncountable/types/notifications_t.py +74 -0
  275. uncountable/types/outputs.py +3 -22
  276. uncountable/types/outputs_t.py +29 -0
  277. uncountable/types/overrides.py +9 -0
  278. uncountable/types/overrides_t.py +49 -0
  279. uncountable/types/permissions.py +3 -42
  280. uncountable/types/permissions_t.py +45 -0
  281. uncountable/types/phases.py +3 -19
  282. uncountable/types/phases_t.py +26 -0
  283. uncountable/types/post_base.py +3 -26
  284. uncountable/types/post_base_t.py +29 -0
  285. uncountable/types/queued_job.py +17 -0
  286. uncountable/types/queued_job_t.py +140 -0
  287. uncountable/types/recipe_identifiers.py +7 -58
  288. uncountable/types/recipe_identifiers_t.py +75 -0
  289. uncountable/types/recipe_inputs.py +4 -26
  290. uncountable/types/recipe_inputs_t.py +29 -0
  291. uncountable/types/recipe_links.py +4 -46
  292. uncountable/types/recipe_links_t.py +53 -0
  293. uncountable/types/recipe_metadata.py +5 -48
  294. uncountable/types/recipe_metadata_t.py +57 -0
  295. uncountable/types/recipe_output_metadata.py +3 -20
  296. uncountable/types/recipe_output_metadata_t.py +27 -0
  297. uncountable/types/recipe_tags.py +3 -19
  298. uncountable/types/recipe_tags_t.py +26 -0
  299. uncountable/types/recipe_workflow_steps.py +9 -73
  300. uncountable/types/recipe_workflow_steps_t.py +95 -0
  301. uncountable/types/recipes.py +7 -0
  302. uncountable/types/recipes_t.py +25 -0
  303. uncountable/types/response.py +3 -21
  304. uncountable/types/response_t.py +26 -0
  305. uncountable/types/secret_retrieval.py +11 -0
  306. uncountable/types/secret_retrieval_t.py +75 -0
  307. uncountable/types/sockets.py +20 -0
  308. uncountable/types/sockets_t.py +169 -0
  309. uncountable/types/structured_filters.py +25 -0
  310. uncountable/types/structured_filters_t.py +248 -0
  311. uncountable/types/units.py +3 -19
  312. uncountable/types/units_t.py +26 -0
  313. uncountable/types/uploader.py +24 -0
  314. uncountable/types/uploader_t.py +222 -0
  315. uncountable/types/users.py +3 -20
  316. uncountable/types/users_t.py +27 -0
  317. uncountable/types/webhook_job.py +9 -0
  318. uncountable/types/webhook_job_t.py +48 -0
  319. uncountable/types/workflows.py +4 -28
  320. uncountable/types/workflows_t.py +38 -0
  321. uncountablepythonsdk-0.0.131.dist-info/METADATA +64 -0
  322. uncountablepythonsdk-0.0.131.dist-info/RECORD +363 -0
  323. {UncountablePythonSDK-0.0.24.dist-info → uncountablepythonsdk-0.0.131.dist-info}/WHEEL +1 -1
  324. {UncountablePythonSDK-0.0.24.dist-info → uncountablepythonsdk-0.0.131.dist-info}/top_level.txt +0 -1
  325. UncountablePythonSDK-0.0.24.dist-info/METADATA +0 -47
  326. UncountablePythonSDK-0.0.24.dist-info/RECORD +0 -216
  327. docs/quickstart.md +0 -19
  328. examples/recipe-import/importer.py +0 -39
  329. type_spec/external/api/batch/execute_batch.yaml +0 -56
  330. type_spec/external/api/batch/execute_batch_load_async.yaml +0 -18
  331. type_spec/external/api/chemical/convert_chemical_formats.yaml +0 -33
  332. type_spec/external/api/entity/create_entities.yaml +0 -45
  333. type_spec/external/api/entity/create_entity.yaml +0 -51
  334. type_spec/external/api/entity/get_entities_data.yaml +0 -29
  335. type_spec/external/api/entity/list_entities.yaml +0 -52
  336. type_spec/external/api/entity/resolve_entity_ids.yaml +0 -29
  337. type_spec/external/api/entity/set_values.yaml +0 -18
  338. type_spec/external/api/entity/transition_entity_phase.yaml +0 -44
  339. type_spec/external/api/id_source/list_id_source.yaml +0 -35
  340. type_spec/external/api/id_source/match_id_source.yaml +0 -32
  341. type_spec/external/api/input_groups/get_input_group_names.yaml +0 -29
  342. type_spec/external/api/inputs/create_inputs.yaml +0 -48
  343. type_spec/external/api/inputs/get_input_data.yaml +0 -95
  344. type_spec/external/api/inputs/get_input_names.yaml +0 -38
  345. type_spec/external/api/inputs/get_inputs_data.yaml +0 -82
  346. type_spec/external/api/inputs/set_input_attribute_values.yaml +0 -33
  347. type_spec/external/api/outputs/get_output_data.yaml +0 -92
  348. type_spec/external/api/outputs/get_output_names.yaml +0 -35
  349. type_spec/external/api/outputs/resolve_output_conditions.yaml +0 -50
  350. type_spec/external/api/permissions/set_core_permissions.yaml +0 -69
  351. type_spec/external/api/project/get_projects.yaml +0 -42
  352. type_spec/external/api/project/get_projects_data.yaml +0 -50
  353. type_spec/external/api/recipe_links/create_recipe_link.yaml +0 -25
  354. type_spec/external/api/recipe_metadata/get_recipe_metadata_data.yaml +0 -41
  355. type_spec/external/api/recipes/archive_recipes.yaml +0 -20
  356. type_spec/external/api/recipes/associate_recipe_as_input.yaml +0 -19
  357. type_spec/external/api/recipes/associate_recipe_as_lot.yaml +0 -19
  358. type_spec/external/api/recipes/create_recipe.yaml +0 -39
  359. type_spec/external/api/recipes/create_recipes.yaml +0 -47
  360. type_spec/external/api/recipes/disassociate_recipe_as_input.yaml +0 -16
  361. type_spec/external/api/recipes/edit_recipe_inputs.yaml +0 -85
  362. type_spec/external/api/recipes/get_curve.yaml +0 -21
  363. type_spec/external/api/recipes/get_recipe_calculations.yaml +0 -39
  364. type_spec/external/api/recipes/get_recipe_links.yaml +0 -26
  365. type_spec/external/api/recipes/get_recipe_names.yaml +0 -29
  366. type_spec/external/api/recipes/get_recipe_output_metadata.yaml +0 -36
  367. type_spec/external/api/recipes/get_recipes_data.yaml +0 -244
  368. type_spec/external/api/recipes/set_recipe_inputs.yaml +0 -42
  369. type_spec/external/api/recipes/set_recipe_metadata.yaml +0 -20
  370. type_spec/external/api/recipes/set_recipe_outputs.yaml +0 -52
  371. type_spec/external/api/recipes/set_recipe_tags.yaml +0 -62
  372. type_spec/external/api/triggers/run_trigger.yaml +0 -18
  373. 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,7 @@
1
+ from uncountable.core.client import Client
2
+ from uncountable.core.types import AuthDetailsOAuth
3
+
4
+ client = Client(
5
+ base_url="https://app.uncountable.com",
6
+ auth_details=AuthDetailsOAuth(refresh_token="x"),
7
+ )
@@ -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="/my/file/path.doc"),
16
- MediaFileUpload(path="/my/file/path2.doc"),
15
+ MediaFileUpload(path="Downloads/file"),
17
16
  ]
18
17
  )
19
18
  pprint(uploaded)
@@ -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
@@ -5,6 +5,9 @@ def is_namedtuple_type(x: Any) -> bool:
5
5
  if not hasattr(x, "__annotations__"):
6
6
  return False
7
7
 
8
+ if not hasattr(x, "__bases__"):
9
+ return False
10
+
8
11
  b = x.__bases__
9
12
  if len(b) != 1 or b[0] is not tuple:
10
13
  return False
@@ -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 dataclasses import MISSING, dataclass
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 MissingSentryType, OpaqueKey, get_serial_class_data
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
- @dataclass(frozen=True, eq=True)
34
+ class SourceEncoding(Enum):
35
+ API = auto()
36
+ STORAGE = auto()
37
+
38
+
39
+ @dataclasses.dataclass(frozen=True, eq=True)
25
40
  class ParserOptions:
26
- convert_to_snake_case: bool
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
- @dataclass(frozen=True)
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 typing.get_origin(field_type) is typing.Union and type(
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
- origin = typing.get_origin(field_type)
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
- convert_to_snake_case - internal flag
109
- if convert_to_snake_case is True, and parsed_type is str,
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) # type: ignore[arg-type]
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.convert_to_snake_case
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
- return lambda value: _invoke_membership_parser({None}, value) # type: ignore
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
- k_parser = _build_parser_inner(
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((k_parser(k), v_parser(v)) for k, v in value.items())
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
- "_" not in value
217
- ), "numbers with underscores not considered integers"
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
- int_value == dec_value
223
- ), f"value ({value}) cannot be parsed to int without discarding precision"
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
- return lambda value: dateutil.parser.isoparse(value) # type:ignore
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
- return lambda value: date.fromisoformat(value) # type:ignore
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, dict, bool, Decimal) or is_string_enum_class(parsed_type):
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
- raise ValueError(f"Unhandled type {parsed_type}")
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
- snake_to_camel_case(field.name)
290
- if (
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.default != MISSING:
302
- field_value = field.default
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 to parse field:{field.name}", field_raw_value
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 CachedParser(typing.Generic[T]):
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: typing.Optional[ParserFunction[T]] = None
378
- self.parser_storage: typing.Optional[ParserFunction[T]] = None
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
- convert_to_snake_case=True,
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
- convert_to_snake_case=False,
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))