geniespec 0.1.1.dev2__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.
- geniespec-0.1.1.dev2/PKG-INFO +77 -0
- geniespec-0.1.1.dev2/README.md +63 -0
- geniespec-0.1.1.dev2/pyproject.toml +37 -0
- geniespec-0.1.1.dev2/pyproject.toml.orig +38 -0
- geniespec-0.1.1.dev2/src/geniespec/__init__.py +0 -0
- geniespec-0.1.1.dev2/src/geniespec/api_schema.yaml +318 -0
- geniespec-0.1.1.dev2/src/geniespec/cli.py +85 -0
- geniespec-0.1.1.dev2/src/geniespec/genie.py +505 -0
- geniespec-0.1.1.dev2/src/geniespec/languages.py +408 -0
- geniespec-0.1.1.dev2/src/geniespec/renderable_types.py +246 -0
- geniespec-0.1.1.dev2/src/geniespec/templates/python/pyproject.toml.j2 +8 -0
- geniespec-0.1.1.dev2/src/geniespec/templates/python/src/{service}/__init__.py.j2 +1 -0
- geniespec-0.1.1.dev2/src/geniespec/templates/python/src/{service}/py.typed +0 -0
- geniespec-0.1.1.dev2/src/geniespec/templates/python/src/{service}/v{version}/__init__.py.j2 +1 -0
- geniespec-0.1.1.dev2/src/geniespec/templates/python/src/{service}/v{version}/api.py.j2 +68 -0
- geniespec-0.1.1.dev2/src/geniespec/templates/python/src/{service}/v{version}/endpoints.py.j2 +38 -0
- geniespec-0.1.1.dev2/src/geniespec/templates/python/src/{service}/v{version}/exceptions.py.j2 +68 -0
- geniespec-0.1.1.dev2/src/geniespec/templates/python/src/{service}/v{version}/internal.py.j2 +30 -0
- geniespec-0.1.1.dev2/src/geniespec/templates/python/src/{service}/v{version}/resources.py.j2 +22 -0
- geniespec-0.1.1.dev2/src/geniespec/types.py +147 -0
|
@@ -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,63 @@
|
|
|
1
|
+
# Genie
|
|
2
|
+
|
|
3
|
+
Genie (`geniespec` on PyPI) generates API code from an `api.yaml` spec. The output is a package you install, not code you commit.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
uvx geniespec build --spec api.yaml [--output .genie]
|
|
7
|
+
uvx geniespec watch --spec api.yaml [--output .genie]
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
- `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.
|
|
11
|
+
- `watch` rebuilds whenever the spec changes. If the spec is invalid it logs the error and keeps the previous output.
|
|
12
|
+
- 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`.
|
|
13
|
+
- Generated code needs `kiba-core` (`core.api`, `core.util.typing_util`), `pydantic` and `starlette` from the consumer.
|
|
14
|
+
|
|
15
|
+
# Development
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
make install
|
|
19
|
+
make lint-check type-check test
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
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.
|
|
23
|
+
|
|
24
|
+
# Things that need work
|
|
25
|
+
|
|
26
|
+
## Typing of params and fields in general
|
|
27
|
+
|
|
28
|
+
Enum and OneOf not supported yet
|
|
29
|
+
|
|
30
|
+
I think we need to create an object for Type. what would it look like?
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
class BaseFieldType:
|
|
35
|
+
isNullable: bool
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class RawFieldType(BaseFieldType):
|
|
39
|
+
name: str
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ClassFieldType(BaseFieldType):
|
|
43
|
+
fieldType: RawFieldType
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ListFieldType:(BaseFieldType)
|
|
47
|
+
fieldType: RawFieldType
|
|
48
|
+
isNullable: bool
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class MappingFieldType(BaseFieldType):
|
|
52
|
+
keyFieldType: RawFieldType
|
|
53
|
+
valueFieldType: RawFieldType
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class OneOfFieldType(BaseFieldType):
|
|
57
|
+
fieldTypes: list[RawFieldType]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class EnumFieldType(BaseFieldType):
|
|
61
|
+
values: list[str]
|
|
62
|
+
|
|
63
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "geniespec"
|
|
3
|
+
version = "0.1.1.dev2"
|
|
4
|
+
description = "Generate API code from an api.yaml spec"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
requires-python = ">=3.13, <4"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"jinja2>=3.1.6",
|
|
10
|
+
"pyyaml>=6.0.3",
|
|
11
|
+
"jsonschema>=4.26.0",
|
|
12
|
+
"pydantic>=2.13.5",
|
|
13
|
+
"kiba-core>=0.5.3.dev81,<0.5.4",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.scripts]
|
|
17
|
+
geniespec = "geniespec.cli:main"
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
repository = "https://github.com/kibalabs/genie"
|
|
21
|
+
|
|
22
|
+
[dependency-groups]
|
|
23
|
+
dev = [
|
|
24
|
+
"kiba-build==0.1.11.dev15",
|
|
25
|
+
"kiba-core[core-api,requester]>=0.5.3.dev81,<0.5.4",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[build-system]
|
|
29
|
+
requires = ["uv_build>=0.12,<0.13"]
|
|
30
|
+
build-backend = "uv_build"
|
|
31
|
+
|
|
32
|
+
[tool.pytest.ini_options]
|
|
33
|
+
pythonpath = ["src"]
|
|
34
|
+
|
|
35
|
+
[tool.bumpversion]
|
|
36
|
+
search = 'version = "{current_version}"'
|
|
37
|
+
replace = 'version = "{new_version}"'
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "geniespec"
|
|
3
|
+
version = "0.1.1.dev2"
|
|
4
|
+
description = "Generate API code from an api.yaml spec"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
requires-python = ">=3.13, <4"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"jinja2>=3.1.6",
|
|
10
|
+
"pyyaml>=6.0.3",
|
|
11
|
+
"jsonschema>=4.26.0",
|
|
12
|
+
"pydantic>=2.13.5",
|
|
13
|
+
"kiba-core>=0.5.3.dev81,<0.5.4",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.scripts]
|
|
17
|
+
geniespec = "geniespec.cli:main"
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
repository = "https://github.com/kibalabs/genie"
|
|
21
|
+
|
|
22
|
+
[dependency-groups]
|
|
23
|
+
dev = [
|
|
24
|
+
"kiba-build==0.1.11.dev15",
|
|
25
|
+
"kiba-core[core-api,requester]>=0.5.3.dev81,<0.5.4",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[build-system]
|
|
29
|
+
requires = ["uv_build>=0.12,<0.13"]
|
|
30
|
+
build-backend = "uv_build"
|
|
31
|
+
|
|
32
|
+
[tool.pytest.ini_options]
|
|
33
|
+
pythonpath = ["src"]
|
|
34
|
+
|
|
35
|
+
# NOTE(krishan711): without this, bumping the version also rewrites dependency pins that contain it (e.g. 0.1.1 inside kiba-build==0.1.11.dev15)
|
|
36
|
+
[tool.bumpversion]
|
|
37
|
+
search = "version = \"{current_version}\""
|
|
38
|
+
replace = "version = \"{new_version}\""
|
|
File without changes
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
2
|
+
$id: kibaRestApiSchema
|
|
3
|
+
type: object
|
|
4
|
+
additionalProperties: false
|
|
5
|
+
required:
|
|
6
|
+
- version
|
|
7
|
+
- serviceName
|
|
8
|
+
- pythonClientPackageName
|
|
9
|
+
- typescriptClientPackageName
|
|
10
|
+
properties:
|
|
11
|
+
version:
|
|
12
|
+
type: string
|
|
13
|
+
pattern: '^([0-9]|[1-9][0-9]*)\.([0-9]|[1-9][0-9]*)\.([0-9]|[1-9][0-9]*)$'
|
|
14
|
+
serviceName:
|
|
15
|
+
type: string
|
|
16
|
+
pattern: '^[a-z][a-z-]*$'
|
|
17
|
+
pythonClientPackageName:
|
|
18
|
+
type: string
|
|
19
|
+
pattern: '^[a-z][-a-z]*$'
|
|
20
|
+
typescriptClientPackageName:
|
|
21
|
+
type: string
|
|
22
|
+
pattern: '[@/-a-z]*$'
|
|
23
|
+
transitions:
|
|
24
|
+
type: array
|
|
25
|
+
items:
|
|
26
|
+
$ref: '#/definitions/RootTransition'
|
|
27
|
+
resources:
|
|
28
|
+
type: array
|
|
29
|
+
items:
|
|
30
|
+
$ref: '#/definitions/Resource'
|
|
31
|
+
exceptions:
|
|
32
|
+
type: array
|
|
33
|
+
items:
|
|
34
|
+
$ref: '#/definitions/Exception'
|
|
35
|
+
imports:
|
|
36
|
+
type: array
|
|
37
|
+
items:
|
|
38
|
+
type: string
|
|
39
|
+
defaultAuthentication:
|
|
40
|
+
type:
|
|
41
|
+
- "null"
|
|
42
|
+
- string
|
|
43
|
+
pattern: '^[a-z][a-z0-9-]*$'
|
|
44
|
+
definitions:
|
|
45
|
+
Attribute:
|
|
46
|
+
type: object
|
|
47
|
+
additionalProperties: false
|
|
48
|
+
required:
|
|
49
|
+
- name
|
|
50
|
+
- type
|
|
51
|
+
properties:
|
|
52
|
+
name:
|
|
53
|
+
type: string
|
|
54
|
+
pattern: '^[a-z][A-Za-z0-9-]*$'
|
|
55
|
+
type:
|
|
56
|
+
type: string
|
|
57
|
+
pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
|
|
58
|
+
listValueTypes:
|
|
59
|
+
type: array
|
|
60
|
+
minItems: 1
|
|
61
|
+
items:
|
|
62
|
+
type: string
|
|
63
|
+
pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
|
|
64
|
+
isPrimary:
|
|
65
|
+
type: boolean
|
|
66
|
+
isUnique:
|
|
67
|
+
type: boolean
|
|
68
|
+
shouldDisableGetEndpoint:
|
|
69
|
+
type: boolean
|
|
70
|
+
isUnfilterable:
|
|
71
|
+
type: boolean
|
|
72
|
+
isUpdatable:
|
|
73
|
+
type: boolean
|
|
74
|
+
isNullable:
|
|
75
|
+
type: boolean
|
|
76
|
+
isDeprecated:
|
|
77
|
+
type: boolean
|
|
78
|
+
defaultValue:
|
|
79
|
+
type:
|
|
80
|
+
- string
|
|
81
|
+
- number
|
|
82
|
+
- boolean
|
|
83
|
+
- 'null'
|
|
84
|
+
isSettable:
|
|
85
|
+
type: boolean
|
|
86
|
+
isAccessibleIndependently:
|
|
87
|
+
type: boolean
|
|
88
|
+
mappingKeyType:
|
|
89
|
+
type: string
|
|
90
|
+
pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
|
|
91
|
+
mappingValueTypes:
|
|
92
|
+
type: array
|
|
93
|
+
items:
|
|
94
|
+
type: string
|
|
95
|
+
pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
|
|
96
|
+
Resource:
|
|
97
|
+
type: object
|
|
98
|
+
additionalProperties: false
|
|
99
|
+
required:
|
|
100
|
+
- singleName
|
|
101
|
+
- collectionName
|
|
102
|
+
properties:
|
|
103
|
+
singleName:
|
|
104
|
+
type: string
|
|
105
|
+
pattern: '^[A-Z][A-Za-z-]*$'
|
|
106
|
+
collectionName:
|
|
107
|
+
type: string
|
|
108
|
+
pattern: '^[A-Z][A-Za-z-]*$'
|
|
109
|
+
parentResource:
|
|
110
|
+
type: string
|
|
111
|
+
pattern: '^[A-Z][A-Za-z-]*$'
|
|
112
|
+
attributes:
|
|
113
|
+
type: array
|
|
114
|
+
items:
|
|
115
|
+
$ref: '#/definitions/Attribute'
|
|
116
|
+
transitions:
|
|
117
|
+
type: array
|
|
118
|
+
items:
|
|
119
|
+
$ref: '#/definitions/Transition'
|
|
120
|
+
accessibleFrom:
|
|
121
|
+
type: array
|
|
122
|
+
items:
|
|
123
|
+
type: string
|
|
124
|
+
BaseTransition:
|
|
125
|
+
type: object
|
|
126
|
+
additionalProperties: false
|
|
127
|
+
required:
|
|
128
|
+
- action
|
|
129
|
+
properties:
|
|
130
|
+
action:
|
|
131
|
+
type: string
|
|
132
|
+
pattern: '^[A-Z][A-Z-]*$'
|
|
133
|
+
method:
|
|
134
|
+
type: string
|
|
135
|
+
enum:
|
|
136
|
+
- GET
|
|
137
|
+
- POST
|
|
138
|
+
requestFields:
|
|
139
|
+
type: array
|
|
140
|
+
items:
|
|
141
|
+
$ref: '#/definitions/RequestField'
|
|
142
|
+
responseField:
|
|
143
|
+
$ref: '#/definitions/ResponseField'
|
|
144
|
+
authentication:
|
|
145
|
+
type:
|
|
146
|
+
- "null"
|
|
147
|
+
- string
|
|
148
|
+
pattern: '^[a-z][a-z0-9-]*$'
|
|
149
|
+
responseType:
|
|
150
|
+
type: string
|
|
151
|
+
enum:
|
|
152
|
+
- json
|
|
153
|
+
- stream
|
|
154
|
+
- redirect
|
|
155
|
+
Transition:
|
|
156
|
+
type: object
|
|
157
|
+
additionalProperties: false
|
|
158
|
+
required:
|
|
159
|
+
- action
|
|
160
|
+
properties:
|
|
161
|
+
action:
|
|
162
|
+
type: string
|
|
163
|
+
pattern: '^[A-Z][A-Z-]*$'
|
|
164
|
+
method:
|
|
165
|
+
type: string
|
|
166
|
+
enum:
|
|
167
|
+
- GET
|
|
168
|
+
- POST
|
|
169
|
+
requestFields:
|
|
170
|
+
type: array
|
|
171
|
+
items:
|
|
172
|
+
$ref: '#/definitions/RequestField'
|
|
173
|
+
responseField:
|
|
174
|
+
$ref: '#/definitions/ResponseField'
|
|
175
|
+
authentication:
|
|
176
|
+
type:
|
|
177
|
+
- "null"
|
|
178
|
+
- string
|
|
179
|
+
pattern: '^[a-z][a-z0-9-]*$'
|
|
180
|
+
responseType:
|
|
181
|
+
type: string
|
|
182
|
+
enum:
|
|
183
|
+
- json
|
|
184
|
+
- stream
|
|
185
|
+
- redirect
|
|
186
|
+
operatesOn:
|
|
187
|
+
type: string
|
|
188
|
+
enum:
|
|
189
|
+
- individual
|
|
190
|
+
- collection
|
|
191
|
+
isIndirectAction:
|
|
192
|
+
type: boolean
|
|
193
|
+
RootTransition:
|
|
194
|
+
type: object
|
|
195
|
+
allOf:
|
|
196
|
+
- $ref: '#/definitions/BaseTransition'
|
|
197
|
+
RequestField:
|
|
198
|
+
type: object
|
|
199
|
+
additionalProperties: false
|
|
200
|
+
required:
|
|
201
|
+
- name
|
|
202
|
+
- type
|
|
203
|
+
properties:
|
|
204
|
+
name:
|
|
205
|
+
type: string
|
|
206
|
+
pattern: '^[a-z][A-Za-z0-9-]*$'
|
|
207
|
+
type:
|
|
208
|
+
type: string
|
|
209
|
+
pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
|
|
210
|
+
listValueTypes:
|
|
211
|
+
type: array
|
|
212
|
+
minItems: 1
|
|
213
|
+
items:
|
|
214
|
+
type: string
|
|
215
|
+
pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
|
|
216
|
+
mappingKeyType:
|
|
217
|
+
type: string
|
|
218
|
+
pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
|
|
219
|
+
mappingValueTypes:
|
|
220
|
+
type: array
|
|
221
|
+
items:
|
|
222
|
+
type: string
|
|
223
|
+
pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
|
|
224
|
+
isRequired:
|
|
225
|
+
type: boolean
|
|
226
|
+
defaultValue:
|
|
227
|
+
type:
|
|
228
|
+
- string
|
|
229
|
+
- number
|
|
230
|
+
- boolean
|
|
231
|
+
- 'null'
|
|
232
|
+
isDeprecated:
|
|
233
|
+
type: boolean
|
|
234
|
+
maxValue:
|
|
235
|
+
type:
|
|
236
|
+
- string
|
|
237
|
+
- number
|
|
238
|
+
minValue:
|
|
239
|
+
type:
|
|
240
|
+
- string
|
|
241
|
+
- number
|
|
242
|
+
source:
|
|
243
|
+
type: string
|
|
244
|
+
enum:
|
|
245
|
+
- request
|
|
246
|
+
- query-parameters
|
|
247
|
+
ResponseField:
|
|
248
|
+
type: object
|
|
249
|
+
additionalProperties: false
|
|
250
|
+
required:
|
|
251
|
+
- name
|
|
252
|
+
- type
|
|
253
|
+
properties:
|
|
254
|
+
name:
|
|
255
|
+
type: string
|
|
256
|
+
pattern: '^[a-z][A-Za-z-]*$'
|
|
257
|
+
type:
|
|
258
|
+
type: string
|
|
259
|
+
pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
|
|
260
|
+
listValueTypes:
|
|
261
|
+
type: array
|
|
262
|
+
minItems: 1
|
|
263
|
+
items:
|
|
264
|
+
type: string
|
|
265
|
+
pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
|
|
266
|
+
mappingKeyType:
|
|
267
|
+
type: string
|
|
268
|
+
pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
|
|
269
|
+
mappingValueTypes:
|
|
270
|
+
type: array
|
|
271
|
+
items:
|
|
272
|
+
type: string
|
|
273
|
+
pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
|
|
274
|
+
isNullable:
|
|
275
|
+
type: boolean
|
|
276
|
+
isList:
|
|
277
|
+
type: boolean
|
|
278
|
+
Exception:
|
|
279
|
+
type: object
|
|
280
|
+
additionalProperties: false
|
|
281
|
+
required:
|
|
282
|
+
- name
|
|
283
|
+
properties:
|
|
284
|
+
name:
|
|
285
|
+
type: string
|
|
286
|
+
pattern: '^[A-Z][A-Za-z-]*Exception$'
|
|
287
|
+
message:
|
|
288
|
+
type: string
|
|
289
|
+
extends:
|
|
290
|
+
type: string
|
|
291
|
+
pattern: '^([a-z-]+.exceptions.[A-Za-z-]*)|([A-Za-z-]*)'
|
|
292
|
+
statusCode:
|
|
293
|
+
type: integer
|
|
294
|
+
fields:
|
|
295
|
+
type: array
|
|
296
|
+
items:
|
|
297
|
+
$ref: '#/definitions/ExceptionField'
|
|
298
|
+
ExceptionField:
|
|
299
|
+
type: object
|
|
300
|
+
additionalProperties: false
|
|
301
|
+
required:
|
|
302
|
+
- name
|
|
303
|
+
- type
|
|
304
|
+
properties:
|
|
305
|
+
name:
|
|
306
|
+
type: string
|
|
307
|
+
pattern: '^[a-z][A-Za-z-]*$'
|
|
308
|
+
type:
|
|
309
|
+
type: string
|
|
310
|
+
pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
|
|
311
|
+
listValueTypes:
|
|
312
|
+
type: array
|
|
313
|
+
minItems: 1
|
|
314
|
+
items:
|
|
315
|
+
type: string
|
|
316
|
+
pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
|
|
317
|
+
isRequired:
|
|
318
|
+
type: boolean
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import pathlib
|
|
3
|
+
import tempfile
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
import yaml # type: ignore[import-untyped]
|
|
7
|
+
from core import logging
|
|
8
|
+
|
|
9
|
+
from geniespec.genie import Renderer
|
|
10
|
+
from geniespec.genie import SyntacticApiValidator
|
|
11
|
+
from geniespec.types import KibaApi
|
|
12
|
+
|
|
13
|
+
WATCH_INTERVAL_SECONDS = 0.5
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _sync_directory(sourceDirectoryPath: pathlib.Path, targetDirectoryPath: pathlib.Path) -> list[pathlib.Path]:
|
|
17
|
+
changedPaths: list[pathlib.Path] = []
|
|
18
|
+
sourceRelativePaths = {path.relative_to(sourceDirectoryPath) for path in sourceDirectoryPath.rglob('*') if path.is_file()}
|
|
19
|
+
for relativePath in sorted(sourceRelativePaths):
|
|
20
|
+
content = (sourceDirectoryPath / relativePath).read_bytes()
|
|
21
|
+
targetPath = targetDirectoryPath / relativePath
|
|
22
|
+
if targetPath.is_file() and targetPath.read_bytes() == content:
|
|
23
|
+
continue
|
|
24
|
+
targetPath.parent.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
temporaryPath = targetPath.with_name(f'.{targetPath.name}.tmp')
|
|
26
|
+
temporaryPath.write_bytes(content)
|
|
27
|
+
temporaryPath.replace(targetPath)
|
|
28
|
+
changedPaths.append(targetPath)
|
|
29
|
+
stalePaths = [path for path in targetDirectoryPath.rglob('*') if path.is_file() and '__pycache__' not in path.parts and path.relative_to(targetDirectoryPath) not in sourceRelativePaths]
|
|
30
|
+
for stalePath in stalePaths:
|
|
31
|
+
stalePath.unlink()
|
|
32
|
+
return changedPaths + stalePaths
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def build(specPath: pathlib.Path, outputDirectoryPath: pathlib.Path) -> list[pathlib.Path]:
|
|
36
|
+
with open(specPath) as specFile:
|
|
37
|
+
apiYaml = yaml.safe_load(specFile)
|
|
38
|
+
SyntacticApiValidator().validate(apiYaml)
|
|
39
|
+
api = KibaApi(**apiYaml)
|
|
40
|
+
with tempfile.TemporaryDirectory() as renderDirectory:
|
|
41
|
+
Renderer().render_directory(api=api, outputDirectoryPath=renderDirectory)
|
|
42
|
+
return _sync_directory(sourceDirectoryPath=pathlib.Path(renderDirectory), targetDirectoryPath=outputDirectoryPath / 'python')
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _build_and_log(specPath: pathlib.Path, outputDirectoryPath: pathlib.Path) -> None:
|
|
46
|
+
changedPaths = build(specPath=specPath, outputDirectoryPath=outputDirectoryPath)
|
|
47
|
+
logging.info(f'geniespec: built {specPath} into {outputDirectoryPath} ({len(changedPaths)} files changed)')
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def watch(specPath: pathlib.Path, outputDirectoryPath: pathlib.Path) -> None:
|
|
51
|
+
lastModifiedTime: int | None = None
|
|
52
|
+
while True:
|
|
53
|
+
try:
|
|
54
|
+
modifiedTime: int | None = specPath.stat().st_mtime_ns
|
|
55
|
+
except FileNotFoundError:
|
|
56
|
+
modifiedTime = lastModifiedTime
|
|
57
|
+
if modifiedTime != lastModifiedTime:
|
|
58
|
+
lastModifiedTime = modifiedTime
|
|
59
|
+
try:
|
|
60
|
+
_build_and_log(specPath=specPath, outputDirectoryPath=outputDirectoryPath)
|
|
61
|
+
except Exception as exception: # noqa: BLE001
|
|
62
|
+
logging.error(f'geniespec: failed to build {specPath}, keeping the previous output: {exception}')
|
|
63
|
+
time.sleep(WATCH_INTERVAL_SECONDS)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def main() -> None:
|
|
67
|
+
parser = argparse.ArgumentParser(prog='geniespec')
|
|
68
|
+
subparsers = parser.add_subparsers(dest='command', required=True)
|
|
69
|
+
for command in ('build', 'watch'):
|
|
70
|
+
subparser = subparsers.add_parser(command)
|
|
71
|
+
subparser.add_argument('--spec', type=pathlib.Path, required=True)
|
|
72
|
+
subparser.add_argument('--output', type=pathlib.Path, default=pathlib.Path('.genie'))
|
|
73
|
+
args = parser.parse_args()
|
|
74
|
+
logging.basicConfig(level=logging.INFO)
|
|
75
|
+
if args.command == 'build':
|
|
76
|
+
_build_and_log(specPath=args.spec, outputDirectoryPath=args.output)
|
|
77
|
+
return
|
|
78
|
+
try:
|
|
79
|
+
watch(specPath=args.spec, outputDirectoryPath=args.output)
|
|
80
|
+
except KeyboardInterrupt:
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == '__main__':
|
|
85
|
+
main()
|