geniespec 0.1.1.dev2__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.
- geniespec/__init__.py +0 -0
- geniespec/api_schema.yaml +318 -0
- geniespec/cli.py +85 -0
- geniespec/genie.py +505 -0
- geniespec/languages.py +408 -0
- geniespec/renderable_types.py +246 -0
- geniespec/templates/python/pyproject.toml.j2 +8 -0
- geniespec/templates/python/src/{service}/__init__.py.j2 +1 -0
- geniespec/templates/python/src/{service}/py.typed +0 -0
- geniespec/templates/python/src/{service}/v{version}/__init__.py.j2 +1 -0
- geniespec/templates/python/src/{service}/v{version}/api.py.j2 +68 -0
- geniespec/templates/python/src/{service}/v{version}/endpoints.py.j2 +38 -0
- geniespec/templates/python/src/{service}/v{version}/exceptions.py.j2 +68 -0
- geniespec/templates/python/src/{service}/v{version}/internal.py.j2 +30 -0
- geniespec/templates/python/src/{service}/v{version}/resources.py.j2 +22 -0
- geniespec/types.py +147 -0
- geniespec-0.1.1.dev2.dist-info/METADATA +77 -0
- geniespec-0.1.1.dev2.dist-info/RECORD +20 -0
- geniespec-0.1.1.dev2.dist-info/WHEEL +4 -0
- geniespec-0.1.1.dev2.dist-info/entry_points.txt +3 -0
geniespec/languages.py
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import collections
|
|
2
|
+
import keyword
|
|
3
|
+
|
|
4
|
+
from core.exceptions import KibaException
|
|
5
|
+
|
|
6
|
+
from geniespec.renderable_types import ActionEndpointComponent
|
|
7
|
+
from geniespec.renderable_types import AttributeEndpointComponent
|
|
8
|
+
from geniespec.renderable_types import MappingRenderableType
|
|
9
|
+
from geniespec.renderable_types import RenderableApi
|
|
10
|
+
from geniespec.renderable_types import RenderableEndpointComponent
|
|
11
|
+
from geniespec.renderable_types import RenderableException
|
|
12
|
+
from geniespec.renderable_types import RenderableOperation
|
|
13
|
+
from geniespec.renderable_types import RenderableParameter
|
|
14
|
+
from geniespec.renderable_types import RenderableResource
|
|
15
|
+
from geniespec.renderable_types import RenderableString
|
|
16
|
+
from geniespec.renderable_types import RenderableType
|
|
17
|
+
from geniespec.renderable_types import ResourceEndpointComponent
|
|
18
|
+
from geniespec.renderable_types import SingleResourceEndpointComponent
|
|
19
|
+
from geniespec.types import ComplexTypes
|
|
20
|
+
from geniespec.types import FieldSources
|
|
21
|
+
from geniespec.types import PrimitiveTypes
|
|
22
|
+
from geniespec.types import ResponseTypes
|
|
23
|
+
from geniespec.types import Version
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BaseLanguageDefinition:
|
|
27
|
+
FILE_EXTENSIONS: list[str]
|
|
28
|
+
AUTOGENERATED_NOTICE: str
|
|
29
|
+
|
|
30
|
+
def boolean_type(self, isNullable: bool) -> str:
|
|
31
|
+
raise NotImplementedError
|
|
32
|
+
|
|
33
|
+
def date_type(self, isNullable: bool) -> str:
|
|
34
|
+
raise NotImplementedError
|
|
35
|
+
|
|
36
|
+
def datetime_type(self, isNullable: bool) -> str:
|
|
37
|
+
raise NotImplementedError
|
|
38
|
+
|
|
39
|
+
def double_type(self, isNullable: bool) -> str:
|
|
40
|
+
raise NotImplementedError
|
|
41
|
+
|
|
42
|
+
def float_type(self, isNullable: bool) -> str:
|
|
43
|
+
raise NotImplementedError
|
|
44
|
+
|
|
45
|
+
def integer_type(self, isNullable: bool) -> str:
|
|
46
|
+
raise NotImplementedError
|
|
47
|
+
|
|
48
|
+
def mapping_type(self, isNullable: bool, keyType: str, valueType: str) -> str:
|
|
49
|
+
raise NotImplementedError
|
|
50
|
+
|
|
51
|
+
def json_type(self, isNullable: bool) -> str:
|
|
52
|
+
raise NotImplementedError
|
|
53
|
+
|
|
54
|
+
def list_type(self, isNullable: bool, listItemType: str) -> str:
|
|
55
|
+
raise NotImplementedError
|
|
56
|
+
|
|
57
|
+
def struct_type(self, isNullable: bool) -> str:
|
|
58
|
+
raise NotImplementedError
|
|
59
|
+
|
|
60
|
+
def long_integer_type(self, isNullable: bool) -> str:
|
|
61
|
+
raise NotImplementedError
|
|
62
|
+
|
|
63
|
+
def object_type(self, isNullable: bool, className: RenderableString, apiVersion: Version, isLocalReference: bool) -> str:
|
|
64
|
+
raise NotImplementedError
|
|
65
|
+
|
|
66
|
+
def one_of_type(self, isNullable: bool, subTypes: list[str]) -> str:
|
|
67
|
+
raise NotImplementedError
|
|
68
|
+
|
|
69
|
+
def string_type(self, isNullable: bool) -> str:
|
|
70
|
+
raise NotImplementedError
|
|
71
|
+
|
|
72
|
+
def enum_type(self, isNullable: bool, values: list[str]) -> str:
|
|
73
|
+
raise NotImplementedError
|
|
74
|
+
|
|
75
|
+
def package_name(self, api: RenderableApi) -> str:
|
|
76
|
+
raise NotImplementedError
|
|
77
|
+
|
|
78
|
+
def distribution_name(self, api: RenderableApi) -> str:
|
|
79
|
+
raise NotImplementedError
|
|
80
|
+
|
|
81
|
+
def validation_errors(self, api: RenderableApi) -> list[str]:
|
|
82
|
+
raise NotImplementedError
|
|
83
|
+
|
|
84
|
+
def _render_one_of_type_string(self, oneOfTypeString: str, isNullable: bool, apiVersion: Version) -> list[str]:
|
|
85
|
+
oneOfTypes = ''.join(oneOfTypeString.replace('OneOf[', '', 1).rsplit(']', 1)).split(', ')
|
|
86
|
+
return [self.render_type(renderableType=RenderableType(rawType=oneOfType, isNullable=isNullable), apiVersion=apiVersion) for oneOfType in oneOfTypes]
|
|
87
|
+
|
|
88
|
+
def _render_list_type_string(self, listTypeString: str, apiVersion: Version) -> str:
|
|
89
|
+
listItemType = ''.join(listTypeString.replace('List[', '', 1).rsplit(']', 1))
|
|
90
|
+
return self.render_type(renderableType=RenderableType(rawType=listItemType, isNullable=False), apiVersion=apiVersion)
|
|
91
|
+
|
|
92
|
+
def render_type(self, renderableType: RenderableType, apiVersion: Version, isLocalReference: bool = False) -> str:
|
|
93
|
+
yamlType = renderableType.rawType
|
|
94
|
+
isNullable = renderableType.isNullable
|
|
95
|
+
if renderableType.isList:
|
|
96
|
+
return self.list_type(isNullable=isNullable, listItemType=self.render_type(renderableType=renderableType.model_copy(update={'isList': False, 'isNullable': False}), apiVersion=apiVersion, isLocalReference=isLocalReference))
|
|
97
|
+
if yamlType == PrimitiveTypes.LONG_INTEGER:
|
|
98
|
+
return self.long_integer_type(isNullable=isNullable)
|
|
99
|
+
if yamlType == PrimitiveTypes.INTEGER:
|
|
100
|
+
return self.integer_type(isNullable=isNullable)
|
|
101
|
+
if yamlType == PrimitiveTypes.FLOAT:
|
|
102
|
+
return self.float_type(isNullable=isNullable)
|
|
103
|
+
if yamlType == PrimitiveTypes.DOUBLE:
|
|
104
|
+
return self.double_type(isNullable=isNullable)
|
|
105
|
+
if yamlType == PrimitiveTypes.STRING:
|
|
106
|
+
return self.string_type(isNullable=isNullable)
|
|
107
|
+
if yamlType == PrimitiveTypes.BOOLEAN:
|
|
108
|
+
return self.boolean_type(isNullable=isNullable)
|
|
109
|
+
if yamlType == PrimitiveTypes.DATETIME:
|
|
110
|
+
return self.datetime_type(isNullable=isNullable)
|
|
111
|
+
if yamlType == PrimitiveTypes.DATE:
|
|
112
|
+
return self.date_type(isNullable=isNullable)
|
|
113
|
+
if yamlType == PrimitiveTypes.JSON:
|
|
114
|
+
return self.json_type(isNullable=isNullable)
|
|
115
|
+
# if yamlType == PrimitiveTypes.STRUCT:
|
|
116
|
+
# return self.struct_type(isNullable=isNullable)
|
|
117
|
+
# if yamlType.startswith(PrimitiveTypes.ENUM):
|
|
118
|
+
# values = ''.join(yamlType.replace('Enum[', '', 1).rsplit(']', 1)).split(', ')
|
|
119
|
+
# return self.enum_type(isNullable=isNullable, values=[RenderableString.from_kebab_case_string(string=value) for value in values])
|
|
120
|
+
# if yamlType.startswith(PrimitiveTypes.ONE_OF):
|
|
121
|
+
# subTypes = self._render_one_of_type_string(oneOfTypeString=yamlType, isNullable=isNullable, apiVersion=apiVersion)
|
|
122
|
+
# return self.one_of_type(isNullable=isNullable, subTypes=subTypes)
|
|
123
|
+
if yamlType == ComplexTypes.MAPPING and isinstance(renderableType, MappingRenderableType):
|
|
124
|
+
keyType = self.render_type(renderableType=renderableType.keyType, apiVersion=apiVersion, isLocalReference=isLocalReference)
|
|
125
|
+
valueType = self.render_type(renderableType=renderableType.valueType, apiVersion=apiVersion, isLocalReference=isLocalReference)
|
|
126
|
+
return self.mapping_type(isNullable=isNullable, keyType=keyType, valueType=valueType)
|
|
127
|
+
if yamlType == 'class' and isinstance(renderableType, RenderableType) and renderableType.className is not None:
|
|
128
|
+
return self.object_type(isNullable=isNullable, className=renderableType.className, apiVersion=apiVersion, isLocalReference=isLocalReference)
|
|
129
|
+
raise KibaException(f'Failed to render: {type(renderableType)} {renderableType}')
|
|
130
|
+
|
|
131
|
+
def render_parameter(self, renderableParameter: RenderableParameter, apiVersion: Version) -> str:
|
|
132
|
+
isNullable = renderableParameter.fieldType.isNullable
|
|
133
|
+
if renderableParameter.defaultValue is not None:
|
|
134
|
+
isNullable = False
|
|
135
|
+
return self.render_type(renderableType=renderableParameter.fieldType.model_copy(update={'isNullable': isNullable}), apiVersion=apiVersion)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class PythonLanguageDefinition(BaseLanguageDefinition):
|
|
139
|
+
AUTOGENERATED_NOTICE = '# --- THIS FILE HAS BEEN AUTOGENERATED, DO NOT MODIFY MANUALLY ---'
|
|
140
|
+
|
|
141
|
+
def boolean_type(self, isNullable: bool) -> str:
|
|
142
|
+
return 'bool | None' if isNullable else 'bool'
|
|
143
|
+
|
|
144
|
+
def datetime_type(self, isNullable: bool) -> str:
|
|
145
|
+
return 'datetime.datetime | None' if isNullable else 'datetime.datetime'
|
|
146
|
+
|
|
147
|
+
def date_type(self, isNullable: bool) -> str:
|
|
148
|
+
return 'datetime.date | None' if isNullable else 'datetime.date'
|
|
149
|
+
|
|
150
|
+
def double_type(self, isNullable: bool) -> str:
|
|
151
|
+
return 'float | None' if isNullable else 'float'
|
|
152
|
+
|
|
153
|
+
def float_type(self, isNullable: bool) -> str:
|
|
154
|
+
return 'float | None' if isNullable else 'float'
|
|
155
|
+
|
|
156
|
+
def integer_type(self, isNullable: bool) -> str:
|
|
157
|
+
return 'int | None' if isNullable else 'int'
|
|
158
|
+
|
|
159
|
+
def mapping_type(self, isNullable: bool, keyType: str, valueType: str) -> str:
|
|
160
|
+
return f'collections.abc.Mapping[{keyType}, {valueType}] | None' if isNullable else f'collections.abc.Mapping[{keyType}, {valueType}]'
|
|
161
|
+
|
|
162
|
+
def json_type(self, isNullable: bool) -> str:
|
|
163
|
+
return 'pydantic.JsonValue | None' if isNullable else 'pydantic.JsonValue'
|
|
164
|
+
|
|
165
|
+
def list_type(self, isNullable: bool, listItemType: str) -> str:
|
|
166
|
+
return f'collections.abc.Sequence[{listItemType}] | None' if isNullable else f'collections.abc.Sequence[{listItemType}]'
|
|
167
|
+
|
|
168
|
+
def struct_type(self, isNullable: bool) -> str:
|
|
169
|
+
return 'typing.JsonMapping | None' if isNullable else 'JsonMapping'
|
|
170
|
+
|
|
171
|
+
def long_integer_type(self, isNullable: bool) -> str:
|
|
172
|
+
return 'int | None' if isNullable else 'int'
|
|
173
|
+
|
|
174
|
+
def object_type(self, isNullable: bool, className: RenderableString, apiVersion: Version, isLocalReference: bool) -> str:
|
|
175
|
+
typeName = f'{className.pascalCaseString}V{apiVersion.major}' if isLocalReference else f'resources.{className.pascalCaseString}V{apiVersion.major}'
|
|
176
|
+
return f'{typeName} | None' if isNullable else typeName
|
|
177
|
+
|
|
178
|
+
def one_of_type(self, isNullable: bool, subTypes: list[str]) -> str:
|
|
179
|
+
fieldTypes = ' | '.join(subTypes)
|
|
180
|
+
return f'{fieldTypes} | None' if isNullable else f'Union[{fieldTypes} | None'
|
|
181
|
+
|
|
182
|
+
def string_type(self, isNullable: bool) -> str:
|
|
183
|
+
return 'str | None' if isNullable else 'str'
|
|
184
|
+
|
|
185
|
+
def enum_type(self, isNullable: bool, values: list[str]) -> str:
|
|
186
|
+
valueString = ', '.join([f"'{value}'" for value in values])
|
|
187
|
+
return f'typing.Literal[{valueString}] | None' if isNullable else f'Literal[{valueString} | None'
|
|
188
|
+
|
|
189
|
+
@staticmethod
|
|
190
|
+
def _normalized_name(name: RenderableString) -> RenderableString:
|
|
191
|
+
if len(name.string) != 1 or name.string[0].isupper():
|
|
192
|
+
return name
|
|
193
|
+
return RenderableString.from_camel_case_string(string=name.string[0])
|
|
194
|
+
|
|
195
|
+
@classmethod
|
|
196
|
+
def identifier(cls, name: RenderableString) -> str:
|
|
197
|
+
normalizedName = cls._normalized_name(name=name)
|
|
198
|
+
if len(normalizedName.string) == 1 and normalizedName.string[0].isupper():
|
|
199
|
+
return normalizedName.string[0].lower()
|
|
200
|
+
return normalizedName.camelCaseString
|
|
201
|
+
|
|
202
|
+
@classmethod
|
|
203
|
+
def pascal_name(cls, name: RenderableString) -> str:
|
|
204
|
+
return cls._normalized_name(name=name).pascalCaseString
|
|
205
|
+
|
|
206
|
+
@classmethod
|
|
207
|
+
def snake_name(cls, name: RenderableString) -> str:
|
|
208
|
+
return cls._normalized_name(name=name).snakeCaseString
|
|
209
|
+
|
|
210
|
+
@classmethod
|
|
211
|
+
def kebab_name(cls, name: RenderableString) -> str:
|
|
212
|
+
return cls._normalized_name(name=name).kebabCaseString
|
|
213
|
+
|
|
214
|
+
def package_name(self, api: RenderableApi) -> str:
|
|
215
|
+
return f'{self.snake_name(name=api.serviceName)}_api'
|
|
216
|
+
|
|
217
|
+
def distribution_name(self, api: RenderableApi) -> str:
|
|
218
|
+
return f'{self.kebab_name(name=api.serviceName)}-api'
|
|
219
|
+
|
|
220
|
+
def internal_class_name(self, api: RenderableApi) -> str:
|
|
221
|
+
return f'{self.pascal_name(name=api.serviceName)}ApiV{api.version.major}Internal'
|
|
222
|
+
|
|
223
|
+
def resource_class_name(self, resource: RenderableResource, apiVersion: Version) -> str:
|
|
224
|
+
return f'{self.pascal_name(name=resource.singleName)}V{apiVersion.major}'
|
|
225
|
+
|
|
226
|
+
def exception_class_name(self, exception: RenderableException, apiVersion: Version) -> str:
|
|
227
|
+
return f'{self.pascal_name(name=exception.name)}V{apiVersion.major}'
|
|
228
|
+
|
|
229
|
+
def type(self, renderableType: RenderableType, apiVersion: Version, isLocalReference: bool = False) -> str:
|
|
230
|
+
return self.render_type(renderableType=renderableType, apiVersion=apiVersion, isLocalReference=isLocalReference)
|
|
231
|
+
|
|
232
|
+
def parameter_type(self, renderableParameter: RenderableParameter, apiVersion: Version) -> str:
|
|
233
|
+
return self.render_parameter(renderableParameter=renderableParameter, apiVersion=apiVersion)
|
|
234
|
+
|
|
235
|
+
@staticmethod
|
|
236
|
+
def route_auth(operation: RenderableOperation) -> str | None:
|
|
237
|
+
return operation.authentication if operation.authentication not in (None, 'none') else None
|
|
238
|
+
|
|
239
|
+
@classmethod
|
|
240
|
+
def requires_auth_resolver(cls, api: RenderableApi) -> bool:
|
|
241
|
+
return any(cls.route_auth(operation=operation) is not None for operation in api.operations)
|
|
242
|
+
|
|
243
|
+
def operation_name(self, operation: RenderableOperation) -> str:
|
|
244
|
+
return self.snake_name(name=self._operation_name(operation=operation))
|
|
245
|
+
|
|
246
|
+
def operation_class_name(self, operation: RenderableOperation) -> str:
|
|
247
|
+
return self.pascal_name(name=self._operation_name(operation=operation))
|
|
248
|
+
|
|
249
|
+
def http_method(self, operation: RenderableOperation) -> str:
|
|
250
|
+
return operation.method.method.upperCaseString
|
|
251
|
+
|
|
252
|
+
def is_undefined_parameter(self, operation: RenderableOperation, parameter: RenderableParameter) -> bool:
|
|
253
|
+
return self.http_method(operation=operation) == 'PATCH' and parameter.source == FieldSources.REQUEST and not parameter.isRequired and parameter.defaultValue is None
|
|
254
|
+
|
|
255
|
+
def requires_parameter_preprocessing(self, operation: RenderableOperation, parameter: RenderableParameter) -> bool:
|
|
256
|
+
return (parameter.defaultValue is not None and not self.is_undefined_parameter(operation=operation, parameter=parameter)) or parameter.minValue is not None or parameter.maxValue is not None
|
|
257
|
+
|
|
258
|
+
def requires_undefined(self, api: RenderableApi) -> bool:
|
|
259
|
+
return any(self.is_undefined_parameter(operation=operation, parameter=parameter) for operation in api.operations for parameter in operation.parameters)
|
|
260
|
+
|
|
261
|
+
def requires_optional_field_default(self, api: RenderableApi) -> bool:
|
|
262
|
+
return any(not self.is_undefined_parameter(operation=operation, parameter=parameter) and not parameter.isRequired and not parameter.fieldType.isNullable for operation in api.operations for parameter in self.request_parameters(operation=operation))
|
|
263
|
+
|
|
264
|
+
@staticmethod
|
|
265
|
+
def request_parameters(operation: RenderableOperation) -> list[RenderableParameter]:
|
|
266
|
+
return [parameter for parameter in operation.parameters if parameter.source == FieldSources.REQUEST]
|
|
267
|
+
|
|
268
|
+
@staticmethod
|
|
269
|
+
def is_streaming(operation: RenderableOperation) -> bool:
|
|
270
|
+
return operation.responseType == ResponseTypes.STREAM
|
|
271
|
+
|
|
272
|
+
@staticmethod
|
|
273
|
+
def is_redirect(operation: RenderableOperation) -> bool:
|
|
274
|
+
return operation.responseType == ResponseTypes.REDIRECT
|
|
275
|
+
|
|
276
|
+
def requires_streaming(self, api: RenderableApi) -> bool:
|
|
277
|
+
return any(self.is_streaming(operation=operation) for operation in api.operations)
|
|
278
|
+
|
|
279
|
+
def requires_redirect(self, api: RenderableApi) -> bool:
|
|
280
|
+
return any(self.is_redirect(operation=operation) for operation in api.operations)
|
|
281
|
+
|
|
282
|
+
def return_type(self, operation: RenderableOperation, apiVersion: Version) -> str:
|
|
283
|
+
if operation.responseType == ResponseTypes.REDIRECT:
|
|
284
|
+
return 'str'
|
|
285
|
+
if not operation.responseFields:
|
|
286
|
+
return 'None'
|
|
287
|
+
fieldType = self.type(renderableType=operation.responseFields[0].fieldType, apiVersion=apiVersion)
|
|
288
|
+
return f'collections.abc.AsyncIterator[{fieldType}]' if operation.responseType == ResponseTypes.STREAM else fieldType
|
|
289
|
+
|
|
290
|
+
def parameter_value(self, operation: RenderableOperation, parameter: RenderableParameter) -> str:
|
|
291
|
+
if parameter.source == FieldSources.QUERY_PARAMETERS:
|
|
292
|
+
return 'dict(request.query_params)'
|
|
293
|
+
parameterName = self.identifier(name=parameter.name)
|
|
294
|
+
return parameterName if self.requires_parameter_preprocessing(operation=operation, parameter=parameter) else f'request.data.{parameterName}'
|
|
295
|
+
|
|
296
|
+
@staticmethod
|
|
297
|
+
def _contains_json(renderableType: RenderableType) -> bool:
|
|
298
|
+
if isinstance(renderableType, MappingRenderableType):
|
|
299
|
+
return PythonLanguageDefinition._contains_json(renderableType=renderableType.keyType) or PythonLanguageDefinition._contains_json(renderableType=renderableType.valueType)
|
|
300
|
+
return renderableType.rawType == PrimitiveTypes.JSON
|
|
301
|
+
|
|
302
|
+
def resources_require_json(self, api: RenderableApi) -> bool:
|
|
303
|
+
return any(self._contains_json(renderableType=attribute.fieldType) for resource in api.resources for attribute in resource.attributes)
|
|
304
|
+
|
|
305
|
+
def operations_require_json(self, api: RenderableApi) -> bool:
|
|
306
|
+
fieldTypes = [parameter.fieldType for operation in api.operations for parameter in operation.parameters] + [field.fieldType for operation in api.operations for field in operation.responseFields or []]
|
|
307
|
+
return any(self._contains_json(renderableType=fieldType) for fieldType in fieldTypes)
|
|
308
|
+
|
|
309
|
+
def exceptions_require_json(self, api: RenderableApi) -> bool:
|
|
310
|
+
return any(self._contains_json(renderableType=field.fieldType) for exception in api.exceptions for field in exception.fields)
|
|
311
|
+
|
|
312
|
+
def endpoint(self, operation: RenderableOperation) -> str:
|
|
313
|
+
return '/' + '/'.join([segment for component in operation.endpoint.components for segment in self._endpoint_segments(component=component)])
|
|
314
|
+
|
|
315
|
+
def route_ordered_operations(self, api: RenderableApi) -> list[RenderableOperation]:
|
|
316
|
+
return sorted(api.operations, key=lambda operation: tuple(segment.startswith('{') for segment in self.endpoint(operation=operation).split('/')))
|
|
317
|
+
|
|
318
|
+
@staticmethod
|
|
319
|
+
def literal(value: str | float | bool) -> str:
|
|
320
|
+
return repr(value)
|
|
321
|
+
|
|
322
|
+
def _endpoint_segments(self, component: RenderableEndpointComponent) -> list[str]:
|
|
323
|
+
if isinstance(component, AttributeEndpointComponent):
|
|
324
|
+
return [self.kebab_name(name=component.attribute.name)] if component.attribute is not None else ['attributes']
|
|
325
|
+
if isinstance(component, ActionEndpointComponent):
|
|
326
|
+
return [self.kebab_name(name=component.actionName)] if component.actionName is not None else ['actions']
|
|
327
|
+
if isinstance(component, SingleResourceEndpointComponent):
|
|
328
|
+
return [self.kebab_name(name=component.relativeSingleName)]
|
|
329
|
+
if isinstance(component, ResourceEndpointComponent):
|
|
330
|
+
output = [self.kebab_name(name=component.relativeCollectionName)]
|
|
331
|
+
if component.attribute is None:
|
|
332
|
+
return output
|
|
333
|
+
if component.attribute.isUnique and not component.attribute.isPrimary:
|
|
334
|
+
output.append(self.kebab_name(name=component.attribute.name))
|
|
335
|
+
parameterName = self.identifier(name=component.attribute.name)
|
|
336
|
+
if component.attribute.fieldType.rawType in (PrimitiveTypes.INTEGER, PrimitiveTypes.LONG_INTEGER):
|
|
337
|
+
output.append(f'{{{parameterName}:int}}')
|
|
338
|
+
elif component.attribute.fieldType.rawType == PrimitiveTypes.STRING:
|
|
339
|
+
output.append(f'{{{parameterName}:str}}')
|
|
340
|
+
else:
|
|
341
|
+
raise KibaException(message=f'ResourceEndpointComponent created with an invalid attribute type of {component.attribute.fieldType}. Valid types are: integer, long integer and string.')
|
|
342
|
+
return output
|
|
343
|
+
raise KibaException(message=f'Unsupported endpoint component: {type(component)}')
|
|
344
|
+
|
|
345
|
+
def _operation_name(self, operation: RenderableOperation) -> RenderableString:
|
|
346
|
+
methodName = RenderableString(string=[])
|
|
347
|
+
if self.http_method(operation=operation) == 'POST':
|
|
348
|
+
if isinstance(operation.endpoint.lastComponent, ActionEndpointComponent) and operation.endpoint.lastComponent.actionName is not None:
|
|
349
|
+
methodName += operation.endpoint.lastComponent.actionName
|
|
350
|
+
else:
|
|
351
|
+
methodName += RenderableString(string=['create'])
|
|
352
|
+
elif self.http_method(operation=operation) == 'GET':
|
|
353
|
+
lastIsResourceCollection = isinstance(operation.endpoint.lastComponent, ResourceEndpointComponent) and operation.endpoint.lastComponent.attribute is None
|
|
354
|
+
lastIsAttributeList = isinstance(operation.endpoint.lastComponent, AttributeEndpointComponent) and operation.endpoint.lastComponent.attribute is None
|
|
355
|
+
lastIsActionList = isinstance(operation.endpoint.lastComponent, ActionEndpointComponent) and operation.endpoint.lastComponent.actionName is None
|
|
356
|
+
methodName += RenderableString(string=['list'] if lastIsResourceCollection or lastIsAttributeList or lastIsActionList else ['get'])
|
|
357
|
+
if isinstance(operation.endpoint.lastComponent, ActionEndpointComponent) and operation.endpoint.lastComponent.actionName is not None:
|
|
358
|
+
methodName += operation.endpoint.lastComponent.actionName
|
|
359
|
+
elif self.http_method(operation=operation) == 'DELETE':
|
|
360
|
+
methodName += RenderableString(string=['delete'])
|
|
361
|
+
elif self.http_method(operation=operation) == 'PATCH':
|
|
362
|
+
methodName += RenderableString(string=['update'])
|
|
363
|
+
resourceComponents = [component for component in operation.endpoint.components if isinstance(component, (ResourceEndpointComponent, SingleResourceEndpointComponent))]
|
|
364
|
+
if not resourceComponents:
|
|
365
|
+
return methodName
|
|
366
|
+
target = resourceComponents[-1]
|
|
367
|
+
parent = resourceComponents[-2] if len(resourceComponents) > 1 else None
|
|
368
|
+
lastComponent = operation.endpoint.lastComponent
|
|
369
|
+
returnsSingleTarget = any(field.fieldType.className == target.resource.singleName and not field.fieldType.isList for field in operation.responseFields or [])
|
|
370
|
+
isTargetItem = isinstance(target, SingleResourceEndpointComponent) or target.attribute is not None
|
|
371
|
+
isSingle = isTargetItem or returnsSingleTarget or (target is lastComponent and 'create' in methodName.string)
|
|
372
|
+
if operation.isIndirectAction:
|
|
373
|
+
singleName, collectionName = target.resource.singleName, target.resource.collectionName
|
|
374
|
+
elif isinstance(target, SingleResourceEndpointComponent):
|
|
375
|
+
singleName = collectionName = target.relativeSingleName
|
|
376
|
+
else:
|
|
377
|
+
singleName, collectionName = target.relativeSingleName, target.relativeCollectionName
|
|
378
|
+
targetName = singleName if isSingle else collectionName
|
|
379
|
+
if isinstance(target, ResourceEndpointComponent) and target.attribute is not None and target.attribute.isUnique and not target.attribute.isPrimary:
|
|
380
|
+
targetName += RenderableString(string=['by']) + target.attribute.name
|
|
381
|
+
if operation.isIndirectAction:
|
|
382
|
+
targetName = RenderableString(string=['for']) + targetName
|
|
383
|
+
elif parent is not None:
|
|
384
|
+
targetName += RenderableString(string=['for']) + parent.resource.singleName
|
|
385
|
+
if isinstance(lastComponent, AttributeEndpointComponent):
|
|
386
|
+
attributeName = lastComponent.attribute.name if lastComponent.attribute is not None else RenderableString(string=['attributes'])
|
|
387
|
+
return methodName + attributeName + RenderableString(string=['for']) + targetName
|
|
388
|
+
if isinstance(lastComponent, ActionEndpointComponent) and lastComponent.actionName is None:
|
|
389
|
+
return methodName + RenderableString(string=['actions', 'for']) + targetName
|
|
390
|
+
return methodName + targetName
|
|
391
|
+
|
|
392
|
+
def validation_errors(self, api: RenderableApi) -> list[str]:
|
|
393
|
+
routes = [f'{self.http_method(operation=operation)} {self.endpoint(operation=operation)}' for operation in api.operations]
|
|
394
|
+
errors = [f'Duplicate route: {route}' for route, count in collections.Counter(routes).items() if count > 1]
|
|
395
|
+
routesByOperationName: dict[str, list[str]] = collections.defaultdict(list)
|
|
396
|
+
for operation, route in zip(api.operations, routes, strict=True):
|
|
397
|
+
routesByOperationName[self.operation_name(operation=operation)].append(route)
|
|
398
|
+
errors += [f'Duplicate operation name {operationName}: {", ".join(operationRoutes)}' for operationName, operationRoutes in routesByOperationName.items() if len(operationRoutes) > 1]
|
|
399
|
+
errors += [
|
|
400
|
+
f'Duplicate parameter {name} on {route}' for operation, route in zip(api.operations, routes, strict=True) for name, count in collections.Counter(self.identifier(name=parameter.name) for parameter in operation.parameters).items() if count > 1
|
|
401
|
+
]
|
|
402
|
+
namesWithContext = [(operationName, f'operation {", ".join(operationRoutes)}') for operationName, operationRoutes in routesByOperationName.items()]
|
|
403
|
+
namesWithContext += [(self.identifier(name=parameter.name), f'parameter of {route}') for operation, route in zip(api.operations, routes, strict=True) for parameter in operation.parameters]
|
|
404
|
+
namesWithContext += [(self.identifier(name=field.name), f'response field of {route}') for operation, route in zip(api.operations, routes, strict=True) for field in operation.responseFields or []]
|
|
405
|
+
namesWithContext += [(self.identifier(name=attribute.name), f'attribute of {resource.singleName.pascalCaseString}') for resource in api.resources for attribute in [*resource.attributes, *resource.independentAttributes]]
|
|
406
|
+
namesWithContext += [(self.identifier(name=field.name), f'field of {exception.name.pascalCaseString}') for exception in api.exceptions for field in exception.fields]
|
|
407
|
+
errors += [f'"{name}" is not a valid Python name ({context})' for name, context in namesWithContext if not name.isidentifier() or keyword.iskeyword(name)]
|
|
408
|
+
return errors
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from core.exceptions import KibaException
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from geniespec.types import ComplexTypes
|
|
9
|
+
from geniespec.types import FieldSources
|
|
10
|
+
from geniespec.types import ResponseTypes
|
|
11
|
+
from geniespec.types import Version
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RenderableString(BaseModel):
|
|
15
|
+
string: list[str]
|
|
16
|
+
|
|
17
|
+
def __add__(self, other: RenderableString) -> RenderableString:
|
|
18
|
+
return RenderableString(string=self.string + other.string)
|
|
19
|
+
|
|
20
|
+
def __eq__(self, other: object) -> bool:
|
|
21
|
+
return isinstance(other, RenderableString) and self.lowerCaseString == other.lowerCaseString
|
|
22
|
+
|
|
23
|
+
def __hash__(self) -> int:
|
|
24
|
+
return hash(frozenset(self.string))
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def lowerCaseString(self) -> str: # noqa: N802
|
|
28
|
+
return ''.join([s.lower() for s in self.string])
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def upperCaseString(self) -> str: # noqa: N802
|
|
32
|
+
return ''.join([s.upper() for s in self.string])
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def pascalCaseString(self) -> str: # noqa: N802
|
|
36
|
+
return ''.join([s.lower().capitalize() for s in self.string])
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def camelCaseString(self) -> str: # noqa: N802
|
|
40
|
+
return self.string[0].lower() + ''.join([s.lower().capitalize() for s in self.string[1:]])
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def snakeCaseString(self) -> str: # noqa: N802
|
|
44
|
+
return '_'.join([s.lower() for s in self.string])
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def kebabCaseString(self) -> str: # noqa: N802
|
|
48
|
+
return '-'.join([s.lower() for s in self.string])
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def capitalizedKebabCaseString(self) -> str: # noqa: N802
|
|
52
|
+
return '-'.join([s.capitalize() for s in self.string])
|
|
53
|
+
|
|
54
|
+
def join_string(self, separator: str) -> str:
|
|
55
|
+
return separator.join(self.string)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def endpointUrlString(self) -> str: # noqa: N802
|
|
59
|
+
return '/' + '/'.join([s.lower() for s in self.string])
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def from_pascal_case_string(cls, string: str) -> RenderableString:
|
|
63
|
+
return cls(string=re.sub('([A-Z][a-z]+)', r' \1', re.sub('([A-Z0-9])', r' \1', string)).split())
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def from_camel_case_string(cls, string: str) -> RenderableString:
|
|
67
|
+
return cls(string=re.sub('([a-z][A-Z]+)', r' \1', re.sub('([A-Z0-9])', r' \1', string)).split())
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def from_snake_case_string(cls, string: str) -> RenderableString:
|
|
71
|
+
return cls.from_separated_string(string=string, separator='_')
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def from_kebab_case_string(cls, string: str) -> RenderableString:
|
|
75
|
+
return cls.from_separated_string(string=string, separator='-')
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def from_upper_case_string(cls, string: str) -> RenderableString:
|
|
79
|
+
return cls(string=[string])
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def from_lower_case_string(cls, string: str) -> RenderableString:
|
|
83
|
+
return cls(string=[string])
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def from_separated_string(cls, string: str, separator: str) -> RenderableString:
|
|
87
|
+
return cls(string=string.split(separator))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class RenderableType(BaseModel):
|
|
91
|
+
rawType: str
|
|
92
|
+
isNullable: bool
|
|
93
|
+
isList: bool = False
|
|
94
|
+
className: RenderableString | None = None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class MappingRenderableType(RenderableType):
|
|
98
|
+
keyType: RenderableType
|
|
99
|
+
valueType: RenderableType
|
|
100
|
+
rawType: str = ComplexTypes.MAPPING
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class RenderableField(BaseModel):
|
|
104
|
+
name: RenderableString
|
|
105
|
+
fieldType: RenderableType
|
|
106
|
+
defaultValue: str | int | float | bool | None = None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class RenderableAttribute(RenderableField):
|
|
110
|
+
isUnique: bool = False
|
|
111
|
+
isPrimary: bool = False
|
|
112
|
+
isUpdatable: bool = False
|
|
113
|
+
isNullable: bool = False
|
|
114
|
+
isSettable: bool = False
|
|
115
|
+
isAccessibleIndependently: bool = False
|
|
116
|
+
shouldDisableGetEndpoint: bool = False
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class RenderableResource(BaseModel):
|
|
120
|
+
singleName: RenderableString
|
|
121
|
+
collectionName: RenderableString
|
|
122
|
+
parentResource: str | None = None
|
|
123
|
+
attributes: list[RenderableAttribute]
|
|
124
|
+
independentAttributes: list[RenderableAttribute] = []
|
|
125
|
+
primaryAttribute: RenderableAttribute | None = None
|
|
126
|
+
accessibleFrom: list[str] = []
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class RenderableMethod(BaseModel):
|
|
130
|
+
method: RenderableString
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class RenderableEndpointComponent(BaseModel):
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class AttributeEndpointComponent(RenderableEndpointComponent):
|
|
138
|
+
attribute: RenderableAttribute | None
|
|
139
|
+
resource: RenderableResource | None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class ActionEndpointComponent(RenderableEndpointComponent):
|
|
143
|
+
actionName: RenderableString | None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class ResourceEndpointComponent(RenderableEndpointComponent):
|
|
147
|
+
resource: RenderableResource
|
|
148
|
+
attribute: RenderableAttribute | None
|
|
149
|
+
relativeSingleName: RenderableString
|
|
150
|
+
relativeCollectionName: RenderableString
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class SingleResourceEndpointComponent(RenderableEndpointComponent):
|
|
154
|
+
resource: RenderableResource
|
|
155
|
+
relativeSingleName: RenderableString
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class RenderableEndpoint(BaseModel):
|
|
159
|
+
components: list[RenderableEndpointComponent]
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def lastComponent(self) -> RenderableEndpointComponent: # noqa: N802
|
|
163
|
+
if len(self.components) == 0:
|
|
164
|
+
raise KibaException(message=f'RenderableEndpoint with no components found')
|
|
165
|
+
return self.components[-1]
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def urlParameters(self) -> list[RenderableParameter]: # noqa: N802
|
|
169
|
+
return [
|
|
170
|
+
RenderableParameter(name=component.attribute.name, fieldType=component.attribute.fieldType, isRequired=True, isDeprecated=False)
|
|
171
|
+
for component in self.components
|
|
172
|
+
if isinstance(component, ResourceEndpointComponent) and component.attribute is not None
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class RenderableParameter(RenderableField):
|
|
177
|
+
maxValue: float | int | bool | str | None = None
|
|
178
|
+
minValue: float | int | bool | str | None = None
|
|
179
|
+
isRequired: bool = False
|
|
180
|
+
isDeprecated: bool = False
|
|
181
|
+
source: str = FieldSources.REQUEST
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class RenderableOperation(BaseModel):
|
|
185
|
+
method: RenderableMethod
|
|
186
|
+
endpoint: RenderableEndpoint
|
|
187
|
+
query: list[RenderableParameter] | None
|
|
188
|
+
body: list[RenderableParameter] | None
|
|
189
|
+
responseFields: list[RenderableField] | None
|
|
190
|
+
authentication: str | None
|
|
191
|
+
isIndirectAction: bool = False
|
|
192
|
+
responseType: str = ResponseTypes.JSON
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def urlParameters(self) -> list[RenderableParameter]: # noqa: N802
|
|
196
|
+
return self.endpoint.urlParameters
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def parameters(self) -> list[RenderableParameter]:
|
|
200
|
+
deduplicatedParameters: list[RenderableParameter] = []
|
|
201
|
+
parameterList = self.urlParameters + (self.body if self.body is not None else []) + (self.query if self.query is not None else [])
|
|
202
|
+
for parameter in parameterList:
|
|
203
|
+
if parameter not in deduplicatedParameters:
|
|
204
|
+
deduplicatedParameters.append(parameter)
|
|
205
|
+
deduplicatedParameters.sort(key=lambda deduplicatedParameter: (deduplicatedParameter.isRequired, deduplicatedParameter.defaultValue is None), reverse=True)
|
|
206
|
+
return deduplicatedParameters
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class ExceptionRenderableField(RenderableField):
|
|
210
|
+
isRequired: bool = False
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class RenderableException(BaseModel):
|
|
214
|
+
name: RenderableString
|
|
215
|
+
message: str | None
|
|
216
|
+
statusCode: int | None
|
|
217
|
+
fields: list[ExceptionRenderableField]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class RenderableApi(BaseModel):
|
|
221
|
+
version: Version
|
|
222
|
+
serviceName: RenderableString
|
|
223
|
+
pythonClientPackageName: str
|
|
224
|
+
typescriptClientPackageName: str
|
|
225
|
+
resources: list[RenderableResource]
|
|
226
|
+
operations: list[RenderableOperation]
|
|
227
|
+
exceptions: list[RenderableException]
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class ProcessableResource(BaseModel):
|
|
231
|
+
class IndependentAttributeOccurrence(BaseModel):
|
|
232
|
+
processableResource: ProcessableResource
|
|
233
|
+
attribute: RenderableAttribute
|
|
234
|
+
|
|
235
|
+
class Parent(BaseModel):
|
|
236
|
+
processableResource: ProcessableResource
|
|
237
|
+
isOneToOne: bool
|
|
238
|
+
|
|
239
|
+
renderableResource: RenderableResource
|
|
240
|
+
parent: Parent | None
|
|
241
|
+
occursAsIndependentAttribute: list[IndependentAttributeOccurrence]
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class EndpointContainer(BaseModel):
|
|
245
|
+
individualEndpoints: list[RenderableEndpoint]
|
|
246
|
+
collectionEndpoints: list[RenderableEndpoint]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{{ autogenerated_notice }}
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{{ autogenerated_notice }}
|