geniespec 0.1.1.dev2__py3-none-any.whl

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.
@@ -0,0 +1,68 @@
1
+ {{ autogenerated_notice }}
2
+ # ruff: noqa: ARG001
3
+ {% if target.requires_streaming(api=api) %}
4
+ import collections.abc
5
+ {% endif %}
6
+ import datetime
7
+
8
+ from core.api.api_request import KibaApiRequest
9
+ from core.api.route import route
10
+ from core.api.route_auth import RouteAuthResolver
11
+ {% if target.requires_redirect(api=api) %}
12
+ from core.exceptions import FoundRedirectException
13
+ {% endif %}
14
+ from core.http.rest_method import RestMethod
15
+ from starlette.routing import BaseRoute
16
+ from starlette.routing import Route
17
+ from . import endpoints
18
+ from .internal import {{ target.internal_class_name(api=api) }}
19
+ def create_routes(internal: {{ target.internal_class_name(api=api) }}, {% if target.requires_auth_resolver(api=api) %}authResolver: RouteAuthResolver{% else %}authResolver: RouteAuthResolver | None = None{% endif %}) -> list[BaseRoute]:
20
+
21
+
22
+ {% for operation in api.operations %}
23
+ {% set auth = target.route_auth(operation=operation) %}
24
+ {% set class_name = target.operation_class_name(operation=operation) %}
25
+ {% set result_name = target.identifier(name=operation.responseFields[0].name) if operation.responseFields else 'result' %}
26
+ @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 %})
27
+ 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 %}:
28
+ {% for parameter in target.request_parameters(operation=operation) %}
29
+ {% set param_name = target.identifier(name=parameter.name) %}
30
+ {% if target.requires_parameter_preprocessing(operation=operation, parameter=parameter) %}
31
+ {{ param_name }} = request.data.{{ param_name }}
32
+ {% if parameter.defaultValue is not none and not target.is_undefined_parameter(operation=operation, parameter=parameter) %}
33
+ {{ param_name }} = {{ param_name }} if {{ param_name }} is not None else {{ target.literal(value=parameter.defaultValue) }}
34
+ {% endif %}
35
+ {% if parameter.minValue is not none %}
36
+ {{ param_name }} = max({{ param_name }}, {{ target.literal(value=parameter.minValue) }}){% if parameter.fieldType.isNullable and parameter.defaultValue is none %} if {{ param_name }} is not None else None{% endif +%}
37
+ {% endif %}
38
+ {% if parameter.maxValue is not none %}
39
+ {{ param_name }} = min({{ param_name }}, {{ target.literal(value=parameter.maxValue) }}){% if parameter.fieldType.isNullable and parameter.defaultValue is none %} if {{ param_name }} is not None else None{% endif +%}
40
+ {% endif %}
41
+ {% endif %}
42
+ {% endfor %}
43
+ {% if target.is_streaming(operation=operation) %}
44
+ async for item in internal.{{ target.operation_name(operation=operation) }}_v{{ api.version.major }}(
45
+ {% else %}
46
+ {% if operation.responseFields or target.is_redirect(operation=operation) %}{{ result_name }} = {% endif %}await internal.{{ target.operation_name(operation=operation) }}_v{{ api.version.major }}(
47
+ {% endif %}
48
+ {% for parameter in operation.parameters %}
49
+ {{ target.identifier(name=parameter.name) }}={{ target.parameter_value(operation=operation, parameter=parameter) }},
50
+ {% endfor %}
51
+ {% if target.is_streaming(operation=operation) %}
52
+ ):
53
+ yield endpoints.{{ class_name }}Response({{ result_name }}=item)
54
+ {% else %}
55
+ )
56
+ {% endif %}
57
+ {% if target.is_redirect(operation=operation) %}
58
+ raise FoundRedirectException(location={{ result_name }})
59
+ {% elif not target.is_streaming(operation=operation) %}
60
+ return endpoints.{{ class_name }}Response({% if operation.responseFields %}{{ result_name }}={{ result_name }}{% endif %})
61
+ {% endif %}
62
+
63
+ {% endfor %}
64
+ return [
65
+ {% for operation in target.route_ordered_operations(api=api) %}
66
+ Route(path='{{ target.endpoint(operation=operation) }}', methods=[RestMethod.{{ target.http_method(operation=operation) }}], endpoint={{ target.operation_name(operation=operation) }}),
67
+ {% endfor %}
68
+ ]
@@ -0,0 +1,38 @@
1
+ {{ autogenerated_notice }}
2
+ import collections.abc
3
+ import datetime
4
+ {% if target.requires_optional_field_default(api=api) %}
5
+ import typing
6
+ {% endif %}
7
+
8
+ {% if target.operations_require_json(api=api) %}
9
+ import pydantic
10
+ {% endif %}
11
+ {% if target.requires_optional_field_default(api=api) %}
12
+ from pydantic import BaseModel, Field
13
+ {% else %}
14
+ from pydantic import BaseModel
15
+ {% endif %}
16
+ {% if target.requires_undefined(api=api) %}
17
+ from core.util.typing_util import UNDEFINED, UpdateValue
18
+ {% endif %}
19
+
20
+ from . import resources
21
+
22
+ {% for operation in api.operations %}
23
+
24
+ class {{ target.operation_class_name(operation=operation) }}Request(BaseModel):
25
+ {% for parameter in target.request_parameters(operation=operation) %}
26
+ {{ 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 +%}
27
+ {% else %}
28
+ pass
29
+ {% endfor %}
30
+
31
+ class {{ target.operation_class_name(operation=operation) }}Response(BaseModel):
32
+ {% if operation.responseFields and operation.responseFields | length > 0 %}
33
+ {{ target.identifier(name=operation.responseFields[0].name) }}: {{ target.type(renderableType=operation.responseFields[0].fieldType, apiVersion=api.version) }}
34
+ {% else %}
35
+ pass
36
+ {% endif %}
37
+
38
+ {% endfor %}
@@ -0,0 +1,68 @@
1
+ {{ autogenerated_notice }}
2
+ from __future__ import annotations
3
+ import collections.abc
4
+ import typing
5
+ {% if target.exceptions_require_json(api=api) %}
6
+ import pydantic
7
+ {% endif %}
8
+ from core import exceptions
9
+ from core.util.typing_util import Json
10
+ from . import resources
11
+
12
+
13
+ class KibaExceptionTypedDict(typing.TypedDict):
14
+ message: str
15
+ statusCode: int
16
+ exceptionType: str
17
+
18
+ {% for exception in api.exceptions %}
19
+
20
+ class {{ target.pascal_name(name=exception.name) }}FieldsDict(typing.TypedDict):
21
+ {% for field in exception.fields %}
22
+ {{ target.identifier(name=field.name) }}: {{ target.type(renderableType=field.fieldType, apiVersion=api.version) }}{% if not field.isRequired %} = None{% endif %}
23
+ {% else %}
24
+ pass
25
+ {% endfor %}
26
+
27
+
28
+ class {{ target.pascal_name(name=exception.name) }}TypedDict(KibaExceptionTypedDict):
29
+ fields: {{ target.pascal_name(name=exception.name) }}FieldsDict
30
+
31
+
32
+ class {{ target.exception_class_name(exception=exception, apiVersion=api.version) }}(exceptions.KibaException):
33
+
34
+ def __init__(
35
+ self,
36
+ {% for field in exception.fields %}{% if field.isRequired %}
37
+ {{ target.identifier(name=field.name) }}: {{ target.type(renderableType=field.fieldType, apiVersion=api.version) }},
38
+ {% endif %}{% endfor %}
39
+ {% for field in exception.fields %}{% if not field.isRequired %}
40
+ {{ target.identifier(name=field.name) }}: {{ target.type(renderableType=field.fieldType, apiVersion=api.version) }} = None,
41
+ {% endif %}{% endfor %}
42
+ message: str | None = None,
43
+ ) -> None:
44
+ super().__init__(message=message, statusCode={{ exception.statusCode }})
45
+ {% for field in exception.fields %}
46
+ self.{{ target.identifier(name=field.name) }} = {{ target.identifier(name=field.name) }}
47
+ {% endfor %}
48
+
49
+ def to_dict(self) -> dict[str, Json]:
50
+ return {
51
+ **super().to_dict(),
52
+ 'fields': {
53
+ {% for field in exception.fields %}
54
+ '{{ target.identifier(name=field.name) }}': self.{{ target.identifier(name=field.name) }},
55
+ {% endfor %}
56
+ },
57
+ }
58
+
59
+ @classmethod
60
+ def from_dict(cls, exceptionDict: {{ target.pascal_name(name=exception.name) }}TypedDict) -> {{ target.exception_class_name(exception=exception, apiVersion=api.version) }}: # type: ignore
61
+ return cls(
62
+ message=exceptionDict['message'],
63
+ {% for field in exception.fields %}
64
+ {{ target.identifier(name=field.name) }}=exceptionDict['fields']['{{ target.identifier(name=field.name) }}'],
65
+ {% endfor %}
66
+ )
67
+
68
+ {% endfor %}
@@ -0,0 +1,30 @@
1
+ {{ autogenerated_notice }}
2
+ import abc
3
+ import datetime
4
+ import collections.abc
5
+
6
+ {% if target.operations_require_json(api=api) %}
7
+ import pydantic
8
+
9
+ {% endif %}
10
+ from . import resources
11
+ {% if target.requires_undefined(api=api) %}
12
+ from core.util.typing_util import UNDEFINED, UpdateValue
13
+ {% endif %}
14
+
15
+
16
+ class {{ target.internal_class_name(api=api) }}(abc.ABC):
17
+ {% if api.operations|length == 0 %}
18
+ pass
19
+ {% endif %}
20
+ {% for operation in api.operations %}
21
+
22
+ @abc.abstractmethod
23
+ {% if not target.is_streaming(operation=operation) %}async {% endif %}def {{ target.operation_name(operation=operation) }}_v{{ api.version.major }}(
24
+ self,
25
+ {% for parameter in operation.parameters %}
26
+ {{ target.identifier(name=parameter.name) }}: {% if target.is_undefined_parameter(operation=operation, parameter=parameter) %}UpdateValue[{{ target.parameter_type(renderableParameter=parameter, apiVersion=api.version) }}]{% else %}{{ target.parameter_type(renderableParameter=parameter, apiVersion=api.version) }}{% endif %}{% if target.is_undefined_parameter(operation=operation, parameter=parameter) %} = UNDEFINED{% elif parameter.defaultValue is not none %} = {{ target.literal(value=parameter.defaultValue) }}{% endif %},
27
+ {% endfor %}
28
+ ) -> {{ target.return_type(operation=operation, apiVersion=api.version) }}:
29
+ ...
30
+ {% endfor %}
@@ -0,0 +1,22 @@
1
+ {{ autogenerated_notice }}
2
+ from __future__ import annotations
3
+
4
+ import collections.abc
5
+ import datetime
6
+
7
+ {% if target.resources_require_json(api=api) %}
8
+ import pydantic
9
+ {% endif %}
10
+ from pydantic import BaseModel
11
+
12
+ {% for resource in api.resources %}
13
+
14
+ class {{ target.resource_class_name(resource=resource, apiVersion=api.version) }}(BaseModel):
15
+ {% if resource.attributes|length == 0 %}
16
+ pass
17
+ {% endif %}
18
+ {% for attribute in resource.attributes %}
19
+ {{ target.identifier(name=attribute.name) }}: {{ target.type(renderableType=attribute.fieldType, apiVersion=api.version, isLocalReference=True) }}
20
+ {% endfor %}
21
+
22
+ {% endfor %}
geniespec/types.py ADDED
@@ -0,0 +1,147 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import ClassVar
4
+
5
+ import pydantic
6
+ from pydantic import BaseModel
7
+
8
+
9
+ class PrimitiveTypes:
10
+ LONG_INTEGER = 'LongInteger'
11
+ INTEGER = 'Integer'
12
+ FLOAT = 'Float'
13
+ DOUBLE = 'Double'
14
+ STRING = 'String'
15
+ BOOLEAN = 'Boolean'
16
+ DATETIME = 'Datetime'
17
+ DATE = 'Date'
18
+ JSON = 'Json'
19
+ ALL: ClassVar[list[str]] = [LONG_INTEGER, INTEGER, FLOAT, DOUBLE, STRING, BOOLEAN, DATETIME, DATE, JSON]
20
+
21
+
22
+ class FieldSources:
23
+ REQUEST = 'request'
24
+ QUERY_PARAMETERS = 'query-parameters'
25
+
26
+
27
+ class ResponseTypes:
28
+ JSON = 'json'
29
+ STREAM = 'stream'
30
+ REDIRECT = 'redirect'
31
+
32
+
33
+ class ComplexTypes:
34
+ MAPPING = 'Mapping'
35
+ ALL: ClassVar[list[str]] = [MAPPING]
36
+
37
+
38
+ class Attribute(BaseModel):
39
+ name: str
40
+ fieldType: str = pydantic.Field(alias='type')
41
+ listValueTypes: list[str] = []
42
+ mappingKeyType: str | None = None
43
+ mappingValueTypes: list[str] = []
44
+ isPrimary: bool = False
45
+ isUnique: bool = False
46
+ shouldDisableGetEndpoint: bool = False
47
+ isUnfilterable: bool = False
48
+ isUpdatable: bool = False
49
+ isNullable: bool = False
50
+ isDeprecated: bool = False
51
+ isSettable: bool = False
52
+ isAccessibleIndependently: bool = False
53
+ defaultValue: str | int | float | bool | None = None
54
+
55
+
56
+ class ResponseField(BaseModel):
57
+ name: str
58
+ fieldType: str = pydantic.Field(alias='type')
59
+ listValueTypes: list[str] = []
60
+ mappingKeyType: str | None = None
61
+ mappingValueTypes: list[str] = []
62
+ isNullable: bool = False
63
+ isList: bool = False
64
+
65
+
66
+ class RequestField(BaseModel):
67
+ name: str
68
+ fieldType: str = pydantic.Field(alias='type')
69
+ listValueTypes: list[str] = []
70
+ mappingKeyType: str | None = None
71
+ mappingValueTypes: list[str] = []
72
+ isRequired: bool = False
73
+ isDeprecated: bool = False
74
+ defaultValue: str | int | float | bool | None = None
75
+ maxValue: str | int | float | None = None
76
+ minValue: str | int | float | None = None
77
+ source: str = FieldSources.REQUEST
78
+
79
+
80
+ class BaseTransition(BaseModel):
81
+ action: str
82
+ method: str | None = None
83
+ responseField: ResponseField | None = None
84
+ requestFields: list[RequestField] = []
85
+ authentication: str | None = None
86
+ responseType: str = ResponseTypes.JSON
87
+
88
+
89
+ class Transition(BaseTransition):
90
+ operatesOn: str | None = None
91
+ isIndirectAction: bool = False
92
+
93
+
94
+ class RootTransition(BaseTransition):
95
+ pass
96
+
97
+
98
+ class Resource(BaseModel):
99
+ singleName: str
100
+ collectionName: str
101
+ parentResource: str | None = None
102
+ attributes: list[Attribute] = []
103
+ transitions: list[Transition] = []
104
+ accessibleFrom: list[str] = []
105
+
106
+ @property
107
+ def primaryAttribute(self) -> Attribute | None: # noqa: N802
108
+ return next(iter([attribute for attribute in self.attributes if attribute.isPrimary]), None)
109
+
110
+
111
+ class ExceptionField(BaseModel):
112
+ name: str
113
+ fieldType: str = pydantic.Field(alias='type')
114
+ listValueTypes: list[str] = []
115
+ isRequired: bool = False
116
+ isNullable: bool = False
117
+ defaultValue: str | int | float | bool | None = None
118
+
119
+
120
+ class ApiException(BaseModel):
121
+ name: str
122
+ message: str | None = None
123
+ extends: str | None = None
124
+ statusCode: int | None = None
125
+ fields: list[ExceptionField] = []
126
+
127
+
128
+ class Version(BaseModel):
129
+ major: str
130
+ minor: str
131
+ patch: str
132
+
133
+
134
+ class KibaApi(BaseModel):
135
+ version: str
136
+ serviceName: str
137
+ pythonClientPackageName: str
138
+ typescriptClientPackageName: str
139
+ resources: list[Resource] = []
140
+ transitions: list[RootTransition] = []
141
+ exceptions: list[ApiException] = []
142
+ defaultAuthentication: str | None = None
143
+
144
+ @property
145
+ def versionObject(self) -> Version: # noqa: N802
146
+ parts = self.version.split('.')
147
+ return Version(major=parts[0], minor=parts[1], patch=parts[2])
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.4
2
+ Name: geniespec
3
+ Version: 0.1.1.dev2
4
+ Summary: Generate API code from an api.yaml spec
5
+ License-Expression: MIT
6
+ Requires-Dist: jinja2>=3.1.6
7
+ Requires-Dist: pyyaml>=6.0.3
8
+ Requires-Dist: jsonschema>=4.26.0
9
+ Requires-Dist: pydantic>=2.13.5
10
+ Requires-Dist: kiba-core>=0.5.3.dev81,<0.5.4
11
+ Requires-Python: >=3.13, <4
12
+ Project-URL: repository, https://github.com/kibalabs/genie
13
+ Description-Content-Type: text/markdown
14
+
15
+ # Genie
16
+
17
+ Genie (`geniespec` on PyPI) generates API code from an `api.yaml` spec. The output is a package you install, not code you commit.
18
+
19
+ ```
20
+ uvx geniespec build --spec api.yaml [--output .genie]
21
+ uvx geniespec watch --spec api.yaml [--output .genie]
22
+ ```
23
+
24
+ - `build` writes `<output>/python`: a project with `pyproject.toml` and `src/<service>_api/v<major>/` (`api`, `endpoints`, `resources`, `internal`, `exceptions`). Only changed files are rewritten and files it no longer generates are removed.
25
+ - `watch` rebuilds whenever the spec changes. If the spec is invalid it logs the error and keeps the previous output.
26
+ - Consumers depend on the output as an editable path dependency, e.g. `my-service-api = { path = ".genie/python", editable = true }` in `[tool.uv.sources]`, and import it as `my_service_api.v1`.
27
+ - Generated code needs `kiba-core` (`core.api`, `core.util.typing_util`), `pydantic` and `starlette` from the consumer.
28
+
29
+ # Development
30
+
31
+ ```
32
+ make install
33
+ make lint-check type-check test
34
+ ```
35
+
36
+ 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.
37
+
38
+ # Things that need work
39
+
40
+ ## Typing of params and fields in general
41
+
42
+ Enum and OneOf not supported yet
43
+
44
+ I think we need to create an object for Type. what would it look like?
45
+
46
+
47
+ ```
48
+ class BaseFieldType:
49
+ isNullable: bool
50
+
51
+
52
+ class RawFieldType(BaseFieldType):
53
+ name: str
54
+
55
+
56
+ class ClassFieldType(BaseFieldType):
57
+ fieldType: RawFieldType
58
+
59
+
60
+ class ListFieldType:(BaseFieldType)
61
+ fieldType: RawFieldType
62
+ isNullable: bool
63
+
64
+
65
+ class MappingFieldType(BaseFieldType):
66
+ keyFieldType: RawFieldType
67
+ valueFieldType: RawFieldType
68
+
69
+
70
+ class OneOfFieldType(BaseFieldType):
71
+ fieldTypes: list[RawFieldType]
72
+
73
+
74
+ class EnumFieldType(BaseFieldType):
75
+ values: list[str]
76
+
77
+ ```
@@ -0,0 +1,20 @@
1
+ geniespec/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ geniespec/api_schema.yaml,sha256=7W6bPNsIVN1XLOv0XXKAy3XfyYPYZNpbkREURVFWFXM,6942
3
+ geniespec/cli.py,sha256=KXlDkaO9irBC0lgJrRzgvTJX92soALiJEe2V13epf4E,3664
4
+ geniespec/genie.py,sha256=QTI4__Q7P1nSnoMDdX7HO_RUeVccBCZv-cR14OAVuhg,36985
5
+ geniespec/languages.py,sha256=l_kiIIIiE1nZJfVtX7GX4D8T4iM-9usKqVC-ld02jAM,24793
6
+ geniespec/renderable_types.py,sha256=4AMN-ufAcYG07-ARdzSK1UOM6oC2Xp_cIx1-EfBco4w,7952
7
+ geniespec/templates/python/pyproject.toml.j2,sha256=4qagRCkHxMCZHnz80vArjPWd1IrJQrIEoNtV74xbZMA,183
8
+ geniespec/templates/python/src/{service}/__init__.py.j2,sha256=jfbq5glXrgTEjpXn2jLDIsOkVzsVVvNL1cjWToomAdI,27
9
+ geniespec/templates/python/src/{service}/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ geniespec/templates/python/src/{service}/v{version}/__init__.py.j2,sha256=jfbq5glXrgTEjpXn2jLDIsOkVzsVVvNL1cjWToomAdI,27
11
+ geniespec/templates/python/src/{service}/v{version}/api.py.j2,sha256=e72zeooHm6KUurqIUdlmvSx-I6RlpbJ0ngvTjlOBXJQ,4170
12
+ geniespec/templates/python/src/{service}/v{version}/endpoints.py.j2,sha256=6TCEQ4IUiM_tLntg_FjUXb1pIYZIEz_wHnjCyIi72Ug,1662
13
+ geniespec/templates/python/src/{service}/v{version}/exceptions.py.j2,sha256=Ck-9Cxbg8EkxW6CTpM_TqMpPCVcCpZGjun8uKXcgsjc,2518
14
+ geniespec/templates/python/src/{service}/v{version}/internal.py.j2,sha256=IElSKLhhjWerJhKoCovleaG78k8ZJVsW_XUTmNEwesQ,1337
15
+ geniespec/templates/python/src/{service}/v{version}/resources.py.j2,sha256=oyWlUVyQq1mWIRho3usY9jaA9k5Qn_YDiv1OT2GR948,625
16
+ geniespec/types.py,sha256=mAgtltn5VepXFH12TtDgiHhEbiBG_c8ULaUmi7dUlvk,3783
17
+ geniespec-0.1.1.dev2.dist-info/WHEEL,sha256=cmC5s21ojypbVslldL7IJq3hjZH-tINy4rziKePFsG0,81
18
+ geniespec-0.1.1.dev2.dist-info/entry_points.txt,sha256=C3eQvtORVZ6rXgCW8RyeI-abdYPtqn6CCbjoDuOfX0M,50
19
+ geniespec-0.1.1.dev2.dist-info/METADATA,sha256=mN4OzRpNQ9CXvRjyfki_QZxp3JFltLDnO46QqiJvFuI,2390
20
+ geniespec-0.1.1.dev2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.12.23
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ geniespec = geniespec.cli:main
3
+