UncountablePythonSDK 0.0.7__py3-none-any.whl → 0.0.9__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 (95) hide show
  1. {UncountablePythonSDK-0.0.7.dist-info → UncountablePythonSDK-0.0.9.dist-info}/METADATA +4 -2
  2. UncountablePythonSDK-0.0.9.dist-info/RECORD +145 -0
  3. pkgs/argument_parser/argument_parser.py +2 -2
  4. pkgs/serialization/missing_sentry.py +1 -1
  5. pkgs/serialization_util/serialization_helpers.py +3 -0
  6. pkgs/type_spec/emit_open_api.py +34 -17
  7. pkgs/type_spec/emit_open_api_util.py +4 -9
  8. pkgs/type_spec/emit_python.py +8 -5
  9. pkgs/type_spec/open_api_util.py +13 -12
  10. type_spec/external/api/entity/create_entities.yaml +1 -1
  11. type_spec/external/api/entity/create_entity.yaml +1 -1
  12. type_spec/external/api/entity/get_entities_data.yaml +4 -30
  13. type_spec/external/api/entity/list_entities.yaml +4 -14
  14. type_spec/external/api/entity/resolve_entity_ids.yaml +2 -2
  15. type_spec/external/api/entity/set_values.yaml +3 -30
  16. type_spec/external/api/input_groups/get_input_group_names.yaml +2 -2
  17. type_spec/external/api/inputs/create_inputs.yaml +6 -19
  18. type_spec/external/api/inputs/get_input_data.yaml +11 -24
  19. type_spec/external/api/inputs/get_input_names.yaml +4 -4
  20. type_spec/external/api/inputs/get_inputs_data.yaml +7 -20
  21. type_spec/external/api/inputs/set_input_attribute_values.yaml +3 -7
  22. type_spec/external/api/outputs/get_output_data.yaml +9 -20
  23. type_spec/external/api/outputs/get_output_names.yaml +2 -2
  24. type_spec/external/api/project/get_projects.yaml +6 -16
  25. type_spec/external/api/project/get_projects_data.yaml +10 -46
  26. type_spec/external/api/recipe_metadata/get_recipe_metadata_data.yaml +5 -5
  27. type_spec/external/api/recipes/create_recipes.yaml +5 -18
  28. type_spec/external/api/recipes/get_curve.yaml +2 -34
  29. type_spec/external/api/recipes/get_recipe_calculations.yaml +7 -17
  30. type_spec/external/api/recipes/get_recipe_links.yaml +1 -1
  31. type_spec/external/api/recipes/get_recipe_names.yaml +2 -2
  32. type_spec/external/api/recipes/get_recipe_output_metadata.yaml +4 -17
  33. type_spec/external/api/recipes/get_recipes_data.yaml +31 -165
  34. type_spec/external/api/recipes/set_recipe_inputs.yaml +3 -3
  35. type_spec/external/api/recipes/set_recipe_outputs.yaml +4 -8
  36. uncountable/integration/__init__.py +0 -0
  37. uncountable/integration/construct_client.py +30 -0
  38. uncountable/integration/cron.py +29 -0
  39. uncountable/integration/db/__init__.py +0 -0
  40. uncountable/integration/db/connect.py +8 -0
  41. uncountable/integration/entrypoint.py +41 -0
  42. uncountable/integration/executors/__init__.py +0 -0
  43. uncountable/integration/executors/script_executor.py +18 -0
  44. uncountable/integration/job.py +39 -0
  45. uncountable/integration/server.py +86 -0
  46. uncountable/integration/types.py +89 -0
  47. uncountable/types/__init__.py +30 -0
  48. uncountable/types/api/entity/create_entities.py +2 -1
  49. uncountable/types/api/entity/create_entity.py +1 -1
  50. uncountable/types/api/entity/get_entities_data.py +6 -26
  51. uncountable/types/api/entity/list_entities.py +5 -12
  52. uncountable/types/api/entity/resolve_entity_ids.py +4 -2
  53. uncountable/types/api/entity/set_values.py +7 -29
  54. uncountable/types/api/input_groups/get_input_group_names.py +3 -2
  55. uncountable/types/api/inputs/create_inputs.py +8 -20
  56. uncountable/types/api/inputs/get_input_data.py +12 -25
  57. uncountable/types/api/inputs/get_input_names.py +5 -4
  58. uncountable/types/api/inputs/get_inputs_data.py +8 -21
  59. uncountable/types/api/inputs/set_input_attribute_values.py +5 -7
  60. uncountable/types/api/outputs/get_output_data.py +10 -17
  61. uncountable/types/api/outputs/get_output_names.py +3 -2
  62. uncountable/types/api/project/get_projects.py +8 -14
  63. uncountable/types/api/project/get_projects_data.py +13 -40
  64. uncountable/types/api/recipe_metadata/get_recipe_metadata_data.py +6 -5
  65. uncountable/types/api/recipes/create_recipes.py +6 -18
  66. uncountable/types/api/recipes/get_curve.py +4 -26
  67. uncountable/types/api/recipes/get_recipe_calculations.py +8 -15
  68. uncountable/types/api/recipes/get_recipe_links.py +2 -1
  69. uncountable/types/api/recipes/get_recipe_names.py +3 -2
  70. uncountable/types/api/recipes/get_recipe_output_metadata.py +5 -13
  71. uncountable/types/api/recipes/get_recipes_data.py +36 -122
  72. uncountable/types/api/recipes/set_recipe_inputs.py +4 -3
  73. uncountable/types/api/recipes/set_recipe_outputs.py +7 -8
  74. uncountable/types/calculations.py +23 -0
  75. uncountable/types/client_base.py +43 -43
  76. uncountable/types/curves.py +47 -0
  77. uncountable/types/entity.py +4 -1
  78. uncountable/types/experiment_groups.py +23 -0
  79. uncountable/types/field_values.py +37 -0
  80. uncountable/types/fields.py +24 -0
  81. uncountable/types/input_attributes.py +29 -0
  82. uncountable/types/inputs.py +61 -0
  83. uncountable/types/outputs.py +26 -0
  84. uncountable/types/phases.py +23 -0
  85. uncountable/types/recipe_links.py +3 -2
  86. uncountable/types/recipe_metadata.py +50 -0
  87. uncountable/types/recipe_output_metadata.py +24 -0
  88. uncountable/types/recipe_tags.py +23 -0
  89. uncountable/types/response.py +25 -0
  90. uncountable/types/units.py +23 -0
  91. uncountable/types/users.py +24 -0
  92. uncountable/types/workflows.py +32 -0
  93. UncountablePythonSDK-0.0.7.dist-info/RECORD +0 -119
  94. {UncountablePythonSDK-0.0.7.dist-info → UncountablePythonSDK-0.0.9.dist-info}/WHEEL +0 -0
  95. {UncountablePythonSDK-0.0.7.dist-info → UncountablePythonSDK-0.0.9.dist-info}/top_level.txt +0 -0
@@ -8,17 +8,17 @@ Arguments:
8
8
  type: Object
9
9
  properties:
10
10
  recipe_output_ids:
11
- type: List<Integer>
11
+ type: List<ObjectId>
12
12
  desc: "Ids of the Recipe Outputs to retrieve metadata for"
13
13
 
14
14
  RecipeOutputMetadata:
15
15
  type: Object
16
16
  properties:
17
17
  recipe_output_id:
18
- type: Integer
18
+ type: ObjectId
19
19
  desc: "Identifier for the recipe output"
20
20
  recipe_output_metadata_field_id:
21
- type: Integer
21
+ type: ObjectId
22
22
  desc: "Identifier for the metadata field id"
23
23
  quantity_dec:
24
24
  type: Decimal
@@ -27,23 +27,10 @@ RecipeOutputMetadata:
27
27
  type: JsonValue
28
28
  desc: "The value for the metadata, if it is not of numeric type"
29
29
 
30
- RecipeOutputMetadataField:
31
- type: Object
32
- properties:
33
- id:
34
- type: Integer
35
- desc: "Identifier for the recipe output metadata field"
36
- name:
37
- type: String
38
- desc: "Name of the recipe output metadata field"
39
- quantity_type:
40
- type: String
41
- desc: "The quantity type of the metadata, such as numeric or text"
42
-
43
30
  Data:
44
31
  type: Object
45
32
  properties:
46
33
  recipe_output_metadata:
47
34
  type: List<RecipeOutputMetadata>
48
35
  recipe_output_metadata_fields:
49
- type: List<RecipeOutputMetadataField>
36
+ type: List<recipe_output_metadata.RecipeOutputMetadataField>
@@ -7,89 +7,28 @@ $endpoint:
7
7
  Arguments:
8
8
  type: Object
9
9
  properties:
10
- recipe_ids:
11
- type: Optional<List<Integer>>
10
+ recipe_ids?:
11
+ type: Optional<List<ObjectId>>
12
12
  desc: "The recipes to get the data from. Either these or project_id must be filled in"
13
- project_id:
14
- type: Optional<Integer>
13
+ project_id?:
14
+ type: Optional<ObjectId>
15
15
  desc: "The projects to get the data from. Either these or recipe_ids must be filled in"
16
- offset:
16
+ offset?:
17
17
  type: Optional<Integer>
18
18
  desc: "Used for pagination. All pagination is done in order of Recipe ID"
19
- limit:
19
+ limit?:
20
20
  type: Optional<Integer>
21
21
  desc: "The number of data points to return. If not filled in, it will be set to 100, and cannot be set higher than 100"
22
22
 
23
- SimpleRecipeMetadataField:
24
- type: Object
25
- desc: "A descriptor of metadata. Metadata includes values that are neither ingredients nor process parameters, such as a location of an experiment"
26
- properties:
27
- metadata_id:
28
- type: Integer
29
- desc: "A unique ID for the metadata, referenceable elsewhere"
30
- name:
31
- type: String
32
- desc: "The name of the metadata"
33
- quantity_type:
34
- type: String
35
- desc: "The quantity type of the metadata, such as numeric or text"
36
-
37
- RecipeMetadata:
38
- type: Object
39
- desc: "The specific value of a metadata for a recipe. Recipe may only have 1 instance of each metadata"
40
- properties:
41
- metadata_id:
42
- type: Integer
43
- desc: "The unique ID for the metadata"
44
- quantity_dec:
45
- type: Decimal
46
- desc: "The value for the metadata, if it is of numeric type"
47
- quantity_json:
48
- type: JsonValue
49
- desc: "The value for the metadata, if it is not of numeric type"
50
-
51
- SimpleRecipeTag:
52
- type: Object
53
- desc: "A representation of recipe tags within Uncountable system"
54
- properties:
55
- recipe_tag_id:
56
- type: Integer
57
- desc: "A unique ID for the tag, referenceable elsewhere"
58
- name:
59
- type: String
60
- desc: "The name of the tag"
61
-
62
- SimpleExperimentGroup:
63
- type: Object
64
- desc: "A representation of experiment groups within Uncountable system"
65
- properties:
66
- experiment_group_id:
67
- type: Integer
68
- desc: "A unique ID for the group, referenceable elsewhere"
69
- name:
70
- type: String
71
- desc: "The name of the group"
72
-
73
- SimpleUnit:
74
- type: Object
75
- desc: "A representation of units within Uncountable system"
76
- properties:
77
- unit_id:
78
- type: Integer
79
- desc: "A unique ID for the unit, referenceable elsewhere"
80
- name:
81
- type: String
82
- desc: "The name of the unit"
83
-
84
23
  RecipeOutput:
85
24
  type: Object
86
25
  desc: "A representation of a single measurement associated with a recipe / experiment within Uncountable system"
87
26
  properties:
88
27
  id:
89
- type: Integer
28
+ type: ObjectId
90
29
  desc: "The global unique identifier for the recipe output. For cross-reference elsewhere."
91
30
  output_id:
92
- type: Integer
31
+ type: ObjectId
93
32
  desc: "The output specifically measured in this case"
94
33
  replicate_num:
95
34
  type: Integer
@@ -101,35 +40,18 @@ RecipeOutput:
101
40
  type: JsonValue
102
41
  desc: "The quantity of the output, if not numeric"
103
42
  curve_id:
104
- type: Integer
43
+ type: ObjectId
105
44
  desc: "The id of the curve associated with the output if the output is of curve type."
106
45
  output_condition_id:
107
- type: Integer
46
+ type: ObjectId
108
47
  desc: "A reference to the condition ID for the output (null if there are no conditions). Used to encode complicated test settings such as test standard, aging time, etc."
109
48
 
110
- SimpleOutput:
111
- type: Object
112
- desc: "A condensed object representing an output / measurement within Uncountables system"
113
- properties:
114
- output_id:
115
- type: Integer
116
- desc: "The globally unique identifier for the output"
117
- name:
118
- type: String
119
- desc: "The name of the output"
120
- unit:
121
- type: Optional<SimpleUnit>
122
- desc: "The units for the output, if filled in"
123
- quantity_type:
124
- type: String
125
- desc: "The quantity type of the output, with common types including numeric, text, categorical, curve"
126
-
127
49
  ConditionParameterValue:
128
50
  type: Object
129
51
  desc: "A condition parameter value represents a single association of value with a parameter. For instance, Test Method: ISO 23"
130
52
  properties:
131
53
  condition_parameter_id:
132
- type: Integer
54
+ type: ObjectId
133
55
  desc: "The globally unique identifier for the condition parameter, i.e. Test Method or Aging Time"
134
56
  condition_parameter_name:
135
57
  type: String
@@ -146,47 +68,23 @@ SimpleOutputCondition:
146
68
  desc: "Within Uncountable, Output conditions represent how an output is measured. Common use cases include the test temperature, test standard, aging time, etc. An outputcondition is the full description of these parameters"
147
69
  properties:
148
70
  output_condition_id:
149
- type: Integer
71
+ type: ObjectId
150
72
  desc: "The globally unique ID for the Output Condition, used for cross reference elsewhere"
151
73
  condition_parameter_values:
152
74
  type: List<ConditionParameterValue>
153
75
  desc: "The list of parameters and values that comprise this condition"
154
- SimpleWorkflowStep:
155
- type: Object
156
- desc: "A step associated with a workflow, representing where inputs are placed in the context of a recipe / experiment."
157
- properties:
158
- workflow_step_id:
159
- type: Integer
160
- desc: "The globally unique ID for the workflow step, used for cross reference elsewhere"
161
- name:
162
- type: String
163
- desc: "The name of the workflow step"
164
-
165
- SimpleWorkflow:
166
- type: Object
167
- desc: "A workflow object. Workflows represent broad flowcharts for how experimental creation is done, including a graph of steps that lead into each other."
168
- properties:
169
- workflow_id:
170
- type: Integer
171
- desc: "The globally unique ID for the workflow, used for cross reference elsewhere"
172
- name:
173
- type: String
174
- desc: "The name of the workflow"
175
- workflow_steps:
176
- type: List<SimpleWorkflowStep>
177
- desc: "The steps associated with the workflow, representing where inputs are placed in the context of a recipe / experiment."
178
76
 
179
77
  RecipeInput:
180
78
  type: Object
181
79
  properties:
182
80
  input_id:
183
- type: Integer
81
+ type: ObjectId
184
82
  desc: "The globally unique ID for the input, allowing for cross reference elsewhere"
185
83
  input_lot_recipe_id:
186
- type: Optional<Integer>
84
+ type: Optional<ObjectId>
187
85
  desc: "The recipe id of the lot associated with the input"
188
86
  recipe_step_id:
189
- type: Integer
87
+ type: ObjectId
190
88
  desc: "The unique Recipe Step ID for the input, allowing for placement within the workflow"
191
89
  quantity_dec:
192
90
  type: Decimal
@@ -207,25 +105,6 @@ RecipeInput:
207
105
  type: String
208
106
  desc: "The behavior of the input relative to the experiment. For example 'inclusion' or 'use quantity'."
209
107
 
210
- SimpleInput:
211
- type: Object
212
- properties:
213
- input_id:
214
- type: Integer
215
- desc: "The globally unique ID for the input, allowing for cross reference elsewhere"
216
- quantity_type:
217
- type: String
218
- desc: "The quantity type of the input in string form. Common types are numeric, recipe (representing a nested input), text, categorical"
219
- name:
220
- type: String
221
- desc: "The name of the input"
222
- is_parameter:
223
- type: Boolean
224
- desc: "Whether the input is a process parameter (true) or an ingredient (false)"
225
- intermediate_recipe_id:
226
- type: Optional<Integer>
227
- desc: "The recipe ID of the input if the input is a a recipe (intermediate)."
228
-
229
108
  RecipeStep:
230
109
  type: Object
231
110
  properties:
@@ -233,7 +112,7 @@ RecipeStep:
233
112
  type: Optional<String>
234
113
  desc: "An optional name for the recipe step"
235
114
  recipe_step_id:
236
- type: Integer
115
+ type: ObjectId
237
116
  desc: "An ID value for the recipe step, used elsewhere to reference the part of the recipe / experiment that the input was placed in"
238
117
  recipe_step_number:
239
118
  type: Integer
@@ -257,28 +136,15 @@ RecipeWorkflowStep:
257
136
  desc: "A workflow step associated with a recipe / experiment. Workflow steps are comprised of recipe step groups, in turn comprised of recipe steps, where inputs are located. These groupings exists to give customers the ability to flexibly place inputs into the correct process ordering"
258
137
  properties:
259
138
  recipe_workflow_step_id:
260
- type: Integer
139
+ type: ObjectId
261
140
  desc: "A unique reference for the workflow step within the recipe."
262
141
  workflow_step_id:
263
- type: Integer
142
+ type: ObjectId
264
143
  desc: "A reference to the workflow_step_id for the workflow. This will be shared across recipes / experiments, and can be used to do analysis about what part of a generic process inputs are located in."
265
144
  recipe_step_groups:
266
145
  type: List<RecipeStepGroup>
267
146
  desc: "A listing of recipe step groups within the recipe / experiment"
268
147
 
269
- SimpleUser:
270
- type: Object
271
- properties:
272
- user_id:
273
- type: Integer
274
- desc: "The globally unique ID for the user, allowing for cross reference elsewhere"
275
- display_name:
276
- type: String
277
- desc: "The display name of the user within Uncountable."
278
- email:
279
- type: String
280
- desc: "The email address of the user in Uncountable's system."
281
-
282
148
  RecipeStepRelationship:
283
149
  type: Object
284
150
  properties:
@@ -305,10 +171,10 @@ Recipe:
305
171
  type: Object
306
172
  properties:
307
173
  recipe_id:
308
- type: Integer
174
+ type: ObjectId
309
175
  desc: "A unique identifier for the recipe / experiment. Used for joining elsewhere"
310
176
  creating_user_id?:
311
- type: Integer
177
+ type: ObjectId
312
178
  desc: "The user ID who originally created the recipe / experiment. Null when created from automated scripts, such as data transfers, or from the Uncountable implementation team."
313
179
  create_datetime:
314
180
  type: String
@@ -326,10 +192,10 @@ Recipe:
326
192
  type: String
327
193
  desc: "The value used in the barcoding system to lookup this experiment."
328
194
  workflow_id:
329
- type: Integer
195
+ type: ObjectId
330
196
  desc: "The workflow ID associated with the recipe / experiment. Workflows correspond to a set of experimental steps performed, and are referenced in the workflows return object"
331
197
  metadata:
332
- type: List<RecipeMetadata>
198
+ type: List<recipe_metadata.RecipeMetadata>
333
199
  desc: "Metadata associated with a recipe / experimen. Metadata includes values that are neither ingredients nor process parameters, such as a location of an experiment"
334
200
  inputs:
335
201
  type: List<RecipeInput>
@@ -341,10 +207,10 @@ Recipe:
341
207
  type: List<RecipeWorkflowStep>
342
208
  desc: "A reference of workflow steps in the recipe / experimen. This is used to reference input values to where they occurred in the experimental process."
343
209
  tag_ids:
344
- type: List<Integer>
210
+ type: List<ObjectId>
345
211
  desc: A list of Tag IDs associated with the recipe / experiment
346
212
  experiment_group_ids:
347
- type: List<Integer>
213
+ type: List<ObjectId>
348
214
  desc: A list of experiment group IDs associated with the recipe / experiment
349
215
  step_relationships:
350
216
  type: List<RecipeStepRelationship>
@@ -355,18 +221,18 @@ Data:
355
221
  recipes:
356
222
  type: List<Recipe>
357
223
  workflows:
358
- type: List<SimpleWorkflow>
224
+ type: List<workflows.SimpleWorkflow>
359
225
  metadata:
360
- type: List<SimpleRecipeMetadataField>
226
+ type: List<recipe_metadata.SimpleRecipeMetadataField>
361
227
  inputs:
362
- type: List<SimpleInput>
228
+ type: List<inputs.SimpleInput>
363
229
  outputs:
364
- type: List<SimpleOutput>
230
+ type: List<outputs.SimpleOutput>
365
231
  output_conditions:
366
232
  type: List<SimpleOutputCondition>
367
233
  users:
368
- type: List<SimpleUser>
234
+ type: List<users.SimpleUser>
369
235
  recipe_tags:
370
- type: List<SimpleRecipeTag>
236
+ type: List<recipe_tags.SimpleRecipeTag>
371
237
  experiment_groups:
372
- type: List<SimpleExperimentGroup>
238
+ type: List<experiment_groups.SimpleExperimentGroup>
@@ -9,13 +9,13 @@ RecipeInputValue:
9
9
  type: Object
10
10
  properties:
11
11
  recipe_id:
12
- type: Integer
12
+ type: ObjectId
13
13
  desc: "A unique identifier for the recipe / experiment. Used for joining elsewhere"
14
14
  recipe_step_id?:
15
- type: Optional<Integer>
15
+ type: Optional<ObjectId>
16
16
  desc: "Optionally the step id for the step of the recipe to be written to. If none is passed, the input will be written to first recipe workflow step"
17
17
  input_id:
18
- type: Integer
18
+ type: ObjectId
19
19
  desc: "A unique identifier for the input. Used for joining and identification"
20
20
  value_numeric?:
21
21
  type: Decimal
@@ -19,13 +19,13 @@ RecipeOutputValue:
19
19
  type: Object
20
20
  properties:
21
21
  recipe_id:
22
- type: Integer
22
+ type: ObjectId
23
23
  desc: "A unique identifier for the recipe / experiment. Used for joining elsewhere"
24
24
  output_id:
25
- type: Integer
25
+ type: ObjectId
26
26
  desc: "A unique identifier for the output. Used for joining and identification"
27
27
  condition_id?:
28
- type: Integer
28
+ type: ObjectId
29
29
  desc: "A unique identifier the set of condition parameters associated with the output."
30
30
  experiment_num:
31
31
  type: Integer
@@ -48,9 +48,5 @@ Arguments:
48
48
  desc: "The outputs to set. Must be at most 100 entries long"
49
49
 
50
50
  Data:
51
- type: Object
51
+ type: response.Response
52
52
  properties:
53
- status:
54
- type: Literal<'ok'>
55
- default: "ok"
56
- desc: "'ok' indicates the post request was successful"
File without changes
@@ -0,0 +1,30 @@
1
+ import os
2
+ from typing import assert_never
3
+
4
+ from uncountable.core.client import AuthDetailsApiKey, Client
5
+ from uncountable.integration.types import (
6
+ AuthRetrievalEnv,
7
+ ProfileMetadata,
8
+ )
9
+
10
+
11
+ def construct_uncountable_client(
12
+ profile_meta: ProfileMetadata
13
+ ) -> Client:
14
+ match profile_meta.auth_retrieval:
15
+ case AuthRetrievalEnv():
16
+ api_id = os.getenv(f"UNC_PROFILE_{profile_meta.name}_API_ID")
17
+ api_secret_key = os.getenv(
18
+ f"UNC_PROFILE_{profile_meta.name}_API_SECRET_KEY"
19
+ )
20
+
21
+ assert api_id is not None
22
+ assert api_secret_key is not None
23
+
24
+ return Client(
25
+ base_url=profile_meta.base_url,
26
+ auth_details=AuthDetailsApiKey(
27
+ api_id=api_id, api_secret_key=api_secret_key
28
+ ),
29
+ )
30
+ assert_never(profile_meta.auth_retrieval)
@@ -0,0 +1,29 @@
1
+ from dataclasses import dataclass
2
+
3
+ from pkgs.argument_parser import CachedParser
4
+ from uncountable.integration.construct_client import construct_uncountable_client
5
+ from uncountable.integration.executors.script_executor import resolve_script_executor
6
+ from uncountable.integration.job import CronJobArguments
7
+ from uncountable.integration.types import JobDefinition, ProfileMetadata
8
+
9
+
10
+ @dataclass
11
+ class CronJobArgs:
12
+ definition: JobDefinition
13
+ profile_metadata: ProfileMetadata
14
+
15
+
16
+ cron_args_parser = CachedParser(CronJobArgs)
17
+
18
+
19
+ def cron_job_executor(**kwargs: dict) -> None:
20
+ args_passed = cron_args_parser.parse_storage(kwargs)
21
+ args = CronJobArguments(
22
+ job_definition=args_passed.definition,
23
+ client=construct_uncountable_client(profile_meta=args_passed.profile_metadata),
24
+ )
25
+
26
+ job_class = resolve_script_executor(args_passed.definition.executor)
27
+
28
+ job = job_class()
29
+ job.run(args)
File without changes
@@ -0,0 +1,8 @@
1
+ import os
2
+ from sqlalchemy import create_engine
3
+ from sqlalchemy.engine.base import Engine
4
+
5
+
6
+ def create_db_engine() -> Engine:
7
+ return create_engine(os.environ["UNC_SQLITE_URI"])
8
+
@@ -0,0 +1,41 @@
1
+ import os
2
+ from importlib import resources
3
+
4
+ from uncountable.integration.server import IntegrationServer
5
+ from uncountable.integration.types import ProfileDefinition
6
+ from pkgs.argument_parser import CachedParser
7
+ from uncountable.integration.db.connect import create_db_engine
8
+
9
+
10
+ profile_parser = CachedParser(ProfileDefinition)
11
+
12
+
13
+ def main() -> None:
14
+ profiles_module = os.environ["UNC_PROFILES_MODULE"]
15
+ with IntegrationServer(create_db_engine()) as server:
16
+ # TODO: Loop through all job spec yaml files and call server.add_job
17
+ profiles = [
18
+ entry
19
+ for entry in resources.files(profiles_module).iterdir()
20
+ if entry.is_dir()
21
+ ]
22
+ for profile in profiles:
23
+ profile_name = profile.name
24
+ try:
25
+ profile = profile_parser.parse_yaml_resource(
26
+ package=".".join([profiles_module, profile_name]),
27
+ resource="profile.yaml",
28
+ )
29
+ except FileNotFoundError as e:
30
+ print("WARN: profile.yaml not found", e)
31
+ continue
32
+ server.register_profile(
33
+ profile_name=profile_name,
34
+ base_url=profile.base_url,
35
+ auth_retrieval=profile.auth_retrieval,
36
+ jobs=profile.jobs,
37
+ )
38
+
39
+
40
+ if __name__ == "__main__":
41
+ main()
File without changes
@@ -0,0 +1,18 @@
1
+
2
+
3
+ import importlib
4
+ import inspect
5
+ from uncountable.integration.job import Job
6
+ from uncountable.integration.types import JobExecutorScript
7
+
8
+
9
+ def resolve_script_executor(executor: JobExecutorScript) -> type[Job]:
10
+ job_module = importlib.import_module(executor.import_path)
11
+ found_jobs: list[type[Job]] = []
12
+ for _, job_class in inspect.getmembers(job_module, inspect.isclass):
13
+ if Job in job_class.__bases__:
14
+ found_jobs.append(job_class())
15
+ assert (
16
+ len(found_jobs) == 1
17
+ ), f"expected exactly one job class in {executor.import_path}"
18
+ return found_jobs[0]
@@ -0,0 +1,39 @@
1
+ from dataclasses import dataclass
2
+ from uncountable.core.client import Client
3
+ from uncountable.integration.types import JobDefinition
4
+
5
+ from abc import ABC, abstractmethod
6
+
7
+
8
+ @dataclass
9
+ class JobArgumentsBase:
10
+ job_definition: JobDefinition
11
+ client: Client
12
+
13
+
14
+ @dataclass
15
+ class CronJobArguments(JobArgumentsBase):
16
+ # can imagine passing additional data such as in the sftp or webhook cases
17
+ pass
18
+
19
+
20
+ JobArguments = CronJobArguments
21
+
22
+
23
+ @dataclass
24
+ class JobResult:
25
+ success: bool
26
+
27
+
28
+ class Job(ABC):
29
+
30
+ @abstractmethod
31
+ def run(self, args: JobArguments) -> JobResult:
32
+ ...
33
+
34
+
35
+ class CronJob(Job):
36
+
37
+ @abstractmethod
38
+ def run(self, args: CronJobArguments) -> JobResult:
39
+ ...
@@ -0,0 +1,86 @@
1
+ from dataclasses import asdict
2
+ from typing import assert_never
3
+ from apscheduler.schedulers.background import BackgroundScheduler
4
+ from apscheduler.schedulers.base import BaseScheduler
5
+ from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
6
+ from apscheduler.executors.pool import ThreadPoolExecutor
7
+ from uncountable.integration.cron import CronJobArgs, cron_job_executor
8
+ from apscheduler.triggers.cron import CronTrigger
9
+ from sqlalchemy.engine.base import Engine
10
+
11
+ from uncountable.integration.types import (
12
+ AuthRetrieval,
13
+ CronJobDefinition,
14
+ JobDefinition,
15
+ ProfileMetadata,
16
+ )
17
+
18
+
19
+ _MAX_APSCHEDULER_CONCURRENT_JOBS = 1
20
+
21
+
22
+ class IntegrationServer:
23
+ _scheduler: BaseScheduler
24
+ _engine: Engine
25
+
26
+ def __init__(self, engine: Engine) -> None:
27
+ self._engine = engine
28
+ self._scheduler = BackgroundScheduler(
29
+ timezone="UTC",
30
+ jobstores={"default": SQLAlchemyJobStore(engine=engine)},
31
+ executors={"default": ThreadPoolExecutor(_MAX_APSCHEDULER_CONCURRENT_JOBS)},
32
+ )
33
+
34
+ def register_profile(
35
+ self,
36
+ *,
37
+ profile_name: str,
38
+ base_url: str,
39
+ auth_retrieval: AuthRetrieval,
40
+ jobs: list[JobDefinition],
41
+ ) -> None:
42
+ for job_defn in jobs:
43
+ profile_metadata = ProfileMetadata(
44
+ name=profile_name, auth_retrieval=auth_retrieval, base_url=base_url
45
+ )
46
+ match job_defn:
47
+ case CronJobDefinition():
48
+ # Add to ap scheduler
49
+ job_kwargs = asdict(
50
+ CronJobArgs(
51
+ definition=job_defn, profile_metadata=profile_metadata
52
+ )
53
+ )
54
+ existing_job = self._scheduler.get_job(job_defn.id)
55
+ if existing_job is not None:
56
+ existing_job.modify(
57
+ name=job_defn.name,
58
+ kwargs=job_kwargs,
59
+ )
60
+ existing_job.reschedule(job_defn.cron_spec)
61
+ else:
62
+ self._scheduler.add_job(
63
+ cron_job_executor,
64
+ # IMPROVE: reconsider these defaults
65
+ max_instances=1,
66
+ coalesce=True,
67
+ trigger=CronTrigger.from_crontab(job_defn.cron_spec),
68
+ name=job_defn.name,
69
+ id=job_defn.id,
70
+ kwargs=job_kwargs,
71
+ )
72
+ case _:
73
+ assert_never(job_defn.trigger)
74
+
75
+ def _start_apscheduler(self) -> None:
76
+ self._scheduler.start()
77
+
78
+ def _stop_apscheduler(self) -> None:
79
+ self._scheduler.shutdown()
80
+
81
+ def __enter__(self) -> "IntegrationServer":
82
+ self._start_apscheduler()
83
+ return self
84
+
85
+ def __exit__(self) -> None:
86
+ self._stop_apscheduler()