gendalf 0.1.0__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.
- gendalf-0.1.0/LICENSE +21 -0
- gendalf-0.1.0/PKG-INFO +117 -0
- gendalf-0.1.0/README.md +89 -0
- gendalf-0.1.0/pyproject.toml +160 -0
- gendalf-0.1.0/src/gendalf/__init__.py +0 -0
- gendalf-0.1.0/src/gendalf/_typing.py +24 -0
- gendalf-0.1.0/src/gendalf/cli.py +123 -0
- gendalf-0.1.0/src/gendalf/entrypoint/__init__.py +0 -0
- gendalf-0.1.0/src/gendalf/entrypoint/decorator.py +38 -0
- gendalf-0.1.0/src/gendalf/entrypoint/inspection.py +112 -0
- gendalf-0.1.0/src/gendalf/entrypoint/printer.py +77 -0
- gendalf-0.1.0/src/gendalf/generator/__init__.py +0 -0
- gendalf-0.1.0/src/gendalf/generator/abc.py +9 -0
- gendalf-0.1.0/src/gendalf/generator/fastapi.py +528 -0
- gendalf-0.1.0/src/gendalf/generator/model/__init__.py +0 -0
- gendalf-0.1.0/src/gendalf/generator/model/abc.py +9 -0
- gendalf-0.1.0/src/gendalf/generator/model/builder.py +306 -0
- gendalf-0.1.0/src/gendalf/generator/model/factory.py +31 -0
- gendalf-0.1.0/src/gendalf/generator/model/type_inspection/__init__.py +1 -0
- gendalf-0.1.0/src/gendalf/generator/model/type_inspection/visitor/__init__.py +0 -0
- gendalf-0.1.0/src/gendalf/generator/model/type_inspection/visitor/abc.py +96 -0
- gendalf-0.1.0/src/gendalf/generator/model/type_inspection/visitor/model.py +60 -0
- gendalf-0.1.0/src/gendalf/generator/model/type_inspection/visitor/trait.py +157 -0
- gendalf-0.1.0/src/gendalf/generator/model/type_inspection/visitor/walker.py +111 -0
- gendalf-0.1.0/src/gendalf/model.py +114 -0
- gendalf-0.1.0/src/gendalf/option.py +51 -0
- gendalf-0.1.0/src/gendalf/py.typed +0 -0
- gendalf-0.1.0/src/gendalf/string_case.py +31 -0
gendalf-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Danil Troshnev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
gendalf-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: gendalf
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: You shall pass... your domain to transport! A Python codegen tool for transport layers in DDD, supporting most popular transport / API frameworks.
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: python,codegen
|
|
7
|
+
Author: zerlok
|
|
8
|
+
Author-email: danil.troshnev@gmail.com
|
|
9
|
+
Requires-Python: >=3.9,<4.0
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Dist: astlab (>=0.2.0,<0.3.0)
|
|
23
|
+
Requires-Dist: click (>=8.1.8,<9.0.0)
|
|
24
|
+
Project-URL: Homepage, https://github.com/zerlok/gendalf
|
|
25
|
+
Project-URL: Issues, https://github.com/zerlok/gendalf/issues
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# Gendalf
|
|
29
|
+
|
|
30
|
+
[](https://pypi.python.org/pypi/gendalf)
|
|
31
|
+
[](https://pypi.python.org/pypi/gendalf)
|
|
32
|
+
[](https://mypy.readthedocs.io/en/stable/getting_started.html#strict-mode-and-configuration)
|
|
33
|
+
[](https://codecov.io/gh/zerlok/gendalf)
|
|
34
|
+
[](https://pypistats.org/packages/gendalf)
|
|
35
|
+
[](https://github.com/zerlok/gendalf/stargazers)
|
|
36
|
+
|
|
37
|
+
*You shall pass... your domain to transport!*
|
|
38
|
+
|
|
39
|
+
**Gendalf** is a Python code generation tool that simplifies the creation of **transport layer** code based on
|
|
40
|
+
**Domain-Driven Design (DDD)** principles. With **gendalf**, you can effortlessly generate FastAPI, HTTPX, gRPC,
|
|
41
|
+
aiohttp, and other transport framework code from your domain layer, ensuring that your business logic remains untouched
|
|
42
|
+
while automating the generation of transport-related code.
|
|
43
|
+
|
|
44
|
+
## Key Features
|
|
45
|
+
|
|
46
|
+
- **Domain-Driven Design (DDD) approach**: Keeps your business logic in the domain layer and generates the transport
|
|
47
|
+
layer automatically.
|
|
48
|
+
- **Multi-transport framework support**: Supports FastAPI and HTTPX (next in line: gRPC; and more frameworks planned for
|
|
49
|
+
future versions).
|
|
50
|
+
- **Powered by [astlab](https://github.com/zerlok/astlab) generator**: Uses Python's built-in Abstract Syntax Tree (AST)
|
|
51
|
+
to generate Python modules from your domain entities and interfaces.
|
|
52
|
+
|
|
53
|
+
## Why gendalf?
|
|
54
|
+
|
|
55
|
+
With **gendalf**, you don’t have to worry about manually wiring your domain logic to transport code. Whether you're
|
|
56
|
+
building APIs, microservices, or handling complex asynchronous communication, **gendalf** automates the transport layer
|
|
57
|
+
creation: handles repetitive and error-prone process of writing endpoint handlers and clients from scratch, letting you
|
|
58
|
+
focus on what matters most: your business logic.
|
|
59
|
+
|
|
60
|
+
### Target Audience
|
|
61
|
+
|
|
62
|
+
This tool is designed for Python developers working on services that follow the Domain-Driven Design (DDD) approach.
|
|
63
|
+
It's particularly useful for:
|
|
64
|
+
|
|
65
|
+
* Teams focusing on business logic without needing to handle the intricacies of APIs or transport layers.
|
|
66
|
+
* Developers building Python API services.
|
|
67
|
+
* Those looking for a way to streamline the development of API endpoints and client calls without the overhead of
|
|
68
|
+
boilerplate code.
|
|
69
|
+
|
|
70
|
+
### Comparison with existing codegen solutions
|
|
71
|
+
|
|
72
|
+
There are many tools for code generation in the Python ecosystem, but most are focused on simplifying specific tasks
|
|
73
|
+
like serialization, or generating CRUD / REST operations. Here’s how **gendalf** project differs:
|
|
74
|
+
|
|
75
|
+
* **Domain-Driven Design (DDD) Focus:** Unlike other code generation tools that focus on CRUD or specific transport
|
|
76
|
+
protocols, this project fully integrates with a DDD approach. This means developers work on the domain layer and let
|
|
77
|
+
the tool handle the presentation layer (API endpoints and clients).
|
|
78
|
+
* **Fully Automated Code Generation:** The generated code for the server and client is complete and doesn’t require
|
|
79
|
+
further modifications, saving time and reducing boilerplate.
|
|
80
|
+
* **Cross-Transport Flexibility:** Currently, it supports FastAPI and HTTPX, but future versions will add gRPC support,
|
|
81
|
+
allowing developers to generate code for various transport mechanisms without changing their domain logic.
|
|
82
|
+
|
|
83
|
+
E.g. `grpcio-tools` requires `.proto` files specification first and generates client stubs & server interface, so on the
|
|
84
|
+
server side an additional code is required: implement request deserialization from protobuf python classes to domain
|
|
85
|
+
(value objects), invoke domain layer and then serialize protobuf response.
|
|
86
|
+
|
|
87
|
+
## Transports & frameworks
|
|
88
|
+
|
|
89
|
+
### FastAPI & HTTPX
|
|
90
|
+
|
|
91
|
+
Run with `gendalf gen src fastapi`. It supports:
|
|
92
|
+
|
|
93
|
+
- request-response (POST method, request & response in HTTP body in JSON format)
|
|
94
|
+
- duplex streaming (WebSocket, requests & responses are in WS frames in JSON format)
|
|
95
|
+
|
|
96
|
+
#### What’s Generated
|
|
97
|
+
|
|
98
|
+
* **api/models.py**: Pydantic models for requests and responses that mirror the domain objects.
|
|
99
|
+
* **api/client.py**: Client classes with async methods, ready to make API calls with appropriate typings for request and
|
|
100
|
+
response data.
|
|
101
|
+
* **api/server.py**: Server handler classes, which include data serialization and domain logic invocation.
|
|
102
|
+
|
|
103
|
+
The generated code is complete, with no need for additional modifications.
|
|
104
|
+
|
|
105
|
+
#### Examples
|
|
106
|
+
|
|
107
|
+
- [my greeter service](examples/my_greeter)
|
|
108
|
+
|
|
109
|
+
### gRPC (not supported yet)
|
|
110
|
+
|
|
111
|
+
**This framework support is not supported yet.**
|
|
112
|
+
|
|
113
|
+
- unary-unary
|
|
114
|
+
- stream-stream
|
|
115
|
+
- unary-stream
|
|
116
|
+
- stream-unary
|
|
117
|
+
|
gendalf-0.1.0/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Gendalf
|
|
2
|
+
|
|
3
|
+
[](https://pypi.python.org/pypi/gendalf)
|
|
4
|
+
[](https://pypi.python.org/pypi/gendalf)
|
|
5
|
+
[](https://mypy.readthedocs.io/en/stable/getting_started.html#strict-mode-and-configuration)
|
|
6
|
+
[](https://codecov.io/gh/zerlok/gendalf)
|
|
7
|
+
[](https://pypistats.org/packages/gendalf)
|
|
8
|
+
[](https://github.com/zerlok/gendalf/stargazers)
|
|
9
|
+
|
|
10
|
+
*You shall pass... your domain to transport!*
|
|
11
|
+
|
|
12
|
+
**Gendalf** is a Python code generation tool that simplifies the creation of **transport layer** code based on
|
|
13
|
+
**Domain-Driven Design (DDD)** principles. With **gendalf**, you can effortlessly generate FastAPI, HTTPX, gRPC,
|
|
14
|
+
aiohttp, and other transport framework code from your domain layer, ensuring that your business logic remains untouched
|
|
15
|
+
while automating the generation of transport-related code.
|
|
16
|
+
|
|
17
|
+
## Key Features
|
|
18
|
+
|
|
19
|
+
- **Domain-Driven Design (DDD) approach**: Keeps your business logic in the domain layer and generates the transport
|
|
20
|
+
layer automatically.
|
|
21
|
+
- **Multi-transport framework support**: Supports FastAPI and HTTPX (next in line: gRPC; and more frameworks planned for
|
|
22
|
+
future versions).
|
|
23
|
+
- **Powered by [astlab](https://github.com/zerlok/astlab) generator**: Uses Python's built-in Abstract Syntax Tree (AST)
|
|
24
|
+
to generate Python modules from your domain entities and interfaces.
|
|
25
|
+
|
|
26
|
+
## Why gendalf?
|
|
27
|
+
|
|
28
|
+
With **gendalf**, you don’t have to worry about manually wiring your domain logic to transport code. Whether you're
|
|
29
|
+
building APIs, microservices, or handling complex asynchronous communication, **gendalf** automates the transport layer
|
|
30
|
+
creation: handles repetitive and error-prone process of writing endpoint handlers and clients from scratch, letting you
|
|
31
|
+
focus on what matters most: your business logic.
|
|
32
|
+
|
|
33
|
+
### Target Audience
|
|
34
|
+
|
|
35
|
+
This tool is designed for Python developers working on services that follow the Domain-Driven Design (DDD) approach.
|
|
36
|
+
It's particularly useful for:
|
|
37
|
+
|
|
38
|
+
* Teams focusing on business logic without needing to handle the intricacies of APIs or transport layers.
|
|
39
|
+
* Developers building Python API services.
|
|
40
|
+
* Those looking for a way to streamline the development of API endpoints and client calls without the overhead of
|
|
41
|
+
boilerplate code.
|
|
42
|
+
|
|
43
|
+
### Comparison with existing codegen solutions
|
|
44
|
+
|
|
45
|
+
There are many tools for code generation in the Python ecosystem, but most are focused on simplifying specific tasks
|
|
46
|
+
like serialization, or generating CRUD / REST operations. Here’s how **gendalf** project differs:
|
|
47
|
+
|
|
48
|
+
* **Domain-Driven Design (DDD) Focus:** Unlike other code generation tools that focus on CRUD or specific transport
|
|
49
|
+
protocols, this project fully integrates with a DDD approach. This means developers work on the domain layer and let
|
|
50
|
+
the tool handle the presentation layer (API endpoints and clients).
|
|
51
|
+
* **Fully Automated Code Generation:** The generated code for the server and client is complete and doesn’t require
|
|
52
|
+
further modifications, saving time and reducing boilerplate.
|
|
53
|
+
* **Cross-Transport Flexibility:** Currently, it supports FastAPI and HTTPX, but future versions will add gRPC support,
|
|
54
|
+
allowing developers to generate code for various transport mechanisms without changing their domain logic.
|
|
55
|
+
|
|
56
|
+
E.g. `grpcio-tools` requires `.proto` files specification first and generates client stubs & server interface, so on the
|
|
57
|
+
server side an additional code is required: implement request deserialization from protobuf python classes to domain
|
|
58
|
+
(value objects), invoke domain layer and then serialize protobuf response.
|
|
59
|
+
|
|
60
|
+
## Transports & frameworks
|
|
61
|
+
|
|
62
|
+
### FastAPI & HTTPX
|
|
63
|
+
|
|
64
|
+
Run with `gendalf gen src fastapi`. It supports:
|
|
65
|
+
|
|
66
|
+
- request-response (POST method, request & response in HTTP body in JSON format)
|
|
67
|
+
- duplex streaming (WebSocket, requests & responses are in WS frames in JSON format)
|
|
68
|
+
|
|
69
|
+
#### What’s Generated
|
|
70
|
+
|
|
71
|
+
* **api/models.py**: Pydantic models for requests and responses that mirror the domain objects.
|
|
72
|
+
* **api/client.py**: Client classes with async methods, ready to make API calls with appropriate typings for request and
|
|
73
|
+
response data.
|
|
74
|
+
* **api/server.py**: Server handler classes, which include data serialization and domain logic invocation.
|
|
75
|
+
|
|
76
|
+
The generated code is complete, with no need for additional modifications.
|
|
77
|
+
|
|
78
|
+
#### Examples
|
|
79
|
+
|
|
80
|
+
- [my greeter service](examples/my_greeter)
|
|
81
|
+
|
|
82
|
+
### gRPC (not supported yet)
|
|
83
|
+
|
|
84
|
+
**This framework support is not supported yet.**
|
|
85
|
+
|
|
86
|
+
- unary-unary
|
|
87
|
+
- stream-stream
|
|
88
|
+
- unary-stream
|
|
89
|
+
- stream-unary
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "gendalf"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "You shall pass... your domain to transport! A Python codegen tool for transport layers in DDD, supporting most popular transport / API frameworks."
|
|
5
|
+
authors = ["zerlok <danil.troshnev@gmail.com>"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
keywords = [
|
|
9
|
+
"python",
|
|
10
|
+
"codegen",
|
|
11
|
+
]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
"Topic :: Software Development :: Code Generators",
|
|
17
|
+
"Typing :: Typed",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[tool.poetry.urls]
|
|
21
|
+
Homepage = "https://github.com/zerlok/gendalf"
|
|
22
|
+
Issues = "https://github.com/zerlok/gendalf/issues"
|
|
23
|
+
|
|
24
|
+
[tool.poetry.scripts]
|
|
25
|
+
gendalf = "gendalf.cli:cli"
|
|
26
|
+
|
|
27
|
+
[tool.poetry.dependencies]
|
|
28
|
+
python = "^3.9"
|
|
29
|
+
click = "^8.1.8"
|
|
30
|
+
astlab = "^0.2.0"
|
|
31
|
+
|
|
32
|
+
[tool.poetry.group.dev.dependencies]
|
|
33
|
+
mypy = "^1.13.0"
|
|
34
|
+
pytest = "^8.3.3"
|
|
35
|
+
pytest-cov = "^6.0.0"
|
|
36
|
+
ruff = ">=0.7.4,<0.9.0"
|
|
37
|
+
|
|
38
|
+
[tool.poetry.group.examples.dependencies]
|
|
39
|
+
httpx = "^0.28.1"
|
|
40
|
+
uvicorn = "^0.34.0"
|
|
41
|
+
fastapi = "^0.115.7"
|
|
42
|
+
httpx-ws = "^0.7.1"
|
|
43
|
+
|
|
44
|
+
[build-system]
|
|
45
|
+
requires = ["poetry-core"]
|
|
46
|
+
build-backend = "poetry.core.masonry.api"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
[tool.ruff]
|
|
50
|
+
target-version = "py39"
|
|
51
|
+
include = ["src/**/*.py", "tests/**/*.py"]
|
|
52
|
+
force-exclude = true
|
|
53
|
+
line-length = 120
|
|
54
|
+
output-format = "pylint"
|
|
55
|
+
|
|
56
|
+
[tool.ruff.lint]
|
|
57
|
+
select = ["ALL"]
|
|
58
|
+
ignore = [
|
|
59
|
+
"ANN", # because we use mypy
|
|
60
|
+
"D", # TODO: add docstrings to public code
|
|
61
|
+
"FA", # TODO: consider should we use __annotations__
|
|
62
|
+
"TD", # no task tracking
|
|
63
|
+
"FIX", # TODO: consider enable it against new code on pull requests
|
|
64
|
+
"COM812", # because ruff format suggests to skip it
|
|
65
|
+
"ISC001", # because ruff format suggests to skip it
|
|
66
|
+
"RET505", # clashes with mypy exhaustiveness check
|
|
67
|
+
"S101", # allow asserts for tests checks and mypy help
|
|
68
|
+
# TODO: stop ignore this rule
|
|
69
|
+
"UP007", # because of 3.9 support
|
|
70
|
+
# TODO: stop ignore this rule
|
|
71
|
+
"SIM117", # because ast builder 2.0 uses with statement to build contextual scope statemetns
|
|
72
|
+
"ARG002", # because `ruff` can't handle `override` from internal `gendalf._typing`.
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
[tool.ruff.lint.per-file-ignores]
|
|
76
|
+
# CLI is built with `click` framework
|
|
77
|
+
"src/gendalf/cli.py" = [
|
|
78
|
+
"FBT001", # CLI commands can use non keyword flag options
|
|
79
|
+
"PLR0913", # CLI commands can use a lots of arguments and options
|
|
80
|
+
]
|
|
81
|
+
"tests/**" = [
|
|
82
|
+
"PLR0913", # test functions can use a lots of arguments and fixtures
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
[tool.mypy]
|
|
87
|
+
files = ["src", "tests"]
|
|
88
|
+
strict = true
|
|
89
|
+
disallow_any_unimported = true
|
|
90
|
+
disallow_any_expr = true
|
|
91
|
+
disallow_any_decorated = true
|
|
92
|
+
disallow_any_explicit = true
|
|
93
|
+
disallow_any_generics = true
|
|
94
|
+
disallow_subclassing_any = true
|
|
95
|
+
disallow_untyped_calls = true
|
|
96
|
+
disallow_untyped_defs = true
|
|
97
|
+
disallow_incomplete_defs = true
|
|
98
|
+
disallow_untyped_decorators = true
|
|
99
|
+
warn_redundant_casts = true
|
|
100
|
+
warn_unused_ignores = true
|
|
101
|
+
warn_no_return = true
|
|
102
|
+
warn_return_any = true
|
|
103
|
+
warn_unreachable = true
|
|
104
|
+
strict_equality = true
|
|
105
|
+
strict_optional = true
|
|
106
|
+
enable_error_code = [
|
|
107
|
+
"redundant-self",
|
|
108
|
+
"redundant-expr",
|
|
109
|
+
"possibly-undefined",
|
|
110
|
+
"truthy-bool",
|
|
111
|
+
"truthy-iterable",
|
|
112
|
+
"ignore-without-code",
|
|
113
|
+
"unused-awaitable",
|
|
114
|
+
"explicit-override",
|
|
115
|
+
"mutable-override",
|
|
116
|
+
"unimported-reveal",
|
|
117
|
+
"narrowed-type-not-subtype",
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
# NOTE: in some modules it's hard to disallow `typing.Any` completely, so allow it (e.g. `click` has `typing.Any` in
|
|
121
|
+
# some decorators).
|
|
122
|
+
[[tool.mypy.overrides]]
|
|
123
|
+
module = [
|
|
124
|
+
"gendalf.cli",
|
|
125
|
+
"gendalf.entrypoint.inspection",
|
|
126
|
+
"gendalf.generator.model.type_inspection.visitor.trait",
|
|
127
|
+
]
|
|
128
|
+
disallow_any_expr = false
|
|
129
|
+
|
|
130
|
+
# NOTE: allow return `typing.Any` in test fixtures (e.g. mock objects created with `create_autospec`)
|
|
131
|
+
[[tool.mypy.overrides]]
|
|
132
|
+
module = ["tests.*"]
|
|
133
|
+
disallow_any_expr = false
|
|
134
|
+
disallow_any_explicit = false
|
|
135
|
+
warn_return_any = false
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
[tool.pytest.ini_options]
|
|
139
|
+
pythonpath = [
|
|
140
|
+
"src",
|
|
141
|
+
]
|
|
142
|
+
addopts = [
|
|
143
|
+
"--cov=src",
|
|
144
|
+
"--cov-report=term-missing",
|
|
145
|
+
]
|
|
146
|
+
testpaths = [
|
|
147
|
+
"tests",
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
[tool.coverage.run]
|
|
151
|
+
branch = true
|
|
152
|
+
|
|
153
|
+
[tool.coverage.report]
|
|
154
|
+
exclude_lines = [
|
|
155
|
+
"pragma: no cover",
|
|
156
|
+
"@abc.abstractmethod",
|
|
157
|
+
"if __name__ == .__main__.:",
|
|
158
|
+
"if t.TYPE_CHECKING:",
|
|
159
|
+
]
|
|
160
|
+
show_missing = true
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"Self",
|
|
3
|
+
"TypeAlias",
|
|
4
|
+
"TypeGuard",
|
|
5
|
+
"assert_never",
|
|
6
|
+
"override",
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
import typing as t
|
|
10
|
+
|
|
11
|
+
# NOTE: this allows to use methods with `Self` during runtime (when typing_extensions is not installed).
|
|
12
|
+
if t.TYPE_CHECKING:
|
|
13
|
+
from typing_extensions import Self, TypeAlias, TypeGuard, assert_never, override
|
|
14
|
+
|
|
15
|
+
else:
|
|
16
|
+
Self = t.Any
|
|
17
|
+
TypeAlias = t.Any
|
|
18
|
+
TypeGuard = t.Optional
|
|
19
|
+
|
|
20
|
+
def assert_never(*args: object, **kwargs: object) -> t.NoReturn:
|
|
21
|
+
raise RuntimeError(args, kwargs) # pragma: no cover
|
|
22
|
+
|
|
23
|
+
def override(func: object) -> object:
|
|
24
|
+
return func
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from gendalf._typing import assert_never
|
|
8
|
+
from gendalf.entrypoint.inspection import inspect_source_dir
|
|
9
|
+
from gendalf.entrypoint.printer import Printer
|
|
10
|
+
from gendalf.generator.fastapi import FastAPICodeGenerator
|
|
11
|
+
from gendalf.model import GeneratorContext
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class CLIContext:
|
|
16
|
+
source: Path
|
|
17
|
+
ignore_module_on_import_error: bool
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
pass_cli_context = click.make_pass_decorator(CLIContext)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@click.group()
|
|
24
|
+
@click.pass_context
|
|
25
|
+
@click.argument(
|
|
26
|
+
"source",
|
|
27
|
+
type=click.Path(exists=True, resolve_path=True, path_type=Path),
|
|
28
|
+
)
|
|
29
|
+
@click.option(
|
|
30
|
+
"--ignore-module-on-import-error",
|
|
31
|
+
type=bool,
|
|
32
|
+
is_flag=True,
|
|
33
|
+
default=False,
|
|
34
|
+
)
|
|
35
|
+
def cli(
|
|
36
|
+
context: click.Context,
|
|
37
|
+
source: Path,
|
|
38
|
+
ignore_module_on_import_error: bool,
|
|
39
|
+
) -> None:
|
|
40
|
+
context.obj = CLIContext(
|
|
41
|
+
source=source,
|
|
42
|
+
ignore_module_on_import_error=ignore_module_on_import_error,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
OPT_OUTPUT = click.option(
|
|
47
|
+
"-o",
|
|
48
|
+
"--output",
|
|
49
|
+
type=click.Path(writable=True, resolve_path=True, path_type=Path),
|
|
50
|
+
default=None,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
GenKind = t.Literal["fastapi"]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@cli.command()
|
|
58
|
+
@click.pass_obj
|
|
59
|
+
@click.argument(
|
|
60
|
+
"kind",
|
|
61
|
+
type=click.Choice(t.get_args(GenKind)),
|
|
62
|
+
)
|
|
63
|
+
@OPT_OUTPUT
|
|
64
|
+
@click.option(
|
|
65
|
+
"-p",
|
|
66
|
+
"--package",
|
|
67
|
+
type=str,
|
|
68
|
+
default=None,
|
|
69
|
+
)
|
|
70
|
+
@click.option(
|
|
71
|
+
"--dry-run",
|
|
72
|
+
type=bool,
|
|
73
|
+
is_flag=True,
|
|
74
|
+
default=False,
|
|
75
|
+
)
|
|
76
|
+
def cast(
|
|
77
|
+
context: CLIContext,
|
|
78
|
+
kind: GenKind,
|
|
79
|
+
output: t.Optional[Path],
|
|
80
|
+
package: t.Optional[str],
|
|
81
|
+
dry_run: bool,
|
|
82
|
+
) -> None:
|
|
83
|
+
"""Generate code for specified python package."""
|
|
84
|
+
|
|
85
|
+
gen_context = GeneratorContext(
|
|
86
|
+
entrypoints=list(
|
|
87
|
+
inspect_source_dir(context.source, ignore_module_on_import_error=context.ignore_module_on_import_error)
|
|
88
|
+
),
|
|
89
|
+
source=context.source,
|
|
90
|
+
output=output if output is not None else context.source,
|
|
91
|
+
package=package,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if kind == "fastapi":
|
|
95
|
+
gen = FastAPICodeGenerator()
|
|
96
|
+
else:
|
|
97
|
+
assert_never(kind)
|
|
98
|
+
|
|
99
|
+
for file in gen.generate(gen_context).files:
|
|
100
|
+
if dry_run:
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
file.path.parent.mkdir(parents=True, exist_ok=True)
|
|
104
|
+
with file.path.open("w") as fd:
|
|
105
|
+
fd.write(file.content)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@cli.command()
|
|
109
|
+
@click.pass_obj
|
|
110
|
+
def show(context: CLIContext) -> None:
|
|
111
|
+
"""Show info about the package"""
|
|
112
|
+
|
|
113
|
+
printer = Printer(click.echo)
|
|
114
|
+
|
|
115
|
+
for entrypoint in inspect_source_dir(
|
|
116
|
+
context.source,
|
|
117
|
+
ignore_module_on_import_error=context.ignore_module_on_import_error,
|
|
118
|
+
):
|
|
119
|
+
entrypoint.accept(printer)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
if __name__ == "__main__":
|
|
123
|
+
cli()
|
|
File without changes
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
from functools import partial
|
|
3
|
+
|
|
4
|
+
from gendalf.model import EntrypointOptions
|
|
5
|
+
|
|
6
|
+
T = t.TypeVar("T")
|
|
7
|
+
__ENTRYPOINT_CONFIG = """__gendalf_entrypoint_config__"""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@t.overload
|
|
11
|
+
def entrypoint(obj: type[T]) -> type[T]: ...
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@t.overload
|
|
15
|
+
def entrypoint(*, name: str) -> t.Callable[[type[T]], type[T]]: ...
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def entrypoint(
|
|
19
|
+
obj: t.Optional[type[T]] = None,
|
|
20
|
+
name: t.Optional[str] = None,
|
|
21
|
+
) -> t.Union[type[T], t.Callable[[type[T]], type[T]]]:
|
|
22
|
+
return (
|
|
23
|
+
_mark_entrypoint(obj, EntrypointOptions())
|
|
24
|
+
if obj is not None
|
|
25
|
+
# NOTE: mypy thinks that `T` of `_mark_entrypoint` is not the same `T` of `entrypoint`
|
|
26
|
+
else t.cast(t.Callable[[type[T]], type[T]], partial(_mark_entrypoint, options=EntrypointOptions(name=name)))
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _mark_entrypoint(obj: type[T], options: EntrypointOptions) -> type[T]:
|
|
31
|
+
setattr(obj, __ENTRYPOINT_CONFIG, options)
|
|
32
|
+
return obj
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_entrypoint_options(obj: object) -> t.Optional[EntrypointOptions]:
|
|
36
|
+
opts: object = getattr(obj, __ENTRYPOINT_CONFIG, None)
|
|
37
|
+
assert opts is None or isinstance(opts, EntrypointOptions)
|
|
38
|
+
return opts
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import importlib
|
|
3
|
+
import inspect
|
|
4
|
+
import sys
|
|
5
|
+
import typing as t
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from types import ModuleType
|
|
8
|
+
|
|
9
|
+
from astlab.info import TypeInfo
|
|
10
|
+
from astlab.reader import import_module, walk_package_modules
|
|
11
|
+
|
|
12
|
+
from gendalf.entrypoint.decorator import get_entrypoint_options
|
|
13
|
+
from gendalf.model import EntrypointInfo, MethodInfo, ParameterInfo, StreamStreamMethodInfo, UnaryUnaryMethodInfo
|
|
14
|
+
from gendalf.option import Option
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def inspect_source_dir(
|
|
18
|
+
src: Path,
|
|
19
|
+
*,
|
|
20
|
+
ignore_module_on_import_error: bool = False,
|
|
21
|
+
) -> t.Iterable[EntrypointInfo]:
|
|
22
|
+
sys.path.append(str(src))
|
|
23
|
+
try:
|
|
24
|
+
for path in walk_package_modules(src):
|
|
25
|
+
if path.stem.startswith("_"):
|
|
26
|
+
continue
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
module = import_module(path)
|
|
30
|
+
|
|
31
|
+
except ImportError:
|
|
32
|
+
if not ignore_module_on_import_error:
|
|
33
|
+
raise
|
|
34
|
+
|
|
35
|
+
else:
|
|
36
|
+
yield from inspect_module(module)
|
|
37
|
+
|
|
38
|
+
finally:
|
|
39
|
+
importlib.invalidate_caches()
|
|
40
|
+
sys.path.remove(str(src))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def inspect_module(module: ModuleType) -> t.Iterable[EntrypointInfo]:
|
|
44
|
+
for name, obj in inspect.getmembers(module):
|
|
45
|
+
if not inspect.isclass(obj) or obj.__module__ != module.__name__:
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
opts = get_entrypoint_options(obj)
|
|
49
|
+
if opts is None:
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
type_info = TypeInfo.from_type(obj)
|
|
53
|
+
|
|
54
|
+
yield EntrypointInfo(
|
|
55
|
+
name=opts.name if opts.name is not None else name,
|
|
56
|
+
type_=type_info,
|
|
57
|
+
methods=tuple(
|
|
58
|
+
inspect_method(member_name, member)
|
|
59
|
+
for member_name, member in inspect.getmembers(obj)
|
|
60
|
+
if not member_name.startswith("_") and callable(member)
|
|
61
|
+
),
|
|
62
|
+
doc=inspect.getdoc(obj),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Func(t.Protocol):
|
|
67
|
+
@abc.abstractmethod
|
|
68
|
+
def __call__(self, *args: object, **kwargs: object) -> object:
|
|
69
|
+
raise NotImplementedError
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def inspect_method(name: str, func: Func) -> MethodInfo:
|
|
73
|
+
signature = inspect.signature(func)
|
|
74
|
+
|
|
75
|
+
params = list(signature.parameters.values())[1:]
|
|
76
|
+
|
|
77
|
+
# TODO: uncomment
|
|
78
|
+
if len(params) == 1 and (streaming_type := extract_streaming_type(params[0].annotation)) is not None:
|
|
79
|
+
return StreamStreamMethodInfo(
|
|
80
|
+
name=name,
|
|
81
|
+
input_=_build_param(params[0].replace(annotation=streaming_type)),
|
|
82
|
+
output=extract_streaming_type(signature.return_annotation),
|
|
83
|
+
doc=inspect.getdoc(func),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return UnaryUnaryMethodInfo(
|
|
87
|
+
name=name,
|
|
88
|
+
# skip `self`
|
|
89
|
+
params=[_build_param(param) for param in params],
|
|
90
|
+
returns=signature.return_annotation,
|
|
91
|
+
doc=inspect.getdoc(func),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def extract_streaming_type(obj: object) -> t.Optional[type[object]]:
|
|
96
|
+
origin = t.get_origin(obj)
|
|
97
|
+
if not isinstance(origin, type) or not issubclass(origin, (t.Iterator, t.AsyncIterator)):
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
args = t.get_args(obj)
|
|
101
|
+
assert len(args) == 1
|
|
102
|
+
|
|
103
|
+
# TODO: remove cast, check types
|
|
104
|
+
return t.cast(type[object], args[0])
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _build_param(param: inspect.Parameter) -> ParameterInfo:
|
|
108
|
+
return ParameterInfo(
|
|
109
|
+
name=param.name,
|
|
110
|
+
annotation=param.annotation,
|
|
111
|
+
default=Option(param.default) if param.default is not param.empty else Option[object].empty(),
|
|
112
|
+
)
|