geniespec 0.1.2.dev5__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.dev5 → geniespec-0.1.2.dev6}/PKG-INFO +25 -1
  2. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev6}/README.md +24 -0
  3. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev6}/pyproject.toml +1 -1
  4. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev6}/pyproject.toml.orig +1 -1
  5. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev6}/src/geniespec/api_schema.yaml +32 -0
  6. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev6}/src/geniespec/genie.py +12 -1
  7. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev6}/src/geniespec/languages.py +70 -25
  8. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev6}/src/geniespec/renderable_types.py +8 -1
  9. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev6}/src/geniespec/templates/python-server/{service}/v{version}/api.py.j2 +8 -3
  10. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev6}/src/geniespec/templates/python-server/{service}/v{version}/endpoints.py.j2 +12 -6
  11. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev6}/src/geniespec/templates/python-server/{service}/v{version}/exceptions.py.j2 +11 -2
  12. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev6}/src/geniespec/templates/python-server/{service}/v{version}/internal.py.j2 +9 -3
  13. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev6}/src/geniespec/templates/python-server/{service}/v{version}/resources.py.j2 +9 -3
  14. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev6}/src/geniespec/templates/typescript-client/{service}/v{version}/client.ts.j2 +6 -0
  15. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev6}/src/geniespec/templates/typescript-client/{service}/v{version}/endpoints.ts.j2 +3 -1
  16. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev6}/src/geniespec/templates/typescript-client/{service}/v{version}/index.ts.j2 +2 -0
  17. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev6}/src/geniespec/templates/typescript-client/{service}/v{version}/resources.ts.j2 +5 -4
  18. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev6}/src/geniespec/types.py +8 -0
  19. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev6}/src/geniespec/__init__.py +0 -0
  20. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev6}/src/geniespec/cli.py +0 -0
  21. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev6}/src/geniespec/templates/python-server/{service}/__init__.py.j2 +0 -0
  22. {geniespec-0.1.2.dev5 → 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.dev5
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
@@ -93,6 +93,30 @@ transitions:
93
93
 
94
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.
95
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
+
96
120
  # Development
97
121
 
98
122
  ```
@@ -79,6 +79,30 @@ transitions:
79
79
 
80
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.
81
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
+
82
106
  # Development
83
107
 
84
108
  ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "geniespec"
3
- version = "0.1.2.dev5"
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.dev5"
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"
@@ -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
@@ -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
@@ -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
@@ -405,6 +407,43 @@ class PythonLanguageDefinition(BaseLanguageDefinition):
405
407
  def requires_optional_field_default(self, api: RenderableApi) -> bool:
406
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))
407
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
+
408
447
  def requires_streaming(self, api: RenderableApi) -> bool:
409
448
  return any(self.is_streaming(operation=operation) for operation in api.operations)
410
449
 
@@ -426,25 +465,32 @@ class PythonLanguageDefinition(BaseLanguageDefinition):
426
465
  return parameterName if self.requires_parameter_preprocessing(operation=operation, parameter=parameter) else f'request.data.{parameterName}'
427
466
 
428
467
  @staticmethod
429
- def _contains_json(renderableType: RenderableType) -> bool:
430
- if isinstance(renderableType, MappingRenderableType):
431
- return PythonLanguageDefinition._contains_json(renderableType=renderableType.keyType) or PythonLanguageDefinition._contains_json(renderableType=renderableType.valueType)
432
- return renderableType.rawType == PrimitiveTypes.JSON
433
-
434
- @staticmethod
435
- def _contains_uuid(renderableType: RenderableType) -> bool:
468
+ def _type_parts(renderableType: RenderableType) -> list[RenderableType]:
436
469
  if isinstance(renderableType, MappingRenderableType):
437
- return PythonLanguageDefinition._contains_uuid(renderableType=renderableType.keyType) or PythonLanguageDefinition._contains_uuid(renderableType=renderableType.valueType)
438
- return renderableType.rawType == PrimitiveTypes.UUID
439
-
440
- def requires_uuid(self, api: RenderableApi) -> bool:
441
- fieldTypes = [attribute.fieldType for resource in api.resources for attribute in [*resource.attributes, *resource.independentAttributes]]
442
- 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 []]
443
- fieldTypes += [field.fieldType for exception in api.exceptions for field in exception.fields]
444
- return any(self._contains_uuid(renderableType=fieldType) for fieldType in fieldTypes)
445
-
446
- def resources_require_json(self, api: RenderableApi) -> bool:
447
- 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))
448
494
 
449
495
  def resources_require_typing(self, api: RenderableApi) -> bool:
450
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)
@@ -459,13 +505,6 @@ class PythonLanguageDefinition(BaseLanguageDefinition):
459
505
  def requires_uploaded_file(self, api: RenderableApi) -> bool:
460
506
  return any(self.is_file_operation(operation=operation) for operation in api.operations)
461
507
 
462
- def operations_require_json(self, api: RenderableApi) -> bool:
463
- 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 []]
464
- return any(self._contains_json(renderableType=fieldType) for fieldType in fieldTypes)
465
-
466
- def exceptions_require_json(self, api: RenderableApi) -> bool:
467
- return any(self._contains_json(renderableType=field.fieldType) for exception in api.exceptions for field in exception.fields)
468
-
469
508
  def route_ordered_operations(self, api: RenderableApi) -> list[RenderableOperation]:
470
509
  return sorted(api.operations, key=lambda operation: tuple(segment.startswith('{') for segment in self.endpoint(operation=operation).split('/')))
471
510
 
@@ -672,6 +711,12 @@ class TypeScriptLanguageDefinition(BaseLanguageDefinition):
672
711
  def requires_request_data(self, api: RenderableApi) -> bool:
673
712
  return any(self.body_parameters(operation=operation) and not self.is_file_operation(operation=operation) for operation in self.client_operations(api=api))
674
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
+
675
720
  def form_value(self, renderableType: RenderableType, value: str) -> str:
676
721
  if renderableType.rawType == RenderableRawTypes.FILE:
677
722
  return 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,19 +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 %}
7
- {% if target.requires_uuid(api=api) %}
11
+ {% if target.module_requires(api=api, module='endpoints', requirement='uuid') %}
8
12
  import uuid
9
13
  {% endif %}
10
14
 
11
- {% if target.operations_require_json(api=api) %}
15
+ {% if target.module_requires(api=api, module='endpoints', requirement='json') %}
12
16
  import pydantic
13
17
  {% endif %}
14
- {% if target.requires_optional_field_default(api=api) %}
18
+ {% if target.endpoints_require_field(api=api) %}
15
19
  from pydantic import BaseModel, Field
16
- {% else %}
20
+ {% elif api.operations %}
17
21
  from pydantic import BaseModel
18
22
  {% endif %}
19
23
  {% if target.requires_undefined(api=api) %}
@@ -22,21 +26,23 @@ from core.util.typing_util import UNDEFINED, UpdateValue
22
26
  {% if target.requires_uploaded_file(api=api) %}
23
27
  from core.api.uploaded_file import UploadedFile
24
28
  {% endif %}
29
+ {% if target.module_requires(api=api, module='endpoints', requirement='resources') %}
25
30
 
26
31
  from . import resources
32
+ {% endif %}
27
33
 
28
34
  {% for operation in api.operations %}
29
35
 
30
36
  class {{ target.operation_class_name(operation=operation) }}Request(BaseModel):
31
37
  {% for parameter in target.request_parameters(operation=operation) %}
32
- {{ 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) }}
33
39
  {% else %}
34
40
  pass
35
41
  {% endfor %}
36
42
 
37
43
  class {{ target.operation_class_name(operation=operation) }}Response(BaseModel):
38
44
  {% if operation.responseFields and operation.responseFields | length > 0 %}
39
- {{ 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) }}
40
46
  {% else %}
41
47
  pass
42
48
  {% endif %}
@@ -1,16 +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.requires_uuid(api=api) %}
10
+ {% if target.module_requires(api=api, module='exceptions', requirement='uuid') %}
6
11
  import uuid
7
12
  {% endif %}
8
- {% if target.exceptions_require_json(api=api) %}
13
+ {% if target.module_requires(api=api, module='exceptions', requirement='json') %}
9
14
  import pydantic
10
15
  {% endif %}
16
+ {% if api.exceptions %}
11
17
  from core import exceptions
12
18
  from core.util.typing_util import Json
19
+ {% endif %}
20
+ {% if target.module_requires(api=api, module='exceptions', requirement='resources') %}
13
21
  from . import resources
22
+ {% endif %}
14
23
 
15
24
 
16
25
  class KibaExceptionTypedDict(typing.TypedDict):
@@ -1,19 +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 %}
8
- {% if target.requires_uuid(api=api) %}
12
+ {% if target.module_requires(api=api, module='internal', requirement='uuid') %}
9
13
  import uuid
10
14
  {% endif %}
11
15
 
12
- {% if target.operations_require_json(api=api) %}
16
+ {% if target.module_requires(api=api, module='internal', requirement='json') %}
13
17
  import pydantic
14
18
 
15
19
  {% endif %}
20
+ {% if target.module_requires(api=api, module='internal', requirement='resources') %}
16
21
  from . import resources
22
+ {% endif %}
17
23
  {% if target.requires_undefined(api=api) %}
18
24
  from core.util.typing_util import UNDEFINED, UpdateValue
19
25
  {% endif %}
@@ -1,19 +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 %}
9
- {% if target.requires_uuid(api=api) %}
13
+ {% if target.module_requires(api=api, module='resources', requirement='uuid') %}
10
14
  import uuid
11
15
  {% endif %}
12
16
 
13
- {% 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) %}
14
18
  import pydantic
15
19
  {% endif %}
20
+ {% if api.resources %}
16
21
  from pydantic import BaseModel
22
+ {% endif %}
17
23
 
18
24
  {% for enum in api.enums %}
19
25
  {{ target.enum_class_name(enum=enum, apiVersion=api.version) }} = {{ target.enum_literal(values=enum.values) }}
@@ -28,7 +34,7 @@ class {{ target.resource_class_name(resource=resource, apiVersion=api.version) }
28
34
  pass
29
35
  {% endif %}
30
36
  {% for attribute in resource.attributes %}
31
- {{ 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) }}
32
38
  {% endfor %}
33
39
 
34
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 };
@@ -85,6 +85,7 @@ class Attribute(BaseModel):
85
85
  isSettable: bool = False
86
86
  isAccessibleIndependently: bool = False
87
87
  defaultValue: str | int | float | bool | None = None
88
+ description: str | None = None
88
89
 
89
90
 
90
91
  class ResponseField(BaseModel):
@@ -96,6 +97,7 @@ class ResponseField(BaseModel):
96
97
  enumValues: list[str] = []
97
98
  isNullable: bool = False
98
99
  isList: bool = False
100
+ description: str | None = None
99
101
 
100
102
 
101
103
  class RequestField(BaseModel):
@@ -111,6 +113,7 @@ class RequestField(BaseModel):
111
113
  maxValue: str | int | float | None = None
112
114
  minValue: str | int | float | None = None
113
115
  source: str = FieldSources.REQUEST
116
+ description: str | None = None
114
117
 
115
118
 
116
119
  class BaseTransition(BaseModel):
@@ -121,6 +124,11 @@ class BaseTransition(BaseModel):
121
124
  authentication: str | None = None
122
125
  rateLimit: str | None = None
123
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] = {}
124
132
 
125
133
 
126
134
  class Transition(BaseTransition):