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/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,8 @@
1
+ {{ autogenerated_notice }}
2
+ [project]
3
+ name = "{{ target.distribution_name(api=api) }}"
4
+ version = "0.0.0"
5
+
6
+ [build-system]
7
+ requires = ["uv_build>=0.12,<0.13"]
8
+ build-backend = "uv_build"
@@ -0,0 +1 @@
1
+ {{ autogenerated_notice }}
File without changes
@@ -0,0 +1 @@
1
+ {{ autogenerated_notice }}