UncountablePythonSDK 0.0.20__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.20.dist-info → UncountablePythonSDK-0.0.22.dist-info}/METADATA +3 -1
- {UncountablePythonSDK-0.0.20.dist-info → UncountablePythonSDK-0.0.22.dist-info}/RECORD +55 -34
- examples/async_batch.py +36 -0
- examples/upload_files.py +19 -0
- pkgs/type_spec/actions_registry/__main__.py +35 -23
- pkgs/type_spec/actions_registry/emit_typescript.py +71 -9
- pkgs/type_spec/builder.py +125 -8
- pkgs/type_spec/config.py +1 -0
- pkgs/type_spec/emit_open_api.py +197 -16
- pkgs/type_spec/emit_open_api_util.py +18 -0
- pkgs/type_spec/emit_python.py +241 -55
- pkgs/type_spec/load_types.py +48 -5
- pkgs/type_spec/open_api_util.py +13 -33
- pkgs/type_spec/type_info/emit_type_info.py +129 -8
- type_spec/external/api/entity/create_entities.yaml +13 -1
- type_spec/external/api/entity/create_entity.yaml +13 -1
- type_spec/external/api/entity/transition_entity_phase.yaml +44 -0
- type_spec/external/api/permissions/set_core_permissions.yaml +69 -0
- type_spec/external/api/recipes/associate_recipe_as_input.yaml +4 -4
- type_spec/external/api/recipes/create_recipe.yaml +2 -1
- type_spec/external/api/recipes/disassociate_recipe_as_input.yaml +16 -0
- type_spec/external/api/recipes/edit_recipe_inputs.yaml +86 -0
- type_spec/external/api/recipes/get_curve.yaml +4 -1
- type_spec/external/api/recipes/get_recipes_data.yaml +6 -0
- type_spec/external/api/recipes/set_recipe_metadata.yaml +1 -0
- type_spec/external/api/recipes/set_recipe_tags.yaml +62 -0
- uncountable/core/__init__.py +3 -1
- uncountable/core/async_batch.py +22 -0
- uncountable/core/client.py +84 -10
- uncountable/core/file_upload.py +95 -0
- uncountable/core/types.py +22 -0
- uncountable/types/__init__.py +18 -0
- uncountable/types/api/entity/create_entities.py +1 -1
- uncountable/types/api/entity/create_entity.py +1 -1
- uncountable/types/api/entity/transition_entity_phase.py +66 -0
- uncountable/types/api/permissions/__init__.py +1 -0
- uncountable/types/api/permissions/set_core_permissions.py +89 -0
- uncountable/types/api/recipes/associate_recipe_as_input.py +4 -3
- uncountable/types/api/recipes/create_recipe.py +1 -1
- uncountable/types/api/recipes/disassociate_recipe_as_input.py +35 -0
- uncountable/types/api/recipes/edit_recipe_inputs.py +106 -0
- uncountable/types/api/recipes/get_curve.py +2 -1
- uncountable/types/api/recipes/get_recipes_data.py +2 -0
- uncountable/types/api/recipes/set_recipe_tags.py +91 -0
- uncountable/types/async_batch.py +10 -0
- uncountable/types/async_batch_processor.py +154 -0
- uncountable/types/client_base.py +113 -48
- uncountable/types/identifier.py +3 -3
- uncountable/types/permissions.py +46 -0
- uncountable/types/post_base.py +30 -0
- uncountable/types/recipe_inputs.py +30 -0
- uncountable/types/recipe_metadata.py +2 -0
- uncountable/types/recipe_workflow_steps.py +77 -0
- {UncountablePythonSDK-0.0.20.dist-info → UncountablePythonSDK-0.0.22.dist-info}/WHEEL +0 -0
- {UncountablePythonSDK-0.0.20.dist-info → UncountablePythonSDK-0.0.22.dist-info}/top_level.txt +0 -0
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
|
|
@@ -680,9 +734,11 @@ class SpecEndpoint:
|
|
|
680
734
|
path_basename: str
|
|
681
735
|
data_loader: bool
|
|
682
736
|
is_sdk: bool
|
|
737
|
+
is_beta: bool
|
|
683
738
|
# Don't emit TypeScript endpoint code
|
|
684
739
|
suppress_ts: bool
|
|
685
740
|
function: Optional[str]
|
|
741
|
+
async_batch_path: str | None = None
|
|
686
742
|
result_type: ResultType = ResultType.json
|
|
687
743
|
has_attachment: bool = False
|
|
688
744
|
desc: str | None = None
|
|
@@ -701,6 +757,8 @@ class SpecEndpoint:
|
|
|
701
757
|
"path",
|
|
702
758
|
"data_loader",
|
|
703
759
|
"is_sdk",
|
|
760
|
+
"is_beta",
|
|
761
|
+
"async_batch_path",
|
|
704
762
|
"function",
|
|
705
763
|
"suppress_ts",
|
|
706
764
|
"desc",
|
|
@@ -727,6 +785,15 @@ class SpecEndpoint:
|
|
|
727
785
|
assert isinstance(is_sdk, bool)
|
|
728
786
|
self.is_sdk = is_sdk
|
|
729
787
|
|
|
788
|
+
is_beta = data.get("is_beta", False)
|
|
789
|
+
assert isinstance(is_beta, bool)
|
|
790
|
+
self.is_beta = is_beta
|
|
791
|
+
|
|
792
|
+
async_batch_path = data.get("async_batch_path")
|
|
793
|
+
if async_batch_path is not None:
|
|
794
|
+
assert isinstance(async_batch_path, str)
|
|
795
|
+
self.async_batch_path = async_batch_path
|
|
796
|
+
|
|
730
797
|
self.function = data.get("function")
|
|
731
798
|
|
|
732
799
|
suppress_ts = data.get("suppress_ts", False)
|
|
@@ -735,14 +802,10 @@ class SpecEndpoint:
|
|
|
735
802
|
|
|
736
803
|
self.result_type = ResultType(data.get("result_type", ResultType.json.value))
|
|
737
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
|
|
738
808
|
self.desc = data.get("desc")
|
|
739
|
-
|
|
740
|
-
root_match = RE_ENDPOINT_ROOT.fullmatch(path[0])
|
|
741
|
-
if root_match is None:
|
|
742
|
-
raise Exception(f"invalid-api-path-root:{path[0]}")
|
|
743
|
-
|
|
744
|
-
self.root = root_match.group(1)
|
|
745
|
-
self.path_root = builder.api_endpoints[self.root]
|
|
746
809
|
# IMPROVE: remove need for is_external flag
|
|
747
810
|
self.is_external = self.path_root == "api/external"
|
|
748
811
|
self.has_attachment = data.get("has_attachment", False)
|
|
@@ -751,6 +814,10 @@ class SpecEndpoint:
|
|
|
751
814
|
not is_sdk or self.desc is not None
|
|
752
815
|
), f"Endpoint description required for SDK endpoints, missing: {path}"
|
|
753
816
|
|
|
817
|
+
@property
|
|
818
|
+
def resolved_path(self: Self) -> str:
|
|
819
|
+
return f"{self.path_root}/{self.path_dirname}/{self.path_basename}"
|
|
820
|
+
|
|
754
821
|
|
|
755
822
|
def _parse_const(
|
|
756
823
|
builder: SpecBuilder,
|
|
@@ -1001,6 +1068,8 @@ class SpecBuilder:
|
|
|
1001
1068
|
self.pending: list[NamespaceDataPair] = []
|
|
1002
1069
|
self.parts: dict[str, dict[str, str]] = defaultdict(dict)
|
|
1003
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)
|
|
1004
1073
|
self.api_endpoints = api_endpoints
|
|
1005
1074
|
base_namespace = SpecNamespace(name=base_namespace_name)
|
|
1006
1075
|
for base_type in BaseTypeName:
|
|
@@ -1185,5 +1254,53 @@ class SpecBuilder:
|
|
|
1185
1254
|
def add_prepart_file(self, target: str, name: str, data: str) -> None:
|
|
1186
1255
|
self.preparts[target][name] = data
|
|
1187
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
|
+
|
|
1188
1305
|
def resolve_proper_name(self, stype: SpecTypeDefn) -> str:
|
|
1189
1306
|
return f"{'.'.join(stype.namespace.path)}.{stype.name}"
|
pkgs/type_spec/config.py
CHANGED
|
@@ -56,6 +56,7 @@ class PythonConfig(BaseLanguageConfig):
|
|
|
56
56
|
emit_api_argument_lookup: bool = (
|
|
57
57
|
False # emit a lookup for api endpoint path to argument type.
|
|
58
58
|
)
|
|
59
|
+
emit_async_batch_processor: bool = False # emit the async batch wrapping functions
|
|
59
60
|
emit_client_class: bool = False # emit the base class for the api client
|
|
60
61
|
all_named_type_exports: bool = False # emit __all__ for all named type exports
|
|
61
62
|
sdk_endpoints_only: bool = False # only emit is_sdk endpoints
|
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,15 +105,23 @@ 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
|
|
|
99
119
|
for namespace in sorted(builder.namespaces.values(), key=lambda ns: ns.name):
|
|
100
120
|
ctx = EmitOpenAPIContext(namespace=namespace)
|
|
101
121
|
|
|
122
|
+
if ctx.namespace.endpoint is not None and ctx.namespace.endpoint.is_beta:
|
|
123
|
+
continue
|
|
124
|
+
|
|
102
125
|
if ctx.namespace.name == "base":
|
|
103
126
|
# TODO: add additional base defintions here
|
|
104
127
|
ctx.types["ObjectId"] = OpenAPIIntegerT()
|
|
@@ -109,6 +132,8 @@ def emit_open_api(builder: builder.SpecBuilder, *, config: OpenAPIConfig) -> Non
|
|
|
109
132
|
ctx,
|
|
110
133
|
namespace=namespace,
|
|
111
134
|
config=config,
|
|
135
|
+
examples=builder.examples,
|
|
136
|
+
guides=builder.guides,
|
|
112
137
|
)
|
|
113
138
|
|
|
114
139
|
_rewrite_with_notice(
|
|
@@ -141,15 +166,144 @@ def _serialize_global_context(ctx: EmitOpenAPIGlobalContext) -> str:
|
|
|
141
166
|
return yaml.dump(oa_root, sort_keys=False)
|
|
142
167
|
|
|
143
168
|
|
|
144
|
-
def
|
|
169
|
+
def _is_empty_object_type(typ: OpenAPIType) -> bool:
|
|
145
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
|
+
):
|
|
146
230
|
return {}
|
|
147
231
|
|
|
148
232
|
return {
|
|
149
233
|
"parameters": [
|
|
150
|
-
{
|
|
151
|
-
|
|
234
|
+
{
|
|
235
|
+
"name": "data",
|
|
236
|
+
"required": True,
|
|
237
|
+
"in": "query",
|
|
238
|
+
"content": {
|
|
239
|
+
"application/json": {
|
|
240
|
+
"schema": {"$ref": "#/components/schema/Arguments"}
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
}
|
|
152
244
|
]
|
|
245
|
+
} | _emit_endpoint_parameter_examples(examples)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _emit_is_beta(is_beta: bool) -> DictApiSchema:
|
|
249
|
+
if is_beta:
|
|
250
|
+
return {"x-beta": True}
|
|
251
|
+
return {}
|
|
252
|
+
|
|
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}"
|
|
153
307
|
}
|
|
154
308
|
|
|
155
309
|
|
|
@@ -159,27 +313,36 @@ def _emit_namespace(
|
|
|
159
313
|
namespace: builder.SpecNamespace,
|
|
160
314
|
*,
|
|
161
315
|
config: OpenAPIConfig,
|
|
316
|
+
examples: dict[str, list[builder.SpecEndpointExample]],
|
|
317
|
+
guides: dict[builder.SpecGuideKey, list[builder.SpecGuide]],
|
|
162
318
|
) -> None:
|
|
163
319
|
for stype in namespace.types.values():
|
|
164
320
|
_emit_type(ctx, stype, config=config)
|
|
165
321
|
|
|
166
322
|
if namespace.endpoint is not None:
|
|
167
|
-
|
|
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
|
+
)
|
|
168
330
|
|
|
169
331
|
oa_components: dict[str, object] = dict()
|
|
170
332
|
|
|
171
333
|
if ctx.endpoint is not None:
|
|
172
334
|
endpoint = ctx.endpoint
|
|
173
|
-
|
|
174
335
|
argument_type = ctx.types.get("Arguments")
|
|
175
336
|
oa_endpoint = dict()
|
|
176
337
|
oa_endpoint[endpoint.method] = (
|
|
177
338
|
{
|
|
178
339
|
"tags": endpoint.tags,
|
|
179
340
|
"summary": endpoint.summary,
|
|
180
|
-
"description": endpoint.description,
|
|
181
341
|
}
|
|
182
|
-
|
|
|
342
|
+
| _emit_endpoint_description(endpoint.description, ctx.endpoint.guides)
|
|
343
|
+
| _emit_is_beta(endpoint.is_beta)
|
|
344
|
+
| _emit_endpoint_parameters(endpoint, argument_type, ctx.endpoint.examples)
|
|
345
|
+
| _emit_endpoint_request_body(endpoint, argument_type, ctx.endpoint.examples)
|
|
183
346
|
| {
|
|
184
347
|
"responses": {
|
|
185
348
|
"200": {
|
|
@@ -188,6 +351,7 @@ def _emit_namespace(
|
|
|
188
351
|
"application/json": {
|
|
189
352
|
"schema": {"$ref": "#/components/schema/Data"}
|
|
190
353
|
}
|
|
354
|
+
| _emit_endpoint_response_examples(ctx.endpoint.examples)
|
|
191
355
|
},
|
|
192
356
|
}
|
|
193
357
|
},
|
|
@@ -226,10 +390,7 @@ def _emit_namespace(
|
|
|
226
390
|
|
|
227
391
|
oa_components["schema"] = cast(
|
|
228
392
|
object,
|
|
229
|
-
{
|
|
230
|
-
name: (value.asdict() if name != "Arguments" else value.asarguments())
|
|
231
|
-
for name, value in types.items()
|
|
232
|
-
},
|
|
393
|
+
{name: value.asdict() for name, value in types.items()},
|
|
233
394
|
)
|
|
234
395
|
|
|
235
396
|
path = f"{config.types_output}/common/{'/'.join(namespace.path)}.yaml"
|
|
@@ -338,6 +499,8 @@ def _emit_endpoint(
|
|
|
338
499
|
ctx: EmitOpenAPIContext,
|
|
339
500
|
namespace: builder.SpecNamespace,
|
|
340
501
|
endpoint: builder.SpecEndpoint,
|
|
502
|
+
endpoint_examples: list[builder.SpecEndpointExample],
|
|
503
|
+
endpoint_guides: list[builder.SpecGuide],
|
|
341
504
|
) -> None:
|
|
342
505
|
assert namespace.endpoint is not None
|
|
343
506
|
assert namespace.path[0] == "api"
|
|
@@ -386,6 +549,24 @@ def _emit_endpoint(
|
|
|
386
549
|
tags=[tag_name],
|
|
387
550
|
summary=f"{'/'.join(namespace.path[path_cutoff:])}",
|
|
388
551
|
description=description,
|
|
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
|
+
],
|
|
389
570
|
)
|
|
390
571
|
|
|
391
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,12 +62,24 @@ 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
|
|
62
77
|
tags: list[str]
|
|
63
78
|
summary: str
|
|
64
79
|
description: str
|
|
80
|
+
is_beta: bool
|
|
81
|
+
examples: list[EmitOpenAPIEndpointExample]
|
|
82
|
+
guides: list[EmitOpenAPIGuide]
|
|
65
83
|
|
|
66
84
|
|
|
67
85
|
@dataclass
|