UncountablePythonSDK 0.0.21__py3-none-any.whl → 0.0.22__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.
- {UncountablePythonSDK-0.0.21.dist-info → UncountablePythonSDK-0.0.22.dist-info}/METADATA +1 -1
- {UncountablePythonSDK-0.0.21.dist-info → UncountablePythonSDK-0.0.22.dist-info}/RECORD +25 -23
- examples/async_batch.py +36 -0
- pkgs/type_spec/builder.py +112 -8
- pkgs/type_spec/emit_open_api.py +188 -18
- pkgs/type_spec/emit_open_api_util.py +17 -0
- pkgs/type_spec/emit_python.py +1 -1
- pkgs/type_spec/load_types.py +48 -5
- pkgs/type_spec/open_api_util.py +13 -33
- type_spec/external/api/entity/create_entities.yaml +1 -0
- type_spec/external/api/entity/create_entity.yaml +1 -0
- type_spec/external/api/recipes/edit_recipe_inputs.yaml +1 -3
- uncountable/core/__init__.py +2 -1
- uncountable/core/async_batch.py +22 -0
- uncountable/core/client.py +78 -6
- uncountable/types/api/entity/create_entities.py +1 -1
- uncountable/types/api/entity/create_entity.py +1 -1
- uncountable/types/api/recipes/edit_recipe_inputs.py +2 -3
- uncountable/types/async_batch.py +1 -0
- uncountable/types/async_batch_processor.py +7 -7
- uncountable/types/client_base.py +2 -2
- uncountable/types/identifier.py +3 -3
- uncountable/types/recipe_workflow_steps.py +1 -1
- {UncountablePythonSDK-0.0.21.dist-info → UncountablePythonSDK-0.0.22.dist-info}/WHEEL +0 -0
- {UncountablePythonSDK-0.0.21.dist-info → UncountablePythonSDK-0.0.22.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: UncountablePythonSDK
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.22
|
|
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
|
|
@@ -14,6 +14,7 @@ docs/static/favicons/favicon-32x32.png,sha256=U4UU652zGnSeU3P9kUqxPeEnVf6zhtdNdN
|
|
|
14
14
|
docs/static/favicons/manifest.json,sha256=6q_3nZkcg_x0xut4eE-xpdeMY1TydwiZIcbXlLAq9X8,437
|
|
15
15
|
docs/static/favicons/mstile-150x150.png,sha256=eAK4QdEofhdLtfmjuPTpnX3MJqYnvGXsHYUjlcQekyY,1035
|
|
16
16
|
docs/static/favicons/safari-pinned-tab.svg,sha256=S84fRnz0ZxLnQrKtmmFZytiRyu1xLtMR_RVy5jmwU7k,1926
|
|
17
|
+
examples/async_batch.py,sha256=wpf_3P547375vTIO4pKv5vw6WCkUnzqvw_S3idfhjvM,1122
|
|
17
18
|
examples/create_entity.py,sha256=54AmZt83EpypxGcYZSIMmWlGz2oAgHFOsKuLSZOcHsI,625
|
|
18
19
|
examples/upload_files.py,sha256=ZsMChgOioraVHv207YREpivAOf4dq3IxGIBoROoDX_4,482
|
|
19
20
|
examples/recipe-import/importer.py,sha256=baD71xuNibxDTe3bGHsMEIZEf9Xtb-IumBNpCEV0RZU,1134
|
|
@@ -34,16 +35,16 @@ pkgs/strenum_compat/__init__.py,sha256=wXRFeNvBm8RU6dy1PFJ5sRLgUIEeH_DVR95Sv5qpG
|
|
|
34
35
|
pkgs/strenum_compat/strenum_compat.py,sha256=uOUAgpYTjHs1MX8dG81jRlyTkt3KNbkV_25zp7xTX2s,36
|
|
35
36
|
pkgs/type_spec/__init__.py,sha256=h5DmJTca4QVV10sZR1x0-MlkZfuGYDfapR3zHvXfzto,19
|
|
36
37
|
pkgs/type_spec/__main__.py,sha256=5bJaX9Y_-FavP0qwzhk-z-V97UY7uaezJTa1zhO_HHQ,1048
|
|
37
|
-
pkgs/type_spec/builder.py,sha256=
|
|
38
|
+
pkgs/type_spec/builder.py,sha256=kLiTjVVbtctRfSStI15IZYbyQ4n2KPmvtJIWbizux40,43002
|
|
38
39
|
pkgs/type_spec/config.py,sha256=INfEiDcUsZFUKasHprsE6i33siPB0RnfmTKOsWcGnQ8,5043
|
|
39
40
|
pkgs/type_spec/emit_io_ts.py,sha256=gCEfS81w_ifqjLVJ3_cpy9Gq03o6H5nEsh35WAkqGGE,5606
|
|
40
|
-
pkgs/type_spec/emit_open_api.py,sha256=
|
|
41
|
-
pkgs/type_spec/emit_open_api_util.py,sha256=
|
|
42
|
-
pkgs/type_spec/emit_python.py,sha256=
|
|
41
|
+
pkgs/type_spec/emit_open_api.py,sha256=xy-nXjkqAZMDd-0qU5eDubRz1Hf8IwZSZj5bZyPSvX4,23328
|
|
42
|
+
pkgs/type_spec/emit_open_api_util.py,sha256=F6qouGVm2-WGYkoubbBtRu00V4e30bWJ0fDBhigBEfg,2248
|
|
43
|
+
pkgs/type_spec/emit_python.py,sha256=hj_5RUnJzjI6W9SmOh8ZUJTWB6F7PB1UFsg3OGSXZg0,42416
|
|
43
44
|
pkgs/type_spec/emit_typescript.py,sha256=KOCcPuwSLRSt3pIp06Nq9exledq-kWtrA4RnUpeGMi8,17505
|
|
44
45
|
pkgs/type_spec/emit_typescript_util.py,sha256=93FzJnpYse4PKFzgdw4DGV4zFTi5tF4WR-CIi7cW498,873
|
|
45
|
-
pkgs/type_spec/load_types.py,sha256=
|
|
46
|
-
pkgs/type_spec/open_api_util.py,sha256=
|
|
46
|
+
pkgs/type_spec/load_types.py,sha256=xEHwdB_miR3vNs161Oy1luafE0VC-yk9-utAyCJmbEo,3629
|
|
47
|
+
pkgs/type_spec/open_api_util.py,sha256=TFbK2bkYT6S4qPQGO3_G2mfVgtNB26d31kwaHQ9y99E,6730
|
|
47
48
|
pkgs/type_spec/test.py,sha256=4ueujBq-pEgnX3Z69HyPmD-bullFXmpixcpVzfOkhP4,489
|
|
48
49
|
pkgs/type_spec/util.py,sha256=6m6MPfY-SwjyZf2FWQKclswWB5o7gcdd-3tdpViPYOQ,4844
|
|
49
50
|
pkgs/type_spec/actions_registry/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -61,8 +62,8 @@ pkgs/type_spec/value_spec/types.py,sha256=a2zxbbCRWepY1l8OtjeCDKgBKFPFHVgV99oP6p
|
|
|
61
62
|
type_spec/external/api/batch/execute_batch.yaml,sha256=gpdSev3sLEC_cMVSZdj-9bc_XDFDqdPdOII9Ojme2N8,1170
|
|
62
63
|
type_spec/external/api/batch/execute_batch_load_async.yaml,sha256=gcn51NWLiSvlytz8k3_pDOVJLCGfdivKJPG4I9Q8CZc,435
|
|
63
64
|
type_spec/external/api/chemical/convert_chemical_formats.yaml,sha256=EidTxMCRs-ko5yMFDptJPyAEWYZVruP41OG3cwyBLQQ,1069
|
|
64
|
-
type_spec/external/api/entity/create_entities.yaml,sha256=
|
|
65
|
-
type_spec/external/api/entity/create_entity.yaml,sha256=
|
|
65
|
+
type_spec/external/api/entity/create_entities.yaml,sha256=RKjmb_iY4dVHf3aQUCU-OrlbTLLsCkULQ9uEfa8BMFY,1506
|
|
66
|
+
type_spec/external/api/entity/create_entity.yaml,sha256=Orz-3RZsNy5cWXlA3BKVQDYGnGGXlCXOtsOxDSm_nRY,1701
|
|
66
67
|
type_spec/external/api/entity/get_entities_data.yaml,sha256=3XujG7bOpuBQlfFrYtG3L4fBk7LsmdSekmP9iU0zjF0,796
|
|
67
68
|
type_spec/external/api/entity/list_entities.yaml,sha256=H2YVv6il-XVKd_7IipZqauTDvWCrvHok7z47bDH2sI4,1798
|
|
68
69
|
type_spec/external/api/entity/resolve_entity_ids.yaml,sha256=Zf3OhAohwLJO7wWj0e-sK5lhIsXlD8A5Bu3OGjY4-tA,732
|
|
@@ -89,7 +90,7 @@ type_spec/external/api/recipes/associate_recipe_as_lot.yaml,sha256=8wzeJg5njt4qG
|
|
|
89
90
|
type_spec/external/api/recipes/create_recipe.yaml,sha256=mGLyKJI3pN_7nU4rcSqCO3WjuKhO_odZ2pewVgYcMUU,1322
|
|
90
91
|
type_spec/external/api/recipes/create_recipes.yaml,sha256=eXMlXRpB5TFt1mUTECBa4aAIG3KrxYT2mJ5vxmZ9Q3A,1503
|
|
91
92
|
type_spec/external/api/recipes/disassociate_recipe_as_input.yaml,sha256=qTKQCNBNwLnbr22DQVLA6b80BdBhwnDbX1c4KoCKUm8,477
|
|
92
|
-
type_spec/external/api/recipes/edit_recipe_inputs.yaml,sha256=
|
|
93
|
+
type_spec/external/api/recipes/edit_recipe_inputs.yaml,sha256=mmRcep8It4UIUmABtLKxwH5fuax_9yb0vvF8_6ROLjY,2591
|
|
93
94
|
type_spec/external/api/recipes/get_curve.yaml,sha256=zQpPwOYqojY-YwmTjbqoGtUxpYm3vne2sYpglWbPnpw,779
|
|
94
95
|
type_spec/external/api/recipes/get_recipe_calculations.yaml,sha256=ZE7PzfWrjS7TiO4q7iyCwEj5In8GwO6fFIYGqUlTEXo,1240
|
|
95
96
|
type_spec/external/api/recipes/get_recipe_links.yaml,sha256=Vwm0OVWl3VvDaI7chY_oZQqD8xZ1u09iFWKkZKn1ITo,766
|
|
@@ -103,8 +104,9 @@ type_spec/external/api/recipes/set_recipe_tags.yaml,sha256=IrdkbryxZjNy8n4aMNLRT
|
|
|
103
104
|
type_spec/external/api/triggers/run_trigger.yaml,sha256=c8xDV3bQRjcRRDG4Y7kdQmMMu1fj3ae5eUi-Sdbsi54,405
|
|
104
105
|
uncountable/__init__.py,sha256=281cC2hs8pbrD0jVKMol-tbWSh7Zcsc8oRT42dKteyE,102
|
|
105
106
|
uncountable/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
106
|
-
uncountable/core/__init__.py,sha256=
|
|
107
|
-
uncountable/core/
|
|
107
|
+
uncountable/core/__init__.py,sha256=7xUnbSWJzS31sWg0jCe5nIksn5s0PVdwUrUmDttHfCY,258
|
|
108
|
+
uncountable/core/async_batch.py,sha256=0cRmCr6Z9sNxZyfY9Dl8wlCA4anISVZuHGgBegHhUbc,749
|
|
109
|
+
uncountable/core/client.py,sha256=7bLuACxMJZsckSfL2j-p-XThYdvDAUAwm5nND9s-v1o,6946
|
|
108
110
|
uncountable/core/file_upload.py,sha256=zTpAFSd7_-TmEVWxOn1rDznyWE6_AdZyuDQC3LP34iI,2667
|
|
109
111
|
uncountable/core/types.py,sha256=gQtCw1-WSRak_ypFlGI1Ea9iZBP9zDeFq6XQtiXBlZA,459
|
|
110
112
|
uncountable/integration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -119,19 +121,19 @@ uncountable/integration/db/connect.py,sha256=iI9e8a2hfbFP-dvH0MGLsrG-RpM0dHKCL-o
|
|
|
119
121
|
uncountable/integration/executors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
120
122
|
uncountable/integration/executors/script_executor.py,sha256=6oMPAFe0PUdqt76e8jMi4vXszGVsVHLULob7Qbl3o38,816
|
|
121
123
|
uncountable/types/__init__.py,sha256=fTNNsvFFkjp4acdBA8eS00OZVnDo_Zn7aRwPs8SFnrs,6024
|
|
122
|
-
uncountable/types/async_batch.py,sha256=
|
|
123
|
-
uncountable/types/async_batch_processor.py,sha256=
|
|
124
|
+
uncountable/types/async_batch.py,sha256=FtIoDCeyt9rF5hknQs6Sw-vjiYBMJbe1re0-4bk_6VY,1578
|
|
125
|
+
uncountable/types/async_batch_processor.py,sha256=YksvTyJaZ3rqpZx4UXofUf7XU1Br4aNBNPZXB3LLtkA,5940
|
|
124
126
|
uncountable/types/base.py,sha256=w3BRf8SAvYPlKrcJtJcQ_WhCU3A9zy0VuRTRWRFKVUA,2709
|
|
125
127
|
uncountable/types/calculations.py,sha256=16J-KKMp-I8ZQUkYNmKCHfAn6DGb99cFinALcDIdGHY,562
|
|
126
128
|
uncountable/types/chemical_structure.py,sha256=zQKl53DGtQQONIUHFXuwjWLQaG7FPZY7x6SBSOzkGV0,758
|
|
127
|
-
uncountable/types/client_base.py,sha256=
|
|
129
|
+
uncountable/types/client_base.py,sha256=eAgmDRoHhpsswgqI1q8-eg8joya7qaaFJKH1fyE50PE,46075
|
|
128
130
|
uncountable/types/curves.py,sha256=qYyRntMmFNonEwTrGhquMLbgMqjyP1moQflNTP0FMec,1308
|
|
129
131
|
uncountable/types/entity.py,sha256=NjMZrqBwQ7sZe_oUuJqy9IEG7dWZmFMkQQXJ0_odcnA,11637
|
|
130
132
|
uncountable/types/experiment_groups.py,sha256=ZBEk06F4n98Jz3oEA09WaDmw5rqPs7iVAm_Ysr4gc_o,599
|
|
131
133
|
uncountable/types/field_values.py,sha256=2unBAeBqQPqLQKaL6nGpnDDksZ-5MZepgEF3sgy6oOk,1670
|
|
132
134
|
uncountable/types/fields.py,sha256=eGtZ6axTYGFxLmPAyri2LwlcR4SZ2sX2c6QDX0ybKz0,570
|
|
133
135
|
uncountable/types/id_source.py,sha256=Y3suURq3L1SahZ2oHPD986SU0l3Ik-ZzH38aQKgc1Fg,1341
|
|
134
|
-
uncountable/types/identifier.py,sha256=
|
|
136
|
+
uncountable/types/identifier.py,sha256=94-O3H_qNrA48tf3srwPwdu8HURkLl7_-88kUnwElZg,1455
|
|
135
137
|
uncountable/types/input_attributes.py,sha256=u-JABoZ-Ij1Ynq5g6MxOgRdQeYbM7OnGP2q_N7KuVdw,826
|
|
136
138
|
uncountable/types/inputs.py,sha256=q7fNGaSKIk3R6uXCEhSQpiHvXu82YcK3oZHDI7bxE88,1597
|
|
137
139
|
uncountable/types/outputs.py,sha256=hSUlu41sisYKIZpPrj1G1DRfKm6hsKNcd1eNKFYb-4w,671
|
|
@@ -144,7 +146,7 @@ uncountable/types/recipe_links.py,sha256=RldSV7SdeBYa0bx02DzMg4jfPdgrlMRE40T16Fd
|
|
|
144
146
|
uncountable/types/recipe_metadata.py,sha256=cebGg_lJzqZzGnKnDgmuQFrw4Xhoz6HEiGM6G0az120,1437
|
|
145
147
|
uncountable/types/recipe_output_metadata.py,sha256=XJA8R1r4NTzyR_DhMkmH4ZtYD-vqpvBMji9Be8OcFmo,613
|
|
146
148
|
uncountable/types/recipe_tags.py,sha256=lYpksHAxXCcIjZKR7JoZOTH2cBSovwxZaHwjZy_yqiQ,581
|
|
147
|
-
uncountable/types/recipe_workflow_steps.py,sha256=
|
|
149
|
+
uncountable/types/recipe_workflow_steps.py,sha256=LmyFwWWwJv30vuaQ4qtd0hzDdeJaIxHQZqwRb1Wi_6A,2626
|
|
148
150
|
uncountable/types/response.py,sha256=ZI0CG7ZxBM2k5_W-6mNMU3UlB0p1i-0nrwOvsMaS-vU,620
|
|
149
151
|
uncountable/types/units.py,sha256=_kZ7KkXIbRiY2fOdkTsbJBpWRah5TCC2WWiG05e-1DA,565
|
|
150
152
|
uncountable/types/users.py,sha256=SUjNHBDcImKnnE7IN096Wfr1fmjNjCkQ7yQgKUPffz8,588
|
|
@@ -156,8 +158,8 @@ uncountable/types/api/batch/execute_batch_load_async.py,sha256=dcdGFibO8fUDpC__X
|
|
|
156
158
|
uncountable/types/api/chemical/__init__.py,sha256=gCgbynxG3jA8FQHzercKtrHKHkiIKr8APdZYUniAor8,55
|
|
157
159
|
uncountable/types/api/chemical/convert_chemical_formats.py,sha256=COGzkfpTL_Ermg2cbasoVKGAxDAtJaTFay18IZtrWCA,1305
|
|
158
160
|
uncountable/types/api/entity/__init__.py,sha256=gCgbynxG3jA8FQHzercKtrHKHkiIKr8APdZYUniAor8,55
|
|
159
|
-
uncountable/types/api/entity/create_entities.py,sha256=
|
|
160
|
-
uncountable/types/api/entity/create_entity.py,sha256=
|
|
161
|
+
uncountable/types/api/entity/create_entities.py,sha256=vzo5hS1qcmjQdfyCMarSu8MRcRGSiholOVSCfjXlA1k,1703
|
|
162
|
+
uncountable/types/api/entity/create_entity.py,sha256=ausozCQ3qPM9YUQ87bOTCKOm-zkhn4CSLJr9jLc9n2U,1873
|
|
161
163
|
uncountable/types/api/entity/get_entities_data.py,sha256=XjrJGZucIn1TYUlDLRnRA0JTQw-vXHIAT-m0H9hk37A,1170
|
|
162
164
|
uncountable/types/api/entity/list_entities.py,sha256=_bIIZJj3N0E6YiHgqzfCOKxD1fQW6biWJQMp5wIVbBw,1514
|
|
163
165
|
uncountable/types/api/entity/resolve_entity_ids.py,sha256=AidGpPmI9ATDv0E7vd9LDOl3n3beGxUlRojh5uZrkl4,1086
|
|
@@ -193,7 +195,7 @@ uncountable/types/api/recipes/associate_recipe_as_lot.py,sha256=bTYjbnY3B7GKz4MV
|
|
|
193
195
|
uncountable/types/api/recipes/create_recipe.py,sha256=Ni00efkcPkQ3WTIgDHzkfu1qoc52ReV9VT0wwwPOT4g,1364
|
|
194
196
|
uncountable/types/api/recipes/create_recipes.py,sha256=qwIYa8hfcjY7_VOFt9lxmVtJ-HOJqQN3GDNSbZsRCZU,1544
|
|
195
197
|
uncountable/types/api/recipes/disassociate_recipe_as_input.py,sha256=L25fpiK1Y5PByPVVgsZy9t4podz3xSSLIwKHj8CUrSg,913
|
|
196
|
-
uncountable/types/api/recipes/edit_recipe_inputs.py,sha256=
|
|
198
|
+
uncountable/types/api/recipes/edit_recipe_inputs.py,sha256=pTw606AgLhb-oJjfj1WPyEcJ4B0tgZsvEKeqP5VZ1gY,3281
|
|
197
199
|
uncountable/types/api/recipes/get_curve.py,sha256=UIWfpqtU5sQokaxwYfQFNFl6HMyzWEF_Sjd8UMz0U88,939
|
|
198
200
|
uncountable/types/api/recipes/get_recipe_calculations.py,sha256=eQmkdZzCEuq8S2f_kf_7GPvDLX1pTnY1CRmkK0SkMCI,1472
|
|
199
201
|
uncountable/types/api/recipes/get_recipe_links.py,sha256=hk5dfQjv7yU2r-S9b8vwWEJLPHqU0-M6SFiTLMR3fVk,985
|
|
@@ -206,7 +208,7 @@ uncountable/types/api/recipes/set_recipe_outputs.py,sha256=QYq39TNchQ80ET1C77OE9
|
|
|
206
208
|
uncountable/types/api/recipes/set_recipe_tags.py,sha256=U710hgq9-t6QZGRB-ZGHskpt4iXwYEjIRb67eh3P518,2453
|
|
207
209
|
uncountable/types/api/triggers/__init__.py,sha256=gCgbynxG3jA8FQHzercKtrHKHkiIKr8APdZYUniAor8,55
|
|
208
210
|
uncountable/types/api/triggers/run_trigger.py,sha256=9m9M8-nlGB_sAU2Qm2lWugp4h4Osqj6QpjNfU8osd1U,901
|
|
209
|
-
UncountablePythonSDK-0.0.
|
|
210
|
-
UncountablePythonSDK-0.0.
|
|
211
|
-
UncountablePythonSDK-0.0.
|
|
212
|
-
UncountablePythonSDK-0.0.
|
|
211
|
+
UncountablePythonSDK-0.0.22.dist-info/METADATA,sha256=o1sddgkTqesJ-3NQ_fm1xSgW1vj8bV-PAXnFW7g6-Hs,1613
|
|
212
|
+
UncountablePythonSDK-0.0.22.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
213
|
+
UncountablePythonSDK-0.0.22.dist-info/top_level.txt,sha256=HaMiBnH1wA7SG9-RVHIJPBH3l8X5gee2jUf-77Nz-Dk,41
|
|
214
|
+
UncountablePythonSDK-0.0.22.dist-info/RECORD,,
|
examples/async_batch.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
from uncountable.core import AuthDetailsApiKey, Client
|
|
3
|
+
from uncountable.core import AsyncBatchProcessor
|
|
4
|
+
from uncountable.types import (
|
|
5
|
+
recipe_metadata,
|
|
6
|
+
)
|
|
7
|
+
from uncountable.types.identifier import IdentifierKeyBatchReference
|
|
8
|
+
from uncountable.types.recipe_identifiers import (
|
|
9
|
+
RecipeIdentifierEditableName,
|
|
10
|
+
RecipeIdentifiers,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
client = Client(
|
|
15
|
+
base_url="https://app.uncountable.com",
|
|
16
|
+
auth_details=AuthDetailsApiKey(
|
|
17
|
+
api_id="X",
|
|
18
|
+
api_secret_key="X",
|
|
19
|
+
),
|
|
20
|
+
)
|
|
21
|
+
batch_loader = AsyncBatchProcessor(client=client)
|
|
22
|
+
recipe_identifiers: RecipeIdentifiers = []
|
|
23
|
+
recipe_identifiers.append(
|
|
24
|
+
RecipeIdentifierEditableName(editable_name="My recipe from API")
|
|
25
|
+
)
|
|
26
|
+
req = batch_loader.create_recipe(
|
|
27
|
+
material_family_id=1, workflow_id=1, identifiers=recipe_identifiers
|
|
28
|
+
)
|
|
29
|
+
created_recipe_reference = req.batch_reference
|
|
30
|
+
batch_loader.set_recipe_metadata(
|
|
31
|
+
recipe_key=IdentifierKeyBatchReference(reference=created_recipe_reference),
|
|
32
|
+
recipe_metadata=[
|
|
33
|
+
recipe_metadata.MetadataValue(metadata_id=7, value_numeric=Decimal(38))
|
|
34
|
+
],
|
|
35
|
+
)
|
|
36
|
+
job_id = batch_loader.send()
|
pkgs/type_spec/builder.py
CHANGED
|
@@ -10,7 +10,7 @@ import re
|
|
|
10
10
|
from collections import defaultdict
|
|
11
11
|
from dataclasses import MISSING, dataclass
|
|
12
12
|
from enum import Enum, StrEnum, auto
|
|
13
|
-
from typing import Any, Optional
|
|
13
|
+
from typing import Any, Optional, Self
|
|
14
14
|
|
|
15
15
|
from . import util
|
|
16
16
|
from .util import parse_type_str, unused
|
|
@@ -184,6 +184,34 @@ class SpecTypeInstance(SpecType):
|
|
|
184
184
|
return defn_type + self.parameters
|
|
185
185
|
|
|
186
186
|
|
|
187
|
+
@dataclass(kw_only=True)
|
|
188
|
+
class SpecEndpointExample:
|
|
189
|
+
summary: str
|
|
190
|
+
description: str
|
|
191
|
+
arguments: dict[str, object]
|
|
192
|
+
data: dict[str, object]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@dataclass(kw_only=True)
|
|
196
|
+
class SpecGuide:
|
|
197
|
+
title: str
|
|
198
|
+
markdown_content: str
|
|
199
|
+
html_content: str
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@dataclass(kw_only=True, frozen=True)
|
|
203
|
+
class RootGuideKey:
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@dataclass(kw_only=True, frozen=True)
|
|
208
|
+
class EndpointGuideKey:
|
|
209
|
+
path: str
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
SpecGuideKey = RootGuideKey | EndpointGuideKey
|
|
213
|
+
|
|
214
|
+
|
|
187
215
|
class SpecTypeLiteralWrapper(SpecType):
|
|
188
216
|
def __init__(
|
|
189
217
|
self,
|
|
@@ -672,6 +700,32 @@ class ResultType(StrEnum):
|
|
|
672
700
|
RE_ENDPOINT_ROOT = re.compile(r"\${([_a-z]+)}")
|
|
673
701
|
|
|
674
702
|
|
|
703
|
+
@dataclass(kw_only=True, frozen=True)
|
|
704
|
+
class _EndpointPathDetails:
|
|
705
|
+
root: str
|
|
706
|
+
root_path: str
|
|
707
|
+
resolved_path: str
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def _resolve_endpoint_path(
|
|
711
|
+
path: str, api_endpoints: dict[str, str]
|
|
712
|
+
) -> _EndpointPathDetails:
|
|
713
|
+
root_path_source = path.split("/")[0]
|
|
714
|
+
root_match = RE_ENDPOINT_ROOT.fullmatch(root_path_source)
|
|
715
|
+
if root_match is None:
|
|
716
|
+
raise Exception(f"invalid-api-path-root:{root_path_source}")
|
|
717
|
+
|
|
718
|
+
root_var = root_match.group(1)
|
|
719
|
+
root_path = api_endpoints[root_var]
|
|
720
|
+
|
|
721
|
+
_, *rest_path = path.split("/", 1)
|
|
722
|
+
resolved_path = "/".join([root_path] + rest_path)
|
|
723
|
+
|
|
724
|
+
return _EndpointPathDetails(
|
|
725
|
+
root=root_var, root_path=root_path, resolved_path=resolved_path
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
|
|
675
729
|
class SpecEndpoint:
|
|
676
730
|
method: RouteMethod
|
|
677
731
|
root: str
|
|
@@ -748,14 +802,10 @@ class SpecEndpoint:
|
|
|
748
802
|
|
|
749
803
|
self.result_type = ResultType(data.get("result_type", ResultType.json.value))
|
|
750
804
|
|
|
805
|
+
path_details = _resolve_endpoint_path(data["path"], builder.api_endpoints)
|
|
806
|
+
self.root = path_details.root
|
|
807
|
+
self.path_root = path_details.root_path
|
|
751
808
|
self.desc = data.get("desc")
|
|
752
|
-
|
|
753
|
-
root_match = RE_ENDPOINT_ROOT.fullmatch(path[0])
|
|
754
|
-
if root_match is None:
|
|
755
|
-
raise Exception(f"invalid-api-path-root:{path[0]}")
|
|
756
|
-
|
|
757
|
-
self.root = root_match.group(1)
|
|
758
|
-
self.path_root = builder.api_endpoints[self.root]
|
|
759
809
|
# IMPROVE: remove need for is_external flag
|
|
760
810
|
self.is_external = self.path_root == "api/external"
|
|
761
811
|
self.has_attachment = data.get("has_attachment", False)
|
|
@@ -764,6 +814,10 @@ class SpecEndpoint:
|
|
|
764
814
|
not is_sdk or self.desc is not None
|
|
765
815
|
), f"Endpoint description required for SDK endpoints, missing: {path}"
|
|
766
816
|
|
|
817
|
+
@property
|
|
818
|
+
def resolved_path(self: Self) -> str:
|
|
819
|
+
return f"{self.path_root}/{self.path_dirname}/{self.path_basename}"
|
|
820
|
+
|
|
767
821
|
|
|
768
822
|
def _parse_const(
|
|
769
823
|
builder: SpecBuilder,
|
|
@@ -1014,6 +1068,8 @@ class SpecBuilder:
|
|
|
1014
1068
|
self.pending: list[NamespaceDataPair] = []
|
|
1015
1069
|
self.parts: dict[str, dict[str, str]] = defaultdict(dict)
|
|
1016
1070
|
self.preparts: dict[str, dict[str, str]] = defaultdict(dict)
|
|
1071
|
+
self.examples: dict[str, list[SpecEndpointExample]] = defaultdict(list)
|
|
1072
|
+
self.guides: dict[SpecGuideKey, list[SpecGuide]] = defaultdict(list)
|
|
1017
1073
|
self.api_endpoints = api_endpoints
|
|
1018
1074
|
base_namespace = SpecNamespace(name=base_namespace_name)
|
|
1019
1075
|
for base_type in BaseTypeName:
|
|
@@ -1198,5 +1254,53 @@ class SpecBuilder:
|
|
|
1198
1254
|
def add_prepart_file(self, target: str, name: str, data: str) -> None:
|
|
1199
1255
|
self.preparts[target][name] = data
|
|
1200
1256
|
|
|
1257
|
+
def add_example_file(self, data: dict[str, object]) -> None:
|
|
1258
|
+
path_details = _resolve_endpoint_path(str(data["path"]), self.api_endpoints)
|
|
1259
|
+
|
|
1260
|
+
examples_data = data["examples"]
|
|
1261
|
+
if not isinstance(examples_data, list):
|
|
1262
|
+
raise Exception(
|
|
1263
|
+
f"'examples' in example files are expected to be a list, endpoint_path={path_details.resolved_path}"
|
|
1264
|
+
)
|
|
1265
|
+
for example in examples_data:
|
|
1266
|
+
arguments = example["arguments"]
|
|
1267
|
+
data_example = example["data"]
|
|
1268
|
+
if not isinstance(arguments, dict) or not isinstance(data_example, dict):
|
|
1269
|
+
raise Exception(
|
|
1270
|
+
f"'arguments' and 'data' fields must be dictionaries for each endpoint example, endpoint={path_details.resolved_path}"
|
|
1271
|
+
)
|
|
1272
|
+
self.examples[path_details.resolved_path].append(
|
|
1273
|
+
SpecEndpointExample(
|
|
1274
|
+
summary=str(example["summary"]),
|
|
1275
|
+
description=str(example["description"]),
|
|
1276
|
+
arguments=arguments,
|
|
1277
|
+
data=data_example,
|
|
1278
|
+
)
|
|
1279
|
+
)
|
|
1280
|
+
|
|
1281
|
+
def add_guide_file(self, file_content: str) -> None:
|
|
1282
|
+
import markdown
|
|
1283
|
+
|
|
1284
|
+
md = markdown.Markdown(extensions=["meta"])
|
|
1285
|
+
html = md.convert(file_content)
|
|
1286
|
+
meta: dict[str, list[str]] = md.Meta # type: ignore[attr-defined]
|
|
1287
|
+
title_meta: list[str] | None = meta.get("title")
|
|
1288
|
+
if title_meta is None:
|
|
1289
|
+
raise Exception("guides requier a title in the meta section")
|
|
1290
|
+
|
|
1291
|
+
path_meta: list[str] | None = meta.get("path")
|
|
1292
|
+
guide_key: SpecGuideKey = RootGuideKey()
|
|
1293
|
+
if path_meta is not None:
|
|
1294
|
+
path_details = _resolve_endpoint_path("".join(path_meta), self.api_endpoints)
|
|
1295
|
+
guide_key = EndpointGuideKey(path=path_details.resolved_path)
|
|
1296
|
+
|
|
1297
|
+
self.guides[guide_key].append(
|
|
1298
|
+
SpecGuide(
|
|
1299
|
+
title="".join(title_meta),
|
|
1300
|
+
html_content=html,
|
|
1301
|
+
markdown_content=file_content,
|
|
1302
|
+
)
|
|
1303
|
+
)
|
|
1304
|
+
|
|
1201
1305
|
def resolve_proper_name(self, stype: SpecTypeDefn) -> str:
|
|
1202
1306
|
return f"{'.'.join(stype.namespace.path)}.{stype.name}"
|
pkgs/type_spec/emit_open_api.py
CHANGED
|
@@ -5,18 +5,22 @@ WORK-IN-PROGRESS, DON'T USE!
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import dataclasses
|
|
8
|
+
import json
|
|
8
9
|
import re
|
|
9
|
-
from typing import cast
|
|
10
|
+
from typing import Collection, cast
|
|
10
11
|
|
|
11
12
|
import yaml
|
|
12
13
|
|
|
13
14
|
from . import builder, util
|
|
15
|
+
from .builder import EndpointGuideKey, RootGuideKey
|
|
14
16
|
from .config import OpenAPIConfig
|
|
15
17
|
from .emit_open_api_util import (
|
|
16
18
|
MODIFY_NOTICE,
|
|
17
19
|
EmitOpenAPIContext,
|
|
18
20
|
EmitOpenAPIEndpoint,
|
|
21
|
+
EmitOpenAPIEndpointExample,
|
|
19
22
|
EmitOpenAPIGlobalContext,
|
|
23
|
+
EmitOpenAPIGuide,
|
|
20
24
|
EmitOpenAPIPath,
|
|
21
25
|
EmitOpenAPIServer,
|
|
22
26
|
EmitOpenAPITag,
|
|
@@ -74,12 +78,23 @@ def _rewrite_with_notice(
|
|
|
74
78
|
return util.rewrite_file(file_path, f"{notice}\n{modified_file_content}")
|
|
75
79
|
|
|
76
80
|
|
|
77
|
-
def
|
|
78
|
-
|
|
81
|
+
def _write_guide_as_html(guide: EmitOpenAPIGuide) -> str:
|
|
82
|
+
return f"""
|
|
83
|
+
<details>
|
|
84
|
+
<summary>{guide.title}</summary>
|
|
85
|
+
{guide.html_content}
|
|
86
|
+
</details>"""
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _open_api_info(
|
|
90
|
+
config: OpenAPIConfig, guides: list[EmitOpenAPIGuide]
|
|
91
|
+
) -> GlobalContextInfo:
|
|
92
|
+
full_guides = "<br/>".join([_write_guide_as_html(guide) for guide in guides])
|
|
93
|
+
full_description = f"{config.description}<br/>{full_guides}"
|
|
79
94
|
info: GlobalContextInfo = dict()
|
|
80
95
|
info["version"] = "1.0.0"
|
|
81
96
|
info["title"] = "Uncountable API Documentation"
|
|
82
|
-
info["description"] =
|
|
97
|
+
info["description"] = full_description
|
|
83
98
|
info["x-logo"] = {"url": "../static/images/logo_blue.png", "altText": "Logo"}
|
|
84
99
|
return info
|
|
85
100
|
|
|
@@ -90,9 +105,14 @@ def _open_api_servers(config: OpenAPIConfig) -> list[EmitOpenAPIServer]:
|
|
|
90
105
|
|
|
91
106
|
|
|
92
107
|
def emit_open_api(builder: builder.SpecBuilder, *, config: OpenAPIConfig) -> None:
|
|
108
|
+
root_guides = builder.guides.get(RootGuideKey(), [])
|
|
109
|
+
openapi_guides = [
|
|
110
|
+
EmitOpenAPIGuide(title=guide.title, html_content=guide.html_content)
|
|
111
|
+
for guide in root_guides
|
|
112
|
+
]
|
|
93
113
|
gctx = EmitOpenAPIGlobalContext(
|
|
94
114
|
version="3.0.0",
|
|
95
|
-
info=_open_api_info(config),
|
|
115
|
+
info=_open_api_info(config, openapi_guides),
|
|
96
116
|
servers=_open_api_servers(config),
|
|
97
117
|
)
|
|
98
118
|
|
|
@@ -112,6 +132,8 @@ def emit_open_api(builder: builder.SpecBuilder, *, config: OpenAPIConfig) -> Non
|
|
|
112
132
|
ctx,
|
|
113
133
|
namespace=namespace,
|
|
114
134
|
config=config,
|
|
135
|
+
examples=builder.examples,
|
|
136
|
+
guides=builder.guides,
|
|
115
137
|
)
|
|
116
138
|
|
|
117
139
|
_rewrite_with_notice(
|
|
@@ -144,52 +166,183 @@ def _serialize_global_context(ctx: EmitOpenAPIGlobalContext) -> str:
|
|
|
144
166
|
return yaml.dump(oa_root, sort_keys=False)
|
|
145
167
|
|
|
146
168
|
|
|
147
|
-
def
|
|
169
|
+
def _is_empty_object_type(typ: OpenAPIType) -> bool:
|
|
148
170
|
if not isinstance(typ, OpenAPIObjectType):
|
|
171
|
+
return False
|
|
172
|
+
return len(typ.properties) == 0
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
_QUERY_PARM_METHODS = ("get", "head", "options")
|
|
176
|
+
_REQUEST_BODY_METHODS = ("put", "post", "patch", "delete")
|
|
177
|
+
|
|
178
|
+
ApiSchema = dict[str, "ApiSchema"] | Collection["ApiSchema"] | str | bool
|
|
179
|
+
DictApiSchema = dict[str, ApiSchema]
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _emit_endpoint_argument_examples(
|
|
183
|
+
examples: list[EmitOpenAPIEndpointExample],
|
|
184
|
+
) -> DictApiSchema:
|
|
185
|
+
if len(examples) == 0:
|
|
186
|
+
return {}
|
|
187
|
+
|
|
188
|
+
response_examples = {}
|
|
189
|
+
for example in examples:
|
|
190
|
+
response_examples[example.ref_name] = {
|
|
191
|
+
"summary": example.summary,
|
|
192
|
+
"description": example.description,
|
|
193
|
+
"value": example.arguments,
|
|
194
|
+
}
|
|
195
|
+
return {"examples": response_examples}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _emit_endpoint_parameter_examples(
|
|
199
|
+
examples: list[EmitOpenAPIEndpointExample],
|
|
200
|
+
) -> DictApiSchema:
|
|
201
|
+
if len(examples) == 0:
|
|
202
|
+
return {}
|
|
203
|
+
|
|
204
|
+
paramater_examples = []
|
|
205
|
+
comment_new_line = "\n// "
|
|
206
|
+
new_line = "\n"
|
|
207
|
+
for example in examples:
|
|
208
|
+
javascript_description = (
|
|
209
|
+
f"// {comment_new_line.join(example.description.split(new_line))}"
|
|
210
|
+
)
|
|
211
|
+
javascript_json_payload = f"{json.dumps(example.arguments, indent=2)}"
|
|
212
|
+
paramater_examples.append({
|
|
213
|
+
"lang": "JavaScript",
|
|
214
|
+
"label": f"Payload - {example.summary}",
|
|
215
|
+
"source": f"{javascript_description}\n{javascript_json_payload}",
|
|
216
|
+
})
|
|
217
|
+
return {"x-codeSamples": paramater_examples}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _emit_endpoint_parameters(
|
|
221
|
+
endpoint: EmitOpenAPIEndpoint,
|
|
222
|
+
argument_type: OpenAPIType | None,
|
|
223
|
+
examples: list[EmitOpenAPIEndpointExample],
|
|
224
|
+
) -> DictApiSchema:
|
|
225
|
+
if (
|
|
226
|
+
endpoint.method.lower() not in _QUERY_PARM_METHODS
|
|
227
|
+
or argument_type is None
|
|
228
|
+
or _is_empty_object_type(argument_type)
|
|
229
|
+
):
|
|
149
230
|
return {}
|
|
150
231
|
|
|
151
232
|
return {
|
|
152
233
|
"parameters": [
|
|
153
|
-
{
|
|
154
|
-
|
|
234
|
+
{
|
|
235
|
+
"name": "data",
|
|
236
|
+
"required": True,
|
|
237
|
+
"in": "query",
|
|
238
|
+
"content": {
|
|
239
|
+
"application/json": {
|
|
240
|
+
"schema": {"$ref": "#/components/schema/Arguments"}
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
}
|
|
155
244
|
]
|
|
156
|
-
}
|
|
245
|
+
} | _emit_endpoint_parameter_examples(examples)
|
|
157
246
|
|
|
158
247
|
|
|
159
|
-
def _emit_is_beta(is_beta: bool) ->
|
|
248
|
+
def _emit_is_beta(is_beta: bool) -> DictApiSchema:
|
|
160
249
|
if is_beta:
|
|
161
250
|
return {"x-beta": True}
|
|
162
251
|
return {}
|
|
163
252
|
|
|
164
253
|
|
|
254
|
+
def _emit_endpoint_request_body(
|
|
255
|
+
endpoint: EmitOpenAPIEndpoint,
|
|
256
|
+
arguments_type: OpenAPIType | None,
|
|
257
|
+
examples: list[EmitOpenAPIEndpointExample],
|
|
258
|
+
) -> DictApiSchema:
|
|
259
|
+
if (
|
|
260
|
+
endpoint.method.lower() not in _REQUEST_BODY_METHODS
|
|
261
|
+
or arguments_type is None
|
|
262
|
+
or _is_empty_object_type(arguments_type)
|
|
263
|
+
):
|
|
264
|
+
return {}
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
"requestBody": {
|
|
268
|
+
"content": {
|
|
269
|
+
"application/json": {
|
|
270
|
+
"schema": {
|
|
271
|
+
"type": "object",
|
|
272
|
+
"title": "Body",
|
|
273
|
+
"required": ["data"],
|
|
274
|
+
"properties": {"data": {"$ref": "#/components/schema/Arguments"}},
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
| _emit_endpoint_argument_examples(examples)
|
|
278
|
+
},
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _emit_endpoint_response_examples(
|
|
284
|
+
examples: list[EmitOpenAPIEndpointExample],
|
|
285
|
+
) -> dict[str, dict[str, object]]:
|
|
286
|
+
if len(examples) == 0:
|
|
287
|
+
return {}
|
|
288
|
+
|
|
289
|
+
response_examples: dict[str, object] = {}
|
|
290
|
+
for example in examples:
|
|
291
|
+
response_examples[example.ref_name] = {
|
|
292
|
+
"summary": example.summary,
|
|
293
|
+
"description": example.description,
|
|
294
|
+
"value": example.data,
|
|
295
|
+
}
|
|
296
|
+
return {"examples": response_examples}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _emit_endpoint_description(
|
|
300
|
+
description: str, guides: list[EmitOpenAPIGuide]
|
|
301
|
+
) -> dict[str, str]:
|
|
302
|
+
full_guides = "<br/>".join([_write_guide_as_html(guide) for guide in guides])
|
|
303
|
+
return {
|
|
304
|
+
"description": description
|
|
305
|
+
if len(guides) == 0
|
|
306
|
+
else f"{description}<br/>{full_guides}"
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
|
|
165
310
|
def _emit_namespace(
|
|
166
311
|
gctx: EmitOpenAPIGlobalContext,
|
|
167
312
|
ctx: EmitOpenAPIContext,
|
|
168
313
|
namespace: builder.SpecNamespace,
|
|
169
314
|
*,
|
|
170
315
|
config: OpenAPIConfig,
|
|
316
|
+
examples: dict[str, list[builder.SpecEndpointExample]],
|
|
317
|
+
guides: dict[builder.SpecGuideKey, list[builder.SpecGuide]],
|
|
171
318
|
) -> None:
|
|
172
319
|
for stype in namespace.types.values():
|
|
173
320
|
_emit_type(ctx, stype, config=config)
|
|
174
321
|
|
|
175
322
|
if namespace.endpoint is not None:
|
|
176
|
-
|
|
323
|
+
endpoint_examples = examples.get(namespace.endpoint.resolved_path, [])
|
|
324
|
+
endpoint_guides = guides.get(
|
|
325
|
+
EndpointGuideKey(path=namespace.endpoint.resolved_path), []
|
|
326
|
+
)
|
|
327
|
+
_emit_endpoint(
|
|
328
|
+
gctx, ctx, namespace, namespace.endpoint, endpoint_examples, endpoint_guides
|
|
329
|
+
)
|
|
177
330
|
|
|
178
331
|
oa_components: dict[str, object] = dict()
|
|
179
332
|
|
|
180
333
|
if ctx.endpoint is not None:
|
|
181
334
|
endpoint = ctx.endpoint
|
|
182
|
-
|
|
183
335
|
argument_type = ctx.types.get("Arguments")
|
|
184
336
|
oa_endpoint = dict()
|
|
185
337
|
oa_endpoint[endpoint.method] = (
|
|
186
338
|
{
|
|
187
339
|
"tags": endpoint.tags,
|
|
188
340
|
"summary": endpoint.summary,
|
|
189
|
-
"description": endpoint.description,
|
|
190
341
|
}
|
|
342
|
+
| _emit_endpoint_description(endpoint.description, ctx.endpoint.guides)
|
|
191
343
|
| _emit_is_beta(endpoint.is_beta)
|
|
192
|
-
| _emit_endpoint_parameters(argument_type)
|
|
344
|
+
| _emit_endpoint_parameters(endpoint, argument_type, ctx.endpoint.examples)
|
|
345
|
+
| _emit_endpoint_request_body(endpoint, argument_type, ctx.endpoint.examples)
|
|
193
346
|
| {
|
|
194
347
|
"responses": {
|
|
195
348
|
"200": {
|
|
@@ -198,6 +351,7 @@ def _emit_namespace(
|
|
|
198
351
|
"application/json": {
|
|
199
352
|
"schema": {"$ref": "#/components/schema/Data"}
|
|
200
353
|
}
|
|
354
|
+
| _emit_endpoint_response_examples(ctx.endpoint.examples)
|
|
201
355
|
},
|
|
202
356
|
}
|
|
203
357
|
},
|
|
@@ -236,10 +390,7 @@ def _emit_namespace(
|
|
|
236
390
|
|
|
237
391
|
oa_components["schema"] = cast(
|
|
238
392
|
object,
|
|
239
|
-
{
|
|
240
|
-
name: (value.asdict() if name != "Arguments" else value.asarguments())
|
|
241
|
-
for name, value in types.items()
|
|
242
|
-
},
|
|
393
|
+
{name: value.asdict() for name, value in types.items()},
|
|
243
394
|
)
|
|
244
395
|
|
|
245
396
|
path = f"{config.types_output}/common/{'/'.join(namespace.path)}.yaml"
|
|
@@ -348,6 +499,8 @@ def _emit_endpoint(
|
|
|
348
499
|
ctx: EmitOpenAPIContext,
|
|
349
500
|
namespace: builder.SpecNamespace,
|
|
350
501
|
endpoint: builder.SpecEndpoint,
|
|
502
|
+
endpoint_examples: list[builder.SpecEndpointExample],
|
|
503
|
+
endpoint_guides: list[builder.SpecGuide],
|
|
351
504
|
) -> None:
|
|
352
505
|
assert namespace.endpoint is not None
|
|
353
506
|
assert namespace.path[0] == "api"
|
|
@@ -397,6 +550,23 @@ def _emit_endpoint(
|
|
|
397
550
|
summary=f"{'/'.join(namespace.path[path_cutoff:])}",
|
|
398
551
|
description=description,
|
|
399
552
|
is_beta=namespace.endpoint.is_beta,
|
|
553
|
+
examples=[
|
|
554
|
+
EmitOpenAPIEndpointExample(
|
|
555
|
+
ref_name=f"ex_{i}",
|
|
556
|
+
summary=example.summary,
|
|
557
|
+
description=example.description,
|
|
558
|
+
arguments=example.arguments,
|
|
559
|
+
data=example.data,
|
|
560
|
+
)
|
|
561
|
+
for i, example in enumerate(endpoint_examples)
|
|
562
|
+
],
|
|
563
|
+
guides=[
|
|
564
|
+
EmitOpenAPIGuide(
|
|
565
|
+
title=guide.title,
|
|
566
|
+
html_content=guide.html_content,
|
|
567
|
+
)
|
|
568
|
+
for guide in endpoint_guides
|
|
569
|
+
],
|
|
400
570
|
)
|
|
401
571
|
|
|
402
572
|
|
|
@@ -43,6 +43,12 @@ class EmitOpenAPIServer:
|
|
|
43
43
|
url: str
|
|
44
44
|
|
|
45
45
|
|
|
46
|
+
@dataclass(kw_only=True)
|
|
47
|
+
class EmitOpenAPIGuide:
|
|
48
|
+
title: str
|
|
49
|
+
html_content: str
|
|
50
|
+
|
|
51
|
+
|
|
46
52
|
@dataclass
|
|
47
53
|
class EmitOpenAPIGlobalContext:
|
|
48
54
|
version: str
|
|
@@ -56,6 +62,15 @@ class EmitOpenAPIGlobalContext:
|
|
|
56
62
|
paths: list[EmitOpenAPIPath] = field(default_factory=list)
|
|
57
63
|
|
|
58
64
|
|
|
65
|
+
@dataclass(kw_only=True)
|
|
66
|
+
class EmitOpenAPIEndpointExample:
|
|
67
|
+
ref_name: str
|
|
68
|
+
summary: str
|
|
69
|
+
description: str
|
|
70
|
+
arguments: dict[str, object]
|
|
71
|
+
data: dict[str, object]
|
|
72
|
+
|
|
73
|
+
|
|
59
74
|
@dataclass
|
|
60
75
|
class EmitOpenAPIEndpoint:
|
|
61
76
|
method: str
|
|
@@ -63,6 +78,8 @@ class EmitOpenAPIEndpoint:
|
|
|
63
78
|
summary: str
|
|
64
79
|
description: str
|
|
65
80
|
is_beta: bool
|
|
81
|
+
examples: list[EmitOpenAPIEndpointExample]
|
|
82
|
+
guides: list[EmitOpenAPIGuide]
|
|
66
83
|
|
|
67
84
|
|
|
68
85
|
@dataclass
|
pkgs/type_spec/emit_python.py
CHANGED
|
@@ -29,7 +29,7 @@ ASYNC_BATCH_REQUEST_STYPE = builder.SpecTypeDefnObject(
|
|
|
29
29
|
namespace=ASYNC_BATCH_TYPE_NAMESPACE, name="AsyncBatchRequest"
|
|
30
30
|
)
|
|
31
31
|
QUEUED_BATCH_REQUEST_STYPE = builder.SpecTypeDefnObject(
|
|
32
|
-
namespace=ASYNC_BATCH_TYPE_NAMESPACE, name="
|
|
32
|
+
namespace=ASYNC_BATCH_TYPE_NAMESPACE, name="QueuedAsyncBatchRequest"
|
|
33
33
|
)
|
|
34
34
|
|
|
35
35
|
|
pkgs/type_spec/load_types.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from collections.abc import Callable
|
|
3
|
+
from io import StringIO
|
|
3
4
|
from typing import Optional
|
|
4
5
|
|
|
5
6
|
import yaml
|
|
@@ -13,11 +14,28 @@ ext_map = {
|
|
|
13
14
|
".py": "python",
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
_DOC_FILE_REFEX = ".*/docs/(examples|guides)/.*yaml"
|
|
18
|
+
_EXAMPLE_FILE_REGEX = ".*/docs/examples/.*yaml"
|
|
19
|
+
_GUIDE_FILE_REGEX = ".*/docs/guides/.*md"
|
|
20
|
+
|
|
16
21
|
|
|
17
22
|
def find_and_handle_files(
|
|
18
|
-
|
|
23
|
+
*,
|
|
24
|
+
root_folder: str,
|
|
25
|
+
handler: Callable[[str, str], None],
|
|
26
|
+
name_regex: str | None = None,
|
|
27
|
+
not_name_regex: str | None = None,
|
|
28
|
+
whole_name_regex: str | None = None,
|
|
29
|
+
not_whole_name_regex: str | None = None,
|
|
19
30
|
) -> None:
|
|
20
|
-
for file_name in fs.find(
|
|
31
|
+
for file_name in fs.find(
|
|
32
|
+
root_folder,
|
|
33
|
+
name_regex=name_regex,
|
|
34
|
+
not_name_regex=not_name_regex,
|
|
35
|
+
whole_name_regex=whole_name_regex,
|
|
36
|
+
not_whole_name_regex=not_whole_name_regex,
|
|
37
|
+
relative=True,
|
|
38
|
+
):
|
|
21
39
|
with open(os.path.join(root_folder, file_name), encoding="utf-8") as file:
|
|
22
40
|
handler(file_name, file.read())
|
|
23
41
|
|
|
@@ -32,9 +50,16 @@ def load_types(config: Config) -> Optional[SpecBuilder]:
|
|
|
32
50
|
name, ext = os.path.splitext(by_name)
|
|
33
51
|
handler(ext_map[ext], name, file_content)
|
|
34
52
|
|
|
53
|
+
def handle_builder_example_add(file_name: str, file_content: str) -> None:
|
|
54
|
+
yaml_content = yaml.safe_load(StringIO(file_content))
|
|
55
|
+
builder.add_example_file(yaml_content)
|
|
56
|
+
|
|
57
|
+
def handle_builder_guide_add(file_name: str, file_content: str) -> None:
|
|
58
|
+
builder.add_guide_file(file_content)
|
|
59
|
+
|
|
35
60
|
for folder in config.type_spec_types:
|
|
36
61
|
find_and_handle_files(
|
|
37
|
-
folder,
|
|
62
|
+
root_folder=folder,
|
|
38
63
|
name_regex=".*\\.(ts|py)\\.part",
|
|
39
64
|
handler=lambda file_name, file_content: handle_builder_add(
|
|
40
65
|
file_name, file_content, builder.add_part_file
|
|
@@ -43,7 +68,7 @@ def load_types(config: Config) -> Optional[SpecBuilder]:
|
|
|
43
68
|
|
|
44
69
|
for folder in config.type_spec_types:
|
|
45
70
|
find_and_handle_files(
|
|
46
|
-
folder,
|
|
71
|
+
root_folder=folder,
|
|
47
72
|
name_regex=".*\\.(ts|py)\\.prepart",
|
|
48
73
|
handler=lambda file_name, file_content: handle_builder_add(
|
|
49
74
|
file_name, file_content, builder.add_prepart_file
|
|
@@ -64,9 +89,27 @@ def load_types(config: Config) -> Optional[SpecBuilder]:
|
|
|
64
89
|
|
|
65
90
|
for folder in config.type_spec_types:
|
|
66
91
|
find_and_handle_files(
|
|
67
|
-
folder,
|
|
92
|
+
root_folder=folder,
|
|
93
|
+
name_regex=".*\\.yaml",
|
|
94
|
+
not_whole_name_regex=_DOC_FILE_REFEX,
|
|
95
|
+
handler=builder_prescan_file,
|
|
68
96
|
)
|
|
69
97
|
|
|
98
|
+
if config.open_api is not None:
|
|
99
|
+
for folder in config.type_spec_types:
|
|
100
|
+
find_and_handle_files(
|
|
101
|
+
root_folder=folder,
|
|
102
|
+
whole_name_regex=_EXAMPLE_FILE_REGEX,
|
|
103
|
+
handler=handle_builder_example_add,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
for folder in config.type_spec_types:
|
|
107
|
+
find_and_handle_files(
|
|
108
|
+
root_folder=folder,
|
|
109
|
+
whole_name_regex=_GUIDE_FILE_REGEX,
|
|
110
|
+
handler=handle_builder_guide_add,
|
|
111
|
+
)
|
|
112
|
+
|
|
70
113
|
if not builder.process():
|
|
71
114
|
return None
|
|
72
115
|
|
pkgs/type_spec/open_api_util.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
2
|
from enum import StrEnum
|
|
3
|
-
from io import UnsupportedOperation
|
|
4
3
|
from typing import Optional
|
|
5
4
|
|
|
6
5
|
|
|
@@ -16,9 +15,6 @@ class OpenAPIType(ABC):
|
|
|
16
15
|
def asdict(self) -> dict[str, object]:
|
|
17
16
|
pass
|
|
18
17
|
|
|
19
|
-
def asarguments(self) -> dict[str, dict[str, object]]:
|
|
20
|
-
raise UnsupportedOperation
|
|
21
|
-
|
|
22
18
|
def add_addl_info(self, emitted: dict[str, object]) -> dict[str, object]:
|
|
23
19
|
if self.description is not None:
|
|
24
20
|
emitted["description"] = self.description
|
|
@@ -148,9 +144,6 @@ class OpenAPIFreeFormObjectType(OpenAPIType):
|
|
|
148
144
|
def asdict(self) -> dict[str, object]:
|
|
149
145
|
return self.add_addl_info({"type": "object"})
|
|
150
146
|
|
|
151
|
-
def asarguments(self) -> dict[str, dict[str, object]]:
|
|
152
|
-
return {}
|
|
153
|
-
|
|
154
147
|
|
|
155
148
|
class OpenAPIObjectType(OpenAPIType):
|
|
156
149
|
"""
|
|
@@ -174,31 +167,30 @@ class OpenAPIObjectType(OpenAPIType):
|
|
|
174
167
|
self.property_desc = property_desc
|
|
175
168
|
super().__init__(description=description, nullable=nullable)
|
|
176
169
|
|
|
170
|
+
def _emit_property_desc(self, property_name: str) -> dict[str, str]:
|
|
171
|
+
desc = self.property_desc.get(property_name)
|
|
172
|
+
if desc is None or desc.strip() == "":
|
|
173
|
+
return {}
|
|
174
|
+
|
|
175
|
+
return {"description": desc}
|
|
176
|
+
|
|
177
177
|
def asdict(self) -> dict[str, object]:
|
|
178
178
|
return self.add_addl_info({
|
|
179
179
|
"type": "object",
|
|
180
|
+
"required": [
|
|
181
|
+
property_name
|
|
182
|
+
for property_name, property_type in self.properties.items()
|
|
183
|
+
if not property_type.nullable
|
|
184
|
+
],
|
|
180
185
|
"properties": {
|
|
181
186
|
property_name: {
|
|
182
187
|
**property_type.asdict(),
|
|
183
|
-
"description": self.property_desc.get(property_name),
|
|
184
188
|
}
|
|
189
|
+
| self._emit_property_desc(property_name)
|
|
185
190
|
for property_name, property_type in self.properties.items()
|
|
186
191
|
},
|
|
187
192
|
})
|
|
188
193
|
|
|
189
|
-
def asarguments(self) -> dict[str, dict[str, object]]:
|
|
190
|
-
argument_types: dict[str, dict[str, object]] = {}
|
|
191
|
-
for property_name, property_type in self.properties.items():
|
|
192
|
-
desc = self.property_desc.get(property_name)
|
|
193
|
-
argument_types[property_name] = {
|
|
194
|
-
"name": property_name,
|
|
195
|
-
"in": "query",
|
|
196
|
-
"schema": property_type.asdict(),
|
|
197
|
-
"required": not property_type.nullable,
|
|
198
|
-
"description": desc or "",
|
|
199
|
-
}
|
|
200
|
-
return argument_types
|
|
201
|
-
|
|
202
194
|
|
|
203
195
|
class OpenAPIUnionType(OpenAPIType):
|
|
204
196
|
"""
|
|
@@ -220,12 +212,6 @@ class OpenAPIUnionType(OpenAPIType):
|
|
|
220
212
|
# TODO: use parents description and nullable
|
|
221
213
|
return {"oneOf": [base_type.asdict() for base_type in self.base_types]}
|
|
222
214
|
|
|
223
|
-
def asarguments(self) -> dict[str, dict[str, object]]:
|
|
224
|
-
# TODO handle inheritence (allOf and refs); need to inline here...
|
|
225
|
-
# for now skip this endpoint
|
|
226
|
-
|
|
227
|
-
return {}
|
|
228
|
-
|
|
229
215
|
|
|
230
216
|
class OpenAPIIntersectionType(OpenAPIType):
|
|
231
217
|
"""
|
|
@@ -246,9 +232,3 @@ class OpenAPIIntersectionType(OpenAPIType):
|
|
|
246
232
|
def asdict(self) -> dict[str, object]:
|
|
247
233
|
# TODO: use parents description and nullable
|
|
248
234
|
return {"allOf": [base_type.asdict() for base_type in self.base_types]}
|
|
249
|
-
|
|
250
|
-
def asarguments(self) -> dict[str, dict[str, object]]:
|
|
251
|
-
# TODO handle inheritence (allOf and refs); need to inline here...
|
|
252
|
-
# for now skip this endpoint
|
|
253
|
-
|
|
254
|
-
return {}
|
|
@@ -24,6 +24,7 @@ Arguments:
|
|
|
24
24
|
Literal<entity.EntityType.lab_request>,
|
|
25
25
|
Literal<entity.EntityType.approval>,
|
|
26
26
|
Literal<entity.EntityType.custom_entity>,
|
|
27
|
+
Literal<entity.EntityType.inventory_amount>,
|
|
27
28
|
Literal<entity.EntityType.task>,
|
|
28
29
|
Literal<entity.EntityType.project>,
|
|
29
30
|
Literal<entity.EntityType.equipment>,
|
|
@@ -31,6 +31,7 @@ Arguments:
|
|
|
31
31
|
Literal<entity.EntityType.lab_request>,
|
|
32
32
|
Literal<entity.EntityType.approval>,
|
|
33
33
|
Literal<entity.EntityType.custom_entity>,
|
|
34
|
+
Literal<entity.EntityType.inventory_amount>,
|
|
34
35
|
Literal<entity.EntityType.task>,
|
|
35
36
|
Literal<entity.EntityType.project>,
|
|
36
37
|
Literal<entity.EntityType.equipment>,
|
|
@@ -77,12 +77,10 @@ Arguments:
|
|
|
77
77
|
type: identifier.IdentifierKey
|
|
78
78
|
desc: "Identifier for the recipe"
|
|
79
79
|
recipe_workflow_step_identifier:
|
|
80
|
-
type: recipe_workflow_steps.
|
|
80
|
+
type: recipe_workflow_steps.RecipeWorkflowStepIdentifier
|
|
81
81
|
edits:
|
|
82
82
|
type: List<RecipeInputEdit>
|
|
83
83
|
|
|
84
84
|
Data:
|
|
85
85
|
type: Object
|
|
86
86
|
properties:
|
|
87
|
-
result_id:
|
|
88
|
-
type: ObjectId
|
uncountable/core/__init__.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from .client import AuthDetailsApiKey, Client
|
|
2
2
|
from .file_upload import MediaFileUpload, UploadedFile
|
|
3
|
+
from .async_batch import AsyncBatchProcessor
|
|
3
4
|
|
|
4
|
-
__all__: list[str] = ["AuthDetailsApiKey", "Client", "MediaFileUpload", "UploadedFile"]
|
|
5
|
+
__all__: list[str] = ["AuthDetailsApiKey", "AsyncBatchProcessor", "Client", "MediaFileUpload", "UploadedFile"]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from uncountable.core.client import Client
|
|
2
|
+
from uncountable.types.async_batch import AsyncBatchRequest
|
|
3
|
+
from uncountable.types.async_batch_processor import AsyncBatchProcessorBase
|
|
4
|
+
from uncountable.types import async_batch_t, base_t
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AsyncBatchProcessor(AsyncBatchProcessorBase):
|
|
8
|
+
_client: Client
|
|
9
|
+
_queue: list[AsyncBatchRequest]
|
|
10
|
+
|
|
11
|
+
def __init__(self, *, client: Client) -> None:
|
|
12
|
+
super().__init__()
|
|
13
|
+
self._client = client
|
|
14
|
+
self._queue = []
|
|
15
|
+
|
|
16
|
+
def _enqueue(self, req: async_batch_t.AsyncBatchRequest) -> None:
|
|
17
|
+
self._queue.append(req)
|
|
18
|
+
|
|
19
|
+
def send(self) -> base_t.ObjectId:
|
|
20
|
+
job_id = self._client.execute_batch_load_async(requests=self._queue).job_id
|
|
21
|
+
self._queue = []
|
|
22
|
+
return job_id
|
uncountable/core/client.py
CHANGED
|
@@ -6,9 +6,11 @@ from enum import StrEnum
|
|
|
6
6
|
from urllib.parse import urljoin
|
|
7
7
|
|
|
8
8
|
import requests
|
|
9
|
+
from requests.exceptions import JSONDecodeError
|
|
9
10
|
|
|
10
11
|
from pkgs.argument_parser import CachedParser
|
|
11
12
|
from pkgs.serialization_util import serialize_for_api
|
|
13
|
+
from pkgs.serialization_util.serialization_helpers import JsonValue
|
|
12
14
|
from uncountable.types.client_base import APIRequest, ClientMethods
|
|
13
15
|
|
|
14
16
|
from .file_upload import FileUpload, FileUploader, UploadedFile
|
|
@@ -46,16 +48,77 @@ class HTTPPostRequest(HTTPRequestBase):
|
|
|
46
48
|
HTTPRequest = HTTPPostRequest | HTTPGetRequest
|
|
47
49
|
|
|
48
50
|
|
|
51
|
+
|
|
52
|
+
@dataclass(kw_only=True)
|
|
53
|
+
class ClientConfig():
|
|
54
|
+
allow_insecure_tls: bool = False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class APIResponseError(BaseException):
|
|
58
|
+
status_code: int
|
|
59
|
+
message: str
|
|
60
|
+
extra_details: dict[str, JsonValue] | None
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self, status_code: int, message: str, extra_details: dict[str, JsonValue] | None
|
|
64
|
+
) -> None:
|
|
65
|
+
super().__init__(status_code, message, extra_details)
|
|
66
|
+
self.status_code = status_code
|
|
67
|
+
self.message = message
|
|
68
|
+
self.extra_details = extra_details
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def construct_error(
|
|
72
|
+
cls, status_code: int, extra_details: dict[str, JsonValue] | None
|
|
73
|
+
) -> "APIResponseError":
|
|
74
|
+
message: str
|
|
75
|
+
match status_code:
|
|
76
|
+
case 403:
|
|
77
|
+
message = "unexpected: unauthorized"
|
|
78
|
+
case 410:
|
|
79
|
+
message = "unexpected: not found"
|
|
80
|
+
case 400:
|
|
81
|
+
message = "unexpected: bad arguments"
|
|
82
|
+
case 501:
|
|
83
|
+
message = "unexpected: unimplemented"
|
|
84
|
+
case 504:
|
|
85
|
+
message = "unexpected: timeout"
|
|
86
|
+
case 404:
|
|
87
|
+
message = "not found"
|
|
88
|
+
case 409:
|
|
89
|
+
message = "bad arguments"
|
|
90
|
+
case 422:
|
|
91
|
+
message = "unprocessable"
|
|
92
|
+
case _:
|
|
93
|
+
message = "unknown error"
|
|
94
|
+
return APIResponseError(
|
|
95
|
+
status_code=status_code, message=message, extra_details=extra_details
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class SDKError(BaseException):
|
|
100
|
+
message: str
|
|
101
|
+
|
|
102
|
+
def __init__(self, message: str) -> None:
|
|
103
|
+
super().__init__(message)
|
|
104
|
+
self.message = message
|
|
105
|
+
|
|
106
|
+
def __str__(self) -> str:
|
|
107
|
+
return f"internal SDK error, please contact Uncountable support: {self.message}"
|
|
108
|
+
|
|
109
|
+
|
|
49
110
|
class Client(ClientMethods):
|
|
50
111
|
_parser_map: dict[type, CachedParser] = {}
|
|
51
112
|
_auth_details: AuthDetails
|
|
52
113
|
_base_url: str
|
|
53
114
|
_file_uploader: FileUploader
|
|
115
|
+
_cfg: ClientConfig
|
|
54
116
|
|
|
55
|
-
def __init__(self, *, base_url: str, auth_details: AuthDetails):
|
|
117
|
+
def __init__(self, *, base_url: str, auth_details: AuthDetails, config: ClientConfig | None = None):
|
|
56
118
|
self._auth_details = auth_details
|
|
57
119
|
self._base_url = base_url
|
|
58
120
|
self._file_uploader = FileUploader(self._base_url, self._auth_details)
|
|
121
|
+
self._cfg = config or ClientConfig()
|
|
59
122
|
|
|
60
123
|
def do_request(self, *, api_request: APIRequest, return_type: type[DT]) -> DT:
|
|
61
124
|
http_request = self._build_http_request(api_request=api_request)
|
|
@@ -65,6 +128,7 @@ class Client(ClientMethods):
|
|
|
65
128
|
http_request.url,
|
|
66
129
|
headers=http_request.headers,
|
|
67
130
|
params=http_request.query_params,
|
|
131
|
+
verify=not self._cfg.allow_insecure_tls
|
|
68
132
|
)
|
|
69
133
|
case HTTPPostRequest():
|
|
70
134
|
response = requests.post(
|
|
@@ -72,19 +136,27 @@ class Client(ClientMethods):
|
|
|
72
136
|
headers=http_request.headers,
|
|
73
137
|
data=http_request.body,
|
|
74
138
|
params=http_request.query_params,
|
|
139
|
+
verify=not self._cfg.allow_insecure_tls
|
|
75
140
|
)
|
|
76
141
|
case _:
|
|
77
142
|
typing.assert_never(http_request)
|
|
78
143
|
if response.status_code < 200 or response.status_code > 299:
|
|
79
|
-
|
|
80
|
-
|
|
144
|
+
extra_details: dict[str, JsonValue] | None = None
|
|
145
|
+
try:
|
|
146
|
+
data = response.json()
|
|
147
|
+
if "error" in data:
|
|
148
|
+
extra_details = data["error"]
|
|
149
|
+
except JSONDecodeError:
|
|
150
|
+
pass
|
|
151
|
+
raise APIResponseError.construct_error(
|
|
152
|
+
status_code=response.status_code, extra_details=extra_details
|
|
153
|
+
)
|
|
81
154
|
cached_parser = self._get_cached_parser(return_type)
|
|
82
155
|
try:
|
|
83
156
|
data = response.json()["data"]
|
|
84
157
|
return cached_parser.parse_api(data)
|
|
85
|
-
except ValueError
|
|
86
|
-
|
|
87
|
-
raise err
|
|
158
|
+
except ValueError | JSONDecodeError:
|
|
159
|
+
raise SDKError("unable to process response")
|
|
88
160
|
|
|
89
161
|
def _get_cached_parser(self, data_type: type[DT]) -> CachedParser[DT]:
|
|
90
162
|
if data_type not in self._parser_map:
|
|
@@ -34,7 +34,7 @@ class EntityToCreate:
|
|
|
34
34
|
@dataclass(kw_only=True)
|
|
35
35
|
class Arguments:
|
|
36
36
|
definition_id: base_t.ObjectId
|
|
37
|
-
entity_type: typing.Union[typing.Literal[entity_t.EntityType.LAB_REQUEST], typing.Literal[entity_t.EntityType.APPROVAL], typing.Literal[entity_t.EntityType.CUSTOM_ENTITY], typing.Literal[entity_t.EntityType.TASK], typing.Literal[entity_t.EntityType.PROJECT], typing.Literal[entity_t.EntityType.EQUIPMENT], typing.Literal[entity_t.EntityType.INV_LOCAL_LOCATIONS], typing.Literal[entity_t.EntityType.FIELD_OPTION_SET], typing.Literal[entity_t.EntityType.WEBHOOK]]
|
|
37
|
+
entity_type: typing.Union[typing.Literal[entity_t.EntityType.LAB_REQUEST], typing.Literal[entity_t.EntityType.APPROVAL], typing.Literal[entity_t.EntityType.CUSTOM_ENTITY], typing.Literal[entity_t.EntityType.INVENTORY_AMOUNT], typing.Literal[entity_t.EntityType.TASK], typing.Literal[entity_t.EntityType.PROJECT], typing.Literal[entity_t.EntityType.EQUIPMENT], typing.Literal[entity_t.EntityType.INV_LOCAL_LOCATIONS], typing.Literal[entity_t.EntityType.FIELD_OPTION_SET], typing.Literal[entity_t.EntityType.WEBHOOK]]
|
|
38
38
|
entities_to_create: list[EntityToCreate]
|
|
39
39
|
|
|
40
40
|
|
|
@@ -40,7 +40,7 @@ class EntityFieldInitialValue:
|
|
|
40
40
|
@dataclass(kw_only=True)
|
|
41
41
|
class Arguments:
|
|
42
42
|
definition_id: base_t.ObjectId
|
|
43
|
-
entity_type: typing.Union[typing.Literal[entity_t.EntityType.LAB_REQUEST], typing.Literal[entity_t.EntityType.APPROVAL], typing.Literal[entity_t.EntityType.CUSTOM_ENTITY], typing.Literal[entity_t.EntityType.TASK], typing.Literal[entity_t.EntityType.PROJECT], typing.Literal[entity_t.EntityType.EQUIPMENT], typing.Literal[entity_t.EntityType.INV_LOCAL_LOCATIONS], typing.Literal[entity_t.EntityType.FIELD_OPTION_SET], typing.Literal[entity_t.EntityType.WEBHOOK]]
|
|
43
|
+
entity_type: typing.Union[typing.Literal[entity_t.EntityType.LAB_REQUEST], typing.Literal[entity_t.EntityType.APPROVAL], typing.Literal[entity_t.EntityType.CUSTOM_ENTITY], typing.Literal[entity_t.EntityType.INVENTORY_AMOUNT], typing.Literal[entity_t.EntityType.TASK], typing.Literal[entity_t.EntityType.PROJECT], typing.Literal[entity_t.EntityType.EQUIPMENT], typing.Literal[entity_t.EntityType.INV_LOCAL_LOCATIONS], typing.Literal[entity_t.EntityType.FIELD_OPTION_SET], typing.Literal[entity_t.EntityType.WEBHOOK]]
|
|
44
44
|
field_values: typing.Optional[typing.Optional[list[field_values_t.FieldRefNameValue]]] = None
|
|
45
45
|
|
|
46
46
|
|
|
@@ -10,7 +10,6 @@ from decimal import Decimal # noqa: F401
|
|
|
10
10
|
from pkgs.strenum_compat import StrEnum
|
|
11
11
|
from dataclasses import dataclass
|
|
12
12
|
from pkgs.serialization import serial_class
|
|
13
|
-
from ... import base as base_t
|
|
14
13
|
from ... import identifier as identifier_t
|
|
15
14
|
from ... import recipe_inputs as recipe_inputs_t
|
|
16
15
|
from ... import recipe_workflow_steps as recipe_workflow_steps_t
|
|
@@ -96,12 +95,12 @@ RecipeInputEdit = typing.Union[RecipeInputEditClearInputs, RecipeInputEditUpsert
|
|
|
96
95
|
@dataclass(kw_only=True)
|
|
97
96
|
class Arguments:
|
|
98
97
|
recipe_key: identifier_t.IdentifierKey
|
|
99
|
-
recipe_workflow_step_identifier: recipe_workflow_steps_t.
|
|
98
|
+
recipe_workflow_step_identifier: recipe_workflow_steps_t.RecipeWorkflowStepIdentifier
|
|
100
99
|
edits: list[RecipeInputEdit]
|
|
101
100
|
|
|
102
101
|
|
|
103
102
|
# DO NOT MODIFY -- This file is generated by type_spec
|
|
104
103
|
@dataclass(kw_only=True)
|
|
105
104
|
class Data:
|
|
106
|
-
|
|
105
|
+
pass
|
|
107
106
|
# DO NOT MODIFY -- This file is generated by type_spec
|
uncountable/types/async_batch.py
CHANGED
|
@@ -25,6 +25,7 @@ class AsyncBatchRequestPath(StrEnum):
|
|
|
25
25
|
CREATE_RECIPE = "recipes/create_recipe"
|
|
26
26
|
SET_RECIPE_METADATA = "recipes/set_recipe_metadata"
|
|
27
27
|
SET_RECIPE_TAGS = "recipes/set_recipe_tags"
|
|
28
|
+
EDIT_RECIPE_INPUTS = "recipes/edit_recipe_inputs"
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
# DO NOT MODIFY -- This file is generated by type_spec
|
|
@@ -43,7 +43,7 @@ class AsyncBatchProcessorBase(ABC):
|
|
|
43
43
|
identifiers: typing.Optional[recipe_identifiers_t.RecipeIdentifiers] = None,
|
|
44
44
|
definition_key: typing.Optional[identifier_t.IdentifierKey] = None,
|
|
45
45
|
depends_on: typing.Optional[list[str]] = None,
|
|
46
|
-
) -> async_batch_t.
|
|
46
|
+
) -> async_batch_t.QueuedAsyncBatchRequest:
|
|
47
47
|
"""Returns the id of the recipe being created.
|
|
48
48
|
|
|
49
49
|
:param name: The name for the recipe
|
|
@@ -77,7 +77,7 @@ class AsyncBatchProcessorBase(ABC):
|
|
|
77
77
|
|
|
78
78
|
self._enqueue(req)
|
|
79
79
|
|
|
80
|
-
return async_batch_t.
|
|
80
|
+
return async_batch_t.QueuedAsyncBatchRequest(
|
|
81
81
|
path=req.path,
|
|
82
82
|
batch_reference=req.batch_reference,
|
|
83
83
|
)
|
|
@@ -86,10 +86,10 @@ class AsyncBatchProcessorBase(ABC):
|
|
|
86
86
|
self,
|
|
87
87
|
*,
|
|
88
88
|
recipe_key: identifier_t.IdentifierKey,
|
|
89
|
-
recipe_workflow_step_identifier: recipe_workflow_steps_t.
|
|
89
|
+
recipe_workflow_step_identifier: recipe_workflow_steps_t.RecipeWorkflowStepIdentifier,
|
|
90
90
|
edits: list[edit_recipe_inputs_t.RecipeInputEdit],
|
|
91
91
|
depends_on: typing.Optional[list[str]] = None,
|
|
92
|
-
) -> async_batch_t.
|
|
92
|
+
) -> async_batch_t.QueuedAsyncBatchRequest:
|
|
93
93
|
"""Clear, update, or add inputs on a recipe
|
|
94
94
|
|
|
95
95
|
:param recipe_key: Identifier for the recipe
|
|
@@ -113,7 +113,7 @@ class AsyncBatchProcessorBase(ABC):
|
|
|
113
113
|
|
|
114
114
|
self._enqueue(req)
|
|
115
115
|
|
|
116
|
-
return async_batch_t.
|
|
116
|
+
return async_batch_t.QueuedAsyncBatchRequest(
|
|
117
117
|
path=req.path,
|
|
118
118
|
batch_reference=req.batch_reference,
|
|
119
119
|
)
|
|
@@ -124,7 +124,7 @@ class AsyncBatchProcessorBase(ABC):
|
|
|
124
124
|
recipe_key: identifier_t.IdentifierKey,
|
|
125
125
|
recipe_metadata: list[recipe_metadata_t.MetadataValue],
|
|
126
126
|
depends_on: typing.Optional[list[str]] = None,
|
|
127
|
-
) -> async_batch_t.
|
|
127
|
+
) -> async_batch_t.QueuedAsyncBatchRequest:
|
|
128
128
|
"""Set metadata values on a recipe
|
|
129
129
|
|
|
130
130
|
:param recipe_key: Identifier for the recipe
|
|
@@ -148,7 +148,7 @@ class AsyncBatchProcessorBase(ABC):
|
|
|
148
148
|
|
|
149
149
|
self._enqueue(req)
|
|
150
150
|
|
|
151
|
-
return async_batch_t.
|
|
151
|
+
return async_batch_t.QueuedAsyncBatchRequest(
|
|
152
152
|
path=req.path,
|
|
153
153
|
batch_reference=req.batch_reference,
|
|
154
154
|
)
|
uncountable/types/client_base.py
CHANGED
|
@@ -147,7 +147,7 @@ class ClientMethods(ABC):
|
|
|
147
147
|
self,
|
|
148
148
|
*,
|
|
149
149
|
definition_id: base_t.ObjectId,
|
|
150
|
-
entity_type: typing.Union[typing.Literal[entity_t.EntityType.LAB_REQUEST], typing.Literal[entity_t.EntityType.APPROVAL], typing.Literal[entity_t.EntityType.CUSTOM_ENTITY], typing.Literal[entity_t.EntityType.TASK], typing.Literal[entity_t.EntityType.PROJECT], typing.Literal[entity_t.EntityType.EQUIPMENT], typing.Literal[entity_t.EntityType.INV_LOCAL_LOCATIONS], typing.Literal[entity_t.EntityType.FIELD_OPTION_SET], typing.Literal[entity_t.EntityType.WEBHOOK]],
|
|
150
|
+
entity_type: typing.Union[typing.Literal[entity_t.EntityType.LAB_REQUEST], typing.Literal[entity_t.EntityType.APPROVAL], typing.Literal[entity_t.EntityType.CUSTOM_ENTITY], typing.Literal[entity_t.EntityType.INVENTORY_AMOUNT], typing.Literal[entity_t.EntityType.TASK], typing.Literal[entity_t.EntityType.PROJECT], typing.Literal[entity_t.EntityType.EQUIPMENT], typing.Literal[entity_t.EntityType.INV_LOCAL_LOCATIONS], typing.Literal[entity_t.EntityType.FIELD_OPTION_SET], typing.Literal[entity_t.EntityType.WEBHOOK]],
|
|
151
151
|
entities_to_create: list[create_entities_t.EntityToCreate],
|
|
152
152
|
) -> create_entities_t.Data:
|
|
153
153
|
"""Creates new Uncountable entities
|
|
@@ -172,7 +172,7 @@ class ClientMethods(ABC):
|
|
|
172
172
|
self,
|
|
173
173
|
*,
|
|
174
174
|
definition_id: base_t.ObjectId,
|
|
175
|
-
entity_type: typing.Union[typing.Literal[entity_t.EntityType.LAB_REQUEST], typing.Literal[entity_t.EntityType.APPROVAL], typing.Literal[entity_t.EntityType.CUSTOM_ENTITY], typing.Literal[entity_t.EntityType.TASK], typing.Literal[entity_t.EntityType.PROJECT], typing.Literal[entity_t.EntityType.EQUIPMENT], typing.Literal[entity_t.EntityType.INV_LOCAL_LOCATIONS], typing.Literal[entity_t.EntityType.FIELD_OPTION_SET], typing.Literal[entity_t.EntityType.WEBHOOK]],
|
|
175
|
+
entity_type: typing.Union[typing.Literal[entity_t.EntityType.LAB_REQUEST], typing.Literal[entity_t.EntityType.APPROVAL], typing.Literal[entity_t.EntityType.CUSTOM_ENTITY], typing.Literal[entity_t.EntityType.INVENTORY_AMOUNT], typing.Literal[entity_t.EntityType.TASK], typing.Literal[entity_t.EntityType.PROJECT], typing.Literal[entity_t.EntityType.EQUIPMENT], typing.Literal[entity_t.EntityType.INV_LOCAL_LOCATIONS], typing.Literal[entity_t.EntityType.FIELD_OPTION_SET], typing.Literal[entity_t.EntityType.WEBHOOK]],
|
|
176
176
|
field_values: typing.Optional[typing.Optional[list[field_values_t.FieldRefNameValue]]] = None,
|
|
177
177
|
) -> create_entity_t.Data:
|
|
178
178
|
"""Creates a new Uncountable entity
|
uncountable/types/identifier.py
CHANGED
|
@@ -23,7 +23,7 @@ __all__: list[str] = [
|
|
|
23
23
|
@serial_class(
|
|
24
24
|
parse_require={"type"},
|
|
25
25
|
)
|
|
26
|
-
@dataclass(kw_only=True)
|
|
26
|
+
@dataclass(kw_only=True, frozen=True, eq=True)
|
|
27
27
|
class IdentifierKeyId:
|
|
28
28
|
type: typing.Literal["id"] = "id"
|
|
29
29
|
id: base_t.ObjectId
|
|
@@ -33,7 +33,7 @@ class IdentifierKeyId:
|
|
|
33
33
|
@serial_class(
|
|
34
34
|
parse_require={"type"},
|
|
35
35
|
)
|
|
36
|
-
@dataclass(kw_only=True)
|
|
36
|
+
@dataclass(kw_only=True, frozen=True, eq=True)
|
|
37
37
|
class IdentifierKeyRefName:
|
|
38
38
|
type: typing.Literal["ref_name"] = "ref_name"
|
|
39
39
|
ref_name: str
|
|
@@ -43,7 +43,7 @@ class IdentifierKeyRefName:
|
|
|
43
43
|
@serial_class(
|
|
44
44
|
parse_require={"type"},
|
|
45
45
|
)
|
|
46
|
-
@dataclass(kw_only=True)
|
|
46
|
+
@dataclass(kw_only=True, frozen=True, eq=True)
|
|
47
47
|
class IdentifierKeyBatchReference:
|
|
48
48
|
type: typing.Literal["batch_reference"] = "batch_reference"
|
|
49
49
|
reference: str
|
|
@@ -59,7 +59,7 @@ class RecipeWorkflowStepPosition(StrEnum):
|
|
|
59
59
|
class RecipeWorkflowStepIdentifierWorkflowStep(RecipeWorkflowStepIdentifierBase):
|
|
60
60
|
type: typing.Literal[RecipeWorkflowStepIdentifierType.WORKFLOW_STEP] = RecipeWorkflowStepIdentifierType.WORKFLOW_STEP
|
|
61
61
|
workflow_step_key: identifier_t.IdentifierKey
|
|
62
|
-
position:
|
|
62
|
+
position: RecipeWorkflowStepPosition = RecipeWorkflowStepPosition.FIRST
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
# DO NOT MODIFY -- This file is generated by type_spec
|
|
File without changes
|
{UncountablePythonSDK-0.0.21.dist-info → UncountablePythonSDK-0.0.22.dist-info}/top_level.txt
RENAMED
|
File without changes
|