geniespec 0.1.1.dev2__tar.gz → 0.1.1.dev4__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 → geniespec-0.1.1.dev4}/PKG-INFO +8 -8
- geniespec-0.1.1.dev4/README.md +63 -0
- {geniespec-0.1.1.dev2 → geniespec-0.1.1.dev4}/pyproject.toml +1 -1
- {geniespec-0.1.1.dev2 → geniespec-0.1.1.dev4}/pyproject.toml.orig +1 -1
- geniespec-0.1.1.dev4/src/geniespec/cli.py +123 -0
- {geniespec-0.1.1.dev2 → geniespec-0.1.1.dev4}/src/geniespec/genie.py +4 -3
- {geniespec-0.1.1.dev2 → geniespec-0.1.1.dev4}/src/geniespec/languages.py +0 -6
- {geniespec-0.1.1.dev2/src/geniespec/templates/python/src → geniespec-0.1.1.dev4/src/geniespec/templates/python-server}/{service}/v{version}/internal.py.j2 +3 -0
- geniespec-0.1.1.dev2/README.md +0 -63
- geniespec-0.1.1.dev2/src/geniespec/cli.py +0 -85
- geniespec-0.1.1.dev2/src/geniespec/templates/python/pyproject.toml.j2 +0 -8
- geniespec-0.1.1.dev2/src/geniespec/templates/python/src/{service}/py.typed +0 -0
- {geniespec-0.1.1.dev2 → geniespec-0.1.1.dev4}/src/geniespec/__init__.py +0 -0
- {geniespec-0.1.1.dev2 → geniespec-0.1.1.dev4}/src/geniespec/api_schema.yaml +0 -0
- {geniespec-0.1.1.dev2 → geniespec-0.1.1.dev4}/src/geniespec/renderable_types.py +0 -0
- {geniespec-0.1.1.dev2/src/geniespec/templates/python/src → geniespec-0.1.1.dev4/src/geniespec/templates/python-server}/{service}/__init__.py.j2 +0 -0
- {geniespec-0.1.1.dev2/src/geniespec/templates/python/src → geniespec-0.1.1.dev4/src/geniespec/templates/python-server}/{service}/v{version}/__init__.py.j2 +0 -0
- {geniespec-0.1.1.dev2/src/geniespec/templates/python/src → geniespec-0.1.1.dev4/src/geniespec/templates/python-server}/{service}/v{version}/api.py.j2 +0 -0
- {geniespec-0.1.1.dev2/src/geniespec/templates/python/src → geniespec-0.1.1.dev4/src/geniespec/templates/python-server}/{service}/v{version}/endpoints.py.j2 +0 -0
- {geniespec-0.1.1.dev2/src/geniespec/templates/python/src → geniespec-0.1.1.dev4/src/geniespec/templates/python-server}/{service}/v{version}/exceptions.py.j2 +0 -0
- {geniespec-0.1.1.dev2/src/geniespec/templates/python/src → geniespec-0.1.1.dev4/src/geniespec/templates/python-server}/{service}/v{version}/resources.py.j2 +0 -0
- {geniespec-0.1.1.dev2 → geniespec-0.1.1.dev4}/src/geniespec/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: geniespec
|
|
3
|
-
Version: 0.1.1.
|
|
3
|
+
Version: 0.1.1.dev4
|
|
4
4
|
Summary: Generate API code from an api.yaml spec
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Requires-Dist: jinja2>=3.1.6
|
|
@@ -14,17 +14,17 @@ Description-Content-Type: text/markdown
|
|
|
14
14
|
|
|
15
15
|
# Genie
|
|
16
16
|
|
|
17
|
-
Genie (`geniespec` on PyPI) generates API code from an `api.yaml` spec.
|
|
17
|
+
Genie (`geniespec` on PyPI) generates API code from an `api.yaml` spec. Install it like any other tool and run it from your build scripts; the generated code is gitignored, not committed.
|
|
18
18
|
|
|
19
19
|
```
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
geniespec build --spec api.yaml --target python-server [--output .]
|
|
21
|
+
geniespec watch --spec api.yaml --target python-server [--output .] [-- <command>]
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
- `build` writes
|
|
25
|
-
- `watch` rebuilds whenever the spec changes. If the spec is invalid it logs the error and keeps the previous output.
|
|
26
|
-
-
|
|
27
|
-
-
|
|
24
|
+
- `build` writes the target's package into the output directory, e.g. `./<service>_api/v<major>/` (`api`, `endpoints`, `resources`, `internal`, `exceptions`) for `python-server`. Only files in that package are touched: changed files are rewritten and files it no longer generates are removed.
|
|
25
|
+
- `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`.
|
|
26
|
+
- Code run from the output directory imports the package directly, e.g. `from my_service_api.v1 import api`. Generated `python-server` code needs `kiba-core` (`core.api`, `core.util.typing_util`), `pydantic` and `starlette`.
|
|
27
|
+
- The bundled templates are examples of a kibalabs-style target.
|
|
28
28
|
|
|
29
29
|
# Development
|
|
30
30
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Genie
|
|
2
|
+
|
|
3
|
+
Genie (`geniespec` on PyPI) generates API code from an `api.yaml` spec. Install it like any other tool and run it from your build scripts; the generated code is gitignored, not committed.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
geniespec build --spec api.yaml --target python-server [--output .]
|
|
7
|
+
geniespec watch --spec api.yaml --target python-server [--output .] [-- <command>]
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
- `build` writes the target's package into the output directory, e.g. `./<service>_api/v<major>/` (`api`, `endpoints`, `resources`, `internal`, `exceptions`) for `python-server`. Only files in that package are touched: changed files are rewritten and files it no longer generates are removed.
|
|
11
|
+
- `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`.
|
|
12
|
+
- Code run from the output directory imports the package directly, e.g. `from my_service_api.v1 import api`. Generated `python-server` code needs `kiba-core` (`core.api`, `core.util.typing_util`), `pydantic` and `starlette`.
|
|
13
|
+
- The bundled templates are examples of a kibalabs-style target.
|
|
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,123 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import pathlib
|
|
3
|
+
import signal
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
import tempfile
|
|
7
|
+
import time
|
|
8
|
+
import types
|
|
9
|
+
|
|
10
|
+
import yaml # type: ignore[import-untyped]
|
|
11
|
+
from core import logging
|
|
12
|
+
|
|
13
|
+
from geniespec.genie import TARGET_LANGUAGE_CODES
|
|
14
|
+
from geniespec.genie import Renderer
|
|
15
|
+
from geniespec.genie import SyntacticApiValidator
|
|
16
|
+
from geniespec.types import KibaApi
|
|
17
|
+
|
|
18
|
+
WATCH_INTERVAL_SECONDS = 0.5
|
|
19
|
+
COMMAND_STOP_TIMEOUT_SECONDS = 10
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _sync_directory(sourceDirectoryPath: pathlib.Path, targetDirectoryPath: pathlib.Path) -> list[pathlib.Path]:
|
|
23
|
+
changedPaths: list[pathlib.Path] = []
|
|
24
|
+
sourceRelativePaths = {path.relative_to(sourceDirectoryPath) for path in sourceDirectoryPath.rglob('*') if path.is_file()}
|
|
25
|
+
for relativePath in sorted(sourceRelativePaths):
|
|
26
|
+
content = (sourceDirectoryPath / relativePath).read_bytes()
|
|
27
|
+
targetPath = targetDirectoryPath / relativePath
|
|
28
|
+
if targetPath.is_file() and targetPath.read_bytes() == content:
|
|
29
|
+
continue
|
|
30
|
+
targetPath.parent.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
temporaryPath = targetPath.with_name(f'.{targetPath.name}.tmp')
|
|
32
|
+
temporaryPath.write_bytes(content)
|
|
33
|
+
temporaryPath.replace(targetPath)
|
|
34
|
+
changedPaths.append(targetPath)
|
|
35
|
+
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]
|
|
36
|
+
for stalePath in stalePaths:
|
|
37
|
+
stalePath.unlink()
|
|
38
|
+
return changedPaths + stalePaths
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def build(specPath: pathlib.Path, target: str, outputDirectoryPath: pathlib.Path) -> list[pathlib.Path]:
|
|
42
|
+
with open(specPath) as specFile:
|
|
43
|
+
apiYaml = yaml.safe_load(specFile)
|
|
44
|
+
SyntacticApiValidator().validate(apiYaml)
|
|
45
|
+
api = KibaApi(**apiYaml)
|
|
46
|
+
with tempfile.TemporaryDirectory() as renderDirectory:
|
|
47
|
+
Renderer(target=target).render_directory(api=api, outputDirectoryPath=renderDirectory)
|
|
48
|
+
changedPaths: list[pathlib.Path] = []
|
|
49
|
+
# NOTE(krishan711): only sync the packages the target generates so other files in the output directory are never touched
|
|
50
|
+
for packagePath in sorted(pathlib.Path(renderDirectory).iterdir()):
|
|
51
|
+
changedPaths += _sync_directory(sourceDirectoryPath=packagePath, targetDirectoryPath=outputDirectoryPath / packagePath.name)
|
|
52
|
+
return changedPaths
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _build_and_log(specPath: pathlib.Path, target: str, outputDirectoryPath: pathlib.Path) -> None:
|
|
56
|
+
changedPaths = build(specPath=specPath, target=target, outputDirectoryPath=outputDirectoryPath)
|
|
57
|
+
logging.info(f'geniespec: built {specPath} into {outputDirectoryPath} ({len(changedPaths)} files changed)')
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _stop_process(process: subprocess.Popen[bytes]) -> None:
|
|
61
|
+
if process.poll() is not None:
|
|
62
|
+
return
|
|
63
|
+
process.terminate()
|
|
64
|
+
try:
|
|
65
|
+
process.wait(timeout=COMMAND_STOP_TIMEOUT_SECONDS)
|
|
66
|
+
except subprocess.TimeoutExpired:
|
|
67
|
+
process.kill()
|
|
68
|
+
process.wait()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def watch(specPath: pathlib.Path, target: str, outputDirectoryPath: pathlib.Path, command: list[str]) -> int:
|
|
72
|
+
lastModifiedTime: int | None = None
|
|
73
|
+
process: subprocess.Popen[bytes] | None = None
|
|
74
|
+
try:
|
|
75
|
+
while True:
|
|
76
|
+
try:
|
|
77
|
+
modifiedTime: int | None = specPath.stat().st_mtime_ns
|
|
78
|
+
except FileNotFoundError:
|
|
79
|
+
modifiedTime = lastModifiedTime
|
|
80
|
+
if modifiedTime != lastModifiedTime:
|
|
81
|
+
lastModifiedTime = modifiedTime
|
|
82
|
+
try:
|
|
83
|
+
_build_and_log(specPath=specPath, target=target, outputDirectoryPath=outputDirectoryPath)
|
|
84
|
+
except Exception as exception: # noqa: BLE001
|
|
85
|
+
logging.error(f'geniespec: failed to build {specPath}, keeping the previous output: {exception}')
|
|
86
|
+
if command and process is None:
|
|
87
|
+
process = subprocess.Popen(command) # noqa: S603
|
|
88
|
+
if process is not None and process.poll() is not None:
|
|
89
|
+
return process.returncode
|
|
90
|
+
time.sleep(WATCH_INTERVAL_SECONDS)
|
|
91
|
+
finally:
|
|
92
|
+
if process is not None:
|
|
93
|
+
_stop_process(process=process)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _exit_on_terminate(signalNumber: int, frame: types.FrameType | None) -> None: # noqa: ARG001
|
|
97
|
+
sys.exit(128 + signalNumber)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def main() -> None:
|
|
101
|
+
parser = argparse.ArgumentParser(prog='geniespec')
|
|
102
|
+
subparsers = parser.add_subparsers(dest='subcommand', required=True)
|
|
103
|
+
for subcommand in ('build', 'watch'):
|
|
104
|
+
subparser = subparsers.add_parser(subcommand)
|
|
105
|
+
subparser.add_argument('--spec', type=pathlib.Path, required=True)
|
|
106
|
+
subparser.add_argument('--target', choices=sorted(TARGET_LANGUAGE_CODES), required=True)
|
|
107
|
+
subparser.add_argument('--output', type=pathlib.Path, default=pathlib.Path())
|
|
108
|
+
subparsers.choices['watch'].add_argument('command', nargs=argparse.REMAINDER, help='command to run while watching, after --')
|
|
109
|
+
args = parser.parse_args()
|
|
110
|
+
logging.basicConfig(level=logging.INFO)
|
|
111
|
+
if args.subcommand == 'build':
|
|
112
|
+
_build_and_log(specPath=args.spec, target=args.target, outputDirectoryPath=args.output)
|
|
113
|
+
return
|
|
114
|
+
command = args.command[1:] if args.command[:1] == ['--'] else args.command
|
|
115
|
+
signal.signal(signal.SIGTERM, _exit_on_terminate)
|
|
116
|
+
try:
|
|
117
|
+
sys.exit(watch(specPath=args.spec, target=args.target, outputDirectoryPath=args.output, command=command))
|
|
118
|
+
except KeyboardInterrupt:
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
if __name__ == '__main__':
|
|
123
|
+
main()
|
|
@@ -46,6 +46,7 @@ from geniespec.types import ResponseTypes
|
|
|
46
46
|
|
|
47
47
|
SCHEMA_FILE_PATH = pathlib.Path(__file__).parent / 'api_schema.yaml'
|
|
48
48
|
TEMPLATES_DIRECTORY_PATH = pathlib.Path(__file__).parent / 'templates'
|
|
49
|
+
TARGET_LANGUAGE_CODES = {'python-server': 'python'}
|
|
49
50
|
|
|
50
51
|
|
|
51
52
|
class SyntacticValidationException(BadRequestException):
|
|
@@ -80,9 +81,9 @@ JINJA_FUNCTIONS: dict[str, typing.Callable[[str], BaseLanguageDefinition]] = {
|
|
|
80
81
|
|
|
81
82
|
|
|
82
83
|
class Renderer:
|
|
83
|
-
def __init__(self) -> None:
|
|
84
|
-
self.templateDirectoryPath = str(TEMPLATES_DIRECTORY_PATH /
|
|
85
|
-
self.languageDefinition = get_language_definition(languageCode=
|
|
84
|
+
def __init__(self, target: str) -> None:
|
|
85
|
+
self.templateDirectoryPath = str(TEMPLATES_DIRECTORY_PATH / target)
|
|
86
|
+
self.languageDefinition = get_language_definition(languageCode=TARGET_LANGUAGE_CODES[target])
|
|
86
87
|
self.jinjaEnvironment = jinja2.Environment(loader=jinja2.FileSystemLoader(self.templateDirectoryPath), autoescape=jinja2.select_autoescape(), keep_trailing_newline=True, trim_blocks=True)
|
|
87
88
|
self.jinjaEnvironment.globals['autogenerated_notice'] = self.languageDefinition.AUTOGENERATED_NOTICE
|
|
88
89
|
|
|
@@ -75,9 +75,6 @@ class BaseLanguageDefinition:
|
|
|
75
75
|
def package_name(self, api: RenderableApi) -> str:
|
|
76
76
|
raise NotImplementedError
|
|
77
77
|
|
|
78
|
-
def distribution_name(self, api: RenderableApi) -> str:
|
|
79
|
-
raise NotImplementedError
|
|
80
|
-
|
|
81
78
|
def validation_errors(self, api: RenderableApi) -> list[str]:
|
|
82
79
|
raise NotImplementedError
|
|
83
80
|
|
|
@@ -214,9 +211,6 @@ class PythonLanguageDefinition(BaseLanguageDefinition):
|
|
|
214
211
|
def package_name(self, api: RenderableApi) -> str:
|
|
215
212
|
return f'{self.snake_name(name=api.serviceName)}_api'
|
|
216
213
|
|
|
217
|
-
def distribution_name(self, api: RenderableApi) -> str:
|
|
218
|
-
return f'{self.kebab_name(name=api.serviceName)}-api'
|
|
219
|
-
|
|
220
214
|
def internal_class_name(self, api: RenderableApi) -> str:
|
|
221
215
|
return f'{self.pascal_name(name=api.serviceName)}ApiV{api.version.major}Internal'
|
|
222
216
|
|
|
@@ -22,6 +22,9 @@ class {{ target.internal_class_name(api=api) }}(abc.ABC):
|
|
|
22
22
|
@abc.abstractmethod
|
|
23
23
|
{% if not target.is_streaming(operation=operation) %}async {% endif %}def {{ target.operation_name(operation=operation) }}_v{{ api.version.major }}(
|
|
24
24
|
self,
|
|
25
|
+
{% if operation.parameters %}
|
|
26
|
+
*,
|
|
27
|
+
{% endif %}
|
|
25
28
|
{% for parameter in operation.parameters %}
|
|
26
29
|
{{ 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
30
|
{% endfor %}
|
geniespec-0.1.1.dev2/README.md
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
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
|
-
```
|
|
@@ -1,85 +0,0 @@
|
|
|
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()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|