geniespec 0.1.2.dev5__tar.gz → 0.1.2.dev7__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.dev7}/PKG-INFO +31 -1
  2. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev7}/README.md +30 -0
  3. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev7}/pyproject.toml +1 -1
  4. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev7}/pyproject.toml.orig +1 -1
  5. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev7}/src/geniespec/api_schema.yaml +38 -0
  6. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev7}/src/geniespec/genie.py +16 -3
  7. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev7}/src/geniespec/languages.py +70 -25
  8. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev7}/src/geniespec/renderable_types.py +8 -1
  9. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev7}/src/geniespec/templates/python-server/{service}/v{version}/api.py.j2 +8 -3
  10. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev7}/src/geniespec/templates/python-server/{service}/v{version}/endpoints.py.j2 +12 -6
  11. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev7}/src/geniespec/templates/python-server/{service}/v{version}/exceptions.py.j2 +11 -2
  12. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev7}/src/geniespec/templates/python-server/{service}/v{version}/internal.py.j2 +9 -3
  13. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev7}/src/geniespec/templates/python-server/{service}/v{version}/resources.py.j2 +9 -3
  14. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev7}/src/geniespec/templates/typescript-client/{service}/v{version}/client.ts.j2 +6 -0
  15. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev7}/src/geniespec/templates/typescript-client/{service}/v{version}/endpoints.ts.j2 +3 -1
  16. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev7}/src/geniespec/templates/typescript-client/{service}/v{version}/index.ts.j2 +2 -0
  17. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev7}/src/geniespec/templates/typescript-client/{service}/v{version}/resources.ts.j2 +5 -4
  18. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev7}/src/geniespec/types.py +13 -0
  19. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev7}/src/geniespec/__init__.py +0 -0
  20. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev7}/src/geniespec/cli.py +0 -0
  21. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev7}/src/geniespec/templates/python-server/{service}/__init__.py.j2 +0 -0
  22. {geniespec-0.1.2.dev5 → geniespec-0.1.2.dev7}/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.dev7
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,36 @@ 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
+
120
+ ## Overrides
121
+
122
+ `overrides` keep an API that existed before genie working the same way once it moves to genie. They are meant to be dropped in the next major version of that API.
123
+
124
+ - `shouldPrefixParentName: false` on a child resource names it without its parent's name: `Agent` under `User` stays `Agent` (not `UserAgent`), so its primary attribute is `agent-id` rather than `user-agent-id` and its fields keep their names. Its path is unchanged (`/users/{userId}/agents/{agentId}`).
125
+
96
126
  # Development
97
127
 
98
128
  ```
@@ -79,6 +79,36 @@ 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
+
106
+ ## Overrides
107
+
108
+ `overrides` keep an API that existed before genie working the same way once it moves to genie. They are meant to be dropped in the next major version of that API.
109
+
110
+ - `shouldPrefixParentName: false` on a child resource names it without its parent's name: `Agent` under `User` stays `Agent` (not `UserAgent`), so its primary attribute is `agent-id` rather than `user-agent-id` and its fields keep their names. Its path is unchanged (`/users/{userId}/agents/{agentId}`).
111
+
82
112
  # Development
83
113
 
84
114
  ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "geniespec"
3
- version = "0.1.2.dev5"
3
+ version = "0.1.2.dev7"
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.dev7"
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
@@ -206,6 +208,12 @@ definitions:
206
208
  type: array
207
209
  items:
208
210
  type: string
211
+ overrides:
212
+ type: object
213
+ additionalProperties: false
214
+ properties:
215
+ shouldPrefixParentName:
216
+ type: boolean
209
217
  BaseTransition:
210
218
  type: object
211
219
  additionalProperties: false
@@ -242,6 +250,19 @@ definitions:
242
250
  - json
243
251
  - stream
244
252
  - redirect
253
+ operationId:
254
+ type: string
255
+ pattern: '^[A-Za-z][A-Za-z0-9_]*$'
256
+ summary:
257
+ type: string
258
+ description:
259
+ type: string
260
+ tags:
261
+ type: array
262
+ items:
263
+ type: string
264
+ extensions:
265
+ type: object
245
266
  Transition:
246
267
  type: object
247
268
  additionalProperties: false
@@ -285,6 +306,19 @@ definitions:
285
306
  - collection
286
307
  isIndirectAction:
287
308
  type: boolean
309
+ operationId:
310
+ type: string
311
+ pattern: '^[A-Za-z][A-Za-z0-9_]*$'
312
+ summary:
313
+ type: string
314
+ description:
315
+ type: string
316
+ tags:
317
+ type: array
318
+ items:
319
+ type: string
320
+ extensions:
321
+ type: object
288
322
  RootTransition:
289
323
  type: object
290
324
  allOf:
@@ -328,6 +362,8 @@ definitions:
328
362
  - 'null'
329
363
  isDeprecated:
330
364
  type: boolean
365
+ description:
366
+ type: string
331
367
  maxValue:
332
368
  type:
333
369
  - string
@@ -374,6 +410,8 @@ definitions:
374
410
  type: boolean
375
411
  isList:
376
412
  type: boolean
413
+ description:
414
+ type: string
377
415
  Exception:
378
416
  type: object
379
417
  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,11 +257,16 @@ 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
- return f'{resource.parentResource or ""}{name}'
269
+ return f'{resource.parentResource or ""}{name}' if resource.overrides.shouldPrefixParentName else name
262
270
 
263
271
  def _resource_to_renderable_resource(self, resource: Resource, knownTypeNames: dict[str, str]) -> RenderableResource:
264
272
  attributes = [self._attribute_to_renderable_attribute(attribute=attribute, knownTypeNames=knownTypeNames) for attribute in resource.attributes]
@@ -290,7 +298,9 @@ class Renderer:
290
298
 
291
299
  @staticmethod
292
300
  def _relative_name(name: RenderableString, anchor: RenderableResource | None) -> RenderableString:
293
- return RenderableString(string=name.string[len(anchor.singleName.string) :]) if anchor is not None else name
301
+ if anchor is None or name.string[: len(anchor.singleName.string)] != anchor.singleName.string:
302
+ return name
303
+ return RenderableString(string=name.string[len(anchor.singleName.string) :])
294
304
 
295
305
  def _resource_component(self, resource: RenderableResource, attribute: RenderableAttribute | None, anchor: RenderableResource | None) -> ResourceEndpointComponent:
296
306
  return ResourceEndpointComponent(
@@ -458,7 +468,7 @@ class Renderer:
458
468
  knownTypeNames=knownTypeNames,
459
469
  defaultAuthentication=api.defaultAuthentication,
460
470
  defaultRateLimit=api.defaultRateLimit,
461
- )
471
+ ).model_copy(update=self._operation_documentation(transition=transition))
462
472
  for transition in api.transitions
463
473
  ]
464
474
  renderableExceptions = [
@@ -564,6 +574,7 @@ class Renderer:
564
574
  for endpoint in customEndpoints:
565
575
  customEndpoint = RenderableEndpoint(components=[*endpoint.components, ActionEndpointComponent(actionName=RenderableString.from_separated_string(string=transition.action, separator='-'))])
566
576
  renderableApi.operations.append(self._action_operation(transition=transition, endpoint=customEndpoint, knownTypeNames=knownTypeNames, defaultAuthentication=api.defaultAuthentication, defaultRateLimit=api.defaultRateLimit, isIndirectAction=transition.isIndirectAction))
577
+ renderableApi.operations[operationCount:] = [operation.model_copy(update=self._operation_documentation(transition=transition)) for operation in renderableApi.operations[operationCount:]]
567
578
  if len(renderableApi.operations) == operationCount:
568
579
  uncoveredTransitions.append(f'{renderableResource.singleName.pascalCaseString} {transition.action}')
569
580
  if uncoveredTransitions:
@@ -573,6 +584,8 @@ class Renderer:
573
584
  rateLimitsByName = {rateLimit.name.kebabCaseString: rateLimit for rateLimit in renderableApi.rateLimits}
574
585
  operationErrors = [error for operation in renderableApi.operations for error in self._operation_errors(operation=operation, rateLimitsByName=rateLimitsByName)]
575
586
  operationErrors += self._file_errors(resources=renderableResources, exceptions=renderableExceptions)
587
+ operationIdCounts = collections.Counter(operation.operationId for operation in renderableApi.operations if operation.operationId is not None)
588
+ operationErrors += [f'Duplicate operationId: {operationId}' for operationId, count in operationIdCounts.items() if count > 1]
576
589
  if operationErrors:
577
590
  raise SemanticValidationException(message='\n'.join(operationErrors))
578
591
  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):
@@ -132,6 +140,10 @@ class RootTransition(BaseTransition):
132
140
  pass
133
141
 
134
142
 
143
+ class ResourceOverrides(BaseModel):
144
+ shouldPrefixParentName: bool = True
145
+
146
+
135
147
  class Resource(BaseModel):
136
148
  singleName: str
137
149
  collectionName: str
@@ -139,6 +151,7 @@ class Resource(BaseModel):
139
151
  attributes: list[Attribute] = []
140
152
  transitions: list[Transition] = []
141
153
  accessibleFrom: list[str] = []
154
+ overrides: ResourceOverrides = ResourceOverrides()
142
155
 
143
156
  @property
144
157
  def primaryAttribute(self) -> Attribute | None: # noqa: N802