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.
- geniespec/__init__.py +0 -0
- geniespec/api_schema.yaml +318 -0
- geniespec/cli.py +85 -0
- geniespec/genie.py +505 -0
- geniespec/languages.py +408 -0
- geniespec/renderable_types.py +246 -0
- geniespec/templates/python/pyproject.toml.j2 +8 -0
- geniespec/templates/python/src/{service}/__init__.py.j2 +1 -0
- geniespec/templates/python/src/{service}/py.typed +0 -0
- geniespec/templates/python/src/{service}/v{version}/__init__.py.j2 +1 -0
- geniespec/templates/python/src/{service}/v{version}/api.py.j2 +68 -0
- geniespec/templates/python/src/{service}/v{version}/endpoints.py.j2 +38 -0
- geniespec/templates/python/src/{service}/v{version}/exceptions.py.j2 +68 -0
- geniespec/templates/python/src/{service}/v{version}/internal.py.j2 +30 -0
- geniespec/templates/python/src/{service}/v{version}/resources.py.j2 +22 -0
- geniespec/types.py +147 -0
- geniespec-0.1.1.dev2.dist-info/METADATA +77 -0
- geniespec-0.1.1.dev2.dist-info/RECORD +20 -0
- geniespec-0.1.1.dev2.dist-info/WHEEL +4 -0
- geniespec-0.1.1.dev2.dist-info/entry_points.txt +3 -0
|
@@ -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,,
|