yaafcli 2026.2.4.180929__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
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.
@@ -0,0 +1,206 @@
1
+ Metadata-Version: 2.4
2
+ Name: yaafcli
3
+ Version: 2026.2.4.180929
4
+ Summary: Minimal ASGI app scaffold
5
+ License: MIT License
6
+
7
+ Copyright (c) 2026
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
26
+
27
+ Classifier: License :: OSI Approved :: MIT License
28
+ Classifier: Programming Language :: Python :: 3
29
+ Classifier: Programming Language :: Python :: 3.13
30
+ Classifier: Operating System :: OS Independent
31
+ Classifier: Framework :: AsyncIO
32
+ Classifier: Topic :: Internet :: WWW/HTTP
33
+ Classifier: Topic :: Software Development :: Libraries
34
+ Requires-Python: >=3.13
35
+ Description-Content-Type: text/markdown
36
+ License-File: LICENSE
37
+ Requires-Dist: uvicorn>=0.23
38
+ Provides-Extra: test
39
+ Requires-Dist: pytest>=7.4; extra == "test"
40
+ Requires-Dist: pytest-asyncio>=0.23; extra == "test"
41
+ Dynamic: license-file
42
+
43
+ # yaaf
44
+
45
+ YAAF stands for "Yet Another ASGI Framework".
46
+
47
+ A minimal Python ASGI app scaffold that discovers routes from the filesystem. It includes a tiny router and a CLI wrapper around `uvicorn`.
48
+
49
+ ## Design Goals and Opinions
50
+
51
+ - **Filesystem-first routing.** Routes are inferred from the directory structure under `consumers/**/api` rather than declared with decorators. This keeps routing discoverable by looking at the tree.
52
+ - **Explicit endpoint files.** Each route has `_server.py` and `_service.py` to separate request handling from domain logic.
53
+ - **Dependency injection without wiring.** Services are registered automatically and injected by name/type, so handlers and services focus on behavior, not setup.
54
+ - **Static-first routing precedence.** Static routes always win over dynamic segments, with warnings when a dynamic route would overlap a static route.
55
+ - **Minimal core.** The framework is intentionally small and opinionated, leaving room for you to add auth, middleware, validation, etc.
56
+
57
+ ## Quickstart
58
+
59
+ ```bash
60
+ python -m venv .venv
61
+ source .venv/bin/activate
62
+ pip install -e .
63
+
64
+ # Run the built-in example routes
65
+ yaaf --reload
66
+ ```
67
+
68
+ Example routes:
69
+
70
+ - `GET /api/hello`
71
+ - `GET /api/<name>` (dynamic segment)
72
+
73
+ ## Routing Model
74
+
75
+ Routes are inferred from the directory structure under any `consumers/**/api` directory.
76
+
77
+ - Every route directory must contain `_server.py` and `_service.py`.
78
+ - The route path is `/api/...` plus the sub-path after `api/`.
79
+ - Dynamic segments use `[param]` directory names and are exposed as `params`/`path_params`.
80
+
81
+ Example layout:
82
+
83
+ ```text
84
+ consumers/
85
+ api/
86
+ users/
87
+ _server.py
88
+ _service.py
89
+ hello/
90
+ _server.py
91
+ _service.py
92
+ [name]/
93
+ _server.py
94
+ _service.py
95
+ ```
96
+
97
+ ## Handlers and Services
98
+
99
+ In `_server.py`, export functions named after HTTP methods (lowercase): `get`, `post`, etc. The function signature is resolved via dependency injection:
100
+
101
+ - `request` gives you the `yaaf.Request` object.
102
+ - `params` or `path_params` provides dynamic route parameters.
103
+ - Services are injected by type annotations.
104
+
105
+ Example `_server.py`:
106
+
107
+ ```python
108
+ from consumers.services import HelloService
109
+ from yaaf import Request
110
+ from yaaf.types import Params
111
+
112
+
113
+ async def get(request: Request, service: HelloService, params: Params):
114
+ return {"message": service.message(), "path": request.path, "params": params}
115
+ ```
116
+
117
+ In `_service.py`, expose a module-level `service` instance (or a callable like `Service` or `get_service`). Services are registered and can be injected into other services or handlers:
118
+
119
+ ```python
120
+ from consumers.services import UsersService
121
+
122
+
123
+ class Service:
124
+ def __init__(self, users: UsersService) -> None:
125
+ self._users = users
126
+
127
+ def message(self) -> str:
128
+ user = self._users.get_user("1")
129
+ return f"Hello from yaaf, {user['name']}"
130
+
131
+ service = Service
132
+ ```
133
+
134
+ ## Service-to-Service Injection
135
+
136
+ Services can depend on other services via type annotations. Example layout:
137
+
138
+ ```text
139
+ consumers/
140
+ api/
141
+ users/
142
+ _service.py
143
+ _server.py
144
+ hello/
145
+ _service.py
146
+ _server.py
147
+ ```
148
+
149
+ `consumers/api/users/_service.py`
150
+ ```python
151
+ class Service:
152
+ def get_user(self, user_id: str) -> dict:
153
+ return {"id": user_id, "name": "Austin"}
154
+
155
+ service = Service()
156
+ ```
157
+
158
+ `consumers/api/hello/_service.py`
159
+ ```python
160
+ from yaaf.services import UsersService
161
+
162
+
163
+ class Service:
164
+ def __init__(self, users: UsersService) -> None:
165
+ self._users = users
166
+
167
+ def message(self) -> str:
168
+ user = self._users.get_user("1")
169
+ return f"Hello from yaaf, {user['name']}"
170
+
171
+ service = Service
172
+ ```
173
+
174
+ `consumers/api/hello/_server.py`
175
+ ```python
176
+ from consumers.services import HelloService
177
+ from yaaf import Request
178
+
179
+
180
+ async def get(request: Request, service: HelloService):
181
+ return {"message": service.message(), "path": request.path}
182
+ ```
183
+
184
+ ## Running Another App
185
+
186
+ ```bash
187
+ yaaf --app your_package.app:app
188
+ ```
189
+
190
+ ## Versioning
191
+
192
+ This project uses calendar-based versions with a timestamp (UTC). To bump the version:
193
+
194
+ ```bash
195
+ python scripts/bump_version.py
196
+ ```
197
+
198
+ ## Service Type Generation
199
+
200
+ Every `yaaf` command regenerates `consumers/services.py` for type-checking. You can also run it explicitly:
201
+
202
+ ```bash
203
+ yaaf gen-services
204
+ ```
205
+
206
+ Dynamic route segments like `[name]` get Protocol stubs in the generated file since they are not valid import paths.
@@ -0,0 +1,164 @@
1
+ # yaaf
2
+
3
+ YAAF stands for "Yet Another ASGI Framework".
4
+
5
+ A minimal Python ASGI app scaffold that discovers routes from the filesystem. It includes a tiny router and a CLI wrapper around `uvicorn`.
6
+
7
+ ## Design Goals and Opinions
8
+
9
+ - **Filesystem-first routing.** Routes are inferred from the directory structure under `consumers/**/api` rather than declared with decorators. This keeps routing discoverable by looking at the tree.
10
+ - **Explicit endpoint files.** Each route has `_server.py` and `_service.py` to separate request handling from domain logic.
11
+ - **Dependency injection without wiring.** Services are registered automatically and injected by name/type, so handlers and services focus on behavior, not setup.
12
+ - **Static-first routing precedence.** Static routes always win over dynamic segments, with warnings when a dynamic route would overlap a static route.
13
+ - **Minimal core.** The framework is intentionally small and opinionated, leaving room for you to add auth, middleware, validation, etc.
14
+
15
+ ## Quickstart
16
+
17
+ ```bash
18
+ python -m venv .venv
19
+ source .venv/bin/activate
20
+ pip install -e .
21
+
22
+ # Run the built-in example routes
23
+ yaaf --reload
24
+ ```
25
+
26
+ Example routes:
27
+
28
+ - `GET /api/hello`
29
+ - `GET /api/<name>` (dynamic segment)
30
+
31
+ ## Routing Model
32
+
33
+ Routes are inferred from the directory structure under any `consumers/**/api` directory.
34
+
35
+ - Every route directory must contain `_server.py` and `_service.py`.
36
+ - The route path is `/api/...` plus the sub-path after `api/`.
37
+ - Dynamic segments use `[param]` directory names and are exposed as `params`/`path_params`.
38
+
39
+ Example layout:
40
+
41
+ ```text
42
+ consumers/
43
+ api/
44
+ users/
45
+ _server.py
46
+ _service.py
47
+ hello/
48
+ _server.py
49
+ _service.py
50
+ [name]/
51
+ _server.py
52
+ _service.py
53
+ ```
54
+
55
+ ## Handlers and Services
56
+
57
+ In `_server.py`, export functions named after HTTP methods (lowercase): `get`, `post`, etc. The function signature is resolved via dependency injection:
58
+
59
+ - `request` gives you the `yaaf.Request` object.
60
+ - `params` or `path_params` provides dynamic route parameters.
61
+ - Services are injected by type annotations.
62
+
63
+ Example `_server.py`:
64
+
65
+ ```python
66
+ from consumers.services import HelloService
67
+ from yaaf import Request
68
+ from yaaf.types import Params
69
+
70
+
71
+ async def get(request: Request, service: HelloService, params: Params):
72
+ return {"message": service.message(), "path": request.path, "params": params}
73
+ ```
74
+
75
+ In `_service.py`, expose a module-level `service` instance (or a callable like `Service` or `get_service`). Services are registered and can be injected into other services or handlers:
76
+
77
+ ```python
78
+ from consumers.services import UsersService
79
+
80
+
81
+ class Service:
82
+ def __init__(self, users: UsersService) -> None:
83
+ self._users = users
84
+
85
+ def message(self) -> str:
86
+ user = self._users.get_user("1")
87
+ return f"Hello from yaaf, {user['name']}"
88
+
89
+ service = Service
90
+ ```
91
+
92
+ ## Service-to-Service Injection
93
+
94
+ Services can depend on other services via type annotations. Example layout:
95
+
96
+ ```text
97
+ consumers/
98
+ api/
99
+ users/
100
+ _service.py
101
+ _server.py
102
+ hello/
103
+ _service.py
104
+ _server.py
105
+ ```
106
+
107
+ `consumers/api/users/_service.py`
108
+ ```python
109
+ class Service:
110
+ def get_user(self, user_id: str) -> dict:
111
+ return {"id": user_id, "name": "Austin"}
112
+
113
+ service = Service()
114
+ ```
115
+
116
+ `consumers/api/hello/_service.py`
117
+ ```python
118
+ from yaaf.services import UsersService
119
+
120
+
121
+ class Service:
122
+ def __init__(self, users: UsersService) -> None:
123
+ self._users = users
124
+
125
+ def message(self) -> str:
126
+ user = self._users.get_user("1")
127
+ return f"Hello from yaaf, {user['name']}"
128
+
129
+ service = Service
130
+ ```
131
+
132
+ `consumers/api/hello/_server.py`
133
+ ```python
134
+ from consumers.services import HelloService
135
+ from yaaf import Request
136
+
137
+
138
+ async def get(request: Request, service: HelloService):
139
+ return {"message": service.message(), "path": request.path}
140
+ ```
141
+
142
+ ## Running Another App
143
+
144
+ ```bash
145
+ yaaf --app your_package.app:app
146
+ ```
147
+
148
+ ## Versioning
149
+
150
+ This project uses calendar-based versions with a timestamp (UTC). To bump the version:
151
+
152
+ ```bash
153
+ python scripts/bump_version.py
154
+ ```
155
+
156
+ ## Service Type Generation
157
+
158
+ Every `yaaf` command regenerates `consumers/services.py` for type-checking. You can also run it explicitly:
159
+
160
+ ```bash
161
+ yaaf gen-services
162
+ ```
163
+
164
+ Dynamic route segments like `[name]` get Protocol stubs in the generated file since they are not valid import paths.
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "yaafcli"
7
+ version = "2026.02.04.180929"
8
+ description = "Minimal ASGI app scaffold"
9
+ readme = "README.md"
10
+ requires-python = ">=3.13"
11
+ dependencies = ["uvicorn>=0.23"]
12
+ license = { file = "LICENSE" }
13
+ classifiers = [
14
+ "License :: OSI Approved :: MIT License",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.13",
17
+ "Operating System :: OS Independent",
18
+ "Framework :: AsyncIO",
19
+ "Topic :: Internet :: WWW/HTTP",
20
+ "Topic :: Software Development :: Libraries",
21
+ ]
22
+
23
+ [project.optional-dependencies]
24
+ test = ["pytest>=7.4", "pytest-asyncio>=0.23"]
25
+
26
+ [project.scripts]
27
+ yaaf = "yaaf.cli:main"
28
+
29
+ [tool.setuptools]
30
+ packages = ["yaaf"]
31
+
32
+ [tool.setuptools.package-data]
33
+ yaaf = ["py.typed"]
34
+
35
+ [tool.mypy]
36
+ python_version = "3.13"
37
+ warn_return_any = true
38
+ warn_unused_configs = true
39
+ no_implicit_optional = true
40
+ check_untyped_defs = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,108 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import pytest
6
+
7
+ from yaaf.app import App
8
+ from yaaf.gen_services import generate_services
9
+
10
+
11
+ class DummySend:
12
+ def __init__(self) -> None:
13
+ self.messages: list[dict] = []
14
+
15
+ async def __call__(self, message: dict) -> None:
16
+ self.messages.append(message)
17
+
18
+
19
+ class DummyReceive:
20
+ def __init__(self, body: bytes = b"") -> None:
21
+ self.body = body
22
+ self.sent = False
23
+
24
+ async def __call__(self) -> dict:
25
+ if self.sent:
26
+ return {"type": "http.request", "body": b"", "more_body": False}
27
+ self.sent = True
28
+ return {"type": "http.request", "body": self.body, "more_body": False}
29
+
30
+
31
+ def _write_service(path: Path, code: str) -> None:
32
+ (path / "_service.py").write_text(code)
33
+
34
+
35
+ def _write_server(path: Path, code: str) -> None:
36
+ (path / "_server.py").write_text(code)
37
+
38
+
39
+ @pytest.mark.asyncio
40
+ async def test_app_routes_and_params(tmp_path: Path) -> None:
41
+ base = tmp_path / "consumers" / "api"
42
+ hello = base / "hello"
43
+ dynamic = base / "[name]"
44
+ hello.mkdir(parents=True)
45
+ dynamic.mkdir(parents=True)
46
+ (tmp_path / "consumers" / "__init__.py").write_text("# package\n")
47
+
48
+ _write_service(
49
+ hello,
50
+ "class Service:\n"
51
+ " def message(self) -> str:\n"
52
+ " return 'hi'\n\n"
53
+ "service = Service()\n",
54
+ )
55
+ _write_server(
56
+ hello,
57
+ "from consumers.services import HelloService\n"
58
+ "from yaaf import Request\n\n"
59
+ "async def get(request: Request, service: HelloService):\n"
60
+ " return {'message': service.message(), 'path': request.path}\n",
61
+ )
62
+
63
+ _write_service(
64
+ dynamic,
65
+ "class Service:\n"
66
+ " def greet(self, name: str) -> str:\n"
67
+ " return f'hello {name}'\n\n"
68
+ "service = Service()\n",
69
+ )
70
+ _write_server(
71
+ dynamic,
72
+ "from consumers.services import NameService\n"
73
+ "from yaaf.types import Params\n\n"
74
+ "async def get(params: Params, service: NameService):\n"
75
+ " return {'message': service.greet(params['name'])}\n",
76
+ )
77
+
78
+ generate_services(consumers_dir=str(tmp_path / "consumers"))
79
+ app = App(consumers_dir=str(tmp_path / "consumers"))
80
+
81
+ send = DummySend()
82
+ scope = {"type": "http", "method": "GET", "path": "/api/hello", "headers": []}
83
+ await app(scope, DummyReceive(), send)
84
+ assert send.messages[0]["status"] == 200
85
+ assert b"hi" in send.messages[1]["body"]
86
+
87
+ send = DummySend()
88
+ scope = {"type": "http", "method": "GET", "path": "/api/austin", "headers": []}
89
+ await app(scope, DummyReceive(), send)
90
+ assert send.messages[0]["status"] == 200
91
+ assert b"austin" in send.messages[1]["body"]
92
+
93
+
94
+ @pytest.mark.asyncio
95
+ async def test_app_not_found(tmp_path: Path) -> None:
96
+ base = tmp_path / "consumers" / "api" / "hello"
97
+ base.mkdir(parents=True)
98
+ (tmp_path / "consumers" / "__init__.py").write_text("# package\n")
99
+ _write_service(base, "class Service:...\nservice = Service()\n")
100
+ _write_server(base, "async def get():...\n")
101
+
102
+ generate_services(consumers_dir=str(tmp_path / "consumers"))
103
+ app = App(consumers_dir=str(tmp_path / "consumers"))
104
+
105
+ send = DummySend()
106
+ scope = {"type": "http", "method": "GET", "path": "/api/missing", "headers": []}
107
+ await app(scope, DummyReceive(), send)
108
+ assert send.messages[0]["status"] == 404
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+ from yaaf.di import DependencyResolver, ServiceRegistry
6
+
7
+
8
+ class AlphaService:
9
+ def __init__(self) -> None:
10
+ self.value = "alpha"
11
+
12
+
13
+ def test_registry_resolves_by_type_and_name() -> None:
14
+ class AlphaAlias:
15
+ pass
16
+
17
+ registry = ServiceRegistry(by_type={}, by_alias={})
18
+ alpha = registry.register(AlphaService(), aliases=["AlphaAlias"])
19
+
20
+ assert registry.resolve(AlphaService) is alpha
21
+ assert registry.resolve(AlphaAlias) is alpha
22
+
23
+
24
+ def test_dependency_resolver_injects_context_and_services() -> None:
25
+ registry = ServiceRegistry(by_type={}, by_alias={})
26
+ alpha = registry.register(AlphaService(), aliases=[])
27
+ resolver = DependencyResolver(registry)
28
+
29
+ def handler(alpha: AlphaService, extra: str) -> tuple[str, str]:
30
+ return alpha.value, extra
31
+
32
+ result = resolver.call(handler, {"extra": "context"})
33
+ assert result == ("alpha", "context")
34
+
35
+
36
+ def test_dependency_resolver_errors_on_missing_dependency() -> None:
37
+ registry = ServiceRegistry(by_type={}, by_alias={})
38
+ resolver = DependencyResolver(registry)
39
+
40
+ def handler(missing: AlphaService) -> str:
41
+ return missing.value
42
+
43
+ with pytest.raises(TypeError):
44
+ resolver.call(handler, {})
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import pytest
6
+
7
+ from yaaf.loader import build_pattern, discover_routes
8
+
9
+
10
+ def test_build_pattern_static_and_dynamic() -> None:
11
+ pattern, params, static_count, segment_count = build_pattern(["users", "[id]"] , prefix="api")
12
+ assert params == ["id"]
13
+ assert static_count == 1
14
+ assert segment_count == 2
15
+ assert pattern.startswith("^/api/")
16
+
17
+
18
+ def test_discover_routes_missing_dir(tmp_path: Path) -> None:
19
+ routes, registry = discover_routes(str(tmp_path / "missing"))
20
+ assert routes == []
21
+ assert registry.by_type == {}
22
+ assert registry.by_alias == {}
23
+
24
+
25
+ def test_discover_routes_dynamic_shadow_warning(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
26
+ base = tmp_path / "consumers" / "api"
27
+ hello = base / "hello"
28
+ dynamic = base / "[name]"
29
+ hello.mkdir(parents=True)
30
+ dynamic.mkdir(parents=True)
31
+
32
+ (hello / "_service.py").write_text("class Service:...\nservice = Service()\n")
33
+ (hello / "_server.py").write_text("async def get():...\n")
34
+ (dynamic / "_service.py").write_text("class Service:...\nservice = Service()\n")
35
+ (dynamic / "_server.py").write_text("async def get():...\n")
36
+
37
+ discover_routes(str(tmp_path / "consumers"))
38
+ captured = capsys.readouterr()
39
+ assert "dynamic route /api/[name] matches static route /api/hello" in captured.out
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ from yaaf.responses import Response, as_response
4
+
5
+
6
+ def test_as_response_string() -> None:
7
+ response = as_response("hello")
8
+ assert response.body == b"hello"
9
+ assert response.status == 200
10
+
11
+
12
+ def test_as_response_bytes() -> None:
13
+ response = as_response(b"data")
14
+ assert response.body == b"data"
15
+ assert response.status == 200
16
+
17
+
18
+ def test_as_response_dict_json() -> None:
19
+ response = as_response({"ok": True})
20
+ assert response.status == 200
21
+ assert b"\"ok\"" in response.body
22
+
23
+
24
+ def test_as_response_tuple_status() -> None:
25
+ response = as_response(("nope", 404))
26
+ assert response.status == 404
27
+ assert response.body == b"nope"
28
+
29
+
30
+ def test_response_with_status() -> None:
31
+ base = Response.text("hi")
32
+ updated = base.with_status(201)
33
+ assert updated.status == 201
34
+ assert updated.body == base.body
@@ -0,0 +1,23 @@
1
+ """yaaf package exports with lazy loading to avoid circular imports."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ from typing import Any
7
+
8
+ __all__ = ["App", "Request", "Response", "app"]
9
+
10
+
11
+ def __getattr__(name: str) -> Any:
12
+ if name in {"App", "Request", "app"}:
13
+ app_module = importlib.import_module(f"{__name__}.app")
14
+ return getattr(app_module, name)
15
+ if name == "Response":
16
+ from .responses import Response
17
+
18
+ return Response
19
+ raise AttributeError(f"module {__name__} has no attribute {name}")
20
+
21
+
22
+ def __dir__() -> list[str]:
23
+ return sorted(__all__)
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()