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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: UncountablePythonSDK
3
- Version: 0.0.21
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=W9VA7JhPkLw3lau3FjYlIiBGzRs5LBPTZr0tTO2KVuU,39570
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=4ihGO_neLL1A9zdoytQ5YypgBqyM0WKzBHo9wtsBPuE,17772
41
- pkgs/type_spec/emit_open_api_util.py,sha256=gMUB-fSrnTp6NyK_HbY02SP6cqSRObo-KIojDu4rn44,1900
42
- pkgs/type_spec/emit_python.py,sha256=CCHlOIMKdGu1VoaP7ly6dZ2cYank9jaAr8NDN50rFiE,42411
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=AB334CF-368dpMNXE2QIFT7WxESvuew2RK_GG2MREng,2211
46
- pkgs/type_spec/open_api_util.py,sha256=88zgI_lHi6SjCaNfKpoiCjd1o0Npysjk-7iCXw0a3Hs,7463
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=NVA7I1b4xf81urMKaiaiQPgvykEvjKslfOAN2gOhMIE,1441
65
- type_spec/external/api/entity/create_entity.yaml,sha256=2itcDpiuZHckhn45ZJyrssMOS9_CgBwFGf-MQyUyImw,1636
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=CmolwyHSGJOQ33nJH7SQUYK-p2CWNEQ9yTstNxALv18,2631
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=OYipeBMLl_Mk2LApzJqzvOWSyQ12tdOXURp1GsKSjyE,190
107
- uncountable/core/client.py,sha256=ebBwERBKbdp7x5y77bFeyyGejIi02fjFZ4AMCJUfMpA,4507
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=VMLB3ss1xzsFXovnWIaxgEVb4DWz7Agnf_MUNDaoRQc,1524
123
- uncountable/types/async_batch_processor.py,sha256=fwzfk0XY0L1ulzNi3dT8Aky5MyWPNK6GDStZ_I8Icoc,5914
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=w-UnuzUSg8l_5kFhxo3w_b0_UC0eT-mtHheoA0f-U58,45967
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=O_J3sHsG3pFeSIxUKmgIK97FZl6f4qkRDwsNLvY7ymE,1389
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=bHVkwlsGODLpRFMJ2Ja-5FV1kqYjed8J0J0gU1dmTLQ,2615
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=oZVBovL3pdBfoKpaOXJcPtUyDxjKgFnYEqKfGx_ZcvE,1649
160
- uncountable/types/api/entity/create_entity.py,sha256=J6eY8b_XEQROFR7FI7In3zmFngaCBY5TcasMi4wy9t8,1819
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=Ae7YMUsONp1bUVdaznetWiw-THPndnzFI-t4iMWV_eg,3338
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.21.dist-info/METADATA,sha256=_xqCDeMHkCp6YfUDs-Wr2wc7e2ErYtHzjA1f6YqcGfc,1613
210
- UncountablePythonSDK-0.0.21.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
211
- UncountablePythonSDK-0.0.21.dist-info/top_level.txt,sha256=HaMiBnH1wA7SG9-RVHIJPBH3l8X5gee2jUf-77Nz-Dk,41
212
- UncountablePythonSDK-0.0.21.dist-info/RECORD,,
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,,
@@ -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}"
@@ -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 _open_api_info(config: OpenAPIConfig) -> GlobalContextInfo:
78
- description = config.description
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"] = 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 _emit_endpoint_parameters(typ: OpenAPIType | None) -> dict[str, list[dict[str, str]]]:
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
- {"$ref": f"#/components/schema/Arguments/{prop_name}"}
154
- for prop_name in typ.properties
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) -> dict[str, 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
- _emit_endpoint(gctx, ctx, namespace, namespace.endpoint)
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
@@ -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="QueuedBatchRequest"
32
+ namespace=ASYNC_BATCH_TYPE_NAMESPACE, name="QueuedAsyncBatchRequest"
33
33
  )
34
34
 
35
35
 
@@ -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
- root_folder: str, name_regex: str, handler: Callable[[str, str], None]
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(root_folder, name_regex=name_regex, relative=True):
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, name_regex=".*\\.yaml", handler=builder_prescan_file
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
 
@@ -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.RecipeWorkflowStepIdentifierType
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
@@ -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
@@ -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
- # TODO: handle_error
80
- pass
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 as err:
86
- # TODO: handle parse error
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.RecipeWorkflowStepIdentifierType
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
- result_id: base_t.ObjectId
105
+ pass
107
106
  # DO NOT MODIFY -- This file is generated by type_spec
@@ -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.QueuedBatchRequest:
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.QueuedBatchRequest(
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.RecipeWorkflowStepIdentifierType,
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.QueuedBatchRequest:
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.QueuedBatchRequest(
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.QueuedBatchRequest:
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.QueuedBatchRequest(
151
+ return async_batch_t.QueuedAsyncBatchRequest(
152
152
  path=req.path,
153
153
  batch_reference=req.batch_reference,
154
154
  )
@@ -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
@@ -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: typing.Optional[RecipeWorkflowStepPosition] = None
62
+ position: RecipeWorkflowStepPosition = RecipeWorkflowStepPosition.FIRST
63
63
 
64
64
 
65
65
  # DO NOT MODIFY -- This file is generated by type_spec