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.
Files changed (28) hide show
  1. gendalf-0.1.0/LICENSE +21 -0
  2. gendalf-0.1.0/PKG-INFO +117 -0
  3. gendalf-0.1.0/README.md +89 -0
  4. gendalf-0.1.0/pyproject.toml +160 -0
  5. gendalf-0.1.0/src/gendalf/__init__.py +0 -0
  6. gendalf-0.1.0/src/gendalf/_typing.py +24 -0
  7. gendalf-0.1.0/src/gendalf/cli.py +123 -0
  8. gendalf-0.1.0/src/gendalf/entrypoint/__init__.py +0 -0
  9. gendalf-0.1.0/src/gendalf/entrypoint/decorator.py +38 -0
  10. gendalf-0.1.0/src/gendalf/entrypoint/inspection.py +112 -0
  11. gendalf-0.1.0/src/gendalf/entrypoint/printer.py +77 -0
  12. gendalf-0.1.0/src/gendalf/generator/__init__.py +0 -0
  13. gendalf-0.1.0/src/gendalf/generator/abc.py +9 -0
  14. gendalf-0.1.0/src/gendalf/generator/fastapi.py +528 -0
  15. gendalf-0.1.0/src/gendalf/generator/model/__init__.py +0 -0
  16. gendalf-0.1.0/src/gendalf/generator/model/abc.py +9 -0
  17. gendalf-0.1.0/src/gendalf/generator/model/builder.py +306 -0
  18. gendalf-0.1.0/src/gendalf/generator/model/factory.py +31 -0
  19. gendalf-0.1.0/src/gendalf/generator/model/type_inspection/__init__.py +1 -0
  20. gendalf-0.1.0/src/gendalf/generator/model/type_inspection/visitor/__init__.py +0 -0
  21. gendalf-0.1.0/src/gendalf/generator/model/type_inspection/visitor/abc.py +96 -0
  22. gendalf-0.1.0/src/gendalf/generator/model/type_inspection/visitor/model.py +60 -0
  23. gendalf-0.1.0/src/gendalf/generator/model/type_inspection/visitor/trait.py +157 -0
  24. gendalf-0.1.0/src/gendalf/generator/model/type_inspection/visitor/walker.py +111 -0
  25. gendalf-0.1.0/src/gendalf/model.py +114 -0
  26. gendalf-0.1.0/src/gendalf/option.py +51 -0
  27. gendalf-0.1.0/src/gendalf/py.typed +0 -0
  28. 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
+ [![Latest Version](https://img.shields.io/pypi/v/gendalf.svg)](https://pypi.python.org/pypi/gendalf)
31
+ [![Python Supported Versions](https://img.shields.io/pypi/pyversions/gendalf.svg)](https://pypi.python.org/pypi/gendalf)
32
+ [![MyPy Strict](https://img.shields.io/badge/mypy-strict-blue)](https://mypy.readthedocs.io/en/stable/getting_started.html#strict-mode-and-configuration)
33
+ [![Test Coverage](https://codecov.io/gh/zerlok/gendalf/branch/main/graph/badge.svg)](https://codecov.io/gh/zerlok/gendalf)
34
+ [![Downloads](https://img.shields.io/pypi/dm/gendalf.svg)](https://pypistats.org/packages/gendalf)
35
+ [![GitHub stars](https://img.shields.io/github/stars/zerlok/gendalf)](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
+
@@ -0,0 +1,89 @@
1
+ # Gendalf
2
+
3
+ [![Latest Version](https://img.shields.io/pypi/v/gendalf.svg)](https://pypi.python.org/pypi/gendalf)
4
+ [![Python Supported Versions](https://img.shields.io/pypi/pyversions/gendalf.svg)](https://pypi.python.org/pypi/gendalf)
5
+ [![MyPy Strict](https://img.shields.io/badge/mypy-strict-blue)](https://mypy.readthedocs.io/en/stable/getting_started.html#strict-mode-and-configuration)
6
+ [![Test Coverage](https://codecov.io/gh/zerlok/gendalf/branch/main/graph/badge.svg)](https://codecov.io/gh/zerlok/gendalf)
7
+ [![Downloads](https://img.shields.io/pypi/dm/gendalf.svg)](https://pypistats.org/packages/gendalf)
8
+ [![GitHub stars](https://img.shields.io/github/stars/zerlok/gendalf)](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
+ )