UncountablePythonSDK 0.0.8__py3-none-any.whl → 0.0.10__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: UncountablePythonSDK
3
- Version: 0.0.8
3
+ Version: 0.0.10
4
4
  Summary: Uncountable SDK
5
5
  Project-URL: Homepage, https://github.com/uncountableinc/uncountable-python-sdk
6
6
  Project-URL: Repository, https://github.com/uncountableinc/uncountable-python-sdk.git
@@ -16,7 +16,11 @@ Classifier: Topic :: Utilities
16
16
  Classifier: Typing :: Typed
17
17
  Requires-Python: >=3.11
18
18
  Description-Content-Type: text/markdown
19
- Requires-Dist: requests >=2.31.0
19
+ Requires-Dist: requests ==2.31.0
20
+ Requires-Dist: SQLAlchemy ==2.0.29
21
+ Requires-Dist: APScheduler ==3.10.4
22
+ Requires-Dist: dateutil ==2.9.0
23
+ Requires-Dist: shelljob ==0.6.3
20
24
  Provides-Extra: test
21
25
  Requires-Dist: mypy >=1.8.1 ; extra == 'test'
22
26
  Requires-Dist: ruff >=0.2.1 ; extra == 'test'
@@ -4,7 +4,7 @@ pkgs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  pkgs/argument_parser/__init__.py,sha256=CsQ6QoPKSLLRVl-z6URAmPkiUL9ZPZoV4rJHgy_-RjA,385
5
5
  pkgs/argument_parser/_is_enum.py,sha256=JQPlVIW0z-N9Qux61TQXaqP3lIHEhOOGwe9--JcM4Ro,393
6
6
  pkgs/argument_parser/_is_namedtuple.py,sha256=Rjc1bKanIPPogl3qG5JPBxglG1TqWYOo1nxxhBASQWY,265
7
- pkgs/argument_parser/argument_parser.py,sha256=0jXJ_gUNYj9dxpuyFl64o-zRVDwWBiSC9mXKdP53i6I,14446
7
+ pkgs/argument_parser/argument_parser.py,sha256=TC6cP6OQeLyzYU0m56f9BXSuCcR67FgyRlOa6OZyjgk,14445
8
8
  pkgs/argument_parser/case_convert.py,sha256=J9wahIE-P95LvTqn4M4gDUx_RXeiW2SRo9i_1bz1E6A,558
9
9
  pkgs/serialization/__init__.py,sha256=quvXMSl1szddLTr4Yxo9KA9oBMoeX7qGpFkkAplFBbY,603
10
10
  pkgs/serialization/missing_sentry.py,sha256=aM_9KxbCk9dVvXvcOKgkIQBqFWvLhv8QlIUCiuFEXMo,806
@@ -20,13 +20,13 @@ pkgs/type_spec/__main__.py,sha256=5bJaX9Y_-FavP0qwzhk-z-V97UY7uaezJTa1zhO_HHQ,10
20
20
  pkgs/type_spec/builder.py,sha256=Pl3UzHYgci3XRYsJ-pEsrdKu26mrJeLQv66OS2KePGc,38993
21
21
  pkgs/type_spec/config.py,sha256=wXALehQr_CcteOY_KbeI3rtmGgqVzHcveAhgKCDfQoQ,4934
22
22
  pkgs/type_spec/emit_io_ts.py,sha256=gCEfS81w_ifqjLVJ3_cpy9Gq03o6H5nEsh35WAkqGGE,5606
23
- pkgs/type_spec/emit_open_api.py,sha256=8tidMRWubBmiiq-XCu_2732IvwTc6R3miurdlBCv3-w,16939
24
- pkgs/type_spec/emit_open_api_util.py,sha256=S4eHeryxiGUNujypmwn8UZQSlaCCWn7uMpiZzNJJqCg,2054
23
+ pkgs/type_spec/emit_open_api.py,sha256=dnGazdVBjdEWQ4WK8Ujq966quRZY2TyQ05PGR48Md2I,17461
24
+ pkgs/type_spec/emit_open_api_util.py,sha256=xnzPOuPO8Pbhf6xf1tkHUKgXs1n-jH-lWFac8pOtd4I,1882
25
25
  pkgs/type_spec/emit_python.py,sha256=ixJ5aXzrpeGuNyuX3pfGRSWgUWgRNegX3ltgwqrmQJA,35580
26
26
  pkgs/type_spec/emit_typescript.py,sha256=6tlzvyHc7iIEic01AtjYWuzy7Ac3KijtXRQ3IQSN7JE,18670
27
27
  pkgs/type_spec/emit_typescript_util.py,sha256=MZMigIsYIYOXLjJqp-DSRYsztWbIKd9QQEmYRRqnGQ4,894
28
28
  pkgs/type_spec/load_types.py,sha256=LMIkrk0n-Kh4I4CUOBzuboYtsAHBYuMrL9d2hkU83f4,2184
29
- pkgs/type_spec/open_api_util.py,sha256=eJBYgKmu6twqParqWUIIywzx8RM-2dOIyoBLRMHMmqo,7414
29
+ pkgs/type_spec/open_api_util.py,sha256=kOxDc1P4dgjWRxOQNNRtayUEHiHcxbc4OGzBsCWsKsM,7462
30
30
  pkgs/type_spec/test.py,sha256=4ueujBq-pEgnX3Z69HyPmD-bullFXmpixcpVzfOkhP4,489
31
31
  pkgs/type_spec/util.py,sha256=pTV9EB4_M0tucWwape9_1m5maIi3UKPw44SA_Mc8KNA,4856
32
32
  pkgs/type_spec/parts/base.py.prepart,sha256=2RsjF9nTwERb7M0LKsPjGbN5cL2QMwMVt4QHpUPPM70,2147
@@ -47,11 +47,11 @@ type_spec/external/api/entity/resolve_entity_ids.yaml,sha256=Zf3OhAohwLJO7wWj0e-
47
47
  type_spec/external/api/entity/set_values.yaml,sha256=_UDxSk09Ke0KDABt8ldOScC2CLrbAhBPArZvno5iD4I,422
48
48
  type_spec/external/api/input_groups/get_input_group_names.yaml,sha256=LYgnm2Or7ZWCzDGjl53Y1PcHvej8G7Mpp5H96Z4UZCg,913
49
49
  type_spec/external/api/inputs/create_inputs.yaml,sha256=pX8jatpve4iJxK7kYw7SBlQXlVHJVfLDCOrDdpn8Gsw,1561
50
- type_spec/external/api/inputs/get_input_data.yaml,sha256=-C4-9MbQjwKlWnst2RCA8yXAOVzHssXKfGDx-219pMA,3815
50
+ type_spec/external/api/inputs/get_input_data.yaml,sha256=kjX3VE0y2DxxfUk0J2kjnufSFVAbk_3v28EPuHBhvRE,3825
51
51
  type_spec/external/api/inputs/get_input_names.yaml,sha256=AgGcenh6wEbmhmx8i5CY-0hY0q5geZ04ECayE9Hr0Xw,1522
52
52
  type_spec/external/api/inputs/get_inputs_data.yaml,sha256=9wKwnOXC_KokThWnz5Me5RzIcGlxffwcKJtgFow-kvU,2368
53
53
  type_spec/external/api/inputs/set_input_attribute_values.yaml,sha256=RpOtVIWBlOp_Nd5Xp67IaQmCzGnLkNwFKQDi4WD8oX4,1021
54
- type_spec/external/api/outputs/get_output_data.yaml,sha256=78ywbknD-j8TNUabSCGv4JNE1FIehhvRQtrB0wQ7krY,3465
54
+ type_spec/external/api/outputs/get_output_data.yaml,sha256=ABaKme64kFCZ-A11AUM5ARQs0Pb9f71atWx-QMYFNZs,3485
55
55
  type_spec/external/api/outputs/get_output_names.yaml,sha256=IqEYCOHeyHIu45Lf37sfzn-o_0VFk0u_Bqsb0NKHPzw,1334
56
56
  type_spec/external/api/outputs/resolve_output_conditions.yaml,sha256=lGrG6XPGiQtHx24GccPcO18xxAoMynTSC_2WwxCjFrc,1640
57
57
  type_spec/external/api/project/get_projects.yaml,sha256=pQXGGPHenmFWYUuzEmZ-ORf0_d6lpQkTrXXMRrEqvDI,1676
@@ -70,6 +70,17 @@ uncountable/__init__.py,sha256=281cC2hs8pbrD0jVKMol-tbWSh7Zcsc8oRT42dKteyE,102
70
70
  uncountable/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
71
71
  uncountable/core/__init__.py,sha256=GCXRRhhYv-yKzqKjAij7GcNiOr90BBbgdhhpusWtmUc,100
72
72
  uncountable/core/client.py,sha256=jcJBGwe31fOqfGiUJzW_KMcfE0hax55jgVVliALBNLQ,4160
73
+ uncountable/integration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
74
+ uncountable/integration/construct_client.py,sha256=Ojdj6Mh5orAjXZLxb6SzvH4zRmkvqp6nHF_8Ph7h1ho,896
75
+ uncountable/integration/cron.py,sha256=QZP4yvxdGUZsOH5wtxZzqja_0UBHL4lwERzEHGTR-E0,917
76
+ uncountable/integration/entrypoint.py,sha256=31QJO6pFDvPR8k5Bi7RSm-IUI5kXj_VHdz4XZW3j5DI,1363
77
+ uncountable/integration/job.py,sha256=orPmhLWs5mB4U8XweIb1Pa3fUKKE89yUnHXLkHy7KOs,699
78
+ uncountable/integration/server.py,sha256=VfbJLFqHl6cQAL8e2EOmrtSRfruskQ89h_2Mu3EMeOI,2998
79
+ uncountable/integration/types.py,sha256=n9idu2_qHOA5CQdE6NK8HS6aZ8ugTZKTfTTnm4sy8tI,2066
80
+ uncountable/integration/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
81
+ uncountable/integration/db/connect.py,sha256=iI9e8a2hfbFP-dvH0MGLsrG-RpM0dHKCL-oCLkah9hs,181
82
+ uncountable/integration/executors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
83
+ uncountable/integration/executors/script_executor.py,sha256=l_mniGlS9Ni6FdmspN7bvMWVocOaXKCo8FM9SFW8n8c,600
73
84
  uncountable/types/__init__.py,sha256=SbBDFhHu5iqnvnYjWCBOkjg3_mXuaBHEGfnq021wfHw,3873
74
85
  uncountable/types/base.py,sha256=7yMC3sCQ13wd92896Ga2XbZffCqq98SKv09rFGb1ulg,2682
75
86
  uncountable/types/calculations.py,sha256=16J-KKMp-I8ZQUkYNmKCHfAn6DGb99cFinALcDIdGHY,562
@@ -90,7 +101,7 @@ uncountable/types/recipe_tags.py,sha256=lYpksHAxXCcIjZKR7JoZOTH2cBSovwxZaHwjZy_y
90
101
  uncountable/types/response.py,sha256=ZI0CG7ZxBM2k5_W-6mNMU3UlB0p1i-0nrwOvsMaS-vU,620
91
102
  uncountable/types/units.py,sha256=_kZ7KkXIbRiY2fOdkTsbJBpWRah5TCC2WWiG05e-1DA,565
92
103
  uncountable/types/users.py,sha256=SUjNHBDcImKnnE7IN096Wfr1fmjNjCkQ7yQgKUPffz8,588
93
- uncountable/types/workflows.py,sha256=bj2xaH6mFAPLNeFDepGw1TvuBbsyP43wTx8_LY0nwcM,808
104
+ uncountable/types/workflows.py,sha256=JJKM0SeDqAMA2oxwzlG1c1FxeSBI7E49duoITcUrEZo,825
94
105
  uncountable/types/api/__init__.py,sha256=gCgbynxG3jA8FQHzercKtrHKHkiIKr8APdZYUniAor8,55
95
106
  uncountable/types/api/batch/__init__.py,sha256=gCgbynxG3jA8FQHzercKtrHKHkiIKr8APdZYUniAor8,55
96
107
  uncountable/types/api/batch/execute_batch.py,sha256=cCZo_akwJR4_ET6oOBChdlrCQlxUw8V9i8tDpv7JTIc,1696
@@ -105,12 +116,12 @@ uncountable/types/api/input_groups/__init__.py,sha256=gCgbynxG3jA8FQHzercKtrHKHk
105
116
  uncountable/types/api/input_groups/get_input_group_names.py,sha256=LdHWWEfVNGys6Tudienjich56Zz4bj7uXznpyYitCYA,1033
106
117
  uncountable/types/api/inputs/__init__.py,sha256=gCgbynxG3jA8FQHzercKtrHKHkiIKr8APdZYUniAor8,55
107
118
  uncountable/types/api/inputs/create_inputs.py,sha256=ykXjqKoE8WOy5N47jcjDVzSdiZx9GlB0FcTyuJdn8FU,1675
108
- uncountable/types/api/inputs/get_input_data.py,sha256=X30bb3eJEl7RweXMsSGlEpAZbF4rZvr8Mse5IMNAao4,2161
119
+ uncountable/types/api/inputs/get_input_data.py,sha256=RrgmS4QORMrm1HTNey-A3zzCdYdkmTbokVYPTI_bjPo,2178
109
120
  uncountable/types/api/inputs/get_input_names.py,sha256=8vUA9maZdogngeYbr-DpifNrokJ16BgDN5LQvTrEvSc,1153
110
121
  uncountable/types/api/inputs/get_inputs_data.py,sha256=sqZ6xEjzQqRQYMaKXLGWR8WPhWbO6J6zZT8zSTEer84,1841
111
122
  uncountable/types/api/inputs/set_input_attribute_values.py,sha256=yvWgIVzl818ewhUA4v3ldSBfmZKngdWTtFn7fqk-dOE,1341
112
123
  uncountable/types/api/outputs/__init__.py,sha256=gCgbynxG3jA8FQHzercKtrHKHkiIKr8APdZYUniAor8,55
113
- uncountable/types/api/outputs/get_output_data.py,sha256=Zs1-8jJEPNltMq6DQisSGSItqgTpRD9WFi0wMfDscf8,2182
124
+ uncountable/types/api/outputs/get_output_data.py,sha256=CpYlXNGWWNTiMgDPBRwucSseSrX4gnLuLIp2ljiONrI,2216
114
125
  uncountable/types/api/outputs/get_output_names.py,sha256=Id_ApombSzzFdq5rD4uOfWIKzthic6KBAeyqcrhsx18,1086
115
126
  uncountable/types/api/outputs/resolve_output_conditions.py,sha256=XZqUseXcGhApHmPm-2u7a37Y4blLRxoPoLhvYlBfCpI,1799
116
127
  uncountable/types/api/project/__init__.py,sha256=gCgbynxG3jA8FQHzercKtrHKHkiIKr8APdZYUniAor8,55
@@ -128,7 +139,7 @@ uncountable/types/api/recipes/get_recipe_output_metadata.py,sha256=L9s2ykPP4pd02
128
139
  uncountable/types/api/recipes/get_recipes_data.py,sha256=dOKokz6rJp3AiqNrF8rAZFlmJSs3ejdNIJhwKw0Utr0,5317
129
140
  uncountable/types/api/recipes/set_recipe_inputs.py,sha256=sHEwPocBucWRnnoX7nbNaFqdflxFkqdjuVydNezqluY,1326
130
141
  uncountable/types/api/recipes/set_recipe_outputs.py,sha256=QYq39TNchQ80ET1C77OE9fwhbu_HmIoEDmrQJHkkCu0,1609
131
- UncountablePythonSDK-0.0.8.dist-info/METADATA,sha256=Tbt6PxvCFuyUjcs2twY0ze73oOxiNWsPV8DgdLu6EmI,1125
132
- UncountablePythonSDK-0.0.8.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
133
- UncountablePythonSDK-0.0.8.dist-info/top_level.txt,sha256=8ga1DCSWt4Uc_XJ5cR0QrDQuyoeo2uoSzaAJdQT2KBo,36
134
- UncountablePythonSDK-0.0.8.dist-info/RECORD,,
142
+ UncountablePythonSDK-0.0.10.dist-info/METADATA,sha256=-k3bLSnSPbkgbodLSCUIvBzwdRtpcfIcBKEPHtmxEnY,1261
143
+ UncountablePythonSDK-0.0.10.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
144
+ UncountablePythonSDK-0.0.10.dist-info/top_level.txt,sha256=8ga1DCSWt4Uc_XJ5cR0QrDQuyoeo2uoSzaAJdQT2KBo,36
145
+ UncountablePythonSDK-0.0.10.dist-info/RECORD,,
@@ -407,5 +407,5 @@ class CachedParser(typing.Generic[T]):
407
407
  return self.parse_storage(yaml.safe_load(data_in))
408
408
 
409
409
  def parse_yaml_resource(self, package: resources.Package, resource: str) -> T:
410
- raw_data = resources.read_text(package, resource)
411
- return self.parse_storage(yaml.safe_load(raw_data))
410
+ with resources.open_text(package, resource) as fp:
411
+ return self.parse_storage(yaml.safe_load(fp))
@@ -141,6 +141,18 @@ def _serialize_global_context(ctx: EmitOpenAPIGlobalContext) -> str:
141
141
  return yaml.dump(oa_root, sort_keys=False)
142
142
 
143
143
 
144
+ def _emit_endpoint_parameters(typ: OpenAPIType | None) -> dict[str, list[dict[str, str]]]:
145
+ if not isinstance(typ, OpenAPIObjectType):
146
+ return {}
147
+
148
+ return {
149
+ "parameters": [
150
+ {"$ref": f"#/components/schema/Arguments/{prop_name}"}
151
+ for prop_name in typ.properties
152
+ ]
153
+ }
154
+
155
+
144
156
  def _emit_namespace(
145
157
  gctx: EmitOpenAPIGlobalContext,
146
158
  ctx: EmitOpenAPIContext,
@@ -159,23 +171,28 @@ def _emit_namespace(
159
171
  if ctx.endpoint is not None:
160
172
  endpoint = ctx.endpoint
161
173
 
174
+ argument_type = ctx.types.get("Arguments")
162
175
  oa_endpoint = dict()
163
- oa_endpoint[endpoint.method] = {
164
- "tags": endpoint.tags,
165
- "summary": endpoint.summary,
166
- "description": endpoint.description,
167
- "parameters": {"$ref": "#/components/schema/Arguments"},
168
- "responses": {
169
- "200": {
170
- "description": "OK",
171
- "content": {
172
- "application/json": {
173
- "schema": {"$ref": "#/components/schema/Data"}
174
- }
175
- },
176
- }
177
- },
178
- }
176
+ oa_endpoint[endpoint.method] = (
177
+ {
178
+ "tags": endpoint.tags,
179
+ "summary": endpoint.summary,
180
+ "description": endpoint.description,
181
+ }
182
+ | _emit_endpoint_parameters(argument_type)
183
+ | {
184
+ "responses": {
185
+ "200": {
186
+ "description": "OK",
187
+ "content": {
188
+ "application/json": {
189
+ "schema": {"$ref": "#/components/schema/Data"}
190
+ }
191
+ },
192
+ }
193
+ },
194
+ }
195
+ )
179
196
  oa_components["endpoint"] = oa_endpoint
180
197
 
181
198
  types = ctx.types
@@ -460,5 +477,5 @@ def open_api_type(
460
477
  ctx.namespaces.add(stype.namespace)
461
478
  # external namespace resolution
462
479
  return OpenAPIRefType(
463
- source=f"{resolve_namespace_ref(ctx, stype.namespace, config=config)}/{stype.name}"
480
+ source=f"{resolve_namespace_ref(source_path=ctx.namespace.path, ref_path=stype.namespace.path, ref='/components/schema')}/{stype.name}"
464
481
  )
@@ -9,7 +9,6 @@ from dataclasses import dataclass, field
9
9
  from typing import TypeAlias
10
10
 
11
11
  from . import builder
12
- from .config import OpenAPIConfig
13
12
  from .open_api_util import OpenAPIType
14
13
 
15
14
  MODIFY_NOTICE = "# DO NOT MODIFY -- This file is generated by type_spec"
@@ -75,12 +74,8 @@ class EmitOpenAPIContext:
75
74
 
76
75
 
77
76
  def resolve_namespace_ref(
78
- ctx: EmitOpenAPIContext,
79
- namespace: builder.SpecNamespace,
80
- *,
81
- config: OpenAPIConfig,
77
+ *, source_path: list[str], ref_path: list[str], ref: str
82
78
  ) -> str:
83
- # TODO: Handle namespaces not in root directory
84
- if len(ctx.namespace.path) == 1:
85
- return f"{namespace.name}.yaml#/components/schema"
86
- return f"{config.static_url_path}/common/{namespace.name}.yaml#/components/schema"
79
+ to_root = "/".join(".." for _ in source_path)
80
+ location = "/".join(ref_path)
81
+ return f"{to_root}/common/{location}.yaml#{ref}"
@@ -13,9 +13,10 @@ class OpenAPIType(ABC):
13
13
  self.nullable = nullable
14
14
 
15
15
  @abstractmethod
16
- def asdict(self) -> dict[str, object]: ...
16
+ def asdict(self) -> dict[str, object]:
17
+ pass
17
18
 
18
- def asarguments(self) -> list[dict[str, object]]:
19
+ def asarguments(self) -> dict[str, dict[str, object]]:
19
20
  raise UnsupportedOperation
20
21
 
21
22
  def add_addl_info(self, emitted: dict[str, object]) -> dict[str, object]:
@@ -147,8 +148,8 @@ class OpenAPIFreeFormObjectType(OpenAPIType):
147
148
  def asdict(self) -> dict[str, object]:
148
149
  return self.add_addl_info({"type": "object"})
149
150
 
150
- def asarguments(self) -> list[dict[str, object]]:
151
- return []
151
+ def asarguments(self) -> dict[str, dict[str, object]]:
152
+ return {}
152
153
 
153
154
 
154
155
  class OpenAPIObjectType(OpenAPIType):
@@ -185,17 +186,17 @@ class OpenAPIObjectType(OpenAPIType):
185
186
  },
186
187
  })
187
188
 
188
- def asarguments(self) -> list[dict[str, object]]:
189
- argument_types: list[dict[str, object]] = []
189
+ def asarguments(self) -> dict[str, dict[str, object]]:
190
+ argument_types: dict[str, dict[str, object]] = {}
190
191
  for property_name, property_type in self.properties.items():
191
192
  desc = self.property_desc.get(property_name)
192
- argument_types.append({
193
+ argument_types[property_name] = {
193
194
  "name": property_name,
194
195
  "in": "query",
195
196
  "schema": property_type.asdict(),
196
197
  "required": not property_type.nullable,
197
198
  "description": desc or "",
198
- })
199
+ }
199
200
  return argument_types
200
201
 
201
202
 
@@ -219,11 +220,11 @@ class OpenAPIUnionType(OpenAPIType):
219
220
  # TODO: use parents description and nullable
220
221
  return {"oneOf": [base_type.asdict() for base_type in self.base_types]}
221
222
 
222
- def asarguments(self) -> list[dict[str, object]]:
223
+ def asarguments(self) -> dict[str, dict[str, object]]:
223
224
  # TODO handle inheritence (allOf and refs); need to inline here...
224
225
  # for now skip this endpoint
225
226
 
226
- return []
227
+ return {}
227
228
 
228
229
 
229
230
  class OpenAPIIntersectionType(OpenAPIType):
@@ -246,8 +247,8 @@ class OpenAPIIntersectionType(OpenAPIType):
246
247
  # TODO: use parents description and nullable
247
248
  return {"allOf": [base_type.asdict() for base_type in self.base_types]}
248
249
 
249
- def asarguments(self) -> list[dict[str, object]]:
250
+ def asarguments(self) -> dict[str, dict[str, object]]:
250
251
  # TODO handle inheritence (allOf and refs); need to inline here...
251
252
  # for now skip this endpoint
252
253
 
253
- return []
254
+ return {}
@@ -76,7 +76,7 @@ FullInput:
76
76
  type: List<input_attributes.InputAttributeValue>
77
77
  desc: "Attributes associated with an input, such as CAS number, density, etc."
78
78
  global_category_id:
79
- type: ObjectId
79
+ type: Optional<ObjectId>
80
80
  desc: "The global category ID associated with the input"
81
81
  subcategory_ids:
82
82
  type: List<ObjectId>
@@ -73,10 +73,10 @@ FullOutput:
73
73
  type: List<OutputAttrVal>
74
74
  desc: "Attributes associated with an output, such as a test documentation"
75
75
  global_category_id:
76
- type: ObjectId
76
+ type: Optional<ObjectId>
77
77
  desc: "The global category ID associated with the output"
78
78
  unit_id:
79
- type: ObjectId
79
+ type: Optional<ObjectId>
80
80
  desc: "The unit for the output, which all data is stored in. This may be different than the users default displayed information for the output"
81
81
 
82
82
  Data:
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()
@@ -0,0 +1,89 @@
1
+ # TODO: move to type spec
2
+
3
+
4
+ from dataclasses import dataclass
5
+ from enum import StrEnum
6
+ from typing import Literal
7
+
8
+
9
+ class JobDefinitionType(StrEnum):
10
+ CRON = "cron"
11
+ # also imagine other job types like webhook-triggered or sftp-triggered,
12
+ # manual, etc
13
+
14
+
15
+ class JobExecutorType(StrEnum):
16
+ SCRIPT = "script"
17
+ # also imagine builtin executors like 'sftp_sync'
18
+
19
+
20
+ class AuthRetrievalType(StrEnum):
21
+ ENV = "env"
22
+ # also imagine secrets manager, keyvault, etc
23
+
24
+
25
+ @dataclass
26
+ class JobExecutorBase:
27
+ type: JobExecutorType
28
+
29
+
30
+ @dataclass
31
+ class JobExecutorScript(JobExecutorBase):
32
+ type: Literal[JobExecutorType.SCRIPT]
33
+ import_path: str
34
+
35
+
36
+ JobExecutor = JobExecutorScript
37
+
38
+
39
+ @dataclass
40
+ class JobDefinitionBase:
41
+ id: str
42
+ name: str
43
+
44
+
45
+ @dataclass
46
+ class CronJobDefinition(JobDefinitionBase):
47
+ type: Literal[JobDefinitionType.CRON]
48
+ cron_spec: str
49
+ # Here we assert that the executor has to be a script, but we could add
50
+ # other builtin executor types that cron jobs can support later
51
+ executor: JobExecutorScript
52
+
53
+
54
+ JobDefinition = CronJobDefinition
55
+
56
+
57
+ @dataclass
58
+ class AuthRetrievalBase:
59
+ type: AuthRetrievalType
60
+
61
+
62
+ @dataclass
63
+ class AuthRetrievalEnv:
64
+ # We don't really need any extra info here, we can enforce that the auth
65
+ # keys are named like UNC_PROFILE_{profile name}_API_SECRET_KEY. For
66
+ # supporting pulling secrets from secrets manager etc it will be nice to
67
+ # use dataclass properties to get the secret name, region etc.
68
+ type: Literal[AuthRetrievalType.ENV]
69
+
70
+
71
+ AuthRetrieval = AuthRetrievalEnv
72
+
73
+
74
+ @dataclass
75
+ class ProfileDefinition:
76
+ # profile name (expected to be something like customer_name) will be
77
+ # obtained from the folder name instead of specified here. Forces jobs to
78
+ # be organized nicely in folders that separate their identities.
79
+ auth_retrieval: AuthRetrieval
80
+ base_url: str
81
+ jobs: list[JobDefinition]
82
+
83
+
84
+ @dataclass
85
+ class ProfileMetadata:
86
+ # supplied by inspecting the folder name that the profile is in
87
+ name: str
88
+ base_url: str
89
+ auth_retrieval: AuthRetrieval
@@ -66,7 +66,7 @@ class FullInput:
66
66
  quantity_type: str
67
67
  is_parameter: bool
68
68
  attributes: list[input_attributes_t.InputAttributeValue]
69
- global_category_id: base_t.ObjectId
69
+ global_category_id: typing.Optional[base_t.ObjectId]
70
70
  subcategory_ids: list[base_t.ObjectId]
71
71
 
72
72
 
@@ -70,8 +70,8 @@ class FullOutput:
70
70
  name: str
71
71
  quantity_type: str
72
72
  attributes: list[OutputAttrVal]
73
- global_category_id: base_t.ObjectId
74
- unit_id: base_t.ObjectId
73
+ global_category_id: typing.Optional[base_t.ObjectId]
74
+ unit_id: typing.Optional[base_t.ObjectId]
75
75
 
76
76
 
77
77
  # DO NOT MODIFY -- This file is generated by type_spec
@@ -20,7 +20,7 @@ __all__: list[str] = [
20
20
  @dataclass(kw_only=True)
21
21
  class SimpleWorkflowStep:
22
22
  workflow_step_id: base_t.ObjectId
23
- name: str
23
+ name: typing.Optional[str]
24
24
 
25
25
 
26
26
  # DO NOT MODIFY -- This file is generated by type_spec