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,336 @@
1
+ import datetime
2
+ import io
3
+ import os
4
+ import re
5
+ from datetime import UTC
6
+
7
+ import paramiko
8
+
9
+ from pkgs.filesystem_utils import (
10
+ FileObjectData,
11
+ FileSystemFileReference,
12
+ FileSystemObject,
13
+ FileSystemS3Config,
14
+ FileSystemSession,
15
+ FileSystemSFTPConfig,
16
+ FileTransfer,
17
+ S3Session,
18
+ SFTPSession,
19
+ )
20
+ from uncountable.core.file_upload import DataFileUpload, FileUpload
21
+ from uncountable.integration.job import Job, JobArguments
22
+ from uncountable.integration.secret_retrieval import retrieve_secret
23
+ from uncountable.integration.telemetry import JobLogger
24
+ from uncountable.types.generic_upload_t import (
25
+ GenericRemoteDirectoryScope,
26
+ GenericUploadStrategy,
27
+ )
28
+ from uncountable.types.job_definition_t import (
29
+ GenericUploadDataSource,
30
+ GenericUploadDataSourceS3,
31
+ GenericUploadDataSourceSFTP,
32
+ JobResult,
33
+ S3CloudProvider,
34
+ )
35
+
36
+
37
+ def _get_extension(filename: str) -> str | None:
38
+ _, ext = os.path.splitext(filename)
39
+ return ext.strip().lower()
40
+
41
+
42
+ def _run_keyword_detection(data: io.BytesIO, keyword: str) -> bool:
43
+ try:
44
+ text = io.TextIOWrapper(data, encoding="utf-8")
45
+ for line in text:
46
+ if (
47
+ keyword in line
48
+ or re.search(keyword, line, flags=re.IGNORECASE) is not None
49
+ ):
50
+ return True
51
+ return False
52
+ except re.error:
53
+ return False
54
+ except UnicodeError:
55
+ return False
56
+
57
+
58
+ def _filter_files_by_keyword(
59
+ remote_directory: GenericRemoteDirectoryScope,
60
+ files: list[FileObjectData],
61
+ logger: JobLogger,
62
+ ) -> list[FileObjectData]:
63
+ if remote_directory.detection_keyword is None:
64
+ return files
65
+
66
+ filtered_files = []
67
+
68
+ for file in files:
69
+ extension = _get_extension(file.filename)
70
+
71
+ if extension not in (".txt", ".csv"):
72
+ raise NotImplementedError(
73
+ "keyword detection is only supported for csv, txt files"
74
+ )
75
+
76
+ if _run_keyword_detection(file.file_IO, remote_directory.detection_keyword):
77
+ filtered_files.append(file)
78
+
79
+ return filtered_files
80
+
81
+
82
+ def _filter_by_filename(
83
+ remote_directory: GenericRemoteDirectoryScope, files: list[FileSystemObject]
84
+ ) -> list[FileSystemObject]:
85
+ if remote_directory.filename_regex is None:
86
+ return files
87
+
88
+ return [
89
+ file
90
+ for file in files
91
+ if file.filename is not None
92
+ and re.search(remote_directory.filename_regex, file.filename)
93
+ ]
94
+
95
+
96
+ def _filter_by_file_extension(
97
+ remote_directory: GenericRemoteDirectoryScope, files: list[FileSystemObject]
98
+ ) -> list[FileSystemObject]:
99
+ if remote_directory.valid_file_extensions is None:
100
+ return files
101
+
102
+ return [
103
+ file
104
+ for file in files
105
+ if file.filename is not None
106
+ and os.path.splitext(file.filename)[-1]
107
+ in remote_directory.valid_file_extensions
108
+ ]
109
+
110
+
111
+ def _filter_by_max_files(
112
+ remote_directory: GenericRemoteDirectoryScope, files: list[FileSystemObject]
113
+ ) -> list[FileSystemObject]:
114
+ if remote_directory.max_files is None:
115
+ return files
116
+
117
+ return files[: remote_directory.max_files]
118
+
119
+
120
+ def _pull_remote_directory_data(
121
+ *,
122
+ filesystem_session: FileSystemSession,
123
+ remote_directory: GenericRemoteDirectoryScope,
124
+ logger: JobLogger,
125
+ ) -> list[FileObjectData]:
126
+ files_to_pull = filesystem_session.list_files(
127
+ dir_path=FileSystemFileReference(
128
+ filepath=remote_directory.src_path,
129
+ ),
130
+ recursive=remote_directory.recursive,
131
+ )
132
+ logger.log_info(
133
+ f"Pulled the following files {files_to_pull} from the remote directory {remote_directory}.",
134
+ )
135
+
136
+ files_to_pull = _filter_by_file_extension(remote_directory, files_to_pull)
137
+ files_to_pull = _filter_by_filename(remote_directory, files_to_pull)
138
+ files_to_pull = _filter_by_max_files(remote_directory, files_to_pull)
139
+
140
+ logger.log_info(
141
+ f"Accessing SFTP directory: {remote_directory.src_path} and pulling files: {', '.join([f.filename for f in files_to_pull if f.filename is not None])}",
142
+ )
143
+ return filesystem_session.download_files(files_to_pull)
144
+
145
+
146
+ def _filter_downloaded_file_data(
147
+ remote_directory: GenericRemoteDirectoryScope,
148
+ pulled_file_data: list[FileObjectData],
149
+ logger: JobLogger,
150
+ ) -> list[FileObjectData]:
151
+ filtered_file_data = _filter_files_by_keyword(
152
+ remote_directory=remote_directory, files=pulled_file_data, logger=logger
153
+ )
154
+ return filtered_file_data
155
+
156
+
157
+ def _move_files_post_upload(
158
+ *,
159
+ filesystem_session: FileSystemSession,
160
+ remote_directory_scope: GenericRemoteDirectoryScope,
161
+ success_file_paths: list[str],
162
+ failed_file_paths: list[str],
163
+ ) -> None:
164
+ success_file_transfers: list[FileTransfer] = []
165
+ appended_text = ""
166
+
167
+ if remote_directory_scope.prepend_date_on_archive:
168
+ appended_text = f"-{datetime.datetime.now(UTC).timestamp()}"
169
+
170
+ for file_path in success_file_paths:
171
+ filename = os.path.split(file_path)[-1]
172
+ root, extension = os.path.splitext(filename)
173
+ new_filename = f"{root}{appended_text}{extension}"
174
+ # format is source, dest in the tuple
175
+ success_file_transfers.append((
176
+ FileSystemFileReference(file_path),
177
+ FileSystemFileReference(
178
+ os.path.join(
179
+ remote_directory_scope.success_archive_path,
180
+ new_filename,
181
+ )
182
+ ),
183
+ ))
184
+
185
+ failed_file_transfers: list[FileTransfer] = []
186
+ for file_path in failed_file_paths:
187
+ filename = os.path.split(file_path)[-1]
188
+ root, extension = os.path.splitext(filename)
189
+ new_filename = f"{root}{appended_text}{extension}"
190
+ failed_file_transfers.append((
191
+ FileSystemFileReference(file_path),
192
+ FileSystemFileReference(
193
+ os.path.join(
194
+ remote_directory_scope.failure_archive_path,
195
+ new_filename,
196
+ )
197
+ ),
198
+ ))
199
+
200
+ filesystem_session.move_files([*success_file_transfers, *failed_file_transfers])
201
+
202
+
203
+ class GenericUploadJob(Job[None]):
204
+ def __init__(
205
+ self,
206
+ data_source: GenericUploadDataSource,
207
+ remote_directories: list[GenericRemoteDirectoryScope],
208
+ upload_strategy: GenericUploadStrategy,
209
+ ) -> None:
210
+ super().__init__()
211
+ self.remote_directories = remote_directories
212
+ self.upload_strategy = upload_strategy
213
+ self.data_source = data_source
214
+
215
+ @property
216
+ def payload_type(self) -> type[None]:
217
+ return type(None)
218
+
219
+ def _construct_filesystem_session(self, args: JobArguments) -> FileSystemSession:
220
+ match self.data_source:
221
+ case GenericUploadDataSourceSFTP():
222
+ if self.data_source.pem_secret is not None:
223
+ pem_secret = retrieve_secret(
224
+ self.data_source.pem_secret,
225
+ profile_metadata=args.profile_metadata,
226
+ )
227
+ pem_key = paramiko.RSAKey.from_private_key(io.StringIO(pem_secret))
228
+ sftp_config = FileSystemSFTPConfig(
229
+ ip=self.data_source.host,
230
+ username=self.data_source.username,
231
+ pem_path=None,
232
+ pem_key=pem_key,
233
+ )
234
+ elif self.data_source.password_secret is not None:
235
+ password_secret = retrieve_secret(
236
+ self.data_source.password_secret,
237
+ profile_metadata=args.profile_metadata,
238
+ )
239
+ sftp_config = FileSystemSFTPConfig(
240
+ ip=self.data_source.host,
241
+ username=self.data_source.username,
242
+ pem_path=None,
243
+ password=password_secret,
244
+ )
245
+ else:
246
+ raise ValueError(
247
+ "Either pem_secret or password_secret must be specified for sftp data source"
248
+ )
249
+ return SFTPSession(sftp_config=sftp_config)
250
+ case GenericUploadDataSourceS3():
251
+ if self.data_source.access_key_secret is not None:
252
+ secret_access_key = retrieve_secret(
253
+ self.data_source.access_key_secret,
254
+ profile_metadata=args.profile_metadata,
255
+ )
256
+ else:
257
+ secret_access_key = None
258
+
259
+ if self.data_source.endpoint_url is None:
260
+ assert self.data_source.cloud_provider is not None, (
261
+ "either cloud_provider or endpoint_url must be specified"
262
+ )
263
+ match self.data_source.cloud_provider:
264
+ case S3CloudProvider.AWS:
265
+ endpoint_url = "https://s3.amazonaws.com"
266
+ case S3CloudProvider.OVH:
267
+ assert self.data_source.region_name is not None, (
268
+ "region_name must be specified for cloud_provider OVH"
269
+ )
270
+ endpoint_url = f"https://s3.{self.data_source.region_name}.cloud.ovh.net"
271
+ else:
272
+ endpoint_url = self.data_source.endpoint_url
273
+
274
+ s3_config = FileSystemS3Config(
275
+ endpoint_url=endpoint_url,
276
+ bucket_name=self.data_source.bucket_name,
277
+ region_name=self.data_source.region_name,
278
+ access_key_id=self.data_source.access_key_id,
279
+ secret_access_key=secret_access_key,
280
+ session_token=None,
281
+ )
282
+
283
+ return S3Session(s3_config=s3_config)
284
+
285
+ def run_outer(self, args: JobArguments) -> JobResult:
286
+ client = args.client
287
+ batch_processor = args.batch_processor
288
+ logger = args.logger
289
+
290
+ with self._construct_filesystem_session(args) as filesystem_session:
291
+ files_to_upload: list[FileUpload] = []
292
+ for remote_directory in self.remote_directories:
293
+ pulled_file_data = _pull_remote_directory_data(
294
+ filesystem_session=filesystem_session,
295
+ remote_directory=remote_directory,
296
+ logger=logger,
297
+ )
298
+ filtered_file_data = _filter_downloaded_file_data(
299
+ remote_directory=remote_directory,
300
+ pulled_file_data=pulled_file_data,
301
+ logger=args.logger,
302
+ )
303
+ for file_data in filtered_file_data:
304
+ files_to_upload.append(
305
+ DataFileUpload(
306
+ data=io.BytesIO(file_data.file_data),
307
+ name=file_data.filename,
308
+ )
309
+ )
310
+ if not self.upload_strategy.skip_moving_files:
311
+ _move_files_post_upload(
312
+ filesystem_session=filesystem_session,
313
+ remote_directory_scope=remote_directory,
314
+ success_file_paths=[
315
+ file.filepath
316
+ if file.filepath is not None
317
+ else file.filename
318
+ for file in filtered_file_data
319
+ ],
320
+ # IMPROVE: use triggers/webhooks to mark failed files as failed
321
+ failed_file_paths=[],
322
+ )
323
+
324
+ uploaded_files = client.upload_files(file_uploads=files_to_upload)
325
+
326
+ file_ids = [file.file_id for file in uploaded_files]
327
+
328
+ for destination in self.upload_strategy.destinations:
329
+ for file_id in file_ids:
330
+ batch_processor.invoke_uploader(
331
+ file_id=file_id,
332
+ uploader_key=self.upload_strategy.uploader_key,
333
+ destination=destination,
334
+ )
335
+
336
+ return JobResult(success=True)
@@ -1,19 +1,25 @@
1
-
2
- import os
3
1
  import importlib
4
2
  import inspect
3
+ import os
4
+
5
5
  from uncountable.integration.job import Job
6
- from uncountable.integration.types import JobExecutorScript, ProfileMetadata
6
+ from uncountable.types.job_definition_t import JobExecutorScript, ProfileMetadata
7
7
 
8
8
 
9
- def resolve_script_executor(executor: JobExecutorScript, profile_metadata: ProfileMetadata) -> type[Job]:
10
- job_module_path = ".".join([os.environ["UNC_PROFILES_MODULE"], profile_metadata.name, executor.import_path])
9
+ def resolve_script_executor(
10
+ executor: JobExecutorScript, profile_metadata: ProfileMetadata
11
+ ) -> Job:
12
+ job_module_path = ".".join([
13
+ os.environ["UNC_PROFILES_MODULE"],
14
+ profile_metadata.name,
15
+ executor.import_path,
16
+ ])
11
17
  job_module = importlib.import_module(job_module_path)
12
- found_jobs: list[type[Job]] = []
18
+ found_jobs: list[Job] = []
13
19
  for _, job_class in inspect.getmembers(job_module, inspect.isclass):
14
20
  if getattr(job_class, "_unc_job_registered", False):
15
21
  found_jobs.append(job_class())
16
- assert (
17
- len(found_jobs) == 1
18
- ), f"expected exactly one job class in {executor.import_path}, found {len(found_jobs)}"
22
+ assert len(found_jobs) == 1, (
23
+ f"expected exactly one job class in {executor.import_path}, found {len(found_jobs)}"
24
+ )
19
25
  return found_jobs[0]
@@ -0,0 +1,5 @@
1
+ # CLOSED MODULE
2
+
3
+ from .types import GenericHttpRequest as GenericHttpRequest
4
+ from .types import GenericHttpResponse as GenericHttpResponse
5
+ from .types import HttpException as HttpException
@@ -0,0 +1,69 @@
1
+ import base64
2
+ import functools
3
+ import json
4
+ from dataclasses import dataclass
5
+
6
+ from flask.wrappers import Response
7
+
8
+
9
+ class HttpException(Exception):
10
+ error_code: int
11
+ message: str
12
+
13
+ def __init__(self, *, error_code: int, message: str) -> None:
14
+ self.error_code = error_code
15
+ self.message = message
16
+
17
+ @staticmethod
18
+ def payload_failed_signature() -> "HttpException":
19
+ return HttpException(
20
+ error_code=401, message="webhook payload did not match signature"
21
+ )
22
+
23
+ @staticmethod
24
+ def no_signature_passed() -> "HttpException":
25
+ return HttpException(error_code=400, message="missing signature")
26
+
27
+ @staticmethod
28
+ def body_parse_error() -> "HttpException":
29
+ return HttpException(error_code=400, message="body parse error")
30
+
31
+ @staticmethod
32
+ def unknown_error() -> "HttpException":
33
+ return HttpException(error_code=500, message="internal server error")
34
+
35
+ @staticmethod
36
+ def configuration_error(
37
+ message: str = "internal configuration error",
38
+ ) -> "HttpException":
39
+ return HttpException(error_code=500, message=message)
40
+
41
+ def __str__(self) -> str:
42
+ return f"[{self.error_code}]: {self.message}"
43
+
44
+ def make_error_response(self) -> Response:
45
+ return Response(
46
+ status=self.error_code,
47
+ response=json.dumps({"error": {"message": str(self)}}),
48
+ )
49
+
50
+
51
+ @dataclass(kw_only=True, frozen=True)
52
+ class GenericHttpRequest:
53
+ body_base64: str
54
+ headers: dict[str, str]
55
+
56
+ @functools.cached_property
57
+ def body_bytes(self) -> bytes:
58
+ return base64.b64decode(self.body_base64)
59
+
60
+ @functools.cached_property
61
+ def body_text(self) -> str:
62
+ return self.body_bytes.decode()
63
+
64
+
65
+ @dataclass(kw_only=True)
66
+ class GenericHttpResponse:
67
+ response: str
68
+ status_code: int
69
+ headers: dict[str, str] | None = None