geniespec 0.1.1.dev5__tar.gz → 0.1.1.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.1.dev5 → geniespec-0.1.1.dev7}/PKG-INFO +34 -4
  2. {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/README.md +33 -3
  3. {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/pyproject.toml +2 -2
  4. {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/pyproject.toml.orig +2 -2
  5. {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/api_schema.yaml +29 -0
  6. {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/genie.py +70 -19
  7. {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/languages.py +76 -13
  8. {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/renderable_types.py +13 -0
  9. {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/templates/python-server/{service}/v{version}/endpoints.py.j2 +4 -1
  10. {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/templates/python-server/{service}/v{version}/internal.py.j2 +6 -0
  11. {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/templates/python-server/{service}/v{version}/resources.py.j2 +9 -0
  12. {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/templates/typescript-client/{service}/v{version}/client.ts.j2 +8 -1
  13. {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/templates/typescript-client/{service}/v{version}/endpoints.ts.j2 +25 -1
  14. {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/templates/typescript-client/{service}/v{version}/resources.ts.j2 +4 -0
  15. {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/types.py +16 -0
  16. {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/__init__.py +0 -0
  17. {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/cli.py +0 -0
  18. {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/templates/python-server/{service}/__init__.py.j2 +0 -0
  19. {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/templates/python-server/{service}/v{version}/__init__.py.j2 +0 -0
  20. {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/templates/python-server/{service}/v{version}/api.py.j2 +0 -0
  21. {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/templates/python-server/{service}/v{version}/exceptions.py.j2 +0 -0
  22. {geniespec-0.1.1.dev5 → geniespec-0.1.1.dev7}/src/geniespec/templates/typescript-client/{service}/v{version}/index.ts.j2 +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: geniespec
3
- Version: 0.1.1.dev5
3
+ Version: 0.1.1.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
@@ -22,11 +22,34 @@ geniespec watch --spec api.yaml --target <python-server | typescript-client> [--
22
22
  ```
23
23
 
24
24
  - `build` writes the target's package into the output directory. Only files in that package are touched: changed files are rewritten and files it no longer generates are removed.
25
- - `python-server`: `./<service>_api/v<major>/` (`api`, `endpoints`, `resources`, `internal`, `exceptions`). Code run from the output directory imports it directly, e.g. `from my_service_api.v1 import api`. Needs `kiba-core` (`core.api`, `core.util.typing_util`), `pydantic` and `starlette`.
26
- - `typescript-client`: `./<service>-api/v<major>/` (`client`, `endpoints`, `resources`, `index`) with a `<Service>ApiV<major>Client` that extends `@kibalabs/core`'s `ServiceClient`. Construct it with the url the API version is mounted at, e.g. `new MyServiceApiV1Client(requester, 'https://api.example.com/v1')`. Methods take path parameters, then a params object for the request fields, and return the response field; stream transitions take a callback for each item. Transitions with `responseType: redirect` or a `query-parameters` field are left out, as browsers open redirects directly and query-parameters fields receive third-party callbacks. Needs `@kibalabs/core`.
25
+ - `python-server`: `./<service>_api/v<major>/` (`api`, `endpoints`, `resources`, `internal`, `exceptions`). Code run from the output directory imports it directly, e.g. `from my_service_api.v1 import api`. Needs `kiba-core` (`core.api`, `core.util.typing_util`; 0.5.3.dev87 or later for `File` fields), `pydantic` and `starlette`.
26
+ - `typescript-client`: `./<service>-api/v<major>/` (`client`, `endpoints`, `resources`, `index`) with a `<Service>ApiV<major>Client` that extends `@kibalabs/core`'s `ServiceClient`. Construct it with the url the API version is mounted at, e.g. `new MyServiceApiV1Client(requester, 'https://api.example.com/v1')`. Methods take path parameters, then a params object for the request fields, and return the response field; stream transitions take a callback for each item; transitions with `File` fields send a multipart form. Transitions with `responseType: redirect` or a `query-parameters` field are left out, as browsers open redirects directly and query-parameters fields receive third-party callbacks. Needs `@kibalabs/core`.
27
27
  - `watch` builds, then rebuilds whenever the spec changes. If the spec is invalid it logs the error and keeps the previous output. Given a command after `--`, it runs it once the first build is done, keeps rebuilding while it runs, exits with its exit code and stops it when watch itself is stopped, e.g. `geniespec watch --spec api.yaml --target python-server -- uvicorn app:app --reload`.
28
28
  - The bundled templates are examples of a kibalabs-style target.
29
29
 
30
+ ## Enums and files
31
+
32
+ Enums are string values limited to a list. Name them at the top level to reuse them, or give a field its own:
33
+
34
+ ```yaml
35
+ enums:
36
+ - name: ViewKind
37
+ values: [member, cycle-start]
38
+ resources:
39
+ - singleName: View
40
+ collectionName: Views
41
+ attributes:
42
+ - name: kind
43
+ type: ViewKind
44
+ - name: size
45
+ type: Enum
46
+ enumValues: [small, large]
47
+ ```
48
+
49
+ `python-server` types them as `typing.Literal[...]` (named ones as `ViewKindV1` in `resources`) and rejects requests with other values; `typescript-client` types them as `'member' | 'cycle-start'` (named ones as an exported `ViewKindV1`).
50
+
51
+ `type: File` request fields upload a file. They are only allowed on POST transitions, and the transition's other fields must be single primitive or enum values, as the request is sent as a multipart form. Internal methods receive kiba-core's `UploadedFile` (`filename`, `contentType`, `content`); clients pass a `File`.
52
+
30
53
  # Development
31
54
 
32
55
  ```
@@ -34,13 +57,20 @@ make install
34
57
  make lint-check type-check test
35
58
  ```
36
59
 
60
+ `tests/python_server` covers the core and the `python-server` target (the core tests generate through it) and runs in `Dockerfile`. `tests/typescript_client` covers the `typescript-client` target; it needs node and runs in `typescript-client.Dockerfile`:
61
+
62
+ ```
63
+ make install-typescript-client
64
+ make test-typescript-client
65
+ ```
66
+
37
67
  Merges to `main` publish a dev version to PyPI. To release, bump `version` in `pyproject.toml` in a PR, then run the Release workflow on `main` (Actions → Release → Run workflow). It tags `vX.Y.Z` and the Publish workflow publishes it. Only the release workflow can push version tags.
38
68
 
39
69
  # Things that need work
40
70
 
41
71
  ## Typing of params and fields in general
42
72
 
43
- Enum and OneOf not supported yet
73
+ OneOf not supported yet
44
74
 
45
75
  I think we need to create an object for Type. what would it look like?
46
76
 
@@ -8,11 +8,34 @@ geniespec watch --spec api.yaml --target <python-server | typescript-client> [--
8
8
  ```
9
9
 
10
10
  - `build` writes the target's package into the output directory. Only files in that package are touched: changed files are rewritten and files it no longer generates are removed.
11
- - `python-server`: `./<service>_api/v<major>/` (`api`, `endpoints`, `resources`, `internal`, `exceptions`). Code run from the output directory imports it directly, e.g. `from my_service_api.v1 import api`. Needs `kiba-core` (`core.api`, `core.util.typing_util`), `pydantic` and `starlette`.
12
- - `typescript-client`: `./<service>-api/v<major>/` (`client`, `endpoints`, `resources`, `index`) with a `<Service>ApiV<major>Client` that extends `@kibalabs/core`'s `ServiceClient`. Construct it with the url the API version is mounted at, e.g. `new MyServiceApiV1Client(requester, 'https://api.example.com/v1')`. Methods take path parameters, then a params object for the request fields, and return the response field; stream transitions take a callback for each item. Transitions with `responseType: redirect` or a `query-parameters` field are left out, as browsers open redirects directly and query-parameters fields receive third-party callbacks. Needs `@kibalabs/core`.
11
+ - `python-server`: `./<service>_api/v<major>/` (`api`, `endpoints`, `resources`, `internal`, `exceptions`). Code run from the output directory imports it directly, e.g. `from my_service_api.v1 import api`. Needs `kiba-core` (`core.api`, `core.util.typing_util`; 0.5.3.dev87 or later for `File` fields), `pydantic` and `starlette`.
12
+ - `typescript-client`: `./<service>-api/v<major>/` (`client`, `endpoints`, `resources`, `index`) with a `<Service>ApiV<major>Client` that extends `@kibalabs/core`'s `ServiceClient`. Construct it with the url the API version is mounted at, e.g. `new MyServiceApiV1Client(requester, 'https://api.example.com/v1')`. Methods take path parameters, then a params object for the request fields, and return the response field; stream transitions take a callback for each item; transitions with `File` fields send a multipart form. Transitions with `responseType: redirect` or a `query-parameters` field are left out, as browsers open redirects directly and query-parameters fields receive third-party callbacks. Needs `@kibalabs/core`.
13
13
  - `watch` builds, then rebuilds whenever the spec changes. If the spec is invalid it logs the error and keeps the previous output. Given a command after `--`, it runs it once the first build is done, keeps rebuilding while it runs, exits with its exit code and stops it when watch itself is stopped, e.g. `geniespec watch --spec api.yaml --target python-server -- uvicorn app:app --reload`.
14
14
  - The bundled templates are examples of a kibalabs-style target.
15
15
 
16
+ ## Enums and files
17
+
18
+ Enums are string values limited to a list. Name them at the top level to reuse them, or give a field its own:
19
+
20
+ ```yaml
21
+ enums:
22
+ - name: ViewKind
23
+ values: [member, cycle-start]
24
+ resources:
25
+ - singleName: View
26
+ collectionName: Views
27
+ attributes:
28
+ - name: kind
29
+ type: ViewKind
30
+ - name: size
31
+ type: Enum
32
+ enumValues: [small, large]
33
+ ```
34
+
35
+ `python-server` types them as `typing.Literal[...]` (named ones as `ViewKindV1` in `resources`) and rejects requests with other values; `typescript-client` types them as `'member' | 'cycle-start'` (named ones as an exported `ViewKindV1`).
36
+
37
+ `type: File` request fields upload a file. They are only allowed on POST transitions, and the transition's other fields must be single primitive or enum values, as the request is sent as a multipart form. Internal methods receive kiba-core's `UploadedFile` (`filename`, `contentType`, `content`); clients pass a `File`.
38
+
16
39
  # Development
17
40
 
18
41
  ```
@@ -20,13 +43,20 @@ make install
20
43
  make lint-check type-check test
21
44
  ```
22
45
 
46
+ `tests/python_server` covers the core and the `python-server` target (the core tests generate through it) and runs in `Dockerfile`. `tests/typescript_client` covers the `typescript-client` target; it needs node and runs in `typescript-client.Dockerfile`:
47
+
48
+ ```
49
+ make install-typescript-client
50
+ make test-typescript-client
51
+ ```
52
+
23
53
  Merges to `main` publish a dev version to PyPI. To release, bump `version` in `pyproject.toml` in a PR, then run the Release workflow on `main` (Actions → Release → Run workflow). It tags `vX.Y.Z` and the Publish workflow publishes it. Only the release workflow can push version tags.
24
54
 
25
55
  # Things that need work
26
56
 
27
57
  ## Typing of params and fields in general
28
58
 
29
- Enum and OneOf not supported yet
59
+ OneOf not supported yet
30
60
 
31
61
  I think we need to create an object for Type. what would it look like?
32
62
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "geniespec"
3
- version = "0.1.1.dev5"
3
+ version = "0.1.1.dev7"
4
4
  description = "Generate API code from an api.yaml spec"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -22,7 +22,7 @@ repository = "https://github.com/kibalabs/genie"
22
22
  [dependency-groups]
23
23
  dev = [
24
24
  "kiba-build==0.1.11.dev15",
25
- "kiba-core[core-api,requester]>=0.5.3.dev81,<0.5.4",
25
+ "kiba-core[core-api,requester]>=0.5.3.dev87,<0.5.4",
26
26
  ]
27
27
 
28
28
  [build-system]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "geniespec"
3
- version = "0.1.1.dev5"
3
+ version = "0.1.1.dev7"
4
4
  description = "Generate API code from an api.yaml spec"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -22,7 +22,7 @@ repository = "https://github.com/kibalabs/genie"
22
22
  [dependency-groups]
23
23
  dev = [
24
24
  "kiba-build==0.1.11.dev15",
25
- "kiba-core[core-api,requester]>=0.5.3.dev81,<0.5.4",
25
+ "kiba-core[core-api,requester]>=0.5.3.dev87,<0.5.4",
26
26
  ]
27
27
 
28
28
  [build-system]
@@ -32,6 +32,10 @@ properties:
32
32
  type: array
33
33
  items:
34
34
  $ref: '#/definitions/Exception'
35
+ enums:
36
+ type: array
37
+ items:
38
+ $ref: '#/definitions/Enum'
35
39
  imports:
36
40
  type: array
37
41
  items:
@@ -42,6 +46,25 @@ properties:
42
46
  - string
43
47
  pattern: '^[a-z][a-z0-9-]*$'
44
48
  definitions:
49
+ EnumValues:
50
+ type: array
51
+ minItems: 1
52
+ uniqueItems: true
53
+ items:
54
+ type: string
55
+ pattern: '^[A-Za-z0-9][A-Za-z0-9_-]*$'
56
+ Enum:
57
+ type: object
58
+ additionalProperties: false
59
+ required:
60
+ - name
61
+ - values
62
+ properties:
63
+ name:
64
+ type: string
65
+ pattern: '^[A-Z][A-Za-z]*$'
66
+ values:
67
+ $ref: '#/definitions/EnumValues'
45
68
  Attribute:
46
69
  type: object
47
70
  additionalProperties: false
@@ -93,6 +116,8 @@ definitions:
93
116
  items:
94
117
  type: string
95
118
  pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
119
+ enumValues:
120
+ $ref: '#/definitions/EnumValues'
96
121
  Resource:
97
122
  type: object
98
123
  additionalProperties: false
@@ -221,6 +246,8 @@ definitions:
221
246
  items:
222
247
  type: string
223
248
  pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
249
+ enumValues:
250
+ $ref: '#/definitions/EnumValues'
224
251
  isRequired:
225
252
  type: boolean
226
253
  defaultValue:
@@ -271,6 +298,8 @@ definitions:
271
298
  items:
272
299
  type: string
273
300
  pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
301
+ enumValues:
302
+ $ref: '#/definitions/EnumValues'
274
303
  isNullable:
275
304
  type: boolean
276
305
  isList:
@@ -24,11 +24,13 @@ from geniespec.renderable_types import ProcessableResource
24
24
  from geniespec.renderable_types import RenderableApi
25
25
  from geniespec.renderable_types import RenderableAttribute
26
26
  from geniespec.renderable_types import RenderableEndpoint
27
+ from geniespec.renderable_types import RenderableEnum
27
28
  from geniespec.renderable_types import RenderableException
28
29
  from geniespec.renderable_types import RenderableField
29
30
  from geniespec.renderable_types import RenderableMethod
30
31
  from geniespec.renderable_types import RenderableOperation
31
32
  from geniespec.renderable_types import RenderableParameter
33
+ from geniespec.renderable_types import RenderableRawTypes
32
34
  from geniespec.renderable_types import RenderableResource
33
35
  from geniespec.renderable_types import RenderableString
34
36
  from geniespec.renderable_types import RenderableType
@@ -44,10 +46,12 @@ from geniespec.types import RequestField
44
46
  from geniespec.types import Resource
45
47
  from geniespec.types import ResponseField
46
48
  from geniespec.types import ResponseTypes
49
+ from geniespec.types import SpecialTypes
47
50
 
48
51
  SCHEMA_FILE_PATH = pathlib.Path(__file__).parent / 'api_schema.yaml'
49
52
  TEMPLATES_DIRECTORY_PATH = pathlib.Path(__file__).parent / 'templates'
50
53
  TARGET_LANGUAGE_CODES = {'python-server': 'python', 'typescript-client': 'typescript'}
54
+ FORM_FIELD_RAW_TYPES = frozenset({*PrimitiveTypes.ALL, RenderableRawTypes.ENUM, RenderableRawTypes.FILE} - {PrimitiveTypes.JSON})
51
55
 
52
56
 
53
57
  class SyntacticValidationException(BadRequestException):
@@ -106,7 +110,7 @@ class Renderer:
106
110
  method = transition.method or defaultMethods.get(transition.action.upper(), 'POST')
107
111
  return RenderableMethod(method=RenderableString.from_upper_case_string(string=method))
108
112
 
109
- def _action_operation(self, transition: BaseTransition, endpoint: RenderableEndpoint, knownTypeNames: set[str], defaultAuthentication: str | None, isIndirectAction: bool = False) -> RenderableOperation:
113
+ def _action_operation(self, transition: BaseTransition, endpoint: RenderableEndpoint, knownTypeNames: dict[str, str], defaultAuthentication: str | None, isIndirectAction: bool = False) -> RenderableOperation:
110
114
  method = self._transition_to_renderable_method(transition=transition)
111
115
  fields = [self._request_field_to_renderable_parameter(requestField=requestField, knownTypeNames=knownTypeNames) for requestField in transition.requestFields]
112
116
  isQuery = method.method.upperCaseString == 'GET'
@@ -126,38 +130,50 @@ class Renderer:
126
130
  rawType: str,
127
131
  isNullable: bool,
128
132
  isList: bool,
129
- knownTypeNames: set[str],
133
+ knownTypeNames: dict[str, str],
130
134
  listValueTypes: list[str] | None = None,
131
135
  mappingKeyType: str | None = None,
132
136
  mappingValueTypes: list[str] | None = None,
137
+ enumValues: list[str] | None = None,
133
138
  ) -> RenderableType:
134
139
  listValueTypes = listValueTypes or []
135
140
  mappingValueTypes = mappingValueTypes or []
141
+ enumValues = enumValues or []
136
142
  if rawType == ComplexTypes.MAPPING:
137
143
  if mappingKeyType is None or len(mappingValueTypes) == 0:
138
144
  raise KibaException(message='Mapping types require mappingKeyType and mappingValueTypes')
145
+ if enumValues:
146
+ raise KibaException(message='Mapping types cannot define enumValues, use a named enum as the value type')
139
147
  keyType = self._type_to_renderable_type(rawType=mappingKeyType, isNullable=False, isList=False, knownTypeNames=knownTypeNames)
140
- nonNullValueTypes = [valueType for valueType in mappingValueTypes if valueType != 'Null']
148
+ nonNullValueTypes = [valueType for valueType in mappingValueTypes if valueType != SpecialTypes.NULL]
141
149
  if len(nonNullValueTypes) != 1 or len(nonNullValueTypes) - len(mappingValueTypes) not in (0, -1):
142
150
  raise KibaException(message=f'Mapping types currently support one value type with an optional Null: {mappingValueTypes}')
143
- valueType = self._type_to_renderable_type(rawType=nonNullValueTypes[0], isNullable='Null' in mappingValueTypes, isList=False, knownTypeNames=knownTypeNames)
151
+ valueType = self._type_to_renderable_type(rawType=nonNullValueTypes[0], isNullable=SpecialTypes.NULL in mappingValueTypes, isList=False, knownTypeNames=knownTypeNames)
144
152
  return MappingRenderableType(isNullable=isNullable, isList=isList, keyType=keyType, valueType=valueType)
145
- if rawType == 'List':
153
+ if rawType == SpecialTypes.LIST:
146
154
  if isList or len(listValueTypes) != 1:
147
155
  raise KibaException(message='List types require exactly one listValueTypes entry')
148
- itemType = self._type_to_renderable_type(rawType=listValueTypes[0], isNullable=False, isList=False, knownTypeNames=knownTypeNames)
156
+ itemType = self._type_to_renderable_type(rawType=listValueTypes[0], isNullable=False, isList=False, knownTypeNames=knownTypeNames, enumValues=enumValues)
149
157
  if itemType.isList:
150
158
  raise KibaException(message='Nested list types are not supported')
151
- return RenderableType(rawType=itemType.rawType, isNullable=isNullable, isList=True, className=itemType.className)
159
+ return itemType.model_copy(update={'isNullable': isNullable, 'isList': True})
152
160
  if listValueTypes:
153
161
  raise KibaException(message=f'Only List types may define listValueTypes: {rawType}')
162
+ if rawType == SpecialTypes.ENUM:
163
+ if not enumValues:
164
+ raise KibaException(message='Enum types require enumValues')
165
+ return RenderableType(rawType=RenderableRawTypes.ENUM, isNullable=isNullable, isList=isList, enumValues=enumValues)
166
+ if enumValues:
167
+ raise KibaException(message=f'Only Enum types may define enumValues: {rawType}')
168
+ if rawType == SpecialTypes.FILE:
169
+ return RenderableType(rawType=RenderableRawTypes.FILE, isNullable=isNullable, isList=isList)
154
170
  if rawType in PrimitiveTypes.ALL:
155
171
  return RenderableType(rawType=rawType, isNullable=isNullable, isList=isList)
156
172
  if rawType in knownTypeNames:
157
- return RenderableType(rawType='class', isNullable=isNullable, className=RenderableString.from_pascal_case_string(string=rawType), isList=isList)
173
+ return RenderableType(rawType=knownTypeNames[rawType], isNullable=isNullable, className=RenderableString.from_pascal_case_string(string=rawType), isList=isList)
158
174
  raise KibaException(message=f'Unknown type: {rawType}')
159
175
 
160
- def _request_field_to_renderable_parameter(self, requestField: RequestField, knownTypeNames: set[str]) -> RenderableParameter:
176
+ def _request_field_to_renderable_parameter(self, requestField: RequestField, knownTypeNames: dict[str, str]) -> RenderableParameter:
161
177
  return RenderableParameter(
162
178
  name=RenderableString.from_kebab_case_string(string=requestField.name),
163
179
  fieldType=self._type_to_renderable_type(
@@ -168,6 +184,7 @@ class Renderer:
168
184
  listValueTypes=requestField.listValueTypes,
169
185
  mappingKeyType=requestField.mappingKeyType,
170
186
  mappingValueTypes=requestField.mappingValueTypes,
187
+ enumValues=requestField.enumValues,
171
188
  ),
172
189
  defaultValue=requestField.defaultValue,
173
190
  maxValue=requestField.maxValue,
@@ -177,7 +194,7 @@ class Renderer:
177
194
  source=requestField.source,
178
195
  )
179
196
 
180
- def _response_field_to_renderable_field(self, responseField: ResponseField, knownTypeNames: set[str]) -> RenderableField:
197
+ def _response_field_to_renderable_field(self, responseField: ResponseField, knownTypeNames: dict[str, str]) -> RenderableField:
181
198
  return RenderableField(
182
199
  name=RenderableString.from_kebab_case_string(string=responseField.name),
183
200
  fieldType=self._type_to_renderable_type(
@@ -188,10 +205,11 @@ class Renderer:
188
205
  listValueTypes=responseField.listValueTypes,
189
206
  mappingKeyType=responseField.mappingKeyType,
190
207
  mappingValueTypes=responseField.mappingValueTypes,
208
+ enumValues=responseField.enumValues,
191
209
  ),
192
210
  )
193
211
 
194
- def _attribute_to_renderable_attribute(self, attribute: Attribute, knownTypeNames: set[str], isNullable: bool | None = None) -> RenderableAttribute:
212
+ def _attribute_to_renderable_attribute(self, attribute: Attribute, knownTypeNames: dict[str, str], isNullable: bool | None = None) -> RenderableAttribute:
195
213
  return RenderableAttribute(
196
214
  name=RenderableString.from_kebab_case_string(string=attribute.name),
197
215
  fieldType=self._type_to_renderable_type(
@@ -202,6 +220,7 @@ class Renderer:
202
220
  listValueTypes=attribute.listValueTypes,
203
221
  mappingKeyType=attribute.mappingKeyType,
204
222
  mappingValueTypes=attribute.mappingValueTypes,
223
+ enumValues=attribute.enumValues,
205
224
  ),
206
225
  defaultValue=attribute.defaultValue,
207
226
  isUnique=attribute.isUnique,
@@ -212,11 +231,18 @@ class Renderer:
212
231
  isNullable=attribute.isNullable,
213
232
  )
214
233
 
215
- def _attribute_to_renderable_parameter(self, attribute: Attribute, isRequired: bool, knownTypeNames: set[str], shouldUseDefault: bool = True) -> RenderableParameter:
234
+ def _attribute_to_renderable_parameter(self, attribute: Attribute, isRequired: bool, knownTypeNames: dict[str, str], shouldUseDefault: bool = True) -> RenderableParameter:
216
235
  return RenderableParameter(
217
236
  name=RenderableString.from_kebab_case_string(string=attribute.name),
218
237
  fieldType=self._type_to_renderable_type(
219
- rawType=attribute.fieldType, isNullable=attribute.isNullable, isList=False, knownTypeNames=knownTypeNames, listValueTypes=attribute.listValueTypes, mappingKeyType=attribute.mappingKeyType, mappingValueTypes=attribute.mappingValueTypes
238
+ rawType=attribute.fieldType,
239
+ isNullable=attribute.isNullable,
240
+ isList=False,
241
+ knownTypeNames=knownTypeNames,
242
+ listValueTypes=attribute.listValueTypes,
243
+ mappingKeyType=attribute.mappingKeyType,
244
+ mappingValueTypes=attribute.mappingValueTypes,
245
+ enumValues=attribute.enumValues,
220
246
  ),
221
247
  defaultValue=attribute.defaultValue if shouldUseDefault else None,
222
248
  isRequired=isRequired,
@@ -227,7 +253,7 @@ class Renderer:
227
253
  def _resource_full_name(resource: Resource, name: str) -> str:
228
254
  return f'{resource.parentResource or ""}{name}'
229
255
 
230
- def _resource_to_renderable_resource(self, resource: Resource, knownTypeNames: set[str]) -> RenderableResource:
256
+ def _resource_to_renderable_resource(self, resource: Resource, knownTypeNames: dict[str, str]) -> RenderableResource:
231
257
  attributes = [self._attribute_to_renderable_attribute(attribute=attribute, knownTypeNames=knownTypeNames) for attribute in resource.attributes]
232
258
  return RenderableResource(
233
259
  singleName=RenderableString.from_pascal_case_string(string=self._resource_full_name(resource=resource, name=resource.singleName)),
@@ -239,7 +265,7 @@ class Renderer:
239
265
  accessibleFrom=resource.accessibleFrom,
240
266
  )
241
267
 
242
- def _get_processable_resource(self, renderableResource: RenderableResource, allResources: list[RenderableResource], knownTypeNames: set[str]) -> ProcessableResource:
268
+ def _get_processable_resource(self, renderableResource: RenderableResource, allResources: list[RenderableResource], knownTypeNames: dict[str, str]) -> ProcessableResource:
243
269
  parent: ProcessableResource.Parent | None = None
244
270
  attributeOccurrences: list[ProcessableResource.IndependentAttributeOccurrence] = []
245
271
  parentResource = next(iter([otherResource for otherResource in allResources if renderableResource.parentResource and otherResource.singleName.pascalCaseString == renderableResource.parentResource]), None)
@@ -249,7 +275,7 @@ class Renderer:
249
275
  parent = ProcessableResource.Parent(processableResource=parentProcessableResource, isOneToOne=isOneToOne)
250
276
  for otherResource in allResources:
251
277
  for attribute in otherResource.independentAttributes:
252
- if attribute.fieldType.rawType == 'class' and attribute.fieldType.className == renderableResource.singleName:
278
+ if attribute.fieldType.rawType == RenderableRawTypes.CLASS and attribute.fieldType.className == renderableResource.singleName:
253
279
  occursInResource = self._get_processable_resource(renderableResource=otherResource, allResources=allResources, knownTypeNames=knownTypeNames)
254
280
  occursInAttribute = attribute
255
281
  attributeOccurrences.append(ProcessableResource.IndependentAttributeOccurrence(processableResource=occursInResource, attribute=occursInAttribute))
@@ -328,13 +354,36 @@ class Renderer:
328
354
  errors.append(f'{name}: stream transitions need a responseField')
329
355
  if operation.responseType == ResponseTypes.REDIRECT and operation.responseFields:
330
356
  errors.append(f'{name}: redirect transitions cannot have a responseField')
357
+ errors += [f'{name}: response field {field.name.kebabCaseString} cannot be a File' for field in operation.responseFields or [] if field.fieldType.rawType == RenderableRawTypes.FILE]
358
+ if any(parameter.fieldType.rawType == RenderableRawTypes.FILE for parameter in operation.parameters):
359
+ if operation.method.method.upperCaseString != 'POST':
360
+ errors.append(f'{name}: only POST transitions can have File fields')
361
+ if operation.responseType == ResponseTypes.STREAM:
362
+ errors.append(f'{name}: stream transitions cannot have File fields')
363
+ errors += [
364
+ f'{name}: field {parameter.name.kebabCaseString} cannot be sent with File fields, only single primitive, enum and File values can'
365
+ for parameter in operation.parameters
366
+ if parameter.source == FieldSources.REQUEST and (parameter.fieldType.isList or isinstance(parameter.fieldType, MappingRenderableType) or parameter.fieldType.rawType not in FORM_FIELD_RAW_TYPES)
367
+ ]
368
+ return errors
369
+
370
+ @staticmethod
371
+ def _file_errors(resources: list[RenderableResource], exceptions: list[RenderableException]) -> list[str]:
372
+ errors = [
373
+ f'Attribute {attribute.name.kebabCaseString} of {resource.singleName.pascalCaseString} cannot be a File'
374
+ for resource in resources
375
+ for attribute in [*resource.attributes, *resource.independentAttributes]
376
+ if attribute.fieldType.rawType == RenderableRawTypes.FILE
377
+ ]
378
+ errors += [f'Field {field.name.snakeCaseString} of {exception.name.pascalCaseString} cannot be a File' for exception in exceptions for field in exception.fields if field.fieldType.rawType == RenderableRawTypes.FILE]
331
379
  return errors
332
380
 
333
381
  def build_renderable_api(self, api: KibaApi) -> RenderableApi:
334
382
  fullResourceNames = [self._resource_full_name(resource=resource, name=resource.singleName) for resource in api.resources]
335
- knownTypeNames = set(fullResourceNames)
336
- resourceErrors = [f'Duplicate resource name: {name}' for name, count in collections.Counter(fullResourceNames).items() if count > 1]
337
- resourceErrors += [f'Unknown parent resource {resource.parentResource} for {resource.singleName}' for resource in api.resources if resource.parentResource is not None and resource.parentResource not in knownTypeNames]
383
+ enumNames = [enum.name for enum in api.enums]
384
+ knownTypeNames = {**dict.fromkeys(fullResourceNames, RenderableRawTypes.CLASS), **dict.fromkeys(enumNames, RenderableRawTypes.ENUM)}
385
+ resourceErrors = [f'Duplicate type name: {name}' for name, count in collections.Counter(fullResourceNames + enumNames).items() if count > 1]
386
+ resourceErrors += [f'Unknown parent resource {resource.parentResource} for {resource.singleName}' for resource in api.resources if resource.parentResource is not None and resource.parentResource not in fullResourceNames]
338
387
  resourcesByName = dict(zip(fullResourceNames, api.resources, strict=False))
339
388
  for resource, fullName in zip(api.resources, fullResourceNames, strict=True):
340
389
  ancestorNames: list[str] = []
@@ -389,6 +438,7 @@ class Renderer:
389
438
  resources=[],
390
439
  operations=renderableOperations,
391
440
  exceptions=renderableExceptions,
441
+ enums=[RenderableEnum(name=RenderableString.from_pascal_case_string(string=enum.name), values=enum.values) for enum in api.enums],
392
442
  )
393
443
  renderableResources: list[RenderableResource] = [self._resource_to_renderable_resource(resource=resource, knownTypeNames=knownTypeNames) for resource in api.resources]
394
444
  uncoveredTransitions: list[str] = []
@@ -466,6 +516,7 @@ class Renderer:
466
516
  message=f'Transitions with no endpoint: {", ".join(uncoveredTransitions)}. Item transitions need a primary attribute, collection transitions need direct access or a parent resource, and CREATE is not supported on one-to-one child resources.'
467
517
  )
468
518
  operationErrors = [error for operation in renderableApi.operations for error in self._operation_errors(operation=operation)]
519
+ operationErrors += self._file_errors(resources=renderableResources, exceptions=renderableExceptions)
469
520
  if operationErrors:
470
521
  raise SemanticValidationException(message='\n'.join(operationErrors))
471
522
  renderableApi.resources.extend(renderableResources)
@@ -9,10 +9,12 @@ from geniespec.renderable_types import AttributeEndpointComponent
9
9
  from geniespec.renderable_types import MappingRenderableType
10
10
  from geniespec.renderable_types import RenderableApi
11
11
  from geniespec.renderable_types import RenderableEndpointComponent
12
+ from geniespec.renderable_types import RenderableEnum
12
13
  from geniespec.renderable_types import RenderableException
13
14
  from geniespec.renderable_types import RenderableField
14
15
  from geniespec.renderable_types import RenderableOperation
15
16
  from geniespec.renderable_types import RenderableParameter
17
+ from geniespec.renderable_types import RenderableRawTypes
16
18
  from geniespec.renderable_types import RenderableResource
17
19
  from geniespec.renderable_types import RenderableString
18
20
  from geniespec.renderable_types import RenderableType
@@ -72,7 +74,10 @@ class BaseLanguageDefinition:
72
74
  def string_type(self, isNullable: bool) -> str:
73
75
  raise NotImplementedError
74
76
 
75
- def enum_type(self, isNullable: bool, values: list[str]) -> str:
77
+ def enum_type(self, isNullable: bool, renderableType: RenderableType, apiVersion: Version, isLocalReference: bool) -> str:
78
+ raise NotImplementedError
79
+
80
+ def file_type(self, isNullable: bool) -> str:
76
81
  raise NotImplementedError
77
82
 
78
83
  def package_name(self, api: RenderableApi) -> str:
@@ -118,14 +123,10 @@ class BaseLanguageDefinition:
118
123
  return self.date_type(isNullable=isNullable)
119
124
  if yamlType == PrimitiveTypes.JSON:
120
125
  return self.json_type(isNullable=isNullable)
121
- # if yamlType == PrimitiveTypes.STRUCT:
122
- # return self.struct_type(isNullable=isNullable)
123
- # if yamlType.startswith(PrimitiveTypes.ENUM):
124
- # values = ''.join(yamlType.replace('Enum[', '', 1).rsplit(']', 1)).split(', ')
125
- # return self.enum_type(isNullable=isNullable, values=[RenderableString.from_kebab_case_string(string=value) for value in values])
126
- # if yamlType.startswith(PrimitiveTypes.ONE_OF):
127
- # subTypes = self._render_one_of_type_string(oneOfTypeString=yamlType, isNullable=isNullable, apiVersion=apiVersion)
128
- # return self.one_of_type(isNullable=isNullable, subTypes=subTypes)
126
+ if yamlType == RenderableRawTypes.ENUM:
127
+ return self.enum_type(isNullable=isNullable, renderableType=renderableType, apiVersion=apiVersion, isLocalReference=isLocalReference)
128
+ if yamlType == RenderableRawTypes.FILE:
129
+ return self.file_type(isNullable=isNullable)
129
130
  if yamlType == ComplexTypes.MAPPING and isinstance(renderableType, MappingRenderableType):
130
131
  keyType = self.render_type(renderableType=renderableType.keyType, apiVersion=apiVersion, isLocalReference=isLocalReference)
131
132
  valueType = self.render_type(renderableType=renderableType.valueType, apiVersion=apiVersion, isLocalReference=isLocalReference)
@@ -171,6 +172,17 @@ class BaseLanguageDefinition:
171
172
  def exception_class_name(self, exception: RenderableException, apiVersion: Version) -> str:
172
173
  return f'{self.pascal_name(name=exception.name)}V{apiVersion.major}'
173
174
 
175
+ def enum_class_name(self, enum: RenderableEnum, apiVersion: Version) -> str:
176
+ return f'{self.pascal_name(name=enum.name)}V{apiVersion.major}'
177
+
178
+ @staticmethod
179
+ def is_inline_enum(renderableType: RenderableType) -> bool:
180
+ return renderableType.rawType == RenderableRawTypes.ENUM and renderableType.className is None
181
+
182
+ @staticmethod
183
+ def is_file_operation(operation: RenderableOperation) -> bool:
184
+ return any(parameter.fieldType.rawType == RenderableRawTypes.FILE for parameter in operation.parameters)
185
+
174
186
  def type(self, renderableType: RenderableType, apiVersion: Version, isLocalReference: bool = False) -> str:
175
187
  return self.render_type(renderableType=renderableType, apiVersion=apiVersion, isLocalReference=isLocalReference)
176
188
 
@@ -334,9 +346,19 @@ class PythonLanguageDefinition(BaseLanguageDefinition):
334
346
  def string_type(self, isNullable: bool) -> str:
335
347
  return 'str | None' if isNullable else 'str'
336
348
 
337
- def enum_type(self, isNullable: bool, values: list[str]) -> str:
338
- valueString = ', '.join([f"'{value}'" for value in values])
339
- return f'typing.Literal[{valueString}] | None' if isNullable else f'Literal[{valueString} | None'
349
+ def enum_type(self, isNullable: bool, renderableType: RenderableType, apiVersion: Version, isLocalReference: bool) -> str:
350
+ if renderableType.className is None:
351
+ typeName = self.enum_literal(values=renderableType.enumValues)
352
+ else:
353
+ typeName = f'{renderableType.className.pascalCaseString}V{apiVersion.major}' if isLocalReference else f'resources.{renderableType.className.pascalCaseString}V{apiVersion.major}'
354
+ return f'{typeName} | None' if isNullable else typeName
355
+
356
+ @staticmethod
357
+ def enum_literal(values: list[str]) -> str:
358
+ return f'typing.Literal[{", ".join(repr(value) for value in values)}]'
359
+
360
+ def file_type(self, isNullable: bool) -> str:
361
+ return 'UploadedFile | None' if isNullable else 'UploadedFile'
340
362
 
341
363
  def package_name(self, api: RenderableApi) -> str:
342
364
  return f'{self.snake_name(name=api.serviceName)}_api'
@@ -393,6 +415,16 @@ class PythonLanguageDefinition(BaseLanguageDefinition):
393
415
  def resources_require_json(self, api: RenderableApi) -> bool:
394
416
  return any(self._contains_json(renderableType=attribute.fieldType) for resource in api.resources for attribute in resource.attributes)
395
417
 
418
+ def resources_require_typing(self, api: RenderableApi) -> bool:
419
+ return len(api.enums) > 0 or any(self.is_inline_enum(renderableType=attribute.fieldType) for resource in api.resources for attribute in resource.attributes)
420
+
421
+ def operations_require_typing(self, api: RenderableApi) -> bool:
422
+ 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 []]
423
+ return any(self.is_inline_enum(renderableType=fieldType) for fieldType in fieldTypes)
424
+
425
+ def requires_uploaded_file(self, api: RenderableApi) -> bool:
426
+ return any(self.is_file_operation(operation=operation) for operation in api.operations)
427
+
396
428
  def operations_require_json(self, api: RenderableApi) -> bool:
397
429
  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 []]
398
430
  return any(self._contains_json(renderableType=fieldType) for fieldType in fieldTypes)
@@ -516,6 +548,19 @@ class TypeScriptLanguageDefinition(BaseLanguageDefinition):
516
548
  def string_type(self, isNullable: bool) -> str:
517
549
  return self._nullable(typeName='string', isNullable=isNullable)
518
550
 
551
+ def enum_type(self, isNullable: bool, renderableType: RenderableType, apiVersion: Version, isLocalReference: bool) -> str:
552
+ if renderableType.className is None:
553
+ return self._nullable(typeName=f'({self.enum_union(values=renderableType.enumValues)})', isNullable=isNullable)
554
+ typeName = f'{renderableType.className.pascalCaseString}V{apiVersion.major}'
555
+ return self._nullable(typeName=typeName if isLocalReference else f'Resources.{typeName}', isNullable=isNullable)
556
+
557
+ @staticmethod
558
+ def enum_union(values: list[str]) -> str:
559
+ return ' | '.join(f"'{value}'" for value in values)
560
+
561
+ def file_type(self, isNullable: bool) -> str:
562
+ return self._nullable(typeName='File', isNullable=isNullable)
563
+
519
564
  def package_name(self, api: RenderableApi) -> str:
520
565
  return f'{self.kebab_name(name=api.serviceName)}-api'
521
566
 
@@ -566,7 +611,11 @@ class TypeScriptLanguageDefinition(BaseLanguageDefinition):
566
611
  return f'on{self.pascal_name(name=responseField.name)}'
567
612
 
568
613
  def references_resources(self, renderableTypes: list[RenderableType]) -> bool:
569
- return any(self._contains_raw_type(renderableType=renderableType, rawType=rawType) for renderableType in renderableTypes for rawType in ('class', PrimitiveTypes.JSON))
614
+ valueTypes = [self._value_type(renderableType=renderableType) for renderableType in renderableTypes]
615
+ return any(valueType.rawType in ('class', PrimitiveTypes.JSON) or (valueType.rawType == RenderableRawTypes.ENUM and valueType.className is not None) for valueType in valueTypes)
616
+
617
+ def _value_type(self, renderableType: RenderableType) -> RenderableType:
618
+ return self._value_type(renderableType=renderableType.valueType) if isinstance(renderableType, MappingRenderableType) else renderableType
570
619
 
571
620
  def requires_date_parsing(self, renderableTypes: list[RenderableType]) -> bool:
572
621
  return any(self._contains_raw_type(renderableType=renderableType, rawType=rawType) for renderableType in renderableTypes for rawType in (PrimitiveTypes.DATE, PrimitiveTypes.DATETIME))
@@ -580,6 +629,18 @@ class TypeScriptLanguageDefinition(BaseLanguageDefinition):
580
629
  def body_parameter_types(self, api: RenderableApi) -> list[RenderableType]:
581
630
  return [parameter.fieldType for operation in self.client_operations(api=api) for parameter in self.body_parameters(operation=operation)]
582
631
 
632
+ def requires_request_data(self, api: RenderableApi) -> bool:
633
+ return any(self.body_parameters(operation=operation) and not self.is_file_operation(operation=operation) for operation in self.client_operations(api=api))
634
+
635
+ def form_value(self, renderableType: RenderableType, value: str) -> str:
636
+ if renderableType.rawType == RenderableRawTypes.FILE:
637
+ return value
638
+ if renderableType.rawType == PrimitiveTypes.DATETIME:
639
+ return f'dateToString({value})'
640
+ if renderableType.rawType == PrimitiveTypes.DATE:
641
+ return f"dateToString({value}, 'yyyy-MM-dd')"
642
+ return f'String({value})'
643
+
583
644
  def _contains_raw_type(self, renderableType: RenderableType, rawType: str) -> bool:
584
645
  if isinstance(renderableType, MappingRenderableType):
585
646
  return self._contains_raw_type(renderableType=renderableType.valueType, rawType=rawType)
@@ -609,6 +670,8 @@ class TypeScriptLanguageDefinition(BaseLanguageDefinition):
609
670
  return f'dateFromString(String({value}))'
610
671
  if renderableType.rawType == PrimitiveTypes.JSON:
611
672
  return f'{value} as {self.type(renderableType=renderableType, apiVersion=apiVersion, isLocalReference=isLocalReference)}'
673
+ if renderableType.rawType == RenderableRawTypes.ENUM:
674
+ return f'String({value}) as {self.type(renderableType=renderableType, apiVersion=apiVersion, isLocalReference=isLocalReference)}'
612
675
  raise KibaException(f'Failed to render conversion from object for: {renderableType}')
613
676
 
614
677
  def _is_serialized_as_is(self, renderableType: RenderableType) -> bool:
@@ -87,11 +87,23 @@ class RenderableString(BaseModel):
87
87
  return cls(string=string.split(separator))
88
88
 
89
89
 
90
+ class RenderableRawTypes:
91
+ CLASS = 'class'
92
+ ENUM = 'enum'
93
+ FILE = 'file'
94
+
95
+
90
96
  class RenderableType(BaseModel):
91
97
  rawType: str
92
98
  isNullable: bool
93
99
  isList: bool = False
94
100
  className: RenderableString | None = None
101
+ enumValues: list[str] = []
102
+
103
+
104
+ class RenderableEnum(BaseModel):
105
+ name: RenderableString
106
+ values: list[str]
95
107
 
96
108
 
97
109
  class MappingRenderableType(RenderableType):
@@ -225,6 +237,7 @@ class RenderableApi(BaseModel):
225
237
  resources: list[RenderableResource]
226
238
  operations: list[RenderableOperation]
227
239
  exceptions: list[RenderableException]
240
+ enums: list[RenderableEnum] = []
228
241
 
229
242
 
230
243
  class ProcessableResource(BaseModel):
@@ -1,7 +1,7 @@
1
1
  {{ autogenerated_notice }}
2
2
  import collections.abc
3
3
  import datetime
4
- {% if target.requires_optional_field_default(api=api) %}
4
+ {% if target.requires_optional_field_default(api=api) or target.operations_require_typing(api=api) %}
5
5
  import typing
6
6
  {% endif %}
7
7
 
@@ -16,6 +16,9 @@ from pydantic import BaseModel
16
16
  {% if target.requires_undefined(api=api) %}
17
17
  from core.util.typing_util import UNDEFINED, UpdateValue
18
18
  {% endif %}
19
+ {% if target.requires_uploaded_file(api=api) %}
20
+ from core.api.uploaded_file import UploadedFile
21
+ {% endif %}
19
22
 
20
23
  from . import resources
21
24
 
@@ -2,6 +2,9 @@
2
2
  import abc
3
3
  import datetime
4
4
  import collections.abc
5
+ {% if target.operations_require_typing(api=api) %}
6
+ import typing
7
+ {% endif %}
5
8
 
6
9
  {% if target.operations_require_json(api=api) %}
7
10
  import pydantic
@@ -11,6 +14,9 @@ from . import resources
11
14
  {% if target.requires_undefined(api=api) %}
12
15
  from core.util.typing_util import UNDEFINED, UpdateValue
13
16
  {% endif %}
17
+ {% if target.requires_uploaded_file(api=api) %}
18
+ from core.api.uploaded_file import UploadedFile
19
+ {% endif %}
14
20
 
15
21
 
16
22
  class {{ target.internal_class_name(api=api) }}(abc.ABC):
@@ -3,12 +3,21 @@ from __future__ import annotations
3
3
 
4
4
  import collections.abc
5
5
  import datetime
6
+ {% if target.resources_require_typing(api=api) %}
7
+ import typing
8
+ {% endif %}
6
9
 
7
10
  {% if target.resources_require_json(api=api) %}
8
11
  import pydantic
9
12
  {% endif %}
10
13
  from pydantic import BaseModel
11
14
 
15
+ {% for enum in api.enums %}
16
+ {{ target.enum_class_name(enum=enum, apiVersion=api.version) }} = {{ target.enum_literal(values=enum.values) }}
17
+ {% if loop.last %}
18
+
19
+ {% endif %}
20
+ {% endfor %}
12
21
  {% for resource in api.resources %}
13
22
 
14
23
  class {{ target.resource_class_name(resource=resource, apiVersion=api.version) }}(BaseModel):
@@ -21,7 +21,14 @@ export class {{ target.client_class_name(api=api) }} extends ServiceClient {
21
21
  {% if not loop.first %}
22
22
 
23
23
  {% endif %}
24
- {% if target.is_streaming(operation=operation) %}
24
+ {% if target.is_file_operation(operation=operation) %}
25
+ public {{ target.operation_name(operation=operation) }} = async ({{ arguments | join(', ') }}): Promise<{{ target.type(renderableType=responseField.fieldType, apiVersion=api.version) if responseField else 'void' }}> => {
26
+ {% if responseField %}const response = {% endif %}await this.requester.makeFormRequest(`${this.baseUrl}/{{ path }}`, {{ request }}.toFormData());
27
+ {% if responseField %}
28
+ return Endpoints.{{ className }}Response.fromObject(JSON.parse(response.content)).{{ target.identifier(name=responseField.name) }};
29
+ {% endif %}
30
+ };
31
+ {% elif target.is_streaming(operation=operation) %}
25
32
  public {{ target.operation_name(operation=operation) }} = async ({{ arguments | join(', ') }}): Promise<void> => {
26
33
  await this.makeStreamingRequest(RestMethod.{{ target.http_method(operation=operation) }}, {{ pathLiteral }}, {{ request }}, Endpoints.{{ className }}Response, (response: Endpoints.{{ className }}Response): void => {
27
34
  {{ target.stream_callback_name(operation=operation) }}(response.{{ target.identifier(name=responseField.name) }});
@@ -5,7 +5,7 @@
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
7
  {% set _ = coreImports.append('RawObject') %}
8
- {% if bodyTypes %}{% set _ = coreImports.append('RequestData') %}{% endif %}
8
+ {% if target.requires_request_data(api=api) %}{% set _ = coreImports.append('RequestData') %}{% endif %}
9
9
  {% if responseTypes %}{% set _ = coreImports.append('ResponseData') %}{% endif %}
10
10
  import { {{ coreImports | join(', ') }} } from '@kibalabs/core';
11
11
  {% if target.references_resources(renderableTypes=responseTypes + bodyTypes) %}
@@ -24,6 +24,29 @@ export interface {{ className }}Params {
24
24
  {% endfor %}
25
25
  }
26
26
 
27
+ {% if target.is_file_operation(operation=operation) %}
28
+ export class {{ className }}Request {
29
+ public constructor(
30
+ readonly params: {{ className }}Params,
31
+ ) { }
32
+
33
+ public toFormData = (): FormData => {
34
+ const formData = new FormData();
35
+ {% for parameter in bodyParameters %}
36
+ {% set parameterName = target.identifier(name=parameter.name) %}
37
+ {% set value = 'this.params.' ~ parameterName %}
38
+ {% if target.is_parameter_required(operation=operation, parameter=parameter) and not target.parameter_field_type(operation=operation, parameter=parameter).isNullable %}
39
+ formData.append('{{ parameterName }}', {{ target.form_value(renderableType=parameter.fieldType, value=value) }});
40
+ {% else %}
41
+ if ({{ value }} != null) {
42
+ formData.append('{{ parameterName }}', {{ target.form_value(renderableType=parameter.fieldType, value=value) }});
43
+ }
44
+ {% endif %}
45
+ {% endfor %}
46
+ return formData;
47
+ };
48
+ }
49
+ {% else %}
27
50
  export class {{ className }}Request extends RequestData {
28
51
  public constructor(
29
52
  readonly params: {{ className }}Params,
@@ -40,6 +63,7 @@ export class {{ className }}Request extends RequestData {
40
63
  };
41
64
  }
42
65
  {% endif %}
66
+ {% endif %}
43
67
  {% if responseField %}
44
68
 
45
69
  export class {{ className }}Response extends ResponseData {
@@ -6,6 +6,10 @@ import { RawObject } from '@kibalabs/core';
6
6
  {% endif %}
7
7
 
8
8
  export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
9
+ {% for enum in api.enums %}
10
+
11
+ export type {{ target.enum_class_name(enum=enum, apiVersion=api.version) }} = {{ target.enum_union(values=enum.values) }};
12
+ {% endfor %}
9
13
  {% for resource in api.resources %}
10
14
  {% set className = target.resource_class_name(resource=resource, apiVersion=api.version) %}
11
15
 
@@ -35,12 +35,25 @@ class ComplexTypes:
35
35
  ALL: ClassVar[list[str]] = [MAPPING]
36
36
 
37
37
 
38
+ class SpecialTypes:
39
+ LIST = 'List'
40
+ ENUM = 'Enum'
41
+ FILE = 'File'
42
+ NULL = 'Null'
43
+
44
+
45
+ class ApiEnum(BaseModel):
46
+ name: str
47
+ values: list[str]
48
+
49
+
38
50
  class Attribute(BaseModel):
39
51
  name: str
40
52
  fieldType: str = pydantic.Field(alias='type')
41
53
  listValueTypes: list[str] = []
42
54
  mappingKeyType: str | None = None
43
55
  mappingValueTypes: list[str] = []
56
+ enumValues: list[str] = []
44
57
  isPrimary: bool = False
45
58
  isUnique: bool = False
46
59
  shouldDisableGetEndpoint: bool = False
@@ -59,6 +72,7 @@ class ResponseField(BaseModel):
59
72
  listValueTypes: list[str] = []
60
73
  mappingKeyType: str | None = None
61
74
  mappingValueTypes: list[str] = []
75
+ enumValues: list[str] = []
62
76
  isNullable: bool = False
63
77
  isList: bool = False
64
78
 
@@ -69,6 +83,7 @@ class RequestField(BaseModel):
69
83
  listValueTypes: list[str] = []
70
84
  mappingKeyType: str | None = None
71
85
  mappingValueTypes: list[str] = []
86
+ enumValues: list[str] = []
72
87
  isRequired: bool = False
73
88
  isDeprecated: bool = False
74
89
  defaultValue: str | int | float | bool | None = None
@@ -139,6 +154,7 @@ class KibaApi(BaseModel):
139
154
  resources: list[Resource] = []
140
155
  transitions: list[RootTransition] = []
141
156
  exceptions: list[ApiException] = []
157
+ enums: list[ApiEnum] = []
142
158
  defaultAuthentication: str | None = None
143
159
 
144
160
  @property