geniespec 0.1.2.dev6__tar.gz → 0.1.2.dev8__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.dev6 → geniespec-0.1.2.dev8}/PKG-INFO +9 -1
  2. {geniespec-0.1.2.dev6 → geniespec-0.1.2.dev8}/README.md +8 -0
  3. {geniespec-0.1.2.dev6 → geniespec-0.1.2.dev8}/pyproject.toml +1 -1
  4. {geniespec-0.1.2.dev6 → geniespec-0.1.2.dev8}/pyproject.toml.orig +1 -1
  5. {geniespec-0.1.2.dev6 → geniespec-0.1.2.dev8}/src/geniespec/api_schema.yaml +6 -0
  6. {geniespec-0.1.2.dev6 → geniespec-0.1.2.dev8}/src/geniespec/genie.py +4 -2
  7. {geniespec-0.1.2.dev6 → geniespec-0.1.2.dev8}/src/geniespec/languages.py +27 -7
  8. {geniespec-0.1.2.dev6 → geniespec-0.1.2.dev8}/src/geniespec/templates/python-server/{service}/v{version}/resources.py.j2 +5 -1
  9. {geniespec-0.1.2.dev6 → geniespec-0.1.2.dev8}/src/geniespec/types.py +7 -1
  10. {geniespec-0.1.2.dev6 → geniespec-0.1.2.dev8}/src/geniespec/__init__.py +0 -0
  11. {geniespec-0.1.2.dev6 → geniespec-0.1.2.dev8}/src/geniespec/cli.py +0 -0
  12. {geniespec-0.1.2.dev6 → geniespec-0.1.2.dev8}/src/geniespec/renderable_types.py +0 -0
  13. {geniespec-0.1.2.dev6 → geniespec-0.1.2.dev8}/src/geniespec/templates/python-server/{service}/__init__.py.j2 +0 -0
  14. {geniespec-0.1.2.dev6 → geniespec-0.1.2.dev8}/src/geniespec/templates/python-server/{service}/v{version}/__init__.py.j2 +0 -0
  15. {geniespec-0.1.2.dev6 → geniespec-0.1.2.dev8}/src/geniespec/templates/python-server/{service}/v{version}/api.py.j2 +0 -0
  16. {geniespec-0.1.2.dev6 → geniespec-0.1.2.dev8}/src/geniespec/templates/python-server/{service}/v{version}/endpoints.py.j2 +0 -0
  17. {geniespec-0.1.2.dev6 → geniespec-0.1.2.dev8}/src/geniespec/templates/python-server/{service}/v{version}/exceptions.py.j2 +0 -0
  18. {geniespec-0.1.2.dev6 → geniespec-0.1.2.dev8}/src/geniespec/templates/python-server/{service}/v{version}/internal.py.j2 +0 -0
  19. {geniespec-0.1.2.dev6 → geniespec-0.1.2.dev8}/src/geniespec/templates/typescript-client/{service}/v{version}/client.ts.j2 +0 -0
  20. {geniespec-0.1.2.dev6 → geniespec-0.1.2.dev8}/src/geniespec/templates/typescript-client/{service}/v{version}/endpoints.ts.j2 +0 -0
  21. {geniespec-0.1.2.dev6 → geniespec-0.1.2.dev8}/src/geniespec/templates/typescript-client/{service}/v{version}/index.ts.j2 +0 -0
  22. {geniespec-0.1.2.dev6 → geniespec-0.1.2.dev8}/src/geniespec/templates/typescript-client/{service}/v{version}/resources.ts.j2 +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: geniespec
3
- Version: 0.1.2.dev6
3
+ Version: 0.1.2.dev8
4
4
  Summary: Generate API code from an api.yaml spec
5
5
  License-Expression: MIT
6
6
  Requires-Dist: jinja2>=3.1.6
@@ -70,6 +70,8 @@ Fields use it like any other type (`type: ViewContent`, also inside `List` and `
70
70
 
71
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
72
 
73
+ `type: BigInt` is an integer of any size that travels as a JSON string, because JSON numbers past 2^53 lose precision in JavaScript. `python-server` types it as `int` (a `BigInt` alias in `resources`), accepts `"123"` or `123` in requests, always writes it as a string and documents it as `{"type": "string"}`; `typescript-client` types it as `bigint`, parsed with `BigInt(...)` and sent with `String(...)`.
74
+
73
75
  ## Rate limits
74
76
 
75
77
  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:
@@ -117,6 +119,12 @@ resources:
117
119
 
118
120
  `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
121
 
122
+ ## Overrides
123
+
124
+ `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.
125
+
126
+ - `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}`).
127
+
120
128
  # Development
121
129
 
122
130
  ```
@@ -56,6 +56,8 @@ Fields use it like any other type (`type: ViewContent`, also inside `List` and `
56
56
 
57
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
58
 
59
+ `type: BigInt` is an integer of any size that travels as a JSON string, because JSON numbers past 2^53 lose precision in JavaScript. `python-server` types it as `int` (a `BigInt` alias in `resources`), accepts `"123"` or `123` in requests, always writes it as a string and documents it as `{"type": "string"}`; `typescript-client` types it as `bigint`, parsed with `BigInt(...)` and sent with `String(...)`.
60
+
59
61
  ## Rate limits
60
62
 
61
63
  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:
@@ -103,6 +105,12 @@ resources:
103
105
 
104
106
  `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
107
 
108
+ ## Overrides
109
+
110
+ `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.
111
+
112
+ - `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}`).
113
+
106
114
  # Development
107
115
 
108
116
  ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "geniespec"
3
- version = "0.1.2.dev6"
3
+ version = "0.1.2.dev8"
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.dev6"
3
+ version = "0.1.2.dev8"
4
4
  description = "Generate API code from an api.yaml spec"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -208,6 +208,12 @@ definitions:
208
208
  type: array
209
209
  items:
210
210
  type: string
211
+ overrides:
212
+ type: object
213
+ additionalProperties: false
214
+ properties:
215
+ shouldPrefixParentName:
216
+ type: boolean
211
217
  BaseTransition:
212
218
  type: object
213
219
  additionalProperties: false
@@ -266,7 +266,7 @@ class Renderer:
266
266
 
267
267
  @staticmethod
268
268
  def _resource_full_name(resource: Resource, name: str) -> str:
269
- return f'{resource.parentResource or ""}{name}'
269
+ return f'{resource.parentResource or ""}{name}' if resource.overrides.shouldPrefixParentName else name
270
270
 
271
271
  def _resource_to_renderable_resource(self, resource: Resource, knownTypeNames: dict[str, str]) -> RenderableResource:
272
272
  attributes = [self._attribute_to_renderable_attribute(attribute=attribute, knownTypeNames=knownTypeNames) for attribute in resource.attributes]
@@ -298,7 +298,9 @@ class Renderer:
298
298
 
299
299
  @staticmethod
300
300
  def _relative_name(name: RenderableString, anchor: RenderableResource | None) -> RenderableString:
301
- 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) :])
302
304
 
303
305
  def _resource_component(self, resource: RenderableResource, attribute: RenderableAttribute | None, anchor: RenderableResource | None) -> ResourceEndpointComponent:
304
306
  return ResourceEndpointComponent(
@@ -78,6 +78,9 @@ class BaseLanguageDefinition:
78
78
  def uuid_type(self, isNullable: bool) -> str:
79
79
  raise NotImplementedError
80
80
 
81
+ def big_int_type(self, isNullable: bool, isLocalReference: bool) -> str:
82
+ raise NotImplementedError
83
+
81
84
  def enum_type(self, isNullable: bool, renderableType: RenderableType, apiVersion: Version, isLocalReference: bool) -> str:
82
85
  raise NotImplementedError
83
86
 
@@ -117,6 +120,8 @@ class BaseLanguageDefinition:
117
120
  return self.string_type(isNullable=isNullable)
118
121
  if yamlType == PrimitiveTypes.UUID:
119
122
  return self.uuid_type(isNullable=isNullable)
123
+ if yamlType == PrimitiveTypes.BIG_INT:
124
+ return self.big_int_type(isNullable=isNullable, isLocalReference=isLocalReference)
120
125
  if yamlType == PrimitiveTypes.BOOLEAN:
121
126
  return self.boolean_type(isNullable=isNullable)
122
127
  if yamlType == PrimitiveTypes.DATETIME:
@@ -353,6 +358,10 @@ class PythonLanguageDefinition(BaseLanguageDefinition):
353
358
  def uuid_type(self, isNullable: bool) -> str:
354
359
  return 'uuid.UUID | None' if isNullable else 'uuid.UUID'
355
360
 
361
+ def big_int_type(self, isNullable: bool, isLocalReference: bool) -> str:
362
+ typeName = 'BigInt' if isLocalReference else 'resources.BigInt'
363
+ return f'{typeName} | None' if isNullable else typeName
364
+
356
365
  def enum_type(self, isNullable: bool, renderableType: RenderableType, apiVersion: Version, isLocalReference: bool) -> str:
357
366
  if renderableType.className is None:
358
367
  typeName = self.enum_literal(values=renderableType.enumValues)
@@ -412,10 +421,6 @@ class PythonLanguageDefinition(BaseLanguageDefinition):
412
421
  hasResponseDescriptions = any(field.description is not None for operation in api.operations for field in operation.responseFields or [])
413
422
  return self.requires_optional_field_default(api=api) or hasRequestDescriptions or hasResponseDescriptions
414
423
 
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
424
  @staticmethod
420
425
  def _field_default(default: str | None, description: str | None, fieldFunction: str = 'Field', shouldUseField: bool = False) -> str:
421
426
  if description is None and not shouldUseField:
@@ -487,13 +492,21 @@ class PythonLanguageDefinition(BaseLanguageDefinition):
487
492
  'collections': lambda part: part.isList or isinstance(part, MappingRenderableType),
488
493
  'datetime': lambda part: part.rawType in (PrimitiveTypes.DATETIME, PrimitiveTypes.DATE),
489
494
  'json': lambda part: part.rawType == PrimitiveTypes.JSON,
490
- 'resources': lambda part: part.className is not None,
495
+ 'resources': lambda part: part.className is not None or part.rawType == PrimitiveTypes.BIG_INT,
491
496
  'uuid': lambda part: part.rawType == PrimitiveTypes.UUID,
492
497
  }
493
498
  return any(predicates[requirement](part) for fieldType in self._module_field_types(api=api, module=module) for part in self._type_parts(renderableType=fieldType))
494
499
 
500
+ def requires_big_int(self, api: RenderableApi) -> bool:
501
+ return any(part.rawType == PrimitiveTypes.BIG_INT for module in ('resources', 'internal', 'exceptions') for fieldType in self._module_field_types(api=api, module=module) for part in self._type_parts(renderableType=fieldType))
502
+
495
503
  def resources_require_typing(self, api: RenderableApi) -> bool:
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)
504
+ hasInlineEnums = any(self.is_inline_enum(renderableType=attribute.fieldType) for resource in api.resources for attribute in resource.attributes)
505
+ return len(api.enums) > 0 or len(api.oneOfs) > 0 or hasInlineEnums or self.requires_big_int(api=api)
506
+
507
+ def resources_require_pydantic(self, api: RenderableApi) -> bool:
508
+ hasDescriptions = any(attribute.description is not None for resource in api.resources for attribute in resource.attributes)
509
+ return len(api.oneOfs) > 0 or hasDescriptions or self.module_requires(api=api, module='resources', requirement='json') or self.requires_big_int(api=api)
497
510
 
498
511
  def one_of_alias(self, oneOf: RenderableOneOf, apiVersion: Version) -> str:
499
512
  return f'typing.Annotated[{" | ".join(self.one_of_member_class_names(oneOf=oneOf, apiVersion=apiVersion))}, pydantic.Field(discriminator={self.identifier(name=oneOf.discriminator)!r})]'
@@ -602,6 +615,9 @@ class TypeScriptLanguageDefinition(BaseLanguageDefinition):
602
615
  def long_integer_type(self, isNullable: bool) -> str:
603
616
  return self._nullable(typeName='number', isNullable=isNullable)
604
617
 
618
+ def big_int_type(self, isNullable: bool, isLocalReference: bool) -> str: # noqa: ARG002
619
+ return self._nullable(typeName='bigint', isNullable=isNullable)
620
+
605
621
  def mapping_type(self, isNullable: bool, keyType: str, valueType: str) -> str: # noqa: ARG002
606
622
  # NOTE(krishan711): json object keys are always strings
607
623
  return self._nullable(typeName=f'Record<string, {valueType}>', isNullable=isNullable)
@@ -752,6 +768,8 @@ class TypeScriptLanguageDefinition(BaseLanguageDefinition):
752
768
  return f'String({value})'
753
769
  if renderableType.rawType in (PrimitiveTypes.INTEGER, PrimitiveTypes.LONG_INTEGER, PrimitiveTypes.FLOAT, PrimitiveTypes.DOUBLE):
754
770
  return f'Number({value})'
771
+ if renderableType.rawType == PrimitiveTypes.BIG_INT:
772
+ return f'BigInt(String({value}))'
755
773
  if renderableType.rawType == PrimitiveTypes.BOOLEAN:
756
774
  return f'Boolean({value})'
757
775
  if renderableType.rawType in (PrimitiveTypes.DATE, PrimitiveTypes.DATETIME):
@@ -765,7 +783,7 @@ class TypeScriptLanguageDefinition(BaseLanguageDefinition):
765
783
  def _is_serialized_as_is(self, renderableType: RenderableType) -> bool:
766
784
  if isinstance(renderableType, MappingRenderableType):
767
785
  return self._is_serialized_as_is(renderableType=renderableType.valueType)
768
- return renderableType.rawType not in ('class', RenderableRawTypes.ONE_OF, PrimitiveTypes.DATE, PrimitiveTypes.DATETIME)
786
+ return renderableType.rawType not in ('class', RenderableRawTypes.ONE_OF, PrimitiveTypes.DATE, PrimitiveTypes.DATETIME, PrimitiveTypes.BIG_INT)
769
787
 
770
788
  def to_object(self, renderableType: RenderableType, value: str, apiVersion: Version, isOptional: bool = False, isLocalReference: bool = False) -> str:
771
789
  if self._is_serialized_as_is(renderableType=renderableType):
@@ -787,4 +805,6 @@ class TypeScriptLanguageDefinition(BaseLanguageDefinition):
787
805
  return f'dateToString({value})'
788
806
  if renderableType.rawType == PrimitiveTypes.DATE:
789
807
  return f"dateToString({value}, 'yyyy-MM-dd')"
808
+ if renderableType.rawType == PrimitiveTypes.BIG_INT:
809
+ return f'String({value})'
790
810
  raise KibaException(f'Failed to render conversion to object for: {renderableType}')
@@ -14,13 +14,17 @@ import typing
14
14
  import uuid
15
15
  {% endif %}
16
16
 
17
- {% if target.module_requires(api=api, module='resources', requirement='json') or api.oneOfs or target.resources_require_field(api=api) %}
17
+ {% if target.resources_require_pydantic(api=api) %}
18
18
  import pydantic
19
19
  {% endif %}
20
20
  {% if api.resources %}
21
21
  from pydantic import BaseModel
22
22
  {% endif %}
23
23
 
24
+ {% if target.requires_big_int(api=api) %}
25
+ BigInt = typing.Annotated[int, pydantic.PlainSerializer(str, return_type=str), pydantic.WithJsonSchema({'type': 'string'})]
26
+
27
+ {% endif %}
24
28
  {% for enum in api.enums %}
25
29
  {{ target.enum_class_name(enum=enum, apiVersion=api.version) }} = {{ target.enum_literal(values=enum.values) }}
26
30
  {% if loop.last %}
@@ -13,11 +13,12 @@ class PrimitiveTypes:
13
13
  DOUBLE = 'Double'
14
14
  STRING = 'String'
15
15
  UUID = 'Uuid'
16
+ BIG_INT = 'BigInt'
16
17
  BOOLEAN = 'Boolean'
17
18
  DATETIME = 'Datetime'
18
19
  DATE = 'Date'
19
20
  JSON = 'Json'
20
- ALL: ClassVar[list[str]] = [LONG_INTEGER, INTEGER, FLOAT, DOUBLE, STRING, UUID, BOOLEAN, DATETIME, DATE, JSON]
21
+ ALL: ClassVar[list[str]] = [LONG_INTEGER, INTEGER, FLOAT, DOUBLE, STRING, UUID, BIG_INT, BOOLEAN, DATETIME, DATE, JSON]
21
22
 
22
23
 
23
24
  class FieldSources:
@@ -140,6 +141,10 @@ class RootTransition(BaseTransition):
140
141
  pass
141
142
 
142
143
 
144
+ class ResourceOverrides(BaseModel):
145
+ shouldPrefixParentName: bool = True
146
+
147
+
143
148
  class Resource(BaseModel):
144
149
  singleName: str
145
150
  collectionName: str
@@ -147,6 +152,7 @@ class Resource(BaseModel):
147
152
  attributes: list[Attribute] = []
148
153
  transitions: list[Transition] = []
149
154
  accessibleFrom: list[str] = []
155
+ overrides: ResourceOverrides = ResourceOverrides()
150
156
 
151
157
  @property
152
158
  def primaryAttribute(self) -> Attribute | None: # noqa: N802