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,250 @@
1
+ import datetime
2
+ import uuid
3
+ from datetime import UTC
4
+
5
+ from sqlalchemy import delete, insert, or_, select, text, update
6
+ from sqlalchemy.engine import Engine
7
+
8
+ from pkgs.argument_parser import CachedParser
9
+ from pkgs.serialization_util import serialize_for_storage
10
+ from uncountable.integration.db.session import DBSessionMaker
11
+ from uncountable.integration.queue_runner.datastore.interface import Datastore
12
+ from uncountable.integration.queue_runner.datastore.model import Base, QueuedJob
13
+ from uncountable.types import queued_job_t
14
+
15
+ queued_job_payload_parser = CachedParser(queued_job_t.QueuedJobPayload)
16
+
17
+ MAX_QUEUE_WINDOW_DAYS = 30
18
+
19
+
20
+ class DatastoreSqlite(Datastore):
21
+ def __init__(self, session_maker: DBSessionMaker) -> None:
22
+ self.session_maker = session_maker
23
+ super().__init__()
24
+
25
+ @classmethod
26
+ def setup(cls, engine: Engine) -> None:
27
+ Base.metadata.create_all(engine)
28
+ with engine.connect() as connection:
29
+ if not bool(
30
+ connection.execute(
31
+ text(
32
+ "select exists (select 1 from pragma_table_info('queued_jobs') where name='status');"
33
+ )
34
+ ).scalar()
35
+ ):
36
+ connection.execute(
37
+ text("alter table queued_jobs add column status VARCHAR")
38
+ )
39
+
40
+ def add_job_to_queue(
41
+ self, job_payload: queued_job_t.QueuedJobPayload, job_ref_name: str
42
+ ) -> queued_job_t.QueuedJob:
43
+ with self.session_maker() as session:
44
+ serialized_payload = serialize_for_storage(job_payload)
45
+ queued_job_uuid = str(uuid.uuid4())
46
+ num_attempts = 0
47
+ submitted_at = datetime.datetime.now(UTC)
48
+ insert_stmt = insert(QueuedJob).values({
49
+ QueuedJob.id.key: queued_job_uuid,
50
+ QueuedJob.job_ref_name.key: job_ref_name,
51
+ QueuedJob.payload.key: serialized_payload,
52
+ QueuedJob.status.key: queued_job_t.JobStatus.QUEUED,
53
+ QueuedJob.num_attempts: num_attempts,
54
+ QueuedJob.submitted_at: submitted_at,
55
+ })
56
+ session.execute(insert_stmt)
57
+ return queued_job_t.QueuedJob(
58
+ queued_job_uuid=queued_job_uuid,
59
+ job_ref_name=job_ref_name,
60
+ payload=job_payload,
61
+ status=queued_job_t.JobStatus.QUEUED,
62
+ submitted_at=submitted_at,
63
+ num_attempts=num_attempts,
64
+ )
65
+
66
+ def retry_job(
67
+ self,
68
+ queued_job_uuid: str,
69
+ ) -> queued_job_t.QueuedJob | None:
70
+ with self.session_maker() as session:
71
+ select_stmt = select(
72
+ QueuedJob.id,
73
+ QueuedJob.payload,
74
+ QueuedJob.num_attempts,
75
+ QueuedJob.job_ref_name,
76
+ QueuedJob.status,
77
+ QueuedJob.submitted_at,
78
+ ).filter(QueuedJob.id == queued_job_uuid)
79
+ existing_job = session.execute(select_stmt).one_or_none()
80
+
81
+ if (
82
+ existing_job is None
83
+ or existing_job.status != queued_job_t.JobStatus.FAILED
84
+ ):
85
+ return None
86
+
87
+ update_stmt = (
88
+ update(QueuedJob)
89
+ .values({QueuedJob.status.key: queued_job_t.JobStatus.QUEUED})
90
+ .filter(QueuedJob.id == queued_job_uuid)
91
+ )
92
+ session.execute(update_stmt)
93
+
94
+ return queued_job_t.QueuedJob(
95
+ queued_job_uuid=existing_job.id,
96
+ job_ref_name=existing_job.job_ref_name,
97
+ num_attempts=existing_job.num_attempts,
98
+ status=queued_job_t.JobStatus.QUEUED,
99
+ submitted_at=existing_job.submitted_at,
100
+ payload=queued_job_payload_parser.parse_storage(existing_job.payload),
101
+ )
102
+
103
+ def increment_num_attempts(self, queued_job_uuid: str) -> int:
104
+ with self.session_maker() as session:
105
+ update_stmt = (
106
+ update(QueuedJob)
107
+ .values({QueuedJob.num_attempts.key: QueuedJob.num_attempts + 1})
108
+ .filter(QueuedJob.id == queued_job_uuid)
109
+ )
110
+ session.execute(update_stmt)
111
+ session.flush()
112
+ # IMPROVE: python3's sqlite does not support the RETURNING clause
113
+ select_stmt = select(QueuedJob.num_attempts).filter(
114
+ QueuedJob.id == queued_job_uuid
115
+ )
116
+ return int(session.execute(select_stmt).one().num_attempts)
117
+
118
+ def remove_job_from_queue(self, queued_job_uuid: str) -> None:
119
+ with self.session_maker() as session:
120
+ delete_stmt = delete(QueuedJob).filter(QueuedJob.id == queued_job_uuid)
121
+ session.execute(delete_stmt)
122
+
123
+ def update_job_status(
124
+ self, queued_job_uuid: str, status: queued_job_t.JobStatus
125
+ ) -> None:
126
+ with self.session_maker() as session:
127
+ update_stmt = (
128
+ update(QueuedJob)
129
+ .values({QueuedJob.status.key: status})
130
+ .filter(QueuedJob.id == queued_job_uuid)
131
+ )
132
+ session.execute(update_stmt)
133
+
134
+ def list_queued_job_metadata(
135
+ self, offset: int = 0, limit: int | None = 100
136
+ ) -> list[queued_job_t.QueuedJobMetadata]:
137
+ with self.session_maker() as session:
138
+ select_statement = (
139
+ select(
140
+ QueuedJob.id,
141
+ QueuedJob.job_ref_name,
142
+ QueuedJob.num_attempts,
143
+ QueuedJob.status,
144
+ QueuedJob.submitted_at,
145
+ )
146
+ .order_by(QueuedJob.submitted_at)
147
+ .offset(offset)
148
+ .limit(limit)
149
+ )
150
+
151
+ queued_job_metadata: list[queued_job_t.QueuedJobMetadata] = [
152
+ queued_job_t.QueuedJobMetadata(
153
+ queued_job_uuid=row.id,
154
+ job_ref_name=row.job_ref_name,
155
+ num_attempts=row.num_attempts,
156
+ status=row.status or queued_job_t.JobStatus.QUEUED,
157
+ submitted_at=row.submitted_at,
158
+ )
159
+ for row in session.execute(select_statement)
160
+ ]
161
+
162
+ return queued_job_metadata
163
+
164
+ def get_next_queued_job_for_ref_name(
165
+ self, job_ref_name: str
166
+ ) -> queued_job_t.QueuedJob | None:
167
+ with self.session_maker() as session:
168
+ select_stmt = (
169
+ select(
170
+ QueuedJob.id,
171
+ QueuedJob.payload,
172
+ QueuedJob.num_attempts,
173
+ QueuedJob.job_ref_name,
174
+ QueuedJob.status,
175
+ QueuedJob.submitted_at,
176
+ )
177
+ .filter(QueuedJob.job_ref_name == job_ref_name)
178
+ .filter(
179
+ or_(
180
+ QueuedJob.status == queued_job_t.JobStatus.QUEUED,
181
+ QueuedJob.status.is_(None),
182
+ )
183
+ )
184
+ .limit(1)
185
+ .order_by(QueuedJob.submitted_at)
186
+ )
187
+
188
+ for row in session.execute(select_stmt):
189
+ parsed_payload = queued_job_payload_parser.parse_storage(row.payload)
190
+ return queued_job_t.QueuedJob(
191
+ queued_job_uuid=row.id,
192
+ job_ref_name=row.job_ref_name,
193
+ num_attempts=row.num_attempts,
194
+ status=row.status or queued_job_t.JobStatus.QUEUED,
195
+ submitted_at=row.submitted_at,
196
+ payload=parsed_payload,
197
+ )
198
+
199
+ return None
200
+
201
+ def load_job_queue(self) -> list[queued_job_t.QueuedJob]:
202
+ with self.session_maker() as session:
203
+ select_stmt = (
204
+ select(
205
+ QueuedJob.id,
206
+ QueuedJob.payload,
207
+ QueuedJob.num_attempts,
208
+ QueuedJob.job_ref_name,
209
+ QueuedJob.status,
210
+ QueuedJob.submitted_at,
211
+ )
212
+ .filter(
213
+ or_(
214
+ QueuedJob.status == queued_job_t.JobStatus.QUEUED,
215
+ QueuedJob.status.is_(None),
216
+ )
217
+ )
218
+ .order_by(QueuedJob.submitted_at)
219
+ )
220
+
221
+ queued_jobs: list[queued_job_t.QueuedJob] = []
222
+ for row in session.execute(select_stmt):
223
+ parsed_payload = queued_job_payload_parser.parse_storage(row.payload)
224
+ queued_jobs.append(
225
+ queued_job_t.QueuedJob(
226
+ queued_job_uuid=row.id,
227
+ job_ref_name=row.job_ref_name,
228
+ num_attempts=row.num_attempts,
229
+ status=row.status or queued_job_t.JobStatus.QUEUED,
230
+ submitted_at=row.submitted_at,
231
+ payload=parsed_payload,
232
+ )
233
+ )
234
+
235
+ return queued_jobs
236
+
237
+ def vaccuum_queued_jobs(self) -> None:
238
+ with self.session_maker() as session:
239
+ delete_stmt = (
240
+ delete(QueuedJob)
241
+ .filter(QueuedJob.status == queued_job_t.JobStatus.QUEUED)
242
+ .filter(
243
+ QueuedJob.submitted_at
244
+ <= (
245
+ datetime.datetime.now(UTC)
246
+ - datetime.timedelta(days=MAX_QUEUE_WINDOW_DAYS)
247
+ )
248
+ )
249
+ )
250
+ session.execute(delete_stmt)
@@ -0,0 +1,29 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from uncountable.types import queued_job_t
4
+
5
+
6
+ class Datastore(ABC):
7
+ @abstractmethod
8
+ def add_job_to_queue(
9
+ self, job_payload: queued_job_t.QueuedJobPayload, job_ref_name: str
10
+ ) -> queued_job_t.QueuedJob: ...
11
+
12
+ @abstractmethod
13
+ def remove_job_from_queue(self, queued_job_uuid: str) -> None: ...
14
+
15
+ @abstractmethod
16
+ def increment_num_attempts(self, queued_job_uuid: str) -> int: ...
17
+
18
+ @abstractmethod
19
+ def load_job_queue(self) -> list[queued_job_t.QueuedJob]: ...
20
+
21
+ @abstractmethod
22
+ def get_next_queued_job_for_ref_name(
23
+ self, job_ref_name: str
24
+ ) -> queued_job_t.QueuedJob | None: ...
25
+
26
+ @abstractmethod
27
+ def list_queued_job_metadata(
28
+ self, offset: int, limit: int | None
29
+ ) -> list[queued_job_t.QueuedJobMetadata]: ...
@@ -0,0 +1,24 @@
1
+ from sqlalchemy import JSON, BigInteger, Column, DateTime, Enum, Text
2
+ from sqlalchemy.orm import declarative_base
3
+ from sqlalchemy.sql import func
4
+
5
+ from uncountable.types import queued_job_t
6
+
7
+ Base = declarative_base()
8
+
9
+
10
+ class QueuedJob(Base):
11
+ __tablename__ = "queued_jobs"
12
+
13
+ id = Column(Text, primary_key=True)
14
+ job_ref_name = Column(Text, nullable=False, index=True)
15
+ submitted_at = Column(
16
+ DateTime(timezone=True), server_default=func.current_timestamp(), nullable=False
17
+ )
18
+ payload = Column(JSON, nullable=False)
19
+ num_attempts = Column(BigInteger, nullable=False, default=0, server_default="0")
20
+ status = Column(
21
+ Enum(queued_job_t.JobStatus, length=None),
22
+ default=queued_job_t.JobStatus.QUEUED,
23
+ nullable=True,
24
+ )
@@ -0,0 +1,200 @@
1
+ import asyncio
2
+ import sys
3
+ import typing
4
+ from concurrent.futures import ProcessPoolExecutor
5
+ from dataclasses import dataclass
6
+
7
+ from opentelemetry.trace import get_current_span
8
+
9
+ from uncountable.integration.queue_runner.command_server import (
10
+ CommandEnqueueJob,
11
+ CommandEnqueueJobResponse,
12
+ CommandQueue,
13
+ CommandRetryJob,
14
+ CommandRetryJobResponse,
15
+ CommandTask,
16
+ )
17
+ from uncountable.integration.queue_runner.command_server.types import (
18
+ CommandVaccuumQueuedJobs,
19
+ )
20
+ from uncountable.integration.queue_runner.datastore import DatastoreSqlite
21
+ from uncountable.integration.queue_runner.datastore.interface import Datastore
22
+ from uncountable.integration.queue_runner.worker import Worker
23
+ from uncountable.integration.scan_profiles import load_profiles
24
+ from uncountable.integration.telemetry import Logger
25
+ from uncountable.types import job_definition_t, queued_job_t
26
+
27
+ from .types import ResultQueue, ResultTask
28
+
29
+ _MAX_JOB_WORKERS = 5
30
+
31
+
32
+ @dataclass(kw_only=True, frozen=True)
33
+ class JobListenerKey:
34
+ profile_name: str
35
+ subqueue_name: str = "default"
36
+
37
+
38
+ def _get_job_worker_key(
39
+ job_definition: job_definition_t.JobDefinition, profile_name: str
40
+ ) -> JobListenerKey:
41
+ if job_definition.subqueue_name is not None:
42
+ return JobListenerKey(
43
+ profile_name=profile_name, subqueue_name=job_definition.subqueue_name
44
+ )
45
+ return JobListenerKey(profile_name=profile_name)
46
+
47
+
48
+ def on_worker_crash(
49
+ worker_key: JobListenerKey,
50
+ ) -> typing.Callable[[asyncio.Task], None]:
51
+ def hook(task: asyncio.Task) -> None:
52
+ Logger(get_current_span()).log_exception(
53
+ Exception(
54
+ f"worker {worker_key.profile_name}_{worker_key.subqueue_name} crashed unexpectedly"
55
+ )
56
+ )
57
+ sys.exit(1)
58
+
59
+ return hook
60
+
61
+
62
+ def _start_workers(
63
+ process_pool: ProcessPoolExecutor, result_queue: ResultQueue, datastore: Datastore
64
+ ) -> dict[str, Worker]:
65
+ profiles = load_profiles()
66
+ job_queue_worker_lookup: dict[JobListenerKey, Worker] = {}
67
+ job_worker_lookup: dict[str, Worker] = {}
68
+ job_definition_lookup: dict[str, job_definition_t.JobDefinition] = {}
69
+ for profile in profiles:
70
+ for job_definition in profile.jobs:
71
+ job_definition_lookup[job_definition.id] = job_definition
72
+ job_worker_key = _get_job_worker_key(job_definition, profile.name)
73
+ if job_worker_key not in job_queue_worker_lookup:
74
+ worker = Worker(
75
+ process_pool=process_pool,
76
+ listen_queue=asyncio.Queue(),
77
+ result_queue=result_queue,
78
+ datastore=datastore,
79
+ )
80
+ task = asyncio.create_task(worker.run_worker_loop())
81
+ task.add_done_callback(on_worker_crash(job_worker_key))
82
+ job_queue_worker_lookup[job_worker_key] = worker
83
+ job_worker_lookup[job_definition.id] = job_queue_worker_lookup[
84
+ job_worker_key
85
+ ]
86
+ return job_worker_lookup
87
+
88
+
89
+ async def start_scheduler(
90
+ command_queue: CommandQueue, datastore: DatastoreSqlite
91
+ ) -> None:
92
+ logger = Logger(get_current_span())
93
+ result_queue: ResultQueue = asyncio.Queue()
94
+
95
+ with ProcessPoolExecutor(max_workers=_MAX_JOB_WORKERS) as process_pool:
96
+ job_worker_lookup = _start_workers(
97
+ process_pool, result_queue, datastore=datastore
98
+ )
99
+
100
+ queued_jobs = datastore.load_job_queue()
101
+
102
+ async def enqueue_queued_job(queued_job: queued_job_t.QueuedJob) -> None:
103
+ try:
104
+ worker = job_worker_lookup[queued_job.job_ref_name]
105
+ except KeyError as e:
106
+ logger.log_exception(e)
107
+ datastore.update_job_status(
108
+ queued_job.queued_job_uuid, queued_job_t.JobStatus.FAILED
109
+ )
110
+ return
111
+ await worker.listen_queue.put(queued_job)
112
+
113
+ async def _enqueue_or_deduplicate_job(
114
+ job_ref_name: str,
115
+ payload: queued_job_t.QueuedJobPayload,
116
+ ) -> str:
117
+ if isinstance(
118
+ payload.invocation_context,
119
+ (
120
+ queued_job_t.InvocationContextCron,
121
+ queued_job_t.InvocationContextManual,
122
+ ),
123
+ ):
124
+ existing_queued_job = datastore.get_next_queued_job_for_ref_name(
125
+ job_ref_name=job_ref_name
126
+ )
127
+ if existing_queued_job is not None:
128
+ return existing_queued_job.queued_job_uuid
129
+ queued_job = datastore.add_job_to_queue(
130
+ job_payload=payload,
131
+ job_ref_name=job_ref_name,
132
+ )
133
+ await enqueue_queued_job(queued_job)
134
+ return queued_job.queued_job_uuid
135
+
136
+ async def _handle_enqueue_job_command(command: CommandEnqueueJob) -> None:
137
+ queued_job_uuid = await _enqueue_or_deduplicate_job(
138
+ job_ref_name=command.job_ref_name,
139
+ payload=command.payload,
140
+ )
141
+ await command.response_queue.put(
142
+ CommandEnqueueJobResponse(queued_job_uuid=queued_job_uuid)
143
+ )
144
+
145
+ async def _handle_retry_job_command(command: CommandRetryJob) -> None:
146
+ queued_job = datastore.retry_job(command.queued_job_uuid)
147
+ if queued_job is None:
148
+ await command.response_queue.put(
149
+ CommandRetryJobResponse(queued_job_uuid=None)
150
+ )
151
+ return
152
+
153
+ await enqueue_queued_job(queued_job)
154
+ await command.response_queue.put(
155
+ CommandRetryJobResponse(queued_job_uuid=queued_job.queued_job_uuid)
156
+ )
157
+
158
+ def _handle_vaccuum_queued_jobs_command(
159
+ command: CommandVaccuumQueuedJobs,
160
+ ) -> None:
161
+ logger.log_info("Vaccuuming queued jobs...")
162
+ datastore.vaccuum_queued_jobs()
163
+
164
+ for queued_job in queued_jobs:
165
+ await enqueue_queued_job(queued_job)
166
+
167
+ result_task: ResultTask = asyncio.create_task(result_queue.get())
168
+ command_task: CommandTask = asyncio.create_task(command_queue.get())
169
+ while True:
170
+ finished, _ = await asyncio.wait(
171
+ [result_task, command_task], return_when=asyncio.FIRST_COMPLETED
172
+ )
173
+
174
+ for task in finished:
175
+ if task == command_task:
176
+ command = command_task.result()
177
+ match command:
178
+ case CommandEnqueueJob():
179
+ await _handle_enqueue_job_command(command=command)
180
+ case CommandRetryJob():
181
+ await _handle_retry_job_command(command=command)
182
+ case CommandVaccuumQueuedJobs():
183
+ _handle_vaccuum_queued_jobs_command(command=command)
184
+ case _:
185
+ typing.assert_never(command)
186
+ command_task = asyncio.create_task(command_queue.get())
187
+ elif task == result_task:
188
+ queued_job_result = result_task.result()
189
+ match queued_job_result.job_result.success:
190
+ case True:
191
+ datastore.update_job_status(
192
+ queued_job_result.queued_job_uuid,
193
+ queued_job_t.JobStatus.SUCCESS,
194
+ )
195
+ case False:
196
+ datastore.update_job_status(
197
+ queued_job_result.queued_job_uuid,
198
+ queued_job_t.JobStatus.FAILED,
199
+ )
200
+ result_task = asyncio.create_task(result_queue.get())
@@ -0,0 +1,34 @@
1
+ import asyncio
2
+
3
+ from uncountable.integration.db.connect import IntegrationDBService, create_db_engine
4
+ from uncountable.integration.db.session import get_session_maker
5
+ from uncountable.integration.queue_runner.command_server import serve
6
+ from uncountable.integration.queue_runner.command_server.types import CommandQueue
7
+ from uncountable.integration.queue_runner.datastore import DatastoreSqlite
8
+ from uncountable.integration.queue_runner.job_scheduler import start_scheduler
9
+
10
+
11
+ async def queue_runner_loop() -> None:
12
+ command_queue: CommandQueue = asyncio.Queue()
13
+ engine = create_db_engine(IntegrationDBService.RUNNER)
14
+ session_maker = get_session_maker(engine)
15
+
16
+ datastore = DatastoreSqlite(session_maker)
17
+ datastore.setup(engine)
18
+
19
+ command_server = asyncio.create_task(serve(command_queue, datastore))
20
+
21
+ scheduler = asyncio.create_task(start_scheduler(command_queue, datastore))
22
+
23
+ await scheduler
24
+ await command_server
25
+
26
+
27
+ def start_queue_runner() -> None:
28
+ loop = asyncio.new_event_loop()
29
+ loop.run_until_complete(queue_runner_loop())
30
+ loop.close()
31
+
32
+
33
+ if __name__ == "__main__":
34
+ start_queue_runner()
@@ -0,0 +1,7 @@
1
+ from asyncio import Queue, Task
2
+
3
+ from uncountable.types import queued_job_t
4
+
5
+ ListenQueue = Queue[queued_job_t.QueuedJob]
6
+ ResultQueue = Queue[queued_job_t.QueuedJobResult]
7
+ ResultTask = Task[queued_job_t.QueuedJobResult]
@@ -0,0 +1,116 @@
1
+ import asyncio
2
+ from concurrent.futures import ProcessPoolExecutor
3
+ from dataclasses import dataclass
4
+
5
+ from opentelemetry.trace import get_current_span
6
+
7
+ from uncountable.core.async_batch import AsyncBatchProcessor
8
+ from uncountable.integration.construct_client import construct_uncountable_client
9
+ from uncountable.integration.executors.executors import execute_job
10
+ from uncountable.integration.job import JobArguments
11
+ from uncountable.integration.queue_runner.datastore.interface import Datastore
12
+ from uncountable.integration.queue_runner.types import ListenQueue, ResultQueue
13
+ from uncountable.integration.scan_profiles import load_profiles
14
+ from uncountable.integration.telemetry import JobLogger, Logger, get_otel_tracer
15
+ from uncountable.types import base_t, job_definition_t, queued_job_t
16
+
17
+
18
+ class Worker:
19
+ def __init__(
20
+ self,
21
+ *,
22
+ process_pool: ProcessPoolExecutor,
23
+ listen_queue: ListenQueue,
24
+ result_queue: ResultQueue,
25
+ datastore: Datastore,
26
+ ) -> None:
27
+ self.process_pool = process_pool
28
+ self.listen_queue = listen_queue
29
+ self.result_queue = result_queue
30
+ self.datastore = datastore
31
+
32
+ async def run_worker_loop(self) -> None:
33
+ logger = Logger(get_current_span())
34
+ while True:
35
+ try:
36
+ queued_job = await self.listen_queue.get()
37
+ self.datastore.increment_num_attempts(queued_job.queued_job_uuid)
38
+ loop = asyncio.get_event_loop()
39
+ result = await loop.run_in_executor(
40
+ self.process_pool, run_queued_job, queued_job
41
+ )
42
+ assert isinstance(result, job_definition_t.JobResult)
43
+ await self.result_queue.put(
44
+ queued_job_t.QueuedJobResult(
45
+ job_result=result, queued_job_uuid=queued_job.queued_job_uuid
46
+ )
47
+ )
48
+ except BaseException as e:
49
+ logger.log_exception(e)
50
+ raise e
51
+
52
+
53
+ @dataclass(kw_only=True)
54
+ class RegisteredJobDetails:
55
+ profile_metadata: job_definition_t.ProfileMetadata
56
+ job_definition: job_definition_t.JobDefinition
57
+
58
+
59
+ def get_registered_job_details(job_ref_name: str) -> RegisteredJobDetails:
60
+ profiles = load_profiles()
61
+ for profile_metadata in profiles:
62
+ for job_definition in profile_metadata.jobs:
63
+ if job_definition.id == job_ref_name:
64
+ return RegisteredJobDetails(
65
+ profile_metadata=profile_metadata,
66
+ job_definition=job_definition,
67
+ )
68
+ raise Exception(f"profile not found for job {job_ref_name}")
69
+
70
+
71
+ def _resolve_queued_job_payload(queued_job: queued_job_t.QueuedJob) -> base_t.JsonValue:
72
+ match queued_job.payload.invocation_context:
73
+ case queued_job_t.InvocationContextCron():
74
+ return None
75
+ case queued_job_t.InvocationContextManual():
76
+ return None
77
+ case queued_job_t.InvocationContextWebhook():
78
+ return queued_job.payload.invocation_context.webhook_payload
79
+
80
+
81
+ def run_queued_job(
82
+ queued_job: queued_job_t.QueuedJob,
83
+ ) -> job_definition_t.JobResult:
84
+ with get_otel_tracer().start_as_current_span(name="run_queued_job") as span:
85
+ job_details = get_registered_job_details(queued_job.job_ref_name)
86
+ job_logger = JobLogger(
87
+ base_span=span,
88
+ profile_metadata=job_details.profile_metadata,
89
+ job_definition=job_details.job_definition,
90
+ )
91
+ try:
92
+ client = construct_uncountable_client(
93
+ profile_meta=job_details.profile_metadata, logger=job_logger
94
+ )
95
+ batch_processor = AsyncBatchProcessor(client=client)
96
+
97
+ payload = _resolve_queued_job_payload(queued_job)
98
+
99
+ args = JobArguments(
100
+ job_definition=job_details.job_definition,
101
+ client=client,
102
+ batch_processor=batch_processor,
103
+ profile_metadata=job_details.profile_metadata,
104
+ logger=job_logger,
105
+ payload=payload,
106
+ job_uuid=queued_job.queued_job_uuid,
107
+ )
108
+
109
+ return execute_job(
110
+ args=args,
111
+ profile_metadata=job_details.profile_metadata,
112
+ job_definition=job_details.job_definition,
113
+ )
114
+ except BaseException as e:
115
+ job_logger.log_exception(e)
116
+ return job_definition_t.JobResult(success=False)