geniespec 0.1.2.dev4__tar.gz → 0.1.2.dev6__tar.gz

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.
Files changed (22) hide show
  1. {geniespec-0.1.2.dev4 → geniespec-0.1.2.dev6}/PKG-INFO +27 -1
  2. {geniespec-0.1.2.dev4 → geniespec-0.1.2.dev6}/README.md +26 -0
  3. {geniespec-0.1.2.dev4 → geniespec-0.1.2.dev6}/pyproject.toml +1 -1
  4. {geniespec-0.1.2.dev4 → geniespec-0.1.2.dev6}/pyproject.toml.orig +1 -1
  5. {geniespec-0.1.2.dev4 → geniespec-0.1.2.dev6}/src/geniespec/api_schema.yaml +35 -3
  6. {geniespec-0.1.2.dev4 → geniespec-0.1.2.dev6}/src/geniespec/genie.py +12 -1
  7. {geniespec-0.1.2.dev4 → geniespec-0.1.2.dev6}/src/geniespec/languages.py +85 -17
  8. {geniespec-0.1.2.dev4 → geniespec-0.1.2.dev6}/src/geniespec/renderable_types.py +8 -1
  9. {geniespec-0.1.2.dev4 → geniespec-0.1.2.dev6}/src/geniespec/templates/python-server/{service}/v{version}/api.py.j2 +8 -3
  10. {geniespec-0.1.2.dev4 → geniespec-0.1.2.dev6}/src/geniespec/templates/python-server/{service}/v{version}/endpoints.py.j2 +14 -5
  11. {geniespec-0.1.2.dev4 → geniespec-0.1.2.dev6}/src/geniespec/templates/python-server/{service}/v{version}/exceptions.py.j2 +13 -1
  12. {geniespec-0.1.2.dev4 → geniespec-0.1.2.dev6}/src/geniespec/templates/python-server/{service}/v{version}/internal.py.j2 +11 -2
  13. {geniespec-0.1.2.dev4 → geniespec-0.1.2.dev6}/src/geniespec/templates/python-server/{service}/v{version}/resources.py.j2 +11 -2
  14. {geniespec-0.1.2.dev4 → geniespec-0.1.2.dev6}/src/geniespec/templates/typescript-client/{service}/v{version}/client.ts.j2 +6 -0
  15. {geniespec-0.1.2.dev4 → geniespec-0.1.2.dev6}/src/geniespec/templates/typescript-client/{service}/v{version}/endpoints.ts.j2 +3 -1
  16. {geniespec-0.1.2.dev4 → geniespec-0.1.2.dev6}/src/geniespec/templates/typescript-client/{service}/v{version}/index.ts.j2 +2 -0
  17. {geniespec-0.1.2.dev4 → geniespec-0.1.2.dev6}/src/geniespec/templates/typescript-client/{service}/v{version}/resources.ts.j2 +5 -4
  18. {geniespec-0.1.2.dev4 → geniespec-0.1.2.dev6}/src/geniespec/types.py +10 -1
  19. {geniespec-0.1.2.dev4 → geniespec-0.1.2.dev6}/src/geniespec/__init__.py +0 -0
  20. {geniespec-0.1.2.dev4 → geniespec-0.1.2.dev6}/src/geniespec/cli.py +0 -0
  21. {geniespec-0.1.2.dev4 → geniespec-0.1.2.dev6}/src/geniespec/templates/python-server/{service}/__init__.py.j2 +0 -0
  22. {geniespec-0.1.2.dev4 → geniespec-0.1.2.dev6}/src/geniespec/templates/python-server/{service}/v{version}/__init__.py.j2 +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: geniespec
3
- Version: 0.1.2.dev4
3
+ Version: 0.1.2.dev6
4
4
  Summary: Generate API code from an api.yaml spec
5
5
  License-Expression: MIT
6
6
  Requires-Dist: jinja2>=3.1.6
@@ -68,6 +68,8 @@ resources:
68
68
 
69
69
  Fields use it like any other type (`type: ViewContent`, also inside `List` and `Mapping`). `python-server` generates a `ViewContentV1` pydantic discriminated union in `resources`; `typescript-client` generates a `ViewContentV1` union type and a `viewContentV1FromObject` function that builds the member class named by the discriminator (and throws for unknown values).
70
70
 
71
+ `type: Uuid` is a string that must be a valid UUID. It works anywhere `String` does, including as a primary attribute in paths. `python-server` types it as `uuid.UUID`, so pydantic rejects other strings with a 400, internal methods get `uuid.UUID` objects and responses write them in the standard lowercase hyphenated form; `typescript-client` types it as `string`.
72
+
71
73
  ## Rate limits
72
74
 
73
75
  Name rate limits at the top level, then pick one per transition with `rateLimit`. `defaultRateLimit` applies to every transition that doesn't set one, and `rateLimit: none` turns it off for a transition, the same way `defaultAuthentication` and `authentication` work:
@@ -91,6 +93,30 @@ transitions:
91
93
 
92
94
  A rate limit needs at least one of `perMinute`, `perFiveMinutes`, `perHour` and `perDay`. `keyBy` is `user` (the default) or `ip`. Requests over any window are rejected with a 429. Limits keyed by `user` count per authenticated user, so a transition using one needs `authentication`. `python-server` writes each rate limit as a `RATE_LIMIT_<NAME>` kiba-core `RateLimitConfig` and passes it to `route`; keying by `ip` needs kiba-core's `OriginIpMiddleware`. `typescript-client` ignores rate limits.
93
95
 
96
+ ## API docs
97
+
98
+ Transitions can set `operationId`, `summary`, `description`, `tags` and `extensions`, and attributes, request fields and response fields can set a `description`:
99
+
100
+ ```yaml
101
+ resources:
102
+ - singleName: Member
103
+ collectionName: Members
104
+ attributes:
105
+ - name: member-id
106
+ type: Uuid
107
+ isPrimary: true
108
+ description: The member's id.
109
+ transitions:
110
+ - action: GET
111
+ operationId: getMember
112
+ summary: Get a member
113
+ description: Returns one member.
114
+ tags: [Members]
115
+ extensions: {deprecated: true}
116
+ ```
117
+
118
+ `python-server` passes the transition fields to kiba-core's `route`, whose `OpenApiSchemaGenerator` documents every route with an `operationId` (and skips the rest); `extensions` reach `OpenApiExtension`s as `metadata['extensions']`. Field descriptions become pydantic `Field(description=...)`, so they show up in the request and response schemas. Each `operationId` must be unique, so a transition that generates more than one route (for example `GET` on a resource with unique attributes) can't set one. `typescript-client` ignores these fields.
119
+
94
120
  # Development
95
121
 
96
122
  ```
@@ -54,6 +54,8 @@ resources:
54
54
 
55
55
  Fields use it like any other type (`type: ViewContent`, also inside `List` and `Mapping`). `python-server` generates a `ViewContentV1` pydantic discriminated union in `resources`; `typescript-client` generates a `ViewContentV1` union type and a `viewContentV1FromObject` function that builds the member class named by the discriminator (and throws for unknown values).
56
56
 
57
+ `type: Uuid` is a string that must be a valid UUID. It works anywhere `String` does, including as a primary attribute in paths. `python-server` types it as `uuid.UUID`, so pydantic rejects other strings with a 400, internal methods get `uuid.UUID` objects and responses write them in the standard lowercase hyphenated form; `typescript-client` types it as `string`.
58
+
57
59
  ## Rate limits
58
60
 
59
61
  Name rate limits at the top level, then pick one per transition with `rateLimit`. `defaultRateLimit` applies to every transition that doesn't set one, and `rateLimit: none` turns it off for a transition, the same way `defaultAuthentication` and `authentication` work:
@@ -77,6 +79,30 @@ transitions:
77
79
 
78
80
  A rate limit needs at least one of `perMinute`, `perFiveMinutes`, `perHour` and `perDay`. `keyBy` is `user` (the default) or `ip`. Requests over any window are rejected with a 429. Limits keyed by `user` count per authenticated user, so a transition using one needs `authentication`. `python-server` writes each rate limit as a `RATE_LIMIT_<NAME>` kiba-core `RateLimitConfig` and passes it to `route`; keying by `ip` needs kiba-core's `OriginIpMiddleware`. `typescript-client` ignores rate limits.
79
81
 
82
+ ## API docs
83
+
84
+ Transitions can set `operationId`, `summary`, `description`, `tags` and `extensions`, and attributes, request fields and response fields can set a `description`:
85
+
86
+ ```yaml
87
+ resources:
88
+ - singleName: Member
89
+ collectionName: Members
90
+ attributes:
91
+ - name: member-id
92
+ type: Uuid
93
+ isPrimary: true
94
+ description: The member's id.
95
+ transitions:
96
+ - action: GET
97
+ operationId: getMember
98
+ summary: Get a member
99
+ description: Returns one member.
100
+ tags: [Members]
101
+ extensions: {deprecated: true}
102
+ ```
103
+
104
+ `python-server` passes the transition fields to kiba-core's `route`, whose `OpenApiSchemaGenerator` documents every route with an `operationId` (and skips the rest); `extensions` reach `OpenApiExtension`s as `metadata['extensions']`. Field descriptions become pydantic `Field(description=...)`, so they show up in the request and response schemas. Each `operationId` must be unique, so a transition that generates more than one route (for example `GET` on a resource with unique attributes) can't set one. `typescript-client` ignores these fields.
105
+
80
106
  # Development
81
107
 
82
108
  ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "geniespec"
3
- version = "0.1.2.dev4"
3
+ version = "0.1.2.dev6"
4
4
  description = "Generate API code from an api.yaml spec"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "geniespec"
3
- version = "0.1.2.dev4"
3
+ version = "0.1.2.dev6"
4
4
  description = "Generate API code from an api.yaml spec"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -48,7 +48,7 @@ properties:
48
48
  type:
49
49
  - "null"
50
50
  - string
51
- pattern: '^[a-z][a-z0-9-]*$'
51
+ pattern: '^[a-z][a-z0-9_-]*$'
52
52
  rateLimits:
53
53
  type: array
54
54
  items:
@@ -158,6 +158,8 @@ definitions:
158
158
  type: boolean
159
159
  isDeprecated:
160
160
  type: boolean
161
+ description:
162
+ type: string
161
163
  defaultValue:
162
164
  type:
163
165
  - string
@@ -230,7 +232,7 @@ definitions:
230
232
  type:
231
233
  - "null"
232
234
  - string
233
- pattern: '^[a-z][a-z0-9-]*$'
235
+ pattern: '^[a-z][a-z0-9_-]*$'
234
236
  rateLimit:
235
237
  type:
236
238
  - "null"
@@ -242,6 +244,19 @@ definitions:
242
244
  - json
243
245
  - stream
244
246
  - redirect
247
+ operationId:
248
+ type: string
249
+ pattern: '^[A-Za-z][A-Za-z0-9_]*$'
250
+ summary:
251
+ type: string
252
+ description:
253
+ type: string
254
+ tags:
255
+ type: array
256
+ items:
257
+ type: string
258
+ extensions:
259
+ type: object
245
260
  Transition:
246
261
  type: object
247
262
  additionalProperties: false
@@ -266,7 +281,7 @@ definitions:
266
281
  type:
267
282
  - "null"
268
283
  - string
269
- pattern: '^[a-z][a-z0-9-]*$'
284
+ pattern: '^[a-z][a-z0-9_-]*$'
270
285
  rateLimit:
271
286
  type:
272
287
  - "null"
@@ -285,6 +300,19 @@ definitions:
285
300
  - collection
286
301
  isIndirectAction:
287
302
  type: boolean
303
+ operationId:
304
+ type: string
305
+ pattern: '^[A-Za-z][A-Za-z0-9_]*$'
306
+ summary:
307
+ type: string
308
+ description:
309
+ type: string
310
+ tags:
311
+ type: array
312
+ items:
313
+ type: string
314
+ extensions:
315
+ type: object
288
316
  RootTransition:
289
317
  type: object
290
318
  allOf:
@@ -328,6 +356,8 @@ definitions:
328
356
  - 'null'
329
357
  isDeprecated:
330
358
  type: boolean
359
+ description:
360
+ type: string
331
361
  maxValue:
332
362
  type:
333
363
  - string
@@ -374,6 +404,8 @@ definitions:
374
404
  type: boolean
375
405
  isList:
376
406
  type: boolean
407
+ description:
408
+ type: string
377
409
  Exception:
378
410
  type: object
379
411
  additionalProperties: false
@@ -199,6 +199,7 @@ class Renderer:
199
199
  isRequired=requestField.isRequired,
200
200
  isDeprecated=requestField.isDeprecated,
201
201
  source=requestField.source,
202
+ description=requestField.description,
202
203
  )
203
204
 
204
205
  def _response_field_to_renderable_field(self, responseField: ResponseField, knownTypeNames: dict[str, str]) -> RenderableField:
@@ -214,6 +215,7 @@ class Renderer:
214
215
  mappingValueTypes=responseField.mappingValueTypes,
215
216
  enumValues=responseField.enumValues,
216
217
  ),
218
+ description=responseField.description,
217
219
  )
218
220
 
219
221
  def _attribute_to_renderable_attribute(self, attribute: Attribute, knownTypeNames: dict[str, str], isNullable: bool | None = None) -> RenderableAttribute:
@@ -236,6 +238,7 @@ class Renderer:
236
238
  isAccessibleIndependently=attribute.isAccessibleIndependently,
237
239
  isSettable=attribute.isSettable,
238
240
  isNullable=attribute.isNullable,
241
+ description=attribute.description,
239
242
  )
240
243
 
241
244
  def _attribute_to_renderable_parameter(self, attribute: Attribute, isRequired: bool, knownTypeNames: dict[str, str], shouldUseDefault: bool = True) -> RenderableParameter:
@@ -254,8 +257,13 @@ class Renderer:
254
257
  defaultValue=attribute.defaultValue if shouldUseDefault else None,
255
258
  isRequired=isRequired,
256
259
  isDeprecated=attribute.isDeprecated,
260
+ description=attribute.description,
257
261
  )
258
262
 
263
+ @staticmethod
264
+ def _operation_documentation(transition: BaseTransition) -> dict[str, object]:
265
+ return {'operationId': transition.operationId, 'summary': transition.summary, 'description': transition.description, 'tags': transition.tags, 'extensions': transition.extensions}
266
+
259
267
  @staticmethod
260
268
  def _resource_full_name(resource: Resource, name: str) -> str:
261
269
  return f'{resource.parentResource or ""}{name}'
@@ -458,7 +466,7 @@ class Renderer:
458
466
  knownTypeNames=knownTypeNames,
459
467
  defaultAuthentication=api.defaultAuthentication,
460
468
  defaultRateLimit=api.defaultRateLimit,
461
- )
469
+ ).model_copy(update=self._operation_documentation(transition=transition))
462
470
  for transition in api.transitions
463
471
  ]
464
472
  renderableExceptions = [
@@ -564,6 +572,7 @@ class Renderer:
564
572
  for endpoint in customEndpoints:
565
573
  customEndpoint = RenderableEndpoint(components=[*endpoint.components, ActionEndpointComponent(actionName=RenderableString.from_separated_string(string=transition.action, separator='-'))])
566
574
  renderableApi.operations.append(self._action_operation(transition=transition, endpoint=customEndpoint, knownTypeNames=knownTypeNames, defaultAuthentication=api.defaultAuthentication, defaultRateLimit=api.defaultRateLimit, isIndirectAction=transition.isIndirectAction))
575
+ renderableApi.operations[operationCount:] = [operation.model_copy(update=self._operation_documentation(transition=transition)) for operation in renderableApi.operations[operationCount:]]
567
576
  if len(renderableApi.operations) == operationCount:
568
577
  uncoveredTransitions.append(f'{renderableResource.singleName.pascalCaseString} {transition.action}')
569
578
  if uncoveredTransitions:
@@ -573,6 +582,8 @@ class Renderer:
573
582
  rateLimitsByName = {rateLimit.name.kebabCaseString: rateLimit for rateLimit in renderableApi.rateLimits}
574
583
  operationErrors = [error for operation in renderableApi.operations for error in self._operation_errors(operation=operation, rateLimitsByName=rateLimitsByName)]
575
584
  operationErrors += self._file_errors(resources=renderableResources, exceptions=renderableExceptions)
585
+ operationIdCounts = collections.Counter(operation.operationId for operation in renderableApi.operations if operation.operationId is not None)
586
+ operationErrors += [f'Duplicate operationId: {operationId}' for operationId, count in operationIdCounts.items() if count > 1]
576
587
  if operationErrors:
577
588
  raise SemanticValidationException(message='\n'.join(operationErrors))
578
589
  renderableApi.resources.extend(renderableResources)
@@ -1,6 +1,7 @@
1
1
  import collections
2
2
  import keyword
3
3
  import re
4
+ import typing
4
5
 
5
6
  from core.exceptions import KibaException
6
7
 
@@ -8,6 +9,7 @@ from geniespec.renderable_types import ActionEndpointComponent
8
9
  from geniespec.renderable_types import AttributeEndpointComponent
9
10
  from geniespec.renderable_types import MappingRenderableType
10
11
  from geniespec.renderable_types import RenderableApi
12
+ from geniespec.renderable_types import RenderableAttribute
11
13
  from geniespec.renderable_types import RenderableEndpointComponent
12
14
  from geniespec.renderable_types import RenderableEnum
13
15
  from geniespec.renderable_types import RenderableException
@@ -73,6 +75,9 @@ class BaseLanguageDefinition:
73
75
  def string_type(self, isNullable: bool) -> str:
74
76
  raise NotImplementedError
75
77
 
78
+ def uuid_type(self, isNullable: bool) -> str:
79
+ raise NotImplementedError
80
+
76
81
  def enum_type(self, isNullable: bool, renderableType: RenderableType, apiVersion: Version, isLocalReference: bool) -> str:
77
82
  raise NotImplementedError
78
83
 
@@ -110,6 +115,8 @@ class BaseLanguageDefinition:
110
115
  return self.double_type(isNullable=isNullable)
111
116
  if yamlType == PrimitiveTypes.STRING:
112
117
  return self.string_type(isNullable=isNullable)
118
+ if yamlType == PrimitiveTypes.UUID:
119
+ return self.uuid_type(isNullable=isNullable)
113
120
  if yamlType == PrimitiveTypes.BOOLEAN:
114
121
  return self.boolean_type(isNullable=isNullable)
115
122
  if yamlType == PrimitiveTypes.DATETIME:
@@ -227,9 +234,9 @@ class BaseLanguageDefinition:
227
234
  return output
228
235
  if component.attribute.isUnique and not component.attribute.isPrimary:
229
236
  output.append(self.kebab_name(name=component.attribute.name))
230
- if component.attribute.fieldType.rawType not in (PrimitiveTypes.INTEGER, PrimitiveTypes.LONG_INTEGER, PrimitiveTypes.STRING):
231
- raise KibaException(message=f'ResourceEndpointComponent created with an invalid attribute type of {component.attribute.fieldType}. Valid types are: integer, long integer and string.')
232
- output.append(self.path_parameter_segment(parameterName=self.identifier(name=component.attribute.name), isInteger=component.attribute.fieldType.rawType != PrimitiveTypes.STRING))
237
+ if component.attribute.fieldType.rawType not in (PrimitiveTypes.INTEGER, PrimitiveTypes.LONG_INTEGER, PrimitiveTypes.STRING, PrimitiveTypes.UUID):
238
+ raise KibaException(message=f'ResourceEndpointComponent created with an invalid attribute type of {component.attribute.fieldType}. Valid types are: integer, long integer, string and uuid.')
239
+ output.append(self.path_parameter_segment(parameterName=self.identifier(name=component.attribute.name), isInteger=component.attribute.fieldType.rawType in (PrimitiveTypes.INTEGER, PrimitiveTypes.LONG_INTEGER)))
233
240
  return output
234
241
  raise KibaException(message=f'Unsupported endpoint component: {type(component)}')
235
242
 
@@ -343,6 +350,9 @@ class PythonLanguageDefinition(BaseLanguageDefinition):
343
350
  def string_type(self, isNullable: bool) -> str:
344
351
  return 'str | None' if isNullable else 'str'
345
352
 
353
+ def uuid_type(self, isNullable: bool) -> str:
354
+ return 'uuid.UUID | None' if isNullable else 'uuid.UUID'
355
+
346
356
  def enum_type(self, isNullable: bool, renderableType: RenderableType, apiVersion: Version, isLocalReference: bool) -> str:
347
357
  if renderableType.className is None:
348
358
  typeName = self.enum_literal(values=renderableType.enumValues)
@@ -397,6 +407,43 @@ class PythonLanguageDefinition(BaseLanguageDefinition):
397
407
  def requires_optional_field_default(self, api: RenderableApi) -> bool:
398
408
  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))
399
409
 
410
+ def endpoints_require_field(self, api: RenderableApi) -> bool:
411
+ hasRequestDescriptions = any(parameter.description is not None for operation in api.operations for parameter in self.request_parameters(operation=operation))
412
+ hasResponseDescriptions = any(field.description is not None for operation in api.operations for field in operation.responseFields or [])
413
+ return self.requires_optional_field_default(api=api) or hasRequestDescriptions or hasResponseDescriptions
414
+
415
+ @staticmethod
416
+ def resources_require_field(api: RenderableApi) -> bool:
417
+ return any(attribute.description is not None for resource in api.resources for attribute in resource.attributes)
418
+
419
+ @staticmethod
420
+ def _field_default(default: str | None, description: str | None, fieldFunction: str = 'Field', shouldUseField: bool = False) -> str:
421
+ if description is None and not shouldUseField:
422
+ return '' if default is None else f' = {default}'
423
+ arguments = ([] if default is None else [f'default={default}']) + ([] if description is None else [f'description={description!r}'])
424
+ return f' = {fieldFunction}({", ".join(arguments)})'
425
+
426
+ def request_field_declaration(self, operation: RenderableOperation, parameter: RenderableParameter, apiVersion: Version) -> str:
427
+ typeName = self.type(renderableType=parameter.fieldType, apiVersion=apiVersion)
428
+ if self.is_undefined_parameter(operation=operation, parameter=parameter):
429
+ return f'UpdateValue[{typeName}]{self._field_default(default="UNDEFINED", description=parameter.description)}'
430
+ if parameter.isRequired:
431
+ return f'{typeName}{self._field_default(default=None, description=parameter.description)}'
432
+ if parameter.fieldType.isNullable:
433
+ return f'{typeName}{self._field_default(default="None", description=parameter.description)}'
434
+ return f'{typeName}{self._field_default(default=f"typing.cast({typeName}, None)", description=parameter.description, shouldUseField=True)}'
435
+
436
+ def response_field_declaration(self, field: RenderableField, apiVersion: Version) -> str:
437
+ return f'{self.type(renderableType=field.fieldType, apiVersion=apiVersion)}{self._field_default(default=None, description=field.description)}'
438
+
439
+ def attribute_declaration(self, attribute: RenderableAttribute, apiVersion: Version) -> str:
440
+ return f'{self.type(renderableType=attribute.fieldType, apiVersion=apiVersion, isLocalReference=True)}{self._field_default(default=None, description=attribute.description, fieldFunction="pydantic.Field")}'
441
+
442
+ @staticmethod
443
+ def route_documentation_arguments(operation: RenderableOperation) -> str:
444
+ arguments = {'operationId': operation.operationId, 'summary': operation.summary, 'description': operation.description, 'tags': operation.tags or None, 'extensions': operation.extensions or None}
445
+ return ''.join(f', {name}={value!r}' for name, value in arguments.items() if value is not None)
446
+
400
447
  def requires_streaming(self, api: RenderableApi) -> bool:
401
448
  return any(self.is_streaming(operation=operation) for operation in api.operations)
402
449
 
@@ -418,13 +465,32 @@ class PythonLanguageDefinition(BaseLanguageDefinition):
418
465
  return parameterName if self.requires_parameter_preprocessing(operation=operation, parameter=parameter) else f'request.data.{parameterName}'
419
466
 
420
467
  @staticmethod
421
- def _contains_json(renderableType: RenderableType) -> bool:
468
+ def _type_parts(renderableType: RenderableType) -> list[RenderableType]:
422
469
  if isinstance(renderableType, MappingRenderableType):
423
- return PythonLanguageDefinition._contains_json(renderableType=renderableType.keyType) or PythonLanguageDefinition._contains_json(renderableType=renderableType.valueType)
424
- return renderableType.rawType == PrimitiveTypes.JSON
425
-
426
- def resources_require_json(self, api: RenderableApi) -> bool:
427
- return any(self._contains_json(renderableType=attribute.fieldType) for resource in api.resources for attribute in resource.attributes)
470
+ return [renderableType, *PythonLanguageDefinition._type_parts(renderableType=renderableType.keyType), *PythonLanguageDefinition._type_parts(renderableType=renderableType.valueType)]
471
+ return [renderableType]
472
+
473
+ def _module_field_types(self, api: RenderableApi, module: str) -> list[RenderableType]:
474
+ responseFieldTypes = [field.fieldType for operation in api.operations for field in operation.responseFields or []]
475
+ if module == 'resources':
476
+ return [attribute.fieldType for resource in api.resources for attribute in resource.attributes]
477
+ if module == 'endpoints':
478
+ return [parameter.fieldType for operation in api.operations for parameter in self.request_parameters(operation=operation)] + responseFieldTypes
479
+ if module == 'internal':
480
+ return [parameter.fieldType for operation in api.operations for parameter in operation.parameters] + responseFieldTypes
481
+ if module == 'exceptions':
482
+ return [field.fieldType for exception in api.exceptions for field in exception.fields]
483
+ raise KibaException(message=f'Unknown module: {module}')
484
+
485
+ def module_requires(self, api: RenderableApi, module: str, requirement: str) -> bool:
486
+ predicates: dict[str, typing.Callable[[RenderableType], bool]] = {
487
+ 'collections': lambda part: part.isList or isinstance(part, MappingRenderableType),
488
+ 'datetime': lambda part: part.rawType in (PrimitiveTypes.DATETIME, PrimitiveTypes.DATE),
489
+ 'json': lambda part: part.rawType == PrimitiveTypes.JSON,
490
+ 'resources': lambda part: part.className is not None,
491
+ 'uuid': lambda part: part.rawType == PrimitiveTypes.UUID,
492
+ }
493
+ return any(predicates[requirement](part) for fieldType in self._module_field_types(api=api, module=module) for part in self._type_parts(renderableType=fieldType))
428
494
 
429
495
  def resources_require_typing(self, api: RenderableApi) -> bool:
430
496
  return len(api.enums) > 0 or len(api.oneOfs) > 0 or any(self.is_inline_enum(renderableType=attribute.fieldType) for resource in api.resources for attribute in resource.attributes)
@@ -439,13 +505,6 @@ class PythonLanguageDefinition(BaseLanguageDefinition):
439
505
  def requires_uploaded_file(self, api: RenderableApi) -> bool:
440
506
  return any(self.is_file_operation(operation=operation) for operation in api.operations)
441
507
 
442
- def operations_require_json(self, api: RenderableApi) -> bool:
443
- 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 []]
444
- return any(self._contains_json(renderableType=fieldType) for fieldType in fieldTypes)
445
-
446
- def exceptions_require_json(self, api: RenderableApi) -> bool:
447
- return any(self._contains_json(renderableType=field.fieldType) for exception in api.exceptions for field in exception.fields)
448
-
449
508
  def route_ordered_operations(self, api: RenderableApi) -> list[RenderableOperation]:
450
509
  return sorted(api.operations, key=lambda operation: tuple(segment.startswith('{') for segment in self.endpoint(operation=operation).split('/')))
451
510
 
@@ -562,6 +621,9 @@ class TypeScriptLanguageDefinition(BaseLanguageDefinition):
562
621
  def string_type(self, isNullable: bool) -> str:
563
622
  return self._nullable(typeName='string', isNullable=isNullable)
564
623
 
624
+ def uuid_type(self, isNullable: bool) -> str:
625
+ return self._nullable(typeName='string', isNullable=isNullable)
626
+
565
627
  def enum_type(self, isNullable: bool, renderableType: RenderableType, apiVersion: Version, isLocalReference: bool) -> str:
566
628
  if renderableType.className is None:
567
629
  return self._nullable(typeName=f'({self.enum_union(values=renderableType.enumValues)})', isNullable=isNullable)
@@ -649,6 +711,12 @@ class TypeScriptLanguageDefinition(BaseLanguageDefinition):
649
711
  def requires_request_data(self, api: RenderableApi) -> bool:
650
712
  return any(self.body_parameters(operation=operation) and not self.is_file_operation(operation=operation) for operation in self.client_operations(api=api))
651
713
 
714
+ def requires_rest_method(self, api: RenderableApi) -> bool:
715
+ return any(not self.is_file_operation(operation=operation) for operation in self.client_operations(api=api))
716
+
717
+ def has_endpoint_classes(self, api: RenderableApi) -> bool:
718
+ return len(self.body_parameter_types(api=api)) > 0 or len(self.response_field_types(api=api)) > 0
719
+
652
720
  def form_value(self, renderableType: RenderableType, value: str) -> str:
653
721
  if renderableType.rawType == RenderableRawTypes.FILE:
654
722
  return value
@@ -680,7 +748,7 @@ class TypeScriptLanguageDefinition(BaseLanguageDefinition):
680
748
  return f'{parserName if isLocalReference else f"Resources.{parserName}"}({value} as RawObject)'
681
749
  if renderableType.rawType == 'class' and renderableType.className is not None:
682
750
  return f'{self.type(renderableType=renderableType, apiVersion=apiVersion, isLocalReference=isLocalReference)}.fromObject({value} as RawObject)'
683
- if renderableType.rawType == PrimitiveTypes.STRING:
751
+ if renderableType.rawType in (PrimitiveTypes.STRING, PrimitiveTypes.UUID):
684
752
  return f'String({value})'
685
753
  if renderableType.rawType in (PrimitiveTypes.INTEGER, PrimitiveTypes.LONG_INTEGER, PrimitiveTypes.FLOAT, PrimitiveTypes.DOUBLE):
686
754
  return f'Number({value})'
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import re
4
4
 
5
+ import pydantic
5
6
  from core.exceptions import KibaException
6
7
  from pydantic import BaseModel
7
8
 
@@ -128,6 +129,7 @@ class RenderableField(BaseModel):
128
129
  name: RenderableString
129
130
  fieldType: RenderableType
130
131
  defaultValue: str | int | float | bool | None = None
132
+ description: str | None = None
131
133
 
132
134
 
133
135
  class RenderableAttribute(RenderableField):
@@ -191,7 +193,7 @@ class RenderableEndpoint(BaseModel):
191
193
  @property
192
194
  def urlParameters(self) -> list[RenderableParameter]: # noqa: N802
193
195
  return [
194
- RenderableParameter(name=component.attribute.name, fieldType=component.attribute.fieldType, isRequired=True, isDeprecated=False)
196
+ RenderableParameter(name=component.attribute.name, fieldType=component.attribute.fieldType, isRequired=True, isDeprecated=False, description=component.attribute.description)
195
197
  for component in self.components
196
198
  if isinstance(component, ResourceEndpointComponent) and component.attribute is not None
197
199
  ]
@@ -215,6 +217,11 @@ class RenderableOperation(BaseModel):
215
217
  rateLimit: str | None = None
216
218
  isIndirectAction: bool = False
217
219
  responseType: str = ResponseTypes.JSON
220
+ operationId: str | None = None
221
+ summary: str | None = None
222
+ description: str | None = None
223
+ tags: list[str] = []
224
+ extensions: dict[str, pydantic.JsonValue] = {}
218
225
 
219
226
  @property
220
227
  def urlParameters(self) -> list[RenderableParameter]: # noqa: N802
@@ -2,11 +2,12 @@
2
2
  # ruff: noqa: ARG001
3
3
  {% if target.requires_streaming(api=api) %}
4
4
  import collections.abc
5
- {% endif %}
6
- import datetime
7
5
 
6
+ {% endif %}
7
+ {% if api.operations %}
8
8
  from core.api.api_request import KibaApiRequest
9
9
  from core.api.route import route
10
+ {% endif %}
10
11
  from core.api.route_auth import RouteAuthResolver
11
12
  {% if api.rateLimits %}
12
13
  from core.api.route_metadata import RateLimitConfig
@@ -14,10 +15,14 @@ from core.api.route_metadata import RateLimitConfig
14
15
  {% if target.requires_redirect(api=api) %}
15
16
  from core.exceptions import FoundRedirectException
16
17
  {% endif %}
18
+ {% if api.operations %}
17
19
  from core.http.rest_method import RestMethod
20
+ {% endif %}
18
21
  from starlette.routing import BaseRoute
22
+ {% if api.operations %}
19
23
  from starlette.routing import Route
20
24
  from . import endpoints
25
+ {% endif %}
21
26
  from .internal import {{ target.internal_class_name(api=api) }}
22
27
  {% for rateLimit in api.rateLimits %}
23
28
  {{ target.rate_limit_constant_name(name=rateLimit.name.kebabCaseString) }}: RateLimitConfig = {{ target.rate_limit_config(rateLimit=rateLimit) }}
@@ -30,7 +35,7 @@ def create_routes(internal: {{ target.internal_class_name(api=api) }}, {% if tar
30
35
  {% set rate_limit = target.route_rate_limit(operation=operation) %}
31
36
  {% set class_name = target.operation_class_name(operation=operation) %}
32
37
  {% set result_name = target.identifier(name=operation.responseFields[0].name) if operation.responseFields else 'result' %}
33
- @route(requestType=endpoints.{{ class_name }}Request, responseType=endpoints.{{ class_name }}Response{% if target.is_streaming(operation=operation) %}, isStreaming=True{% endif %}{% if auth is not none %}, authResolver=authResolver, auth='{{ auth }}'{% endif %}{% if rate_limit is not none %}, rateLimit={{ target.rate_limit_constant_name(name=rate_limit) }}{% endif %})
38
+ @route(requestType=endpoints.{{ class_name }}Request, responseType=endpoints.{{ class_name }}Response{% if target.is_streaming(operation=operation) %}, isStreaming=True{% endif %}{% if auth is not none %}, authResolver=authResolver, auth='{{ auth }}'{% endif %}{% if rate_limit is not none %}, rateLimit={{ target.rate_limit_constant_name(name=rate_limit) }}{% endif %}{{ target.route_documentation_arguments(operation=operation) }})
34
39
  async def {{ target.operation_name(operation=operation) }}(request: KibaApiRequest[endpoints.{{ class_name }}Request]) -> {% if target.is_streaming(operation=operation) %}collections.abc.AsyncIterator[endpoints.{{ class_name }}Response]{% else %}endpoints.{{ class_name }}Response{% endif %}:
35
40
  {% for parameter in target.request_parameters(operation=operation) %}
36
41
  {% set param_name = target.identifier(name=parameter.name) %}
@@ -1,16 +1,23 @@
1
1
  {{ autogenerated_notice }}
2
+ {% if target.module_requires(api=api, module='endpoints', requirement='collections') %}
2
3
  import collections.abc
4
+ {% endif %}
5
+ {% if target.module_requires(api=api, module='endpoints', requirement='datetime') %}
3
6
  import datetime
7
+ {% endif %}
4
8
  {% if target.requires_optional_field_default(api=api) or target.operations_require_typing(api=api) %}
5
9
  import typing
6
10
  {% endif %}
11
+ {% if target.module_requires(api=api, module='endpoints', requirement='uuid') %}
12
+ import uuid
13
+ {% endif %}
7
14
 
8
- {% if target.operations_require_json(api=api) %}
15
+ {% if target.module_requires(api=api, module='endpoints', requirement='json') %}
9
16
  import pydantic
10
17
  {% endif %}
11
- {% if target.requires_optional_field_default(api=api) %}
18
+ {% if target.endpoints_require_field(api=api) %}
12
19
  from pydantic import BaseModel, Field
13
- {% else %}
20
+ {% elif api.operations %}
14
21
  from pydantic import BaseModel
15
22
  {% endif %}
16
23
  {% if target.requires_undefined(api=api) %}
@@ -19,21 +26,23 @@ from core.util.typing_util import UNDEFINED, UpdateValue
19
26
  {% if target.requires_uploaded_file(api=api) %}
20
27
  from core.api.uploaded_file import UploadedFile
21
28
  {% endif %}
29
+ {% if target.module_requires(api=api, module='endpoints', requirement='resources') %}
22
30
 
23
31
  from . import resources
32
+ {% endif %}
24
33
 
25
34
  {% for operation in api.operations %}
26
35
 
27
36
  class {{ target.operation_class_name(operation=operation) }}Request(BaseModel):
28
37
  {% for parameter in target.request_parameters(operation=operation) %}
29
- {{ target.identifier(name=parameter.name) }}: {% if target.is_undefined_parameter(operation=operation, parameter=parameter) %}UpdateValue[{{ target.type(renderableType=parameter.fieldType, apiVersion=api.version) }}] = UNDEFINED{% else %}{{ target.type(renderableType=parameter.fieldType, apiVersion=api.version) }}{% if not parameter.isRequired %}{% if parameter.fieldType.isNullable %} = None{% else %} = Field(default=typing.cast({{ target.type(renderableType=parameter.fieldType, apiVersion=api.version) }}, None)){% endif %}{% endif %}{% endif +%}
38
+ {{ target.identifier(name=parameter.name) }}: {{ target.request_field_declaration(operation=operation, parameter=parameter, apiVersion=api.version) }}
30
39
  {% else %}
31
40
  pass
32
41
  {% endfor %}
33
42
 
34
43
  class {{ target.operation_class_name(operation=operation) }}Response(BaseModel):
35
44
  {% if operation.responseFields and operation.responseFields | length > 0 %}
36
- {{ target.identifier(name=operation.responseFields[0].name) }}: {{ target.type(renderableType=operation.responseFields[0].fieldType, apiVersion=api.version) }}
45
+ {{ target.identifier(name=operation.responseFields[0].name) }}: {{ target.response_field_declaration(field=operation.responseFields[0], apiVersion=api.version) }}
37
46
  {% else %}
38
47
  pass
39
48
  {% endif %}
@@ -1,13 +1,25 @@
1
1
  {{ autogenerated_notice }}
2
2
  from __future__ import annotations
3
+ {% if target.module_requires(api=api, module='exceptions', requirement='collections') %}
3
4
  import collections.abc
5
+ {% endif %}
6
+ {% if target.module_requires(api=api, module='exceptions', requirement='datetime') %}
7
+ import datetime
8
+ {% endif %}
4
9
  import typing
5
- {% if target.exceptions_require_json(api=api) %}
10
+ {% if target.module_requires(api=api, module='exceptions', requirement='uuid') %}
11
+ import uuid
12
+ {% endif %}
13
+ {% if target.module_requires(api=api, module='exceptions', requirement='json') %}
6
14
  import pydantic
7
15
  {% endif %}
16
+ {% if api.exceptions %}
8
17
  from core import exceptions
9
18
  from core.util.typing_util import Json
19
+ {% endif %}
20
+ {% if target.module_requires(api=api, module='exceptions', requirement='resources') %}
10
21
  from . import resources
22
+ {% endif %}
11
23
 
12
24
 
13
25
  class KibaExceptionTypedDict(typing.TypedDict):
@@ -1,16 +1,25 @@
1
1
  {{ autogenerated_notice }}
2
2
  import abc
3
- import datetime
3
+ {% if target.module_requires(api=api, module='internal', requirement='collections') or target.requires_streaming(api=api) %}
4
4
  import collections.abc
5
+ {% endif %}
6
+ {% if target.module_requires(api=api, module='internal', requirement='datetime') %}
7
+ import datetime
8
+ {% endif %}
5
9
  {% if target.operations_require_typing(api=api) %}
6
10
  import typing
7
11
  {% endif %}
12
+ {% if target.module_requires(api=api, module='internal', requirement='uuid') %}
13
+ import uuid
14
+ {% endif %}
8
15
 
9
- {% if target.operations_require_json(api=api) %}
16
+ {% if target.module_requires(api=api, module='internal', requirement='json') %}
10
17
  import pydantic
11
18
 
12
19
  {% endif %}
20
+ {% if target.module_requires(api=api, module='internal', requirement='resources') %}
13
21
  from . import resources
22
+ {% endif %}
14
23
  {% if target.requires_undefined(api=api) %}
15
24
  from core.util.typing_util import UNDEFINED, UpdateValue
16
25
  {% endif %}
@@ -1,16 +1,25 @@
1
1
  {{ autogenerated_notice }}
2
2
  from __future__ import annotations
3
3
 
4
+ {% if target.module_requires(api=api, module='resources', requirement='collections') %}
4
5
  import collections.abc
6
+ {% endif %}
7
+ {% if target.module_requires(api=api, module='resources', requirement='datetime') %}
5
8
  import datetime
9
+ {% endif %}
6
10
  {% if target.resources_require_typing(api=api) %}
7
11
  import typing
8
12
  {% endif %}
13
+ {% if target.module_requires(api=api, module='resources', requirement='uuid') %}
14
+ import uuid
15
+ {% endif %}
9
16
 
10
- {% if target.resources_require_json(api=api) or api.oneOfs %}
17
+ {% if target.module_requires(api=api, module='resources', requirement='json') or api.oneOfs or target.resources_require_field(api=api) %}
11
18
  import pydantic
12
19
  {% endif %}
20
+ {% if api.resources %}
13
21
  from pydantic import BaseModel
22
+ {% endif %}
14
23
 
15
24
  {% for enum in api.enums %}
16
25
  {{ target.enum_class_name(enum=enum, apiVersion=api.version) }} = {{ target.enum_literal(values=enum.values) }}
@@ -25,7 +34,7 @@ class {{ target.resource_class_name(resource=resource, apiVersion=api.version) }
25
34
  pass
26
35
  {% endif %}
27
36
  {% for attribute in resource.attributes %}
28
- {{ target.identifier(name=attribute.name) }}: {{ target.type(renderableType=attribute.fieldType, apiVersion=api.version, isLocalReference=True) }}
37
+ {{ target.identifier(name=attribute.name) }}: {{ target.attribute_declaration(attribute=attribute, apiVersion=api.version) }}
29
38
  {% endfor %}
30
39
 
31
40
  {% endfor %}
@@ -1,7 +1,13 @@
1
1
  {{ autogenerated_notice }}
2
+ {% if target.requires_rest_method(api=api) %}
2
3
  import { RestMethod, ServiceClient } from '@kibalabs/core';
4
+ {% else %}
5
+ import { ServiceClient } from '@kibalabs/core';
6
+ {% endif %}
7
+ {% if target.has_endpoint_classes(api=api) %}
3
8
 
4
9
  import * as Endpoints from './endpoints';
10
+ {% endif %}
5
11
  {% if target.references_resources(renderableTypes=target.response_field_types(api=api)) %}
6
12
  import * as Resources from './resources';
7
13
  {% endif %}
@@ -4,10 +4,12 @@
4
4
  {% set coreImports = [] %}
5
5
  {% if target.requires_date_parsing(renderableTypes=responseTypes) %}{% set _ = coreImports.append('dateFromString') %}{% endif %}
6
6
  {% if target.requires_date_parsing(renderableTypes=bodyTypes) %}{% set _ = coreImports.append('dateToString') %}{% endif %}
7
- {% set _ = coreImports.append('RawObject') %}
7
+ {% if target.requires_request_data(api=api) or responseTypes %}{% set _ = coreImports.append('RawObject') %}{% endif %}
8
8
  {% if target.requires_request_data(api=api) %}{% set _ = coreImports.append('RequestData') %}{% endif %}
9
9
  {% if responseTypes %}{% set _ = coreImports.append('ResponseData') %}{% endif %}
10
+ {% if coreImports %}
10
11
  import { {{ coreImports | join(', ') }} } from '@kibalabs/core';
12
+ {% endif %}
11
13
  {% if target.references_resources(renderableTypes=responseTypes + bodyTypes) %}
12
14
 
13
15
  import * as Resources from './resources';
@@ -1,4 +1,6 @@
1
1
  {{ autogenerated_notice }}
2
2
  export * from './client';
3
+ {% if target.has_endpoint_classes(api=api) %}
3
4
  export * from './endpoints';
5
+ {% endif %}
4
6
  export * from './resources';
@@ -1,8 +1,9 @@
1
1
  {{ autogenerated_notice }}
2
- {% if target.requires_date_parsing(renderableTypes=target.resource_field_types(api=api)) %}
3
- import { dateFromString, dateToString, RawObject } from '@kibalabs/core';
4
- {% else %}
5
- import { RawObject } from '@kibalabs/core';
2
+ {% set coreImports = [] %}
3
+ {% if target.requires_date_parsing(renderableTypes=target.resource_field_types(api=api)) %}{% set _ = coreImports.extend(['dateFromString', 'dateToString']) %}{% endif %}
4
+ {% if api.resources or api.oneOfs %}{% set _ = coreImports.append('RawObject') %}{% endif %}
5
+ {% if coreImports %}
6
+ import { {{ coreImports | join(', ') }} } from '@kibalabs/core';
6
7
  {% endif %}
7
8
 
8
9
  export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
@@ -12,11 +12,12 @@ class PrimitiveTypes:
12
12
  FLOAT = 'Float'
13
13
  DOUBLE = 'Double'
14
14
  STRING = 'String'
15
+ UUID = 'Uuid'
15
16
  BOOLEAN = 'Boolean'
16
17
  DATETIME = 'Datetime'
17
18
  DATE = 'Date'
18
19
  JSON = 'Json'
19
- ALL: ClassVar[list[str]] = [LONG_INTEGER, INTEGER, FLOAT, DOUBLE, STRING, BOOLEAN, DATETIME, DATE, JSON]
20
+ ALL: ClassVar[list[str]] = [LONG_INTEGER, INTEGER, FLOAT, DOUBLE, STRING, UUID, BOOLEAN, DATETIME, DATE, JSON]
20
21
 
21
22
 
22
23
  class FieldSources:
@@ -84,6 +85,7 @@ class Attribute(BaseModel):
84
85
  isSettable: bool = False
85
86
  isAccessibleIndependently: bool = False
86
87
  defaultValue: str | int | float | bool | None = None
88
+ description: str | None = None
87
89
 
88
90
 
89
91
  class ResponseField(BaseModel):
@@ -95,6 +97,7 @@ class ResponseField(BaseModel):
95
97
  enumValues: list[str] = []
96
98
  isNullable: bool = False
97
99
  isList: bool = False
100
+ description: str | None = None
98
101
 
99
102
 
100
103
  class RequestField(BaseModel):
@@ -110,6 +113,7 @@ class RequestField(BaseModel):
110
113
  maxValue: str | int | float | None = None
111
114
  minValue: str | int | float | None = None
112
115
  source: str = FieldSources.REQUEST
116
+ description: str | None = None
113
117
 
114
118
 
115
119
  class BaseTransition(BaseModel):
@@ -120,6 +124,11 @@ class BaseTransition(BaseModel):
120
124
  authentication: str | None = None
121
125
  rateLimit: str | None = None
122
126
  responseType: str = ResponseTypes.JSON
127
+ operationId: str | None = None
128
+ summary: str | None = None
129
+ description: str | None = None
130
+ tags: list[str] = []
131
+ extensions: dict[str, pydantic.JsonValue] = {}
123
132
 
124
133
 
125
134
  class Transition(BaseTransition):