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,67 @@
1
+ import functools
2
+ from importlib import resources
3
+
4
+ from pkgs.argument_parser import CachedParser
5
+ from uncountable.core import environment
6
+ from uncountable.types import integration_server_t, job_definition_t
7
+
8
+ profile_parser = CachedParser(job_definition_t.ProfileDefinition)
9
+
10
+ _DEFAULT_PROFILE_ENV = integration_server_t.IntegrationEnvironment.PROD
11
+ _IGNORED_PROFILE_FOLDERS = ["__pycache__"]
12
+
13
+
14
+ @functools.cache
15
+ def load_profiles() -> list[job_definition_t.ProfileMetadata]:
16
+ profiles_module = environment.get_profiles_module()
17
+ integration_envs = environment.get_integration_envs()
18
+ profiles = [
19
+ entry
20
+ for entry in resources.files(profiles_module).iterdir()
21
+ if entry.is_dir() and entry.name not in _IGNORED_PROFILE_FOLDERS
22
+ ]
23
+ profile_details: list[job_definition_t.ProfileMetadata] = []
24
+ seen_job_ids: set[str] = set()
25
+ for profile_file in profiles:
26
+ profile_name = profile_file.name
27
+ try:
28
+ definition = profile_parser.parse_yaml_resource(
29
+ package=f"{profiles_module}.{profile_name}",
30
+ resource="profile.yaml",
31
+ )
32
+ for job in definition.jobs:
33
+ if job.id in seen_job_ids:
34
+ raise Exception(f"multiple jobs with id {job.id}")
35
+ seen_job_ids.add(job.id)
36
+
37
+ if definition.environments is not None:
38
+ for integration_env in integration_envs:
39
+ environment_config = definition.environments.get(integration_env)
40
+ if environment_config is not None:
41
+ profile_details.append(
42
+ job_definition_t.ProfileMetadata(
43
+ name=profile_name,
44
+ jobs=definition.jobs,
45
+ base_url=environment_config.base_url,
46
+ auth_retrieval=environment_config.auth_retrieval,
47
+ client_options=environment_config.client_options,
48
+ )
49
+ )
50
+ elif _DEFAULT_PROFILE_ENV in integration_envs:
51
+ assert (
52
+ definition.base_url is not None
53
+ and definition.auth_retrieval is not None
54
+ ), f"define environments in profile.yaml for {profile_name}"
55
+ profile_details.append(
56
+ job_definition_t.ProfileMetadata(
57
+ name=profile_name,
58
+ jobs=definition.jobs,
59
+ base_url=definition.base_url,
60
+ auth_retrieval=definition.auth_retrieval,
61
+ client_options=definition.client_options,
62
+ )
63
+ )
64
+ except FileNotFoundError as e:
65
+ print(f"WARN: profile.yaml not found for {profile_name}", e)
66
+ continue
67
+ return profile_details
@@ -0,0 +1,199 @@
1
+ import datetime
2
+ import multiprocessing
3
+ import subprocess
4
+ import sys
5
+ import time
6
+ from dataclasses import dataclass
7
+ from datetime import UTC
8
+ from enum import StrEnum
9
+
10
+ from opentelemetry.trace import get_current_span
11
+
12
+ from uncountable.core.environment import get_local_admin_server_port
13
+ from uncountable.integration.entrypoint import main as cron_target
14
+ from uncountable.integration.queue_runner.command_server import (
15
+ CommandServerTimeout,
16
+ check_health,
17
+ )
18
+ from uncountable.integration.queue_runner.queue_runner import start_queue_runner
19
+ from uncountable.integration.telemetry import Logger
20
+
21
+ SHUTDOWN_TIMEOUT_SECS = 30
22
+
23
+ AnyProcess = multiprocessing.Process | subprocess.Popen[bytes]
24
+
25
+
26
+ class ProcessName(StrEnum):
27
+ QUEUE_RUNNER = "queue_runner"
28
+ CRON_SERVER = "cron_server"
29
+ UWSGI = "uwsgi"
30
+
31
+
32
+ @dataclass(kw_only=True)
33
+ class ProcessInfo:
34
+ name: ProcessName
35
+ process: AnyProcess
36
+
37
+ @property
38
+ def is_alive(self) -> bool:
39
+ match self.process:
40
+ case multiprocessing.Process():
41
+ return self.process.is_alive()
42
+ case subprocess.Popen():
43
+ return self.process.poll() is None
44
+
45
+ @property
46
+ def pid(self) -> int | None:
47
+ return self.process.pid
48
+
49
+ @property
50
+ def exitcode(self) -> int | None:
51
+ match self.process:
52
+ case multiprocessing.Process():
53
+ return self.process.exitcode
54
+ case subprocess.Popen():
55
+ return self.process.poll()
56
+
57
+
58
+ def handle_shutdown(logger: Logger, processes: dict[ProcessName, ProcessInfo]) -> None:
59
+ logger.log_info("received shutdown command, shutting down sub-processes")
60
+ for proc_info in processes.values():
61
+ if proc_info.is_alive:
62
+ proc_info.process.terminate()
63
+
64
+ shutdown_start = time.time()
65
+ still_living_processes = list(processes.values())
66
+ while (
67
+ time.time() - shutdown_start < SHUTDOWN_TIMEOUT_SECS
68
+ and len(still_living_processes) > 0
69
+ ):
70
+ current_loop_processes = [*still_living_processes]
71
+ logger.log_info(
72
+ "waiting for sub-processes to shut down",
73
+ attributes={
74
+ "still_living_processes": [
75
+ proc_info.name for proc_info in still_living_processes
76
+ ]
77
+ },
78
+ )
79
+ still_living_processes = []
80
+ for proc_info in current_loop_processes:
81
+ if not proc_info.is_alive:
82
+ logger.log_info(f"{proc_info.name} shut down successfully")
83
+ else:
84
+ still_living_processes.append(proc_info)
85
+ time.sleep(1)
86
+
87
+ for proc_info in still_living_processes:
88
+ logger.log_warning(
89
+ f"{proc_info.name} failed to shut down after {SHUTDOWN_TIMEOUT_SECS} seconds, forcefully terminating"
90
+ )
91
+ proc_info.process.kill()
92
+
93
+
94
+ def restart_process(
95
+ logger: Logger, proc_info: ProcessInfo, processes: dict[ProcessName, ProcessInfo]
96
+ ) -> None:
97
+ logger.log_error(
98
+ f"process {proc_info.name} shut down unexpectedly - exit code {proc_info.exitcode}. Restarting..."
99
+ )
100
+
101
+ match proc_info.name:
102
+ case ProcessName.QUEUE_RUNNER:
103
+ queue_proc = multiprocessing.Process(target=start_queue_runner)
104
+ queue_proc.start()
105
+ new_info = ProcessInfo(name=ProcessName.QUEUE_RUNNER, process=queue_proc)
106
+ processes[ProcessName.QUEUE_RUNNER] = new_info
107
+ try:
108
+ _wait_queue_runner_online()
109
+ logger.log_info("queue runner restarted successfully")
110
+ except Exception as e:
111
+ logger.log_exception(e)
112
+ logger.log_error(
113
+ "queue runner failed to restart, shutting down scheduler"
114
+ )
115
+ handle_shutdown(logger, processes)
116
+ sys.exit(1)
117
+
118
+ case ProcessName.CRON_SERVER:
119
+ cron_proc = multiprocessing.Process(target=cron_target)
120
+ cron_proc.start()
121
+ new_info = ProcessInfo(name=ProcessName.CRON_SERVER, process=cron_proc)
122
+ processes[ProcessName.CRON_SERVER] = new_info
123
+ logger.log_info("cron server restarted successfully")
124
+
125
+ case ProcessName.UWSGI:
126
+ uwsgi_proc: AnyProcess = subprocess.Popen(["uwsgi", "--die-on-term"])
127
+ new_info = ProcessInfo(name=ProcessName.UWSGI, process=uwsgi_proc)
128
+ processes[ProcessName.UWSGI] = new_info
129
+ logger.log_info("uwsgi restarted successfully")
130
+
131
+
132
+ def check_process_alive(
133
+ logger: Logger, processes: dict[ProcessName, ProcessInfo]
134
+ ) -> None:
135
+ for proc_info in processes.values():
136
+ if not proc_info.is_alive:
137
+ restart_process(logger, proc_info, processes)
138
+
139
+
140
+ def _wait_queue_runner_online() -> None:
141
+ MAX_QUEUE_RUNNER_HEALTH_CHECKS = 10
142
+ QUEUE_RUNNER_HEALTH_CHECK_DELAY_SECS = 1
143
+
144
+ num_attempts = 0
145
+ before = datetime.datetime.now(UTC)
146
+ while num_attempts < MAX_QUEUE_RUNNER_HEALTH_CHECKS:
147
+ try:
148
+ if check_health(port=get_local_admin_server_port()):
149
+ return
150
+ except CommandServerTimeout:
151
+ pass
152
+ num_attempts += 1
153
+ time.sleep(QUEUE_RUNNER_HEALTH_CHECK_DELAY_SECS)
154
+ after = datetime.datetime.now(UTC)
155
+ duration_secs = (after - before).seconds
156
+ raise Exception(f"queue runner failed to come online after {duration_secs} seconds")
157
+
158
+
159
+ def main() -> None:
160
+ logger = Logger(get_current_span())
161
+ processes: dict[ProcessName, ProcessInfo] = {}
162
+
163
+ multiprocessing.set_start_method("forkserver")
164
+
165
+ def add_process(process: ProcessInfo) -> None:
166
+ processes[process.name] = process
167
+ logger.log_info(f"started process {process.name}")
168
+
169
+ runner_process = multiprocessing.Process(target=start_queue_runner)
170
+ runner_process.start()
171
+ add_process(ProcessInfo(name=ProcessName.QUEUE_RUNNER, process=runner_process))
172
+
173
+ try:
174
+ _wait_queue_runner_online()
175
+ except Exception as e:
176
+ logger.log_exception(e)
177
+ handle_shutdown(logger, processes=processes)
178
+ return
179
+
180
+ cron_process = multiprocessing.Process(target=cron_target)
181
+ cron_process.start()
182
+ add_process(ProcessInfo(name=ProcessName.CRON_SERVER, process=cron_process))
183
+
184
+ uwsgi_process = subprocess.Popen([
185
+ "uwsgi",
186
+ "--die-on-term",
187
+ ])
188
+ add_process(ProcessInfo(name=ProcessName.UWSGI, process=uwsgi_process))
189
+
190
+ try:
191
+ while True:
192
+ check_process_alive(logger, processes=processes)
193
+ time.sleep(1)
194
+ except KeyboardInterrupt:
195
+ handle_shutdown(logger, processes=processes)
196
+
197
+
198
+ if __name__ == "__main__":
199
+ main()
@@ -0,0 +1,3 @@
1
+ from .retrieve_secret import retrieve_secret
2
+
3
+ __all__: list[str] = ["retrieve_secret"]
@@ -0,0 +1,93 @@
1
+ import base64
2
+ import functools
3
+ import json
4
+ import os
5
+
6
+ import boto3
7
+
8
+ from pkgs.argument_parser import CachedParser
9
+ from uncountable.types import overrides_t
10
+ from uncountable.types.job_definition_t import ProfileMetadata
11
+ from uncountable.types.secret_retrieval_t import (
12
+ SecretRetrieval,
13
+ SecretRetrievalAWS,
14
+ SecretRetrievalEnv,
15
+ )
16
+
17
+
18
+ class SecretRetrievalError(Exception):
19
+ def __init__(
20
+ self, secret_retrieval: SecretRetrieval, message: str | None = None
21
+ ) -> None:
22
+ self.secret_retrieval = secret_retrieval
23
+ self.message = message
24
+
25
+ def __str__(self) -> str:
26
+ append_message = ""
27
+ if self.message is not None:
28
+ append_message = f": {self.message}"
29
+ return f"{self.secret_retrieval.type} secret retrieval failed{append_message}"
30
+
31
+
32
+ @functools.cache
33
+ def _get_aws_secret(*, secret_name: str, region_name: str, sub_key: str | None) -> str:
34
+ client = boto3.client("secretsmanager", region_name=region_name)
35
+ response = client.get_secret_value(SecretId=secret_name)
36
+
37
+ if "SecretString" in response:
38
+ secret = response["SecretString"]
39
+ else:
40
+ secret = base64.b64decode(response["SecretBinary"])
41
+
42
+ value = json.loads(secret)
43
+
44
+ if sub_key is not None:
45
+ assert isinstance(value, dict)
46
+ return str(value[sub_key])
47
+ else:
48
+ return str(value)
49
+
50
+
51
+ @functools.cache
52
+ def _load_secret_overrides(profile_name: str) -> dict[SecretRetrieval, str]:
53
+ overrides_parser = CachedParser(overrides_t.Overrides)
54
+ profiles_module = os.environ["UNC_PROFILES_MODULE"]
55
+ try:
56
+ overrides = overrides_parser.parse_yaml_resource(
57
+ package=f"{profiles_module}.{profile_name}",
58
+ resource="local_overrides.yaml",
59
+ )
60
+ return {
61
+ override.secret_retrieval: override.value for override in overrides.secrets
62
+ }
63
+ except FileNotFoundError:
64
+ return {}
65
+
66
+
67
+ def retrieve_secret(
68
+ secret_retrieval: SecretRetrieval, profile_metadata: ProfileMetadata
69
+ ) -> str:
70
+ value_from_override = _load_secret_overrides(profile_metadata.name).get(
71
+ secret_retrieval
72
+ )
73
+ if value_from_override is not None:
74
+ return value_from_override
75
+
76
+ match secret_retrieval:
77
+ case SecretRetrievalEnv():
78
+ env_name = f"UNC_{profile_metadata.name.upper()}_{secret_retrieval.env_key.upper()}"
79
+ secret = os.environ.get(env_name)
80
+ if secret is None:
81
+ raise SecretRetrievalError(
82
+ secret_retrieval, f"environment variable {env_name} missing"
83
+ )
84
+ return secret
85
+ case SecretRetrievalAWS():
86
+ try:
87
+ return _get_aws_secret(
88
+ secret_name=secret_retrieval.secret_name,
89
+ region_name=secret_retrieval.region,
90
+ sub_key=secret_retrieval.sub_key,
91
+ )
92
+ except Exception as e:
93
+ raise SecretRetrievalError(secret_retrieval) from e
@@ -1,29 +1,43 @@
1
1
  import signal
2
- from types import TracebackType
3
2
  from dataclasses import asdict
4
- from typing import Optional, assert_never
3
+ from types import TracebackType
4
+ from typing import assert_never
5
+
6
+ from apscheduler.executors.pool import ThreadPoolExecutor
7
+ from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
5
8
  from apscheduler.schedulers.background import BackgroundScheduler
6
9
  from apscheduler.schedulers.base import BaseScheduler
7
- from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
8
- from apscheduler.executors.pool import ThreadPoolExecutor
9
- from uncountable.integration.cron import CronJobArgs, cron_job_executor
10
10
  from apscheduler.triggers.cron import CronTrigger
11
+ from opentelemetry.trace import get_current_span
11
12
  from sqlalchemy.engine.base import Engine
12
13
 
13
- from uncountable.integration.types import (
14
- AuthRetrieval,
14
+ from uncountable.core.environment import get_local_admin_server_port
15
+ from uncountable.integration.cron import CronJobArgs, cron_job_executor
16
+ from uncountable.integration.queue_runner.command_server.command_client import (
17
+ send_vaccuum_queued_jobs_message,
18
+ )
19
+ from uncountable.integration.telemetry import Logger
20
+ from uncountable.types import base_t, job_definition_t
21
+ from uncountable.types.job_definition_t import (
15
22
  CronJobDefinition,
16
- JobDefinition,
17
- ProfileMetadata,
23
+ HttpJobDefinitionBase,
18
24
  )
19
25
 
20
-
21
26
  _MAX_APSCHEDULER_CONCURRENT_JOBS = 1
22
27
 
28
+ VACCUUM_QUEUED_JOBS_JOB_ID = "vacuum_queued_jobs"
29
+
30
+ STATIC_JOB_IDS = {VACCUUM_QUEUED_JOBS_JOB_ID}
31
+
32
+
33
+ def vaccuum_queued_jobs() -> None:
34
+ send_vaccuum_queued_jobs_message(port=get_local_admin_server_port())
35
+
23
36
 
24
37
  class IntegrationServer:
25
38
  _scheduler: BaseScheduler
26
39
  _engine: Engine
40
+ _server_logger: Logger
27
41
 
28
42
  def __init__(self, engine: Engine) -> None:
29
43
  self._engine = engine
@@ -32,49 +46,83 @@ class IntegrationServer:
32
46
  jobstores={"default": SQLAlchemyJobStore(engine=engine)},
33
47
  executors={"default": ThreadPoolExecutor(_MAX_APSCHEDULER_CONCURRENT_JOBS)},
34
48
  )
49
+ self._server_logger = Logger(get_current_span())
35
50
 
36
- def register_profile(
37
- self,
38
- *,
39
- profile_name: str,
40
- base_url: str,
41
- auth_retrieval: AuthRetrieval,
42
- jobs: list[JobDefinition],
43
- ) -> None:
44
- for job_defn in jobs:
45
- profile_metadata = ProfileMetadata(
46
- name=profile_name, auth_retrieval=auth_retrieval, base_url=base_url
47
- )
48
- match job_defn:
49
- case CronJobDefinition():
50
- # Add to ap scheduler
51
- job_kwargs = asdict(
52
- CronJobArgs(
53
- definition=job_defn, profile_metadata=profile_metadata
54
- )
55
- )
56
- existing_job = self._scheduler.get_job(job_defn.id)
57
- if existing_job is not None:
58
- existing_job.modify(
59
- name=job_defn.name,
60
- kwargs=job_kwargs,
61
- )
62
- existing_job.reschedule(
63
- CronTrigger.from_crontab(job_defn.cron_spec)
64
- )
65
- else:
66
- self._scheduler.add_job(
67
- cron_job_executor,
68
- # IMPROVE: reconsider these defaults
69
- max_instances=1,
70
- coalesce=True,
71
- trigger=CronTrigger.from_crontab(job_defn.cron_spec),
72
- name=job_defn.name,
73
- id=job_defn.id,
74
- kwargs=job_kwargs,
51
+ def _register_static_jobs(self) -> None:
52
+ all_job_ids = {job.id for job in self._scheduler.get_jobs()}
53
+ if VACCUUM_QUEUED_JOBS_JOB_ID in all_job_ids:
54
+ self._scheduler.remove_job(VACCUUM_QUEUED_JOBS_JOB_ID)
55
+
56
+ self._scheduler.add_job(
57
+ vaccuum_queued_jobs,
58
+ max_instances=1,
59
+ coalesce=True,
60
+ trigger=CronTrigger.from_crontab("5 4 * * 4"),
61
+ name="Vaccuum queued jobs",
62
+ id=VACCUUM_QUEUED_JOBS_JOB_ID,
63
+ kwargs={},
64
+ misfire_grace_time=None,
65
+ )
66
+
67
+ def register_jobs(self, profiles: list[job_definition_t.ProfileMetadata]) -> None:
68
+ valid_job_ids: set[str] = set()
69
+ for profile_metadata in profiles:
70
+ for job_defn in profile_metadata.jobs:
71
+ valid_job_ids.add(job_defn.id)
72
+ match job_defn:
73
+ case CronJobDefinition():
74
+ # Add to ap scheduler
75
+ job_kwargs = asdict(
76
+ CronJobArgs(
77
+ definition=job_defn, profile_metadata=profile_metadata
78
+ )
75
79
  )
76
- case _:
77
- assert_never(job_defn.trigger)
80
+ try:
81
+ existing_job = self._scheduler.get_job(job_defn.id)
82
+ except ValueError as e:
83
+ self._server_logger.log_warning(
84
+ f"could not reconstitute job {job_defn.id}: {e}"
85
+ )
86
+ self._scheduler.remove_job(job_defn.id)
87
+ existing_job = None
88
+ if existing_job is not None:
89
+ existing_job.modify(
90
+ name=job_defn.name,
91
+ kwargs=job_kwargs,
92
+ misfire_grace_time=None,
93
+ )
94
+ existing_job.reschedule(
95
+ CronTrigger.from_crontab(job_defn.cron_spec)
96
+ )
97
+ if not job_defn.enabled:
98
+ existing_job.pause()
99
+ else:
100
+ existing_job.resume()
101
+ else:
102
+ job_opts: dict[str, base_t.JsonValue] = {}
103
+ if not job_defn.enabled:
104
+ job_opts["next_run_time"] = None
105
+ self._scheduler.add_job(
106
+ cron_job_executor,
107
+ # IMPROVE: reconsider these defaults
108
+ max_instances=1,
109
+ coalesce=True,
110
+ trigger=CronTrigger.from_crontab(job_defn.cron_spec),
111
+ name=job_defn.name,
112
+ id=job_defn.id,
113
+ kwargs=job_kwargs,
114
+ misfire_grace_time=None,
115
+ **job_opts,
116
+ )
117
+ case HttpJobDefinitionBase():
118
+ pass
119
+ case _:
120
+ assert_never(job_defn)
121
+ all_job_ids = {job.id for job in self._scheduler.get_jobs()}
122
+ invalid_job_ids = all_job_ids.difference(valid_job_ids.union(STATIC_JOB_IDS))
123
+
124
+ for job_id in invalid_job_ids:
125
+ self._scheduler.remove_job(job_id)
78
126
 
79
127
  def serve_forever(self) -> None:
80
128
  signal.pause()
@@ -87,12 +135,13 @@ class IntegrationServer:
87
135
 
88
136
  def __enter__(self) -> "IntegrationServer":
89
137
  self._start_apscheduler()
138
+ self._register_static_jobs()
90
139
  return self
91
140
 
92
141
  def __exit__(
93
142
  self,
94
- exc_type: Optional[type[BaseException]],
95
- exc_val: Optional[BaseException],
96
- exc_tb: Optional[TracebackType],
143
+ exc_type: type[BaseException] | None,
144
+ exc_val: BaseException | None,
145
+ exc_tb: TracebackType | None,
97
146
  ) -> None:
98
147
  self._stop_apscheduler()