UncountablePythonSDK 0.0.8__py3-none-any.whl → 0.0.92__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 (312) hide show
  1. UncountablePythonSDK-0.0.92.dist-info/METADATA +61 -0
  2. UncountablePythonSDK-0.0.92.dist-info/RECORD +301 -0
  3. {UncountablePythonSDK-0.0.8.dist-info → UncountablePythonSDK-0.0.92.dist-info}/WHEEL +1 -1
  4. {UncountablePythonSDK-0.0.8.dist-info → UncountablePythonSDK-0.0.92.dist-info}/top_level.txt +1 -1
  5. docs/.gitignore +1 -0
  6. docs/conf.py +57 -0
  7. docs/index.md +13 -0
  8. docs/justfile +12 -0
  9. docs/quickstart.md +19 -0
  10. docs/requirements.txt +7 -0
  11. docs/static/favicons/android-chrome-192x192.png +0 -0
  12. docs/static/favicons/android-chrome-512x512.png +0 -0
  13. docs/static/favicons/apple-touch-icon.png +0 -0
  14. docs/static/favicons/browserconfig.xml +9 -0
  15. docs/static/favicons/favicon-16x16.png +0 -0
  16. docs/static/favicons/favicon-32x32.png +0 -0
  17. docs/static/favicons/manifest.json +18 -0
  18. docs/static/favicons/mstile-150x150.png +0 -0
  19. docs/static/favicons/safari-pinned-tab.svg +32 -0
  20. docs/static/logo_blue.png +0 -0
  21. examples/async_batch.py +35 -0
  22. examples/create_entity.py +22 -17
  23. examples/download_files.py +26 -0
  24. examples/edit_recipe_inputs.py +50 -0
  25. examples/integration-server/jobs/materials_auto/example_cron.py +18 -0
  26. examples/integration-server/jobs/materials_auto/example_wh.py +15 -0
  27. examples/integration-server/jobs/materials_auto/profile.yaml +43 -0
  28. examples/integration-server/pyproject.toml +224 -0
  29. examples/invoke_uploader.py +26 -0
  30. examples/set_recipe_metadata_file.py +40 -0
  31. examples/set_recipe_output_file_sdk.py +26 -0
  32. examples/upload_files.py +18 -0
  33. pkgs/argument_parser/__init__.py +5 -0
  34. pkgs/argument_parser/_is_enum.py +1 -6
  35. pkgs/argument_parser/argument_parser.py +232 -76
  36. pkgs/argument_parser/case_convert.py +4 -3
  37. pkgs/filesystem_utils/__init__.py +20 -0
  38. pkgs/filesystem_utils/_blob_session.py +137 -0
  39. pkgs/filesystem_utils/_gdrive_session.py +309 -0
  40. pkgs/filesystem_utils/_local_session.py +69 -0
  41. pkgs/filesystem_utils/_s3_session.py +117 -0
  42. pkgs/filesystem_utils/_sftp_session.py +147 -0
  43. pkgs/filesystem_utils/file_type_utils.py +91 -0
  44. pkgs/filesystem_utils/filesystem_session.py +39 -0
  45. pkgs/py.typed +0 -0
  46. pkgs/serialization/__init__.py +8 -1
  47. pkgs/serialization/annotation.py +64 -0
  48. pkgs/serialization/missing_sentry.py +1 -1
  49. pkgs/serialization/opaque_key.py +1 -1
  50. pkgs/serialization/serial_alias.py +47 -0
  51. pkgs/serialization/serial_class.py +65 -50
  52. pkgs/serialization/serial_generic.py +16 -0
  53. pkgs/serialization/serial_union.py +84 -0
  54. pkgs/serialization/yaml.py +57 -0
  55. pkgs/serialization_util/__init__.py +7 -7
  56. pkgs/serialization_util/_get_type_for_serialization.py +1 -3
  57. pkgs/serialization_util/convert_to_snakecase.py +27 -0
  58. pkgs/serialization_util/dataclasses.py +14 -0
  59. pkgs/serialization_util/serialization_helpers.py +116 -74
  60. pkgs/strenum_compat/strenum_compat.py +1 -9
  61. pkgs/type_spec/actions_registry/__init__.py +0 -0
  62. pkgs/type_spec/actions_registry/__main__.py +126 -0
  63. pkgs/type_spec/actions_registry/emit_typescript.py +182 -0
  64. pkgs/type_spec/builder.py +475 -89
  65. pkgs/type_spec/config.py +24 -19
  66. pkgs/type_spec/emit_io_ts.py +5 -2
  67. pkgs/type_spec/emit_open_api.py +266 -32
  68. pkgs/type_spec/emit_open_api_util.py +32 -13
  69. pkgs/type_spec/emit_python.py +599 -151
  70. pkgs/type_spec/emit_typescript.py +74 -273
  71. pkgs/type_spec/emit_typescript_util.py +239 -5
  72. pkgs/type_spec/load_types.py +55 -10
  73. pkgs/type_spec/open_api_util.py +30 -41
  74. pkgs/type_spec/parts/base.py.prepart +4 -3
  75. pkgs/type_spec/type_info/emit_type_info.py +178 -16
  76. pkgs/type_spec/util.py +11 -11
  77. pkgs/type_spec/value_spec/__main__.py +3 -3
  78. pkgs/type_spec/value_spec/convert_type.py +8 -1
  79. pkgs/type_spec/value_spec/emit_python.py +13 -4
  80. uncountable/__init__.py +1 -2
  81. uncountable/core/__init__.py +12 -2
  82. uncountable/core/async_batch.py +37 -0
  83. uncountable/core/client.py +293 -43
  84. uncountable/core/environment.py +41 -0
  85. uncountable/core/file_upload.py +135 -0
  86. uncountable/core/types.py +17 -0
  87. uncountable/integration/__init__.py +0 -0
  88. uncountable/integration/cli.py +49 -0
  89. uncountable/integration/construct_client.py +51 -0
  90. uncountable/integration/cron.py +29 -0
  91. uncountable/integration/db/__init__.py +0 -0
  92. uncountable/integration/db/connect.py +18 -0
  93. uncountable/integration/db/session.py +25 -0
  94. uncountable/integration/entrypoint.py +13 -0
  95. uncountable/integration/executors/__init__.py +0 -0
  96. uncountable/integration/executors/executors.py +148 -0
  97. uncountable/integration/executors/generic_upload_executor.py +284 -0
  98. uncountable/integration/executors/script_executor.py +25 -0
  99. uncountable/integration/job.py +87 -0
  100. uncountable/integration/queue_runner/__init__.py +0 -0
  101. uncountable/integration/queue_runner/command_server/__init__.py +24 -0
  102. uncountable/integration/queue_runner/command_server/command_client.py +68 -0
  103. uncountable/integration/queue_runner/command_server/command_server.py +64 -0
  104. uncountable/integration/queue_runner/command_server/protocol/__init__.py +0 -0
  105. uncountable/integration/queue_runner/command_server/protocol/command_server.proto +22 -0
  106. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +40 -0
  107. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +38 -0
  108. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2_grpc.py +129 -0
  109. uncountable/integration/queue_runner/command_server/types.py +52 -0
  110. uncountable/integration/queue_runner/datastore/__init__.py +3 -0
  111. uncountable/integration/queue_runner/datastore/datastore_sqlite.py +93 -0
  112. uncountable/integration/queue_runner/datastore/interface.py +19 -0
  113. uncountable/integration/queue_runner/datastore/model.py +17 -0
  114. uncountable/integration/queue_runner/job_scheduler.py +163 -0
  115. uncountable/integration/queue_runner/queue_runner.py +26 -0
  116. uncountable/integration/queue_runner/types.py +7 -0
  117. uncountable/integration/queue_runner/worker.py +119 -0
  118. uncountable/integration/scan_profiles.py +67 -0
  119. uncountable/integration/scheduler.py +150 -0
  120. uncountable/integration/secret_retrieval/__init__.py +3 -0
  121. uncountable/integration/secret_retrieval/retrieve_secret.py +93 -0
  122. uncountable/integration/server.py +117 -0
  123. uncountable/integration/telemetry.py +209 -0
  124. uncountable/integration/webhook_server/entrypoint.py +170 -0
  125. uncountable/types/__init__.py +136 -20
  126. uncountable/types/api/batch/execute_batch.py +15 -7
  127. uncountable/types/api/batch/execute_batch_load_async.py +42 -0
  128. uncountable/types/api/chemical/__init__.py +1 -0
  129. uncountable/types/api/chemical/convert_chemical_formats.py +63 -0
  130. uncountable/types/api/entity/create_entities.py +23 -11
  131. uncountable/types/api/entity/create_entity.py +21 -12
  132. uncountable/types/api/entity/get_entities_data.py +18 -8
  133. uncountable/types/api/entity/grant_entity_permissions.py +48 -0
  134. uncountable/types/api/entity/list_entities.py +27 -12
  135. uncountable/types/api/entity/lock_entity.py +45 -0
  136. uncountable/types/api/entity/resolve_entity_ids.py +17 -7
  137. uncountable/types/api/entity/set_entity_field_values.py +44 -0
  138. uncountable/types/api/entity/set_values.py +14 -7
  139. uncountable/types/api/entity/transition_entity_phase.py +80 -0
  140. uncountable/types/api/entity/unlock_entity.py +44 -0
  141. uncountable/types/api/equipment/__init__.py +1 -0
  142. uncountable/types/api/equipment/associate_equipment_input.py +44 -0
  143. uncountable/types/api/field_options/__init__.py +1 -0
  144. uncountable/types/api/field_options/upsert_field_options.py +55 -0
  145. uncountable/types/api/files/__init__.py +1 -0
  146. uncountable/types/api/files/download_file.py +77 -0
  147. uncountable/types/api/id_source/__init__.py +1 -0
  148. uncountable/types/api/id_source/list_id_source.py +56 -0
  149. uncountable/types/api/id_source/match_id_source.py +54 -0
  150. uncountable/types/api/input_groups/get_input_group_names.py +16 -6
  151. uncountable/types/api/inputs/create_inputs.py +24 -11
  152. uncountable/types/api/inputs/get_input_data.py +32 -13
  153. uncountable/types/api/inputs/get_input_names.py +18 -8
  154. uncountable/types/api/inputs/get_inputs_data.py +29 -10
  155. uncountable/types/api/inputs/set_input_attribute_values.py +16 -9
  156. uncountable/types/api/inputs/set_input_category.py +44 -0
  157. uncountable/types/api/inputs/set_input_subcategories.py +45 -0
  158. uncountable/types/api/inputs/set_intermediate_type.py +50 -0
  159. uncountable/types/api/material_families/__init__.py +1 -0
  160. uncountable/types/api/material_families/update_entity_material_families.py +48 -0
  161. uncountable/types/api/outputs/get_output_data.py +32 -16
  162. uncountable/types/api/outputs/get_output_names.py +18 -8
  163. uncountable/types/api/outputs/resolve_output_conditions.py +23 -10
  164. uncountable/types/api/permissions/__init__.py +1 -0
  165. uncountable/types/api/permissions/set_core_permissions.py +105 -0
  166. uncountable/types/api/project/get_projects.py +17 -7
  167. uncountable/types/api/project/get_projects_data.py +21 -11
  168. uncountable/types/api/recipe_links/__init__.py +1 -0
  169. uncountable/types/api/recipe_links/create_recipe_link.py +46 -0
  170. uncountable/types/api/recipe_links/remove_recipe_link.py +45 -0
  171. uncountable/types/api/recipe_metadata/get_recipe_metadata_data.py +18 -8
  172. uncountable/types/api/recipes/add_recipe_to_project.py +42 -0
  173. uncountable/types/api/recipes/archive_recipes.py +42 -0
  174. uncountable/types/api/recipes/associate_recipe_as_input.py +44 -0
  175. uncountable/types/api/recipes/associate_recipe_as_lot.py +43 -0
  176. uncountable/types/api/recipes/clear_recipe_outputs.py +42 -0
  177. uncountable/types/api/recipes/create_recipe.py +51 -0
  178. uncountable/types/api/recipes/create_recipes.py +25 -12
  179. uncountable/types/api/recipes/disassociate_recipe_as_input.py +42 -0
  180. uncountable/types/api/recipes/edit_recipe_inputs.py +283 -0
  181. uncountable/types/api/recipes/get_column_calculation_values.py +58 -0
  182. uncountable/types/api/recipes/get_curve.py +15 -7
  183. uncountable/types/api/recipes/get_recipe_calculations.py +17 -10
  184. uncountable/types/api/recipes/get_recipe_links.py +13 -6
  185. uncountable/types/api/recipes/get_recipe_names.py +16 -6
  186. uncountable/types/api/recipes/get_recipe_output_metadata.py +14 -7
  187. uncountable/types/api/recipes/get_recipes_data.py +63 -38
  188. uncountable/types/api/recipes/lock_recipes.py +63 -0
  189. uncountable/types/api/recipes/remove_recipe_from_project.py +42 -0
  190. uncountable/types/api/recipes/set_recipe_inputs.py +19 -10
  191. uncountable/types/api/recipes/set_recipe_metadata.py +43 -0
  192. uncountable/types/api/recipes/set_recipe_output_annotations.py +115 -0
  193. uncountable/types/api/recipes/set_recipe_output_file.py +56 -0
  194. uncountable/types/api/recipes/set_recipe_outputs.py +26 -12
  195. uncountable/types/api/recipes/set_recipe_tags.py +109 -0
  196. uncountable/types/api/recipes/unarchive_recipes.py +41 -0
  197. uncountable/types/api/recipes/unlock_recipes.py +50 -0
  198. uncountable/types/api/triggers/__init__.py +1 -0
  199. uncountable/types/api/triggers/run_trigger.py +43 -0
  200. uncountable/types/api/uploader/__init__.py +1 -0
  201. uncountable/types/api/uploader/invoke_uploader.py +47 -0
  202. uncountable/types/async_batch.py +13 -0
  203. uncountable/types/async_batch_processor.py +384 -0
  204. uncountable/types/async_batch_t.py +97 -0
  205. uncountable/types/async_jobs.py +9 -0
  206. uncountable/types/async_jobs_t.py +53 -0
  207. uncountable/types/auth_retrieval.py +12 -0
  208. uncountable/types/auth_retrieval_t.py +75 -0
  209. uncountable/types/base.py +5 -78
  210. uncountable/types/base_t.py +85 -0
  211. uncountable/types/calculations.py +3 -18
  212. uncountable/types/calculations_t.py +27 -0
  213. uncountable/types/chemical_structure.py +8 -0
  214. uncountable/types/chemical_structure_t.py +28 -0
  215. uncountable/types/client_base.py +1093 -54
  216. uncountable/types/client_config.py +8 -0
  217. uncountable/types/client_config_t.py +26 -0
  218. uncountable/types/curves.py +5 -42
  219. uncountable/types/curves_t.py +51 -0
  220. uncountable/types/entity.py +8 -269
  221. uncountable/types/entity_t.py +393 -0
  222. uncountable/types/experiment_groups.py +3 -18
  223. uncountable/types/experiment_groups_t.py +27 -0
  224. uncountable/types/field_values.py +17 -60
  225. uncountable/types/field_values_t.py +204 -0
  226. uncountable/types/fields.py +3 -19
  227. uncountable/types/fields_t.py +28 -0
  228. uncountable/types/generic_upload.py +15 -0
  229. uncountable/types/generic_upload_t.py +119 -0
  230. uncountable/types/id_source.py +12 -0
  231. uncountable/types/id_source_t.py +68 -0
  232. uncountable/types/identifier.py +11 -0
  233. uncountable/types/identifier_t.py +63 -0
  234. uncountable/types/input_attributes.py +3 -24
  235. uncountable/types/input_attributes_t.py +30 -0
  236. uncountable/types/inputs.py +6 -56
  237. uncountable/types/inputs_t.py +83 -0
  238. uncountable/types/integration_server.py +9 -0
  239. uncountable/types/integration_server_t.py +42 -0
  240. uncountable/types/job_definition.py +27 -0
  241. uncountable/types/job_definition_t.py +260 -0
  242. uncountable/types/outputs.py +3 -21
  243. uncountable/types/outputs_t.py +30 -0
  244. uncountable/types/overrides.py +10 -0
  245. uncountable/types/overrides_t.py +49 -0
  246. uncountable/types/permissions.py +8 -0
  247. uncountable/types/permissions_t.py +46 -0
  248. uncountable/types/phases.py +3 -18
  249. uncountable/types/phases_t.py +27 -0
  250. uncountable/types/post_base.py +8 -0
  251. uncountable/types/post_base_t.py +30 -0
  252. uncountable/types/queued_job.py +16 -0
  253. uncountable/types/queued_job_t.py +123 -0
  254. uncountable/types/recipe_identifiers.py +12 -0
  255. uncountable/types/recipe_identifiers_t.py +76 -0
  256. uncountable/types/recipe_inputs.py +9 -0
  257. uncountable/types/recipe_inputs_t.py +30 -0
  258. uncountable/types/recipe_links.py +4 -45
  259. uncountable/types/recipe_links_t.py +54 -0
  260. uncountable/types/recipe_metadata.py +5 -45
  261. uncountable/types/recipe_metadata_t.py +58 -0
  262. uncountable/types/recipe_output_metadata.py +3 -19
  263. uncountable/types/recipe_output_metadata_t.py +28 -0
  264. uncountable/types/recipe_tags.py +3 -18
  265. uncountable/types/recipe_tags_t.py +27 -0
  266. uncountable/types/recipe_workflow_steps.py +14 -0
  267. uncountable/types/recipe_workflow_steps_t.py +95 -0
  268. uncountable/types/recipes.py +8 -0
  269. uncountable/types/recipes_t.py +25 -0
  270. uncountable/types/response.py +3 -20
  271. uncountable/types/response_t.py +26 -0
  272. uncountable/types/secret_retrieval.py +12 -0
  273. uncountable/types/secret_retrieval_t.py +75 -0
  274. uncountable/types/units.py +3 -18
  275. uncountable/types/units_t.py +27 -0
  276. uncountable/types/users.py +3 -19
  277. uncountable/types/users_t.py +28 -0
  278. uncountable/types/webhook_job.py +9 -0
  279. uncountable/types/webhook_job_t.py +37 -0
  280. uncountable/types/workflows.py +4 -27
  281. uncountable/types/workflows_t.py +39 -0
  282. UncountablePythonSDK-0.0.8.dist-info/METADATA +0 -27
  283. UncountablePythonSDK-0.0.8.dist-info/RECORD +0 -134
  284. examples/recipe-import/importer.py +0 -39
  285. type_spec/external/api/batch/execute_batch.yaml +0 -56
  286. type_spec/external/api/entity/create_entities.yaml +0 -33
  287. type_spec/external/api/entity/create_entity.yaml +0 -39
  288. type_spec/external/api/entity/get_entities_data.yaml +0 -29
  289. type_spec/external/api/entity/list_entities.yaml +0 -52
  290. type_spec/external/api/entity/resolve_entity_ids.yaml +0 -29
  291. type_spec/external/api/entity/set_values.yaml +0 -18
  292. type_spec/external/api/input_groups/get_input_group_names.yaml +0 -29
  293. type_spec/external/api/inputs/create_inputs.yaml +0 -48
  294. type_spec/external/api/inputs/get_input_data.yaml +0 -95
  295. type_spec/external/api/inputs/get_input_names.yaml +0 -38
  296. type_spec/external/api/inputs/get_inputs_data.yaml +0 -82
  297. type_spec/external/api/inputs/set_input_attribute_values.yaml +0 -33
  298. type_spec/external/api/outputs/get_output_data.yaml +0 -92
  299. type_spec/external/api/outputs/get_output_names.yaml +0 -35
  300. type_spec/external/api/outputs/resolve_output_conditions.yaml +0 -50
  301. type_spec/external/api/project/get_projects.yaml +0 -42
  302. type_spec/external/api/project/get_projects_data.yaml +0 -50
  303. type_spec/external/api/recipe_metadata/get_recipe_metadata_data.yaml +0 -41
  304. type_spec/external/api/recipes/create_recipes.yaml +0 -47
  305. type_spec/external/api/recipes/get_curve.yaml +0 -18
  306. type_spec/external/api/recipes/get_recipe_calculations.yaml +0 -39
  307. type_spec/external/api/recipes/get_recipe_links.yaml +0 -26
  308. type_spec/external/api/recipes/get_recipe_names.yaml +0 -29
  309. type_spec/external/api/recipes/get_recipe_output_metadata.yaml +0 -36
  310. type_spec/external/api/recipes/get_recipes_data.yaml +0 -238
  311. type_spec/external/api/recipes/set_recipe_inputs.yaml +0 -36
  312. type_spec/external/api/recipes/set_recipe_outputs.yaml +0 -52
@@ -0,0 +1,51 @@
1
+ from uncountable.core import AuthDetailsApiKey, Client
2
+ from uncountable.core.client import ClientConfig
3
+ from uncountable.core.types import AuthDetailsAll, AuthDetailsOAuth
4
+ from uncountable.integration.secret_retrieval.retrieve_secret import retrieve_secret
5
+ from uncountable.integration.telemetry import JobLogger
6
+ from uncountable.types import auth_retrieval_t
7
+ from uncountable.types.job_definition_t import (
8
+ ProfileMetadata,
9
+ )
10
+
11
+
12
+ def _construct_auth_details(profile_meta: ProfileMetadata) -> AuthDetailsAll:
13
+ match profile_meta.auth_retrieval:
14
+ case auth_retrieval_t.AuthRetrievalOAuth():
15
+ refresh_token = retrieve_secret(
16
+ profile_meta.auth_retrieval.refresh_token_secret,
17
+ profile_metadata=profile_meta,
18
+ )
19
+ return AuthDetailsOAuth(refresh_token=refresh_token)
20
+ case auth_retrieval_t.AuthRetrievalBasic():
21
+ api_id = retrieve_secret(
22
+ profile_meta.auth_retrieval.api_id_secret, profile_metadata=profile_meta
23
+ )
24
+ api_key = retrieve_secret(
25
+ profile_meta.auth_retrieval.api_key_secret,
26
+ profile_metadata=profile_meta,
27
+ )
28
+
29
+ return AuthDetailsApiKey(api_id=api_id, api_secret_key=api_key)
30
+
31
+
32
+ def _construct_client_config(
33
+ profile_meta: ProfileMetadata, job_logger: JobLogger
34
+ ) -> ClientConfig | None:
35
+ if profile_meta.client_options is None:
36
+ return None
37
+ return ClientConfig(
38
+ allow_insecure_tls=profile_meta.client_options.allow_insecure_tls,
39
+ extra_headers=profile_meta.client_options.extra_headers,
40
+ logger=job_logger,
41
+ )
42
+
43
+
44
+ def construct_uncountable_client(
45
+ profile_meta: ProfileMetadata, logger: JobLogger
46
+ ) -> Client:
47
+ return Client(
48
+ base_url=profile_meta.base_url,
49
+ auth_details=_construct_auth_details(profile_meta),
50
+ config=_construct_client_config(profile_meta, logger),
51
+ )
@@ -0,0 +1,29 @@
1
+ from dataclasses import dataclass
2
+
3
+ from pkgs.argument_parser import CachedParser
4
+ from uncountable.core.environment import get_local_admin_server_port
5
+ from uncountable.integration.queue_runner.command_server.command_client import (
6
+ send_job_queue_message,
7
+ )
8
+ from uncountable.types import queued_job_t
9
+ from uncountable.types.job_definition_t import JobDefinition, ProfileMetadata
10
+
11
+
12
+ @dataclass
13
+ class CronJobArgs:
14
+ definition: JobDefinition
15
+ profile_metadata: ProfileMetadata
16
+
17
+
18
+ cron_args_parser = CachedParser(CronJobArgs)
19
+
20
+
21
+ def cron_job_executor(**kwargs: dict) -> None:
22
+ args_passed = cron_args_parser.parse_storage(kwargs)
23
+ send_job_queue_message(
24
+ job_ref_name=args_passed.definition.id,
25
+ payload=queued_job_t.QueuedJobPayload(
26
+ invocation_context=queued_job_t.InvocationContextCron()
27
+ ),
28
+ port=get_local_admin_server_port(),
29
+ )
File without changes
@@ -0,0 +1,18 @@
1
+ import os
2
+ from enum import StrEnum
3
+
4
+ from sqlalchemy import create_engine
5
+ from sqlalchemy.engine.base import Engine
6
+
7
+
8
+ class IntegrationDBService(StrEnum):
9
+ CRON = "cron"
10
+ RUNNER = "runner"
11
+
12
+
13
+ def create_db_engine(service: IntegrationDBService) -> Engine:
14
+ match service:
15
+ case IntegrationDBService.CRON:
16
+ return create_engine(os.environ["UNC_CRON_SQLITE_URI"])
17
+ case IntegrationDBService.RUNNER:
18
+ return create_engine(os.environ["UNC_RUNNER_SQLITE_URI"])
@@ -0,0 +1,25 @@
1
+ from contextlib import _GeneratorContextManager, contextmanager
2
+ from typing import Callable, Generator
3
+
4
+ from sqlalchemy.engine import Engine
5
+ from sqlalchemy.orm import Session, sessionmaker
6
+
7
+ DBSessionMaker = Callable[[], _GeneratorContextManager[Session]]
8
+
9
+
10
+ def get_session_maker(engine: Engine) -> DBSessionMaker:
11
+ session_maker = sessionmaker(bind=engine)
12
+
13
+ @contextmanager
14
+ def session_manager() -> Generator[Session, None, None]:
15
+ session = session_maker()
16
+ try:
17
+ yield session
18
+ session.commit()
19
+ except Exception:
20
+ session.rollback()
21
+ raise
22
+ finally:
23
+ session.close()
24
+
25
+ return session_manager
@@ -0,0 +1,13 @@
1
+ from uncountable.integration.db.connect import IntegrationDBService, create_db_engine
2
+ from uncountable.integration.scan_profiles import load_profiles
3
+ from uncountable.integration.server import IntegrationServer
4
+
5
+
6
+ def main() -> None:
7
+ with IntegrationServer(create_db_engine(IntegrationDBService.CRON)) as server:
8
+ server.register_jobs(load_profiles())
9
+ server.serve_forever()
10
+
11
+
12
+ if __name__ == "__main__":
13
+ main()
File without changes
@@ -0,0 +1,148 @@
1
+ from typing import assert_never
2
+
3
+ from uncountable.core.client import Client
4
+ from uncountable.integration.executors.generic_upload_executor import GenericUploadJob
5
+ from uncountable.integration.executors.script_executor import resolve_script_executor
6
+ from uncountable.integration.job import Job, JobArguments
7
+ from uncountable.types import (
8
+ async_jobs_t,
9
+ entity_t,
10
+ field_values_t,
11
+ identifier_t,
12
+ integration_server_t,
13
+ job_definition_t,
14
+ transition_entity_phase_t,
15
+ )
16
+
17
+
18
+ def resolve_executor(
19
+ job_executor: job_definition_t.JobExecutor,
20
+ profile_metadata: job_definition_t.ProfileMetadata,
21
+ ) -> Job:
22
+ match job_executor:
23
+ case job_definition_t.JobExecutorScript():
24
+ return resolve_script_executor(
25
+ job_executor, profile_metadata=profile_metadata
26
+ )
27
+ case job_definition_t.JobExecutorGenericUpload():
28
+ return GenericUploadJob(
29
+ remote_directories=job_executor.remote_directories,
30
+ upload_strategy=job_executor.upload_strategy,
31
+ data_source=job_executor.data_source,
32
+ )
33
+ assert_never(job_executor)
34
+
35
+
36
+ def _create_run_entity(
37
+ *,
38
+ client: Client,
39
+ logging_settings: job_definition_t.JobLoggingSettings,
40
+ job_uuid: str,
41
+ ) -> entity_t.Entity:
42
+ run_entity = client.create_entity(
43
+ entity_type=entity_t.EntityType.ASYNC_JOB,
44
+ definition_key=identifier_t.IdentifierKeyRefName(
45
+ ref_name="unc_integration_server_run_definition"
46
+ ),
47
+ field_values=[
48
+ field_values_t.FieldRefNameValue(
49
+ field_ref_name=async_jobs_t.ASYNC_JOB_TYPE_FIELD_REF_NAME,
50
+ value=async_jobs_t.AsyncJobType.INTEGRATION_SERVER_RUN,
51
+ ),
52
+ field_values_t.FieldRefNameValue(
53
+ field_ref_name=async_jobs_t.ASYNC_JOB_STATUS_FIELD_REF_NAME,
54
+ value=async_jobs_t.AsyncJobStatus.IN_PROGRESS,
55
+ ),
56
+ field_values_t.FieldRefNameValue(
57
+ field_ref_name=integration_server_t.INTEGRATION_SERVER_RUN_UUID_FIELD_REF_NAME,
58
+ value=job_uuid,
59
+ ),
60
+ ],
61
+ ).entity
62
+ client.transition_entity_phase(
63
+ entity=run_entity,
64
+ transition=transition_entity_phase_t.TransitionIdentifierPhases(
65
+ phase_from_key=identifier_t.IdentifierKeyRefName(
66
+ ref_name="unc_integration_server_run__queued"
67
+ ),
68
+ phase_to_key=identifier_t.IdentifierKeyRefName(
69
+ ref_name="unc_integration_server_run__started"
70
+ ),
71
+ ),
72
+ )
73
+ if logging_settings.share_with_user_groups is not None:
74
+ client.grant_entity_permissions(
75
+ entity_type=entity_t.EntityType.ASYNC_JOB,
76
+ entity_key=identifier_t.IdentifierKeyId(id=run_entity.id),
77
+ permission_types=[
78
+ entity_t.EntityPermissionType.READ,
79
+ entity_t.EntityPermissionType.WRITE,
80
+ ],
81
+ user_group_keys=logging_settings.share_with_user_groups,
82
+ )
83
+ return run_entity
84
+
85
+
86
+ def execute_job(
87
+ *,
88
+ job_definition: job_definition_t.JobDefinition,
89
+ profile_metadata: job_definition_t.ProfileMetadata,
90
+ args: JobArguments,
91
+ job_uuid: str,
92
+ ) -> job_definition_t.JobResult:
93
+ with args.logger.push_scope(job_definition.name) as job_logger:
94
+ job = resolve_executor(job_definition.executor, profile_metadata)
95
+
96
+ job_logger.log_info("running job")
97
+
98
+ run_entity: entity_t.Entity | None = None
99
+ try:
100
+ if (
101
+ job_definition.logging_settings is not None
102
+ and job_definition.logging_settings.enabled
103
+ ):
104
+ run_entity = _create_run_entity(
105
+ client=args.client,
106
+ logging_settings=job_definition.logging_settings,
107
+ job_uuid=job_uuid,
108
+ )
109
+ result = job.run_outer(args=args)
110
+ except Exception as e:
111
+ job_logger.log_exception(e)
112
+ if run_entity is not None:
113
+ args.client.set_values(
114
+ entity=run_entity,
115
+ values=[
116
+ field_values_t.ArgumentValueRefName(
117
+ field_ref_name=async_jobs_t.ASYNC_JOB_STATUS_FIELD_REF_NAME,
118
+ value=async_jobs_t.AsyncJobStatus.ERROR,
119
+ ),
120
+ ],
121
+ )
122
+ return job_definition_t.JobResult(success=False)
123
+
124
+ if args.batch_processor.current_queue_size() != 0:
125
+ args.batch_processor.send()
126
+
127
+ submitted_batch_job_ids = args.batch_processor.get_submitted_job_ids()
128
+ job_logger.log_info(
129
+ "completed job",
130
+ attributes={
131
+ "submitted_batch_job_ids": submitted_batch_job_ids,
132
+ "success": result.success,
133
+ },
134
+ )
135
+ if run_entity is not None:
136
+ args.client.set_values(
137
+ entity=run_entity,
138
+ values=[
139
+ field_values_t.ArgumentValueRefName(
140
+ field_ref_name=async_jobs_t.ASYNC_JOB_STATUS_FIELD_REF_NAME,
141
+ value=async_jobs_t.AsyncJobStatus.COMPLETED
142
+ if result.success
143
+ else async_jobs_t.AsyncJobStatus.ERROR,
144
+ ),
145
+ ],
146
+ )
147
+
148
+ return result
@@ -0,0 +1,284 @@
1
+ import io
2
+ import os
3
+ import re
4
+ from datetime import datetime, timezone
5
+
6
+ import paramiko
7
+
8
+ from pkgs.filesystem_utils import (
9
+ FileObjectData,
10
+ FileSystemFileReference,
11
+ FileSystemObject,
12
+ FileSystemS3Config,
13
+ FileSystemSession,
14
+ FileSystemSFTPConfig,
15
+ FileTransfer,
16
+ S3Session,
17
+ SFTPSession,
18
+ )
19
+ from uncountable.core.file_upload import DataFileUpload, FileUpload
20
+ from uncountable.integration.job import Job, JobArguments
21
+ from uncountable.integration.secret_retrieval import retrieve_secret
22
+ from uncountable.integration.telemetry import JobLogger
23
+ from uncountable.types.generic_upload_t import (
24
+ GenericRemoteDirectoryScope,
25
+ GenericUploadStrategy,
26
+ )
27
+ from uncountable.types.job_definition_t import (
28
+ GenericUploadDataSource,
29
+ GenericUploadDataSourceS3,
30
+ GenericUploadDataSourceSFTP,
31
+ JobResult,
32
+ S3CloudProvider,
33
+ )
34
+
35
+
36
+ def _filter_files_by_keyword(
37
+ remote_directory: GenericRemoteDirectoryScope,
38
+ files: list[FileObjectData],
39
+ logger: JobLogger,
40
+ ) -> list[FileObjectData]:
41
+ if remote_directory.detection_keyword is None:
42
+ return files
43
+
44
+ raise NotImplementedError("keyword detection not implemented yet")
45
+
46
+
47
+ def _filter_by_filename(
48
+ remote_directory: GenericRemoteDirectoryScope, files: list[FileSystemObject]
49
+ ) -> list[FileSystemObject]:
50
+ if remote_directory.filename_regex is None:
51
+ return files
52
+
53
+ return [
54
+ file
55
+ for file in files
56
+ if file.filename is not None
57
+ and re.search(remote_directory.filename_regex, file.filename)
58
+ ]
59
+
60
+
61
+ def _filter_by_file_extension(
62
+ remote_directory: GenericRemoteDirectoryScope, files: list[FileSystemObject]
63
+ ) -> list[FileSystemObject]:
64
+ if remote_directory.valid_file_extensions is None:
65
+ return files
66
+
67
+ return [
68
+ file
69
+ for file in files
70
+ if file.filename is not None
71
+ and os.path.splitext(file.filename)[-1]
72
+ in remote_directory.valid_file_extensions
73
+ ]
74
+
75
+
76
+ def _filter_by_max_files(
77
+ remote_directory: GenericRemoteDirectoryScope, files: list[FileSystemObject]
78
+ ) -> list[FileSystemObject]:
79
+ if remote_directory.max_files is None:
80
+ return files
81
+
82
+ return files[: remote_directory.max_files]
83
+
84
+
85
+ def _pull_remote_directory_data(
86
+ *,
87
+ filesystem_session: FileSystemSession,
88
+ remote_directory: GenericRemoteDirectoryScope,
89
+ logger: JobLogger,
90
+ ) -> list[FileObjectData]:
91
+ files_to_pull = filesystem_session.list_files(
92
+ dir_path=FileSystemFileReference(
93
+ filepath=remote_directory.src_path,
94
+ ),
95
+ recursive=remote_directory.recursive,
96
+ )
97
+ logger.log_info(
98
+ f"Pulled the following files {files_to_pull} from the remote directory {remote_directory}.",
99
+ )
100
+
101
+ files_to_pull = _filter_by_file_extension(remote_directory, files_to_pull)
102
+ files_to_pull = _filter_by_filename(remote_directory, files_to_pull)
103
+ files_to_pull = _filter_by_max_files(remote_directory, files_to_pull)
104
+
105
+ logger.log_info(
106
+ 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])}",
107
+ )
108
+ return filesystem_session.download_files(files_to_pull)
109
+
110
+
111
+ def _filter_downloaded_file_data(
112
+ remote_directory: GenericRemoteDirectoryScope,
113
+ pulled_file_data: list[FileObjectData],
114
+ logger: JobLogger,
115
+ ) -> list[FileObjectData]:
116
+ filtered_file_data = _filter_files_by_keyword(
117
+ remote_directory=remote_directory, files=pulled_file_data, logger=logger
118
+ )
119
+ return filtered_file_data
120
+
121
+
122
+ def _move_files_post_upload(
123
+ *,
124
+ filesystem_session: FileSystemSession,
125
+ remote_directory_scope: GenericRemoteDirectoryScope,
126
+ success_file_paths: list[str],
127
+ failed_file_paths: list[str],
128
+ ) -> None:
129
+ success_file_transfers: list[FileTransfer] = []
130
+ appended_text = ""
131
+
132
+ if remote_directory_scope.prepend_date_on_archive:
133
+ appended_text = f"-{datetime.now(timezone.utc).timestamp()}"
134
+
135
+ for file_path in success_file_paths:
136
+ filename = os.path.split(file_path)[-1]
137
+ root, extension = os.path.splitext(filename)
138
+ new_filename = f"{root}{appended_text}{extension}"
139
+ # format is source, dest in the tuple
140
+ success_file_transfers.append((
141
+ FileSystemFileReference(file_path),
142
+ FileSystemFileReference(
143
+ os.path.join(
144
+ remote_directory_scope.success_archive_path,
145
+ new_filename,
146
+ )
147
+ ),
148
+ ))
149
+
150
+ failed_file_transfers: list[FileTransfer] = []
151
+ for file_path in failed_file_paths:
152
+ filename = os.path.split(file_path)[-1]
153
+ root, extension = os.path.splitext(filename)
154
+ new_filename = f"{root}{appended_text}{extension}"
155
+ failed_file_transfers.append((
156
+ FileSystemFileReference(file_path),
157
+ FileSystemFileReference(
158
+ os.path.join(
159
+ remote_directory_scope.failure_archive_path,
160
+ new_filename,
161
+ )
162
+ ),
163
+ ))
164
+
165
+ filesystem_session.move_files([*success_file_transfers, *failed_file_transfers])
166
+
167
+
168
+ class GenericUploadJob(Job[None]):
169
+ def __init__(
170
+ self,
171
+ data_source: GenericUploadDataSource,
172
+ remote_directories: list[GenericRemoteDirectoryScope],
173
+ upload_strategy: GenericUploadStrategy,
174
+ ) -> None:
175
+ super().__init__()
176
+ self.remote_directories = remote_directories
177
+ self.upload_strategy = upload_strategy
178
+ self.data_source = data_source
179
+
180
+ @property
181
+ def payload_type(self) -> type[None]:
182
+ return type(None)
183
+
184
+ def _construct_filesystem_session(self, args: JobArguments) -> FileSystemSession:
185
+ match self.data_source:
186
+ case GenericUploadDataSourceSFTP():
187
+ pem_secret = retrieve_secret(
188
+ self.data_source.pem_secret, profile_metadata=args.profile_metadata
189
+ )
190
+ pem_key = paramiko.RSAKey.from_private_key(io.StringIO(pem_secret))
191
+ sftp_config = FileSystemSFTPConfig(
192
+ ip=self.data_source.host,
193
+ username=self.data_source.username,
194
+ pem_path=None,
195
+ pem_key=pem_key,
196
+ )
197
+ return SFTPSession(sftp_config=sftp_config)
198
+ case GenericUploadDataSourceS3():
199
+ if self.data_source.access_key_secret is not None:
200
+ secret_access_key = retrieve_secret(
201
+ self.data_source.access_key_secret,
202
+ profile_metadata=args.profile_metadata,
203
+ )
204
+ else:
205
+ secret_access_key = None
206
+
207
+ if self.data_source.endpoint_url is None:
208
+ assert self.data_source.cloud_provider is not None, (
209
+ "either cloud_provider or endpoint_url must be specified"
210
+ )
211
+ match self.data_source.cloud_provider:
212
+ case S3CloudProvider.AWS:
213
+ endpoint_url = "https://s3.amazonaws.com"
214
+ case S3CloudProvider.OVH:
215
+ assert self.data_source.region_name is not None, (
216
+ "region_name must be specified for cloud_provider OVH"
217
+ )
218
+ endpoint_url = f"https://s3.{self.data_source.region_name}.cloud.ovh.net"
219
+ else:
220
+ endpoint_url = self.data_source.endpoint_url
221
+
222
+ s3_config = FileSystemS3Config(
223
+ endpoint_url=endpoint_url,
224
+ bucket_name=self.data_source.bucket_name,
225
+ region_name=self.data_source.region_name,
226
+ access_key_id=self.data_source.access_key_id,
227
+ secret_access_key=secret_access_key,
228
+ session_token=None,
229
+ )
230
+
231
+ return S3Session(s3_config=s3_config)
232
+
233
+ def run_outer(self, args: JobArguments) -> JobResult:
234
+ client = args.client
235
+ batch_processor = args.batch_processor
236
+ logger = args.logger
237
+
238
+ with self._construct_filesystem_session(args) as filesystem_session:
239
+ files_to_upload: list[FileUpload] = []
240
+ for remote_directory in self.remote_directories:
241
+ pulled_file_data = _pull_remote_directory_data(
242
+ filesystem_session=filesystem_session,
243
+ remote_directory=remote_directory,
244
+ logger=logger,
245
+ )
246
+ filtered_file_data = _filter_downloaded_file_data(
247
+ remote_directory=remote_directory,
248
+ pulled_file_data=pulled_file_data,
249
+ logger=args.logger,
250
+ )
251
+ for file_data in filtered_file_data:
252
+ files_to_upload.append(
253
+ DataFileUpload(
254
+ data=io.BytesIO(file_data.file_data),
255
+ name=file_data.filename,
256
+ )
257
+ )
258
+ if not self.upload_strategy.skip_moving_files:
259
+ _move_files_post_upload(
260
+ filesystem_session=filesystem_session,
261
+ remote_directory_scope=remote_directory,
262
+ success_file_paths=[
263
+ file.filepath
264
+ if file.filepath is not None
265
+ else file.filename
266
+ for file in filtered_file_data
267
+ ],
268
+ # IMPROVE: use triggers/webhooks to mark failed files as failed
269
+ failed_file_paths=[],
270
+ )
271
+
272
+ uploaded_files = client.upload_files(file_uploads=files_to_upload)
273
+
274
+ file_ids = [file.file_id for file in uploaded_files]
275
+
276
+ for destination in self.upload_strategy.destinations:
277
+ for file_id in file_ids:
278
+ batch_processor.invoke_uploader(
279
+ file_id=file_id,
280
+ uploader_key=self.upload_strategy.uploader_key,
281
+ destination=destination,
282
+ )
283
+
284
+ return JobResult(success=True)
@@ -0,0 +1,25 @@
1
+ import importlib
2
+ import inspect
3
+ import os
4
+
5
+ from uncountable.integration.job import Job
6
+ from uncountable.types.job_definition_t import JobExecutorScript, ProfileMetadata
7
+
8
+
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
+ ])
17
+ job_module = importlib.import_module(job_module_path)
18
+ found_jobs: list[Job] = []
19
+ for _, job_class in inspect.getmembers(job_module, inspect.isclass):
20
+ if getattr(job_class, "_unc_job_registered", False):
21
+ found_jobs.append(job_class())
22
+ assert len(found_jobs) == 1, (
23
+ f"expected exactly one job class in {executor.import_path}, found {len(found_jobs)}"
24
+ )
25
+ return found_jobs[0]
@@ -0,0 +1,87 @@
1
+ import functools
2
+ import typing
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass
5
+
6
+ from pkgs.argument_parser import CachedParser
7
+ from uncountable.core.async_batch import AsyncBatchProcessor
8
+ from uncountable.core.client import Client
9
+ from uncountable.integration.telemetry import JobLogger
10
+ from uncountable.types import base_t, webhook_job_t
11
+ from uncountable.types.job_definition_t import JobDefinition, JobResult, ProfileMetadata
12
+
13
+
14
+ @dataclass(kw_only=True)
15
+ class JobArguments:
16
+ job_definition: JobDefinition
17
+ profile_metadata: ProfileMetadata
18
+ client: Client
19
+ batch_processor: AsyncBatchProcessor
20
+ logger: JobLogger
21
+ payload: base_t.JsonValue
22
+
23
+
24
+ # only for compatibility:
25
+ CronJobArguments = JobArguments
26
+
27
+
28
+ PT = typing.TypeVar("PT")
29
+
30
+
31
+ class Job(ABC, typing.Generic[PT]):
32
+ _unc_job_registered: bool = False
33
+
34
+ @property
35
+ @abstractmethod
36
+ def payload_type(self) -> type[PT]: ...
37
+
38
+ @abstractmethod
39
+ def run_outer(self, args: JobArguments) -> JobResult: ...
40
+
41
+ @functools.cached_property
42
+ def _cached_payload_parser(self) -> CachedParser[PT]:
43
+ return CachedParser(self.payload_type)
44
+
45
+ def get_payload(self, payload: base_t.JsonValue) -> PT:
46
+ return self._cached_payload_parser.parse_storage(payload)
47
+
48
+
49
+ class CronJob(Job):
50
+ @property
51
+ def payload_type(self) -> type[None]:
52
+ return type(None)
53
+
54
+ def run_outer(self, args: JobArguments) -> JobResult:
55
+ assert isinstance(args, CronJobArguments)
56
+ return self.run(args)
57
+
58
+ @abstractmethod
59
+ def run(self, args: JobArguments) -> JobResult: ...
60
+
61
+
62
+ WPT = typing.TypeVar("WPT")
63
+
64
+
65
+ class WebhookJob(Job[webhook_job_t.WebhookEventPayload], typing.Generic[WPT]):
66
+ @property
67
+ def payload_type(self) -> type[webhook_job_t.WebhookEventPayload]:
68
+ return webhook_job_t.WebhookEventPayload
69
+
70
+ @property
71
+ @abstractmethod
72
+ def webhook_payload_type(self) -> type[WPT]: ...
73
+
74
+ def run_outer(self, args: JobArguments) -> JobResult:
75
+ webhook_body = self.get_payload(args.payload)
76
+ inner_payload = CachedParser(self.webhook_payload_type).parse_api(
77
+ webhook_body.data
78
+ )
79
+ return self.run(args, inner_payload)
80
+
81
+ @abstractmethod
82
+ def run(self, args: JobArguments, payload: WPT) -> JobResult: ...
83
+
84
+
85
+ def register_job(cls: type[Job]) -> type[Job]:
86
+ cls._unc_job_registered = True
87
+ return cls
File without changes