yaafcli 2026.2.5.15101__tar.gz → 2026.2.6.24209__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 (26) hide show
  1. {yaafcli-2026.2.5.15101/yaafcli.egg-info → yaafcli-2026.2.6.24209}/PKG-INFO +6 -6
  2. {yaafcli-2026.2.5.15101 → yaafcli-2026.2.6.24209}/README.md +5 -5
  3. {yaafcli-2026.2.5.15101 → yaafcli-2026.2.6.24209}/pyproject.toml +1 -1
  4. {yaafcli-2026.2.5.15101 → yaafcli-2026.2.6.24209}/tests/test_app_integration.py +2 -2
  5. yaafcli-2026.2.6.24209/tests/test_optional_service.py +51 -0
  6. {yaafcli-2026.2.5.15101 → yaafcli-2026.2.6.24209}/yaaf/cli.py +1 -1
  7. {yaafcli-2026.2.5.15101 → yaafcli-2026.2.6.24209}/yaaf/gen_services.py +9 -3
  8. {yaafcli-2026.2.5.15101 → yaafcli-2026.2.6.24209}/yaaf/loader.py +3 -2
  9. {yaafcli-2026.2.5.15101 → yaafcli-2026.2.6.24209/yaafcli.egg-info}/PKG-INFO +6 -6
  10. {yaafcli-2026.2.5.15101 → yaafcli-2026.2.6.24209}/yaafcli.egg-info/SOURCES.txt +1 -0
  11. {yaafcli-2026.2.5.15101 → yaafcli-2026.2.6.24209}/LICENSE +0 -0
  12. {yaafcli-2026.2.5.15101 → yaafcli-2026.2.6.24209}/setup.cfg +0 -0
  13. {yaafcli-2026.2.5.15101 → yaafcli-2026.2.6.24209}/tests/test_di.py +0 -0
  14. {yaafcli-2026.2.5.15101 → yaafcli-2026.2.6.24209}/tests/test_loader.py +0 -0
  15. {yaafcli-2026.2.5.15101 → yaafcli-2026.2.6.24209}/tests/test_responses.py +0 -0
  16. {yaafcli-2026.2.5.15101 → yaafcli-2026.2.6.24209}/yaaf/__init__.py +0 -0
  17. {yaafcli-2026.2.5.15101 → yaafcli-2026.2.6.24209}/yaaf/__main__.py +0 -0
  18. {yaafcli-2026.2.5.15101 → yaafcli-2026.2.6.24209}/yaaf/app.py +0 -0
  19. {yaafcli-2026.2.5.15101 → yaafcli-2026.2.6.24209}/yaaf/di.py +0 -0
  20. {yaafcli-2026.2.5.15101 → yaafcli-2026.2.6.24209}/yaaf/py.typed +0 -0
  21. {yaafcli-2026.2.5.15101 → yaafcli-2026.2.6.24209}/yaaf/responses.py +0 -0
  22. {yaafcli-2026.2.5.15101 → yaafcli-2026.2.6.24209}/yaaf/types.py +0 -0
  23. {yaafcli-2026.2.5.15101 → yaafcli-2026.2.6.24209}/yaafcli.egg-info/dependency_links.txt +0 -0
  24. {yaafcli-2026.2.5.15101 → yaafcli-2026.2.6.24209}/yaafcli.egg-info/entry_points.txt +0 -0
  25. {yaafcli-2026.2.5.15101 → yaafcli-2026.2.6.24209}/yaafcli.egg-info/requires.txt +0 -0
  26. {yaafcli-2026.2.5.15101 → yaafcli-2026.2.6.24209}/yaafcli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yaafcli
3
- Version: 2026.2.5.15101
3
+ Version: 2026.2.6.24209
4
4
  Summary: Minimal ASGI app scaffold
5
5
  License: MIT License
6
6
 
@@ -105,7 +105,7 @@ In `_server.py`, export functions named after HTTP methods (lowercase): `get`, `
105
105
  Example `_server.py`:
106
106
 
107
107
  ```python
108
- from consumers.services import HelloService
108
+ from consumers.api import HelloService
109
109
  from yaaf import Request
110
110
  from yaaf.types import Params
111
111
 
@@ -117,7 +117,7 @@ async def get(request: Request, service: HelloService, params: Params):
117
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
118
 
119
119
  ```python
120
- from consumers.services import UsersService
120
+ from consumers.api import UsersService
121
121
 
122
122
 
123
123
  class Service:
@@ -157,7 +157,7 @@ service = Service()
157
157
 
158
158
  `consumers/api/hello/_service.py`
159
159
  ```python
160
- from yaaf.services import UsersService
160
+ from consumers.api import UsersService
161
161
 
162
162
 
163
163
  class Service:
@@ -173,7 +173,7 @@ service = Service
173
173
 
174
174
  `consumers/api/hello/_server.py`
175
175
  ```python
176
- from consumers.services import HelloService
176
+ from consumers.api import HelloService
177
177
  from yaaf import Request
178
178
 
179
179
 
@@ -197,7 +197,7 @@ python scripts/bump_version.py
197
197
 
198
198
  ## Service Type Generation
199
199
 
200
- Every `yaaf` command regenerates `consumers/services.py` for type-checking. You can also run it explicitly:
200
+ Every `yaaf` command regenerates `consumers/api/__init__.py` for type-checking. You can also run it explicitly:
201
201
 
202
202
  ```bash
203
203
  yaaf gen-services
@@ -63,7 +63,7 @@ In `_server.py`, export functions named after HTTP methods (lowercase): `get`, `
63
63
  Example `_server.py`:
64
64
 
65
65
  ```python
66
- from consumers.services import HelloService
66
+ from consumers.api import HelloService
67
67
  from yaaf import Request
68
68
  from yaaf.types import Params
69
69
 
@@ -75,7 +75,7 @@ async def get(request: Request, service: HelloService, params: Params):
75
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
76
 
77
77
  ```python
78
- from consumers.services import UsersService
78
+ from consumers.api import UsersService
79
79
 
80
80
 
81
81
  class Service:
@@ -115,7 +115,7 @@ service = Service()
115
115
 
116
116
  `consumers/api/hello/_service.py`
117
117
  ```python
118
- from yaaf.services import UsersService
118
+ from consumers.api import UsersService
119
119
 
120
120
 
121
121
  class Service:
@@ -131,7 +131,7 @@ service = Service
131
131
 
132
132
  `consumers/api/hello/_server.py`
133
133
  ```python
134
- from consumers.services import HelloService
134
+ from consumers.api import HelloService
135
135
  from yaaf import Request
136
136
 
137
137
 
@@ -155,7 +155,7 @@ python scripts/bump_version.py
155
155
 
156
156
  ## Service Type Generation
157
157
 
158
- Every `yaaf` command regenerates `consumers/services.py` for type-checking. You can also run it explicitly:
158
+ Every `yaaf` command regenerates `consumers/api/__init__.py` for type-checking. You can also run it explicitly:
159
159
 
160
160
  ```bash
161
161
  yaaf gen-services
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "yaafcli"
7
- version = "2026.02.05.015101"
7
+ version = "2026.02.06.024209"
8
8
  description = "Minimal ASGI app scaffold"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -54,7 +54,7 @@ async def test_app_routes_and_params(tmp_path: Path) -> None:
54
54
  )
55
55
  _write_server(
56
56
  hello,
57
- "from consumers.services import HelloService\n"
57
+ "from consumers.api import HelloService\n"
58
58
  "from yaaf import Request\n\n"
59
59
  "async def get(request: Request, service: HelloService):\n"
60
60
  " return {'message': service.message(), 'path': request.path}\n",
@@ -69,7 +69,7 @@ async def test_app_routes_and_params(tmp_path: Path) -> None:
69
69
  )
70
70
  _write_server(
71
71
  dynamic,
72
- "from consumers.services import NameService\n"
72
+ "from consumers.api import NameService\n"
73
73
  "from yaaf.types import Params\n\n"
74
74
  "async def get(params: Params, service: NameService):\n"
75
75
  " return {'message': service.greet(params['name'])}\n",
@@ -0,0 +1,51 @@
1
+
2
+ import pytest
3
+ from pathlib import Path
4
+ from yaaf.app import App
5
+
6
+ class DummySend:
7
+ def __init__(self) -> None:
8
+ self.messages: list[dict] = []
9
+
10
+ async def __call__(self, message: dict) -> None:
11
+ self.messages.append(message)
12
+
13
+ class DummyReceive:
14
+ def __init__(self) -> None:
15
+ self.sent = False
16
+
17
+ async def __call__(self) -> dict:
18
+ if self.sent:
19
+ return {"type": "http.request", "body": b"", "more_body": False}
20
+ self.sent = True
21
+ return {"type": "http.request", "body": b"", "more_body": False}
22
+
23
+ @pytest.mark.asyncio
24
+ async def test_route_without_service_file(tmp_path: Path) -> None:
25
+ # Setup a fresh consumer structure
26
+ base = tmp_path / "consumers" / "api" / "noservice"
27
+ base.mkdir(parents=True)
28
+
29
+ # Only write _server.py, NO _service.py
30
+ (base / "_server.py").write_text(
31
+ "from yaaf import Request\n"
32
+ "async def get(request: Request):\n"
33
+ " return {'message': 'ok without service'}\n"
34
+ )
35
+ (tmp_path / "consumers" / "api" / "__init__.py").write_text("# package\n")
36
+
37
+ app = App(consumers_dir=str(tmp_path / "consumers"))
38
+
39
+ send = DummySend()
40
+ scope = {
41
+ "type": "http",
42
+ "method": "GET",
43
+ "path": "/api/noservice",
44
+ "headers": [],
45
+ }
46
+
47
+ await app(scope, DummyReceive(), send)
48
+
49
+ # Expectation: Should fail (404) currently, but we want 200
50
+ assert send.messages[0]["status"] == 200
51
+ assert b"ok without service" in send.messages[1]["body"]
@@ -23,7 +23,7 @@ def main() -> None:
23
23
  serve_parser.add_argument("--consumers-dir", default="consumers")
24
24
  serve_parser.set_defaults(command="serve")
25
25
 
26
- gen_parser = subparsers.add_parser("gen-services", help="Generate consumers/services.py")
26
+ gen_parser = subparsers.add_parser("gen-services", help="Generate consumers/api/__init__.py")
27
27
  gen_parser.add_argument("--consumers-dir", default="consumers")
28
28
  gen_parser.add_argument("--output", default=None)
29
29
  gen_parser.set_defaults(command="gen-services")
@@ -1,4 +1,4 @@
1
- """Generate a consumers/services.py typing module from filesystem routes."""
1
+ """Generate a consumers/api/__init__.py typing module from filesystem routes."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -14,7 +14,9 @@ def _is_identifier(segment: str) -> bool:
14
14
 
15
15
 
16
16
  def _camel_case(parts: Iterable[str]) -> str:
17
- return "".join(part[:1].upper() + part[1:] for part in parts if part)
17
+ return "".join(
18
+ sub[:1].upper() + sub[1:] for part in parts if part for sub in part.split("_") if sub
19
+ )
18
20
 
19
21
 
20
22
  def _strip_dynamic(segment: str) -> str:
@@ -32,9 +34,11 @@ def _service_alias(route_parts: list[str]) -> str:
32
34
 
33
35
  def generate_services(consumers_dir: str = "consumers", output_path: str | None = None) -> Path:
34
36
  base = Path(consumers_dir)
35
- out_path = Path(output_path) if output_path else base / "services.py"
37
+ out_path = Path(output_path) if output_path else base / "api" / "__init__.py"
36
38
 
37
39
  if not base.exists():
40
+ if not out_path.parent.exists():
41
+ out_path.parent.mkdir(parents=True, exist_ok=True)
38
42
  out_path.write_text(HEADER + "\n__all__ = []\n")
39
43
  return out_path
40
44
 
@@ -76,5 +80,7 @@ def generate_services(consumers_dir: str = "consumers", output_path: str | None
76
80
  if dynamic_aliases:
77
81
  lines.append("\n# Dynamic routes use Protocol stubs (invalid import paths).\n")
78
82
 
83
+ if not out_path.parent.exists():
84
+ out_path.parent.mkdir(parents=True, exist_ok=True)
79
85
  out_path.write_text("".join(lines))
80
86
  return out_path
@@ -72,7 +72,7 @@ def discover_routes(consumers_dir: str) -> tuple[list[RouteTarget], ServiceRegis
72
72
  service_aliases: dict[Path, list[str]] = {}
73
73
 
74
74
  for root, _dirs, files in os.walk(base):
75
- if "_server.py" not in files or "_service.py" not in files:
75
+ if "_server.py" not in files:
76
76
  continue
77
77
 
78
78
  root_path = Path(root)
@@ -90,7 +90,8 @@ def discover_routes(consumers_dir: str) -> tuple[list[RouteTarget], ServiceRegis
90
90
  aliases.append(route_parts[-1])
91
91
  service_aliases[root_path] = [alias for alias in aliases if alias]
92
92
 
93
- service_modules[root_path] = _load_module(root_path / "_service.py", "service")
93
+ if "_service.py" in files:
94
+ service_modules[root_path] = _load_module(root_path / "_service.py", "service")
94
95
  server_modules[root_path] = _load_module(root_path / "_server.py", "server")
95
96
 
96
97
  registry = ServiceRegistry(by_type={}, by_alias={})
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: yaafcli
3
- Version: 2026.2.5.15101
3
+ Version: 2026.2.6.24209
4
4
  Summary: Minimal ASGI app scaffold
5
5
  License: MIT License
6
6
 
@@ -105,7 +105,7 @@ In `_server.py`, export functions named after HTTP methods (lowercase): `get`, `
105
105
  Example `_server.py`:
106
106
 
107
107
  ```python
108
- from consumers.services import HelloService
108
+ from consumers.api import HelloService
109
109
  from yaaf import Request
110
110
  from yaaf.types import Params
111
111
 
@@ -117,7 +117,7 @@ async def get(request: Request, service: HelloService, params: Params):
117
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
118
 
119
119
  ```python
120
- from consumers.services import UsersService
120
+ from consumers.api import UsersService
121
121
 
122
122
 
123
123
  class Service:
@@ -157,7 +157,7 @@ service = Service()
157
157
 
158
158
  `consumers/api/hello/_service.py`
159
159
  ```python
160
- from yaaf.services import UsersService
160
+ from consumers.api import UsersService
161
161
 
162
162
 
163
163
  class Service:
@@ -173,7 +173,7 @@ service = Service
173
173
 
174
174
  `consumers/api/hello/_server.py`
175
175
  ```python
176
- from consumers.services import HelloService
176
+ from consumers.api import HelloService
177
177
  from yaaf import Request
178
178
 
179
179
 
@@ -197,7 +197,7 @@ python scripts/bump_version.py
197
197
 
198
198
  ## Service Type Generation
199
199
 
200
- Every `yaaf` command regenerates `consumers/services.py` for type-checking. You can also run it explicitly:
200
+ Every `yaaf` command regenerates `consumers/api/__init__.py` for type-checking. You can also run it explicitly:
201
201
 
202
202
  ```bash
203
203
  yaaf gen-services
@@ -4,6 +4,7 @@ pyproject.toml
4
4
  tests/test_app_integration.py
5
5
  tests/test_di.py
6
6
  tests/test_loader.py
7
+ tests/test_optional_service.py
7
8
  tests/test_responses.py
8
9
  yaaf/__init__.py
9
10
  yaaf/__main__.py