macss-modular-api 0.4.4__tar.gz → 0.4.6__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.
- {macss_modular_api-0.4.4/src/macss_modular_api.egg-info → macss_modular_api-0.4.6}/PKG-INFO +8 -8
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/README.md +5 -5
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/pyproject.toml +3 -3
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6/src/macss_modular_api.egg-info}/PKG-INFO +8 -8
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/modular_api/core/modular_api.py +4 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/modular_api/core/usecase.py +4 -2
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/modular_api/core/usecase_handler.py +6 -3
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/modular_api/openapi/swagger_docs.py +1 -1
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/tests/test_fromjson_validation.py +26 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/tests/test_modular_api.py +47 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/tests/test_schema_conformance.py +18 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/tests/test_usecase_handler.py +77 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/LICENSE +0 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/setup.cfg +0 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/macss_modular_api.egg-info/SOURCES.txt +0 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/macss_modular_api.egg-info/dependency_links.txt +0 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/macss_modular_api.egg-info/requires.txt +0 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/macss_modular_api.egg-info/top_level.txt +0 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/modular_api/__init__.py +0 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/modular_api/core/__init__.py +0 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/modular_api/core/health/__init__.py +0 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/modular_api/core/health/health_check.py +0 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/modular_api/core/health/health_handler.py +0 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/modular_api/core/health/health_service.py +0 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/modular_api/core/logger/__init__.py +0 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/modular_api/core/logger/logger.py +0 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/modular_api/core/logger/logging_middleware.py +0 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/modular_api/core/metrics/__init__.py +0 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/modular_api/core/metrics/metric.py +0 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/modular_api/core/metrics/metric_registry.py +0 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/modular_api/core/metrics/metrics_middleware.py +0 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/modular_api/core/module_builder.py +0 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/modular_api/core/registry.py +0 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/modular_api/core/use_case_exception.py +0 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/modular_api/middlewares/__init__.py +0 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/modular_api/middlewares/cors.py +0 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/modular_api/openapi/__init__.py +0 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/modular_api/openapi/openapi.py +0 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/modular_api/py.typed +0 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/tests/test_auto_schema.py +0 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/tests/test_field_example.py +0 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/tests/test_module_builder.py +0 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/tests/test_use_case_exception.py +0 -0
- {macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/tests/test_usecase.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: macss-modular-api
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.6
|
|
4
4
|
Summary: Use-case-centric toolkit for building modular APIs with Starlette. Define UseCase classes (input → validate → execute → output), connect them to HTTP routes, and expose OpenAPI documentation automatically.
|
|
5
5
|
Author: ccisne.dev
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/macss-dev/modular_api
|
|
8
|
-
Project-URL: Repository, https://github.com/macss-dev/modular_api/tree/main/py
|
|
8
|
+
Project-URL: Repository, https://github.com/macss-dev/modular_api/tree/main/code/py
|
|
9
9
|
Project-URL: Issues, https://github.com/macss-dev/modular_api/issues
|
|
10
|
-
Project-URL: Documentation, https://github.com/macss-dev/modular_api/tree/main/py#readme
|
|
10
|
+
Project-URL: Documentation, https://github.com/macss-dev/modular_api/tree/main/code/py#readme
|
|
11
11
|
Keywords: api,usecase,openapi,starlette,modular,macss
|
|
12
12
|
Classifier: Development Status :: 4 - Beta
|
|
13
13
|
Classifier: Framework :: AsyncIO
|
|
@@ -114,7 +114,7 @@ pip install modular-api[serve]
|
|
|
114
114
|
## Error handling
|
|
115
115
|
|
|
116
116
|
```python
|
|
117
|
-
async def execute(self) ->
|
|
117
|
+
async def execute(self) -> FoundUserOutput:
|
|
118
118
|
user = await repository.find_by_id(self.input.user_id)
|
|
119
119
|
if not user:
|
|
120
120
|
raise UseCaseException(
|
|
@@ -122,7 +122,7 @@ async def execute(self) -> None:
|
|
|
122
122
|
message="User not found",
|
|
123
123
|
error_code="USER_NOT_FOUND",
|
|
124
124
|
)
|
|
125
|
-
|
|
125
|
+
return FoundUserOutput(name=user.name)
|
|
126
126
|
```
|
|
127
127
|
|
|
128
128
|
---
|
|
@@ -130,13 +130,13 @@ async def execute(self) -> None:
|
|
|
130
130
|
## Testing
|
|
131
131
|
|
|
132
132
|
```python
|
|
133
|
-
def test_hello_world():
|
|
133
|
+
async def test_hello_world():
|
|
134
134
|
usecase = HelloWorld(HelloInput(name="World"))
|
|
135
135
|
error = usecase.validate()
|
|
136
136
|
assert error is None
|
|
137
137
|
|
|
138
|
-
await usecase.execute()
|
|
139
|
-
assert
|
|
138
|
+
output = await usecase.execute()
|
|
139
|
+
assert output.message == "Hello, World!"
|
|
140
140
|
```
|
|
141
141
|
|
|
142
142
|
See [doc/testing_guide.md](doc/testing_guide.md) for the full testing guide.
|
|
@@ -79,7 +79,7 @@ pip install modular-api[serve]
|
|
|
79
79
|
## Error handling
|
|
80
80
|
|
|
81
81
|
```python
|
|
82
|
-
async def execute(self) ->
|
|
82
|
+
async def execute(self) -> FoundUserOutput:
|
|
83
83
|
user = await repository.find_by_id(self.input.user_id)
|
|
84
84
|
if not user:
|
|
85
85
|
raise UseCaseException(
|
|
@@ -87,7 +87,7 @@ async def execute(self) -> None:
|
|
|
87
87
|
message="User not found",
|
|
88
88
|
error_code="USER_NOT_FOUND",
|
|
89
89
|
)
|
|
90
|
-
|
|
90
|
+
return FoundUserOutput(name=user.name)
|
|
91
91
|
```
|
|
92
92
|
|
|
93
93
|
---
|
|
@@ -95,13 +95,13 @@ async def execute(self) -> None:
|
|
|
95
95
|
## Testing
|
|
96
96
|
|
|
97
97
|
```python
|
|
98
|
-
def test_hello_world():
|
|
98
|
+
async def test_hello_world():
|
|
99
99
|
usecase = HelloWorld(HelloInput(name="World"))
|
|
100
100
|
error = usecase.validate()
|
|
101
101
|
assert error is None
|
|
102
102
|
|
|
103
|
-
await usecase.execute()
|
|
104
|
-
assert
|
|
103
|
+
output = await usecase.execute()
|
|
104
|
+
assert output.message == "Hello, World!"
|
|
105
105
|
```
|
|
106
106
|
|
|
107
107
|
See [doc/testing_guide.md](doc/testing_guide.md) for the full testing guide.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "macss-modular-api"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.6"
|
|
8
8
|
description = "Use-case-centric toolkit for building modular APIs with Starlette. Define UseCase classes (input → validate → execute → output), connect them to HTTP routes, and expose OpenAPI documentation automatically."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -39,9 +39,9 @@ dev = [
|
|
|
39
39
|
|
|
40
40
|
[project.urls]
|
|
41
41
|
Homepage = "https://github.com/macss-dev/modular_api"
|
|
42
|
-
Repository = "https://github.com/macss-dev/modular_api/tree/main/py"
|
|
42
|
+
Repository = "https://github.com/macss-dev/modular_api/tree/main/code/py"
|
|
43
43
|
Issues = "https://github.com/macss-dev/modular_api/issues"
|
|
44
|
-
Documentation = "https://github.com/macss-dev/modular_api/tree/main/py#readme"
|
|
44
|
+
Documentation = "https://github.com/macss-dev/modular_api/tree/main/code/py#readme"
|
|
45
45
|
|
|
46
46
|
[tool.setuptools.packages.find]
|
|
47
47
|
where = ["src"]
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: macss-modular-api
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.6
|
|
4
4
|
Summary: Use-case-centric toolkit for building modular APIs with Starlette. Define UseCase classes (input → validate → execute → output), connect them to HTTP routes, and expose OpenAPI documentation automatically.
|
|
5
5
|
Author: ccisne.dev
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/macss-dev/modular_api
|
|
8
|
-
Project-URL: Repository, https://github.com/macss-dev/modular_api/tree/main/py
|
|
8
|
+
Project-URL: Repository, https://github.com/macss-dev/modular_api/tree/main/code/py
|
|
9
9
|
Project-URL: Issues, https://github.com/macss-dev/modular_api/issues
|
|
10
|
-
Project-URL: Documentation, https://github.com/macss-dev/modular_api/tree/main/py#readme
|
|
10
|
+
Project-URL: Documentation, https://github.com/macss-dev/modular_api/tree/main/code/py#readme
|
|
11
11
|
Keywords: api,usecase,openapi,starlette,modular,macss
|
|
12
12
|
Classifier: Development Status :: 4 - Beta
|
|
13
13
|
Classifier: Framework :: AsyncIO
|
|
@@ -114,7 +114,7 @@ pip install modular-api[serve]
|
|
|
114
114
|
## Error handling
|
|
115
115
|
|
|
116
116
|
```python
|
|
117
|
-
async def execute(self) ->
|
|
117
|
+
async def execute(self) -> FoundUserOutput:
|
|
118
118
|
user = await repository.find_by_id(self.input.user_id)
|
|
119
119
|
if not user:
|
|
120
120
|
raise UseCaseException(
|
|
@@ -122,7 +122,7 @@ async def execute(self) -> None:
|
|
|
122
122
|
message="User not found",
|
|
123
123
|
error_code="USER_NOT_FOUND",
|
|
124
124
|
)
|
|
125
|
-
|
|
125
|
+
return FoundUserOutput(name=user.name)
|
|
126
126
|
```
|
|
127
127
|
|
|
128
128
|
---
|
|
@@ -130,13 +130,13 @@ async def execute(self) -> None:
|
|
|
130
130
|
## Testing
|
|
131
131
|
|
|
132
132
|
```python
|
|
133
|
-
def test_hello_world():
|
|
133
|
+
async def test_hello_world():
|
|
134
134
|
usecase = HelloWorld(HelloInput(name="World"))
|
|
135
135
|
error = usecase.validate()
|
|
136
136
|
assert error is None
|
|
137
137
|
|
|
138
|
-
await usecase.execute()
|
|
139
|
-
assert
|
|
138
|
+
output = await usecase.execute()
|
|
139
|
+
assert output.message == "Hello, World!"
|
|
140
140
|
```
|
|
141
141
|
|
|
142
142
|
See [doc/testing_guide.md](doc/testing_guide.md) for the full testing guide.
|
|
@@ -59,6 +59,7 @@ class ModularApi:
|
|
|
59
59
|
title: str = "Modular API",
|
|
60
60
|
version: str = "0.0.0",
|
|
61
61
|
release_id: str | None = None,
|
|
62
|
+
servers: list[dict[str, str]] | None = None,
|
|
62
63
|
metrics_enabled: bool = False,
|
|
63
64
|
metrics_path: str = "/metrics",
|
|
64
65
|
log_level: LogLevel = LogLevel.info,
|
|
@@ -67,6 +68,7 @@ class ModularApi:
|
|
|
67
68
|
self._title = title
|
|
68
69
|
self._version = version
|
|
69
70
|
self._release_id = release_id or f"{version}-debug"
|
|
71
|
+
self._servers = servers
|
|
70
72
|
self._metrics_enabled = metrics_enabled
|
|
71
73
|
self._metrics_path = metrics_path
|
|
72
74
|
self._log_level = log_level
|
|
@@ -188,6 +190,8 @@ class ModularApi:
|
|
|
188
190
|
|
|
189
191
|
# OpenAPI endpoints
|
|
190
192
|
spec_kwargs: dict[str, Any] = {"title": self._title, "port": port, "version": self._version}
|
|
193
|
+
if self._servers is not None:
|
|
194
|
+
spec_kwargs["servers"] = self._servers
|
|
191
195
|
routes.append(Route("/openapi.json", endpoint=openapi_json_handler(**spec_kwargs)))
|
|
192
196
|
routes.append(Route("/openapi.yaml", endpoint=openapi_yaml_handler(**spec_kwargs)))
|
|
193
197
|
|
|
@@ -49,13 +49,15 @@ def _normalize_schema(raw: dict[str, Any]) -> dict[str, object]:
|
|
|
49
49
|
# Convert examples → example (nullable fields)
|
|
50
50
|
if "examples" in prop and prop["examples"]:
|
|
51
51
|
collapsed["example"] = prop["examples"][0]
|
|
52
|
+
collapsed.pop("additionalProperties", None)
|
|
52
53
|
normalized_props[name] = _reorder_type_first(collapsed)
|
|
53
54
|
if name in required:
|
|
54
55
|
required.remove(name)
|
|
55
56
|
continue
|
|
56
57
|
|
|
57
|
-
# Strip Pydantic's auto-generated ``title
|
|
58
|
-
|
|
58
|
+
# Strip Pydantic's auto-generated ``title``, ``default``, and
|
|
59
|
+
# ``additionalProperties`` (emitted for dict[str, Any] fields) from properties.
|
|
60
|
+
cleaned = {k: v for k, v in prop.items() if k not in ("title", "default", "additionalProperties")}
|
|
59
61
|
|
|
60
62
|
# Convert Pydantic ``examples`` (list, Draft 2020-12) → OpenAPI ``example`` (singular)
|
|
61
63
|
if "examples" in cleaned and cleaned["examples"]:
|
|
@@ -8,7 +8,6 @@ the full use-case lifecycle: parse → validate → execute → respond.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import json
|
|
11
|
-
import sys
|
|
12
11
|
from typing import Any, Callable
|
|
13
12
|
|
|
14
13
|
from pydantic import ValidationError
|
|
@@ -114,7 +113,9 @@ def usecase_handler(factory: UseCaseFactory) -> Any:
|
|
|
114
113
|
)
|
|
115
114
|
|
|
116
115
|
except UseCaseException as exc:
|
|
117
|
-
|
|
116
|
+
logger = getattr(request.state, LOGGER_STATE_KEY, None)
|
|
117
|
+
if logger is not None:
|
|
118
|
+
logger.error("UseCaseException", fields={"error": str(exc), "status": exc.status_code})
|
|
118
119
|
return Response(
|
|
119
120
|
content=json.dumps(exc.to_json()),
|
|
120
121
|
status_code=exc.status_code,
|
|
@@ -131,7 +132,9 @@ def usecase_handler(factory: UseCaseFactory) -> Any:
|
|
|
131
132
|
media_type=_JSON_CONTENT_TYPE,
|
|
132
133
|
)
|
|
133
134
|
except Exception as exc:
|
|
134
|
-
|
|
135
|
+
logger = getattr(request.state, LOGGER_STATE_KEY, None)
|
|
136
|
+
if logger is not None:
|
|
137
|
+
logger.error("Unexpected error in use case handler", fields={"error": str(exc)})
|
|
135
138
|
return Response(
|
|
136
139
|
content=json.dumps({"error": "Internal server error"}),
|
|
137
140
|
status_code=500,
|
|
@@ -5,7 +5,7 @@ system-aware dark mode. The local ``/openapi.json`` endpoint provides
|
|
|
5
5
|
the spec. Styling, dark mode, and Swagger UI loading are handled
|
|
6
6
|
entirely by docs-ui — no inline CSS or JS in this template.
|
|
7
7
|
|
|
8
|
-
See: https://github.com/macss-dev/modular_api/tree/main/docs-ui
|
|
8
|
+
See: https://github.com/macss-dev/modular_api/tree/main/code/docs-ui
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
@@ -9,6 +9,7 @@ Error message contract (identical across all 3 SDKs for parity):
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
import json
|
|
12
|
+
from typing import Any
|
|
12
13
|
|
|
13
14
|
import pytest
|
|
14
15
|
from pydantic import Field, ValidationError
|
|
@@ -120,3 +121,28 @@ class TestFromJsonHandlerErrors:
|
|
|
120
121
|
assert resp.status_code == 200
|
|
121
122
|
body = resp.json()
|
|
122
123
|
assert body["greeting"] == "Hi Alice, age 25"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ── Unit: dict[str, Any] object type validation ──────────────────────────
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class ObjectInput(Input):
|
|
130
|
+
id: str = Field(description="ID")
|
|
131
|
+
details: dict[str, Any] = Field(description="Nested object")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class TestFromJsonObjectType:
|
|
135
|
+
"""Pydantic strict mode validates dict[str, Any] as object type."""
|
|
136
|
+
|
|
137
|
+
def test_accepts_dict_for_object_field(self) -> None:
|
|
138
|
+
result = ObjectInput.from_json({"id": "abc", "details": {"amount": 100}})
|
|
139
|
+
assert result.id == "abc"
|
|
140
|
+
assert result.details == {"amount": 100}
|
|
141
|
+
|
|
142
|
+
def test_rejects_string_for_object_field(self) -> None:
|
|
143
|
+
with pytest.raises(ValidationError):
|
|
144
|
+
ObjectInput.from_json({"id": "abc", "details": "not-a-dict"})
|
|
145
|
+
|
|
146
|
+
def test_rejects_list_for_object_field(self) -> None:
|
|
147
|
+
with pytest.raises(ValidationError):
|
|
148
|
+
ObjectInput.from_json({"id": "abc", "details": [1, 2]})
|
|
@@ -209,6 +209,53 @@ class TestAutoMountedEndpoints:
|
|
|
209
209
|
assert "Test API" in response.text
|
|
210
210
|
|
|
211
211
|
|
|
212
|
+
# ── Custom servers in OpenAPI spec ────────────────────────────
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class TestCustomServers:
|
|
216
|
+
"""ModularApi propagates servers to the OpenAPI spec."""
|
|
217
|
+
|
|
218
|
+
def _build_client(self, **api_options: object) -> TestClient:
|
|
219
|
+
api = _make_api(**api_options)
|
|
220
|
+
api.module("test", lambda m: m.usecase("ping", _PingUseCase.from_json))
|
|
221
|
+
app = api.build()
|
|
222
|
+
return TestClient(app)
|
|
223
|
+
|
|
224
|
+
def test_uses_localhost_default_when_servers_not_provided(self) -> None:
|
|
225
|
+
client = self._build_client()
|
|
226
|
+
spec = client.get("/openapi.json").json()
|
|
227
|
+
assert len(spec["servers"]) == 1
|
|
228
|
+
assert "localhost" in spec["servers"][0]["url"]
|
|
229
|
+
|
|
230
|
+
def test_propagates_custom_servers_to_openapi_spec(self) -> None:
|
|
231
|
+
client = self._build_client(
|
|
232
|
+
servers=[{"url": "https://miapi.example.com", "description": "Production"}],
|
|
233
|
+
)
|
|
234
|
+
spec = client.get("/openapi.json").json()
|
|
235
|
+
assert len(spec["servers"]) == 1
|
|
236
|
+
assert spec["servers"][0]["url"] == "https://miapi.example.com"
|
|
237
|
+
assert spec["servers"][0]["description"] == "Production"
|
|
238
|
+
|
|
239
|
+
def test_supports_multiple_servers(self) -> None:
|
|
240
|
+
client = self._build_client(
|
|
241
|
+
servers=[
|
|
242
|
+
{"url": "https://prod.example.com", "description": "Production"},
|
|
243
|
+
{"url": "http://192.168.5.82:8080", "description": "LAN"},
|
|
244
|
+
],
|
|
245
|
+
)
|
|
246
|
+
spec = client.get("/openapi.json").json()
|
|
247
|
+
assert len(spec["servers"]) == 2
|
|
248
|
+
assert spec["servers"][0]["url"] == "https://prod.example.com"
|
|
249
|
+
assert spec["servers"][1]["url"] == "http://192.168.5.82:8080"
|
|
250
|
+
|
|
251
|
+
def test_preserves_server_descriptions(self) -> None:
|
|
252
|
+
client = self._build_client(
|
|
253
|
+
servers=[{"url": "https://api.example.com", "description": "Main API"}],
|
|
254
|
+
)
|
|
255
|
+
spec = client.get("/openapi.json").json()
|
|
256
|
+
assert spec["servers"][0]["description"] == "Main API"
|
|
257
|
+
|
|
258
|
+
|
|
212
259
|
# ── Step 2.9.3: Middleware pipeline order ─────────────────────
|
|
213
260
|
|
|
214
261
|
|
|
@@ -4,14 +4,19 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
7
8
|
|
|
8
9
|
import sys
|
|
9
10
|
|
|
11
|
+
from pydantic import Field
|
|
12
|
+
|
|
10
13
|
_EXAMPLE_DIR = Path(__file__).resolve().parent.parent / "example"
|
|
11
14
|
sys.path.insert(0, str(_EXAMPLE_DIR))
|
|
12
15
|
|
|
13
16
|
from modules.greetings.usecases.hello_world import HelloWorldInput, HelloWorldOutput # type: ignore[import-untyped]
|
|
14
17
|
|
|
18
|
+
from modular_api.core.usecase import Input
|
|
19
|
+
|
|
15
20
|
_FIXTURES = Path(__file__).resolve().parent.parent.parent / "tests" / "fixtures"
|
|
16
21
|
|
|
17
22
|
|
|
@@ -48,3 +53,16 @@ class TestSchemaConformance:
|
|
|
48
53
|
def test_hello_output_to_json(self) -> None:
|
|
49
54
|
instance = HelloWorldOutput(message="Hello!")
|
|
50
55
|
assert instance.to_json() == {"message": "Hello!"}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class WebhookInput(Input):
|
|
59
|
+
instruction_id: str = Field(description="Payment instruction ID", examples=["20260323ABC"])
|
|
60
|
+
transfer_details: dict[str, Any] = Field(description="Nested transfer info", examples=[{"amount": 2300, "currency": "PEN"}])
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class TestSchemaConformanceObjectType:
|
|
64
|
+
"""Verify dict[str, Any] produces a schema identical to the shared fixture."""
|
|
65
|
+
|
|
66
|
+
def test_webhook_input_schema_matches_fixture(self) -> None:
|
|
67
|
+
fixture = _load_fixture("webhook_input_schema.json")
|
|
68
|
+
assert WebhookInput.to_schema() == fixture
|
|
@@ -205,3 +205,80 @@ class TestUseCaseLifecycle:
|
|
|
205
205
|
client = TestClient(app)
|
|
206
206
|
client.post("/spy", json={})
|
|
207
207
|
assert captured_loggers == ["fake-logger"]
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# ── Scoped-logger error integration (issue #7) ───────────────────────────
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class TestScopedLoggerInErrorPaths:
|
|
214
|
+
"""Catch blocks must log through the request-scoped logger (with trace_id)
|
|
215
|
+
instead of printing to stderr."""
|
|
216
|
+
|
|
217
|
+
@staticmethod
|
|
218
|
+
def _build_app_with_logger(
|
|
219
|
+
factory,
|
|
220
|
+
log_lines: list[str],
|
|
221
|
+
) -> Starlette:
|
|
222
|
+
"""App with logging middleware capturing output into log_lines."""
|
|
223
|
+
from starlette.middleware import Middleware
|
|
224
|
+
|
|
225
|
+
from modular_api.core.logger.logging_middleware import logging_middleware
|
|
226
|
+
from modular_api.core.logger.logger import LogLevel
|
|
227
|
+
|
|
228
|
+
mw_cls = logging_middleware(
|
|
229
|
+
log_level=LogLevel.debug,
|
|
230
|
+
service_name="test-svc",
|
|
231
|
+
write_fn=lambda line: log_lines.append(line),
|
|
232
|
+
)
|
|
233
|
+
return Starlette(
|
|
234
|
+
routes=[Route("/test", usecase_handler(factory), methods=["POST"])],
|
|
235
|
+
middleware=[Middleware(mw_cls)],
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
def test_logs_use_case_exception_through_scoped_logger(self) -> None:
|
|
239
|
+
log_lines: list[str] = []
|
|
240
|
+
app = self._build_app_with_logger(FailingUseCase, log_lines)
|
|
241
|
+
client = TestClient(app, raise_server_exceptions=False)
|
|
242
|
+
resp = client.post(
|
|
243
|
+
"/test",
|
|
244
|
+
json={"name": "x"},
|
|
245
|
+
headers={"X-Request-ID": "trace-py-uce-001"},
|
|
246
|
+
)
|
|
247
|
+
assert resp.status_code == 409
|
|
248
|
+
|
|
249
|
+
error_logs = [
|
|
250
|
+
json.loads(l)
|
|
251
|
+
for l in log_lines
|
|
252
|
+
if '"level": "error"' in l and "UseCaseException" in l
|
|
253
|
+
]
|
|
254
|
+
assert len(error_logs) > 0, "expected error log from scoped logger"
|
|
255
|
+
assert error_logs[0]["trace_id"] == "trace-py-uce-001"
|
|
256
|
+
|
|
257
|
+
def test_logs_unexpected_error_through_scoped_logger(self) -> None:
|
|
258
|
+
log_lines: list[str] = []
|
|
259
|
+
app = self._build_app_with_logger(CrashingUseCase, log_lines)
|
|
260
|
+
client = TestClient(app, raise_server_exceptions=False)
|
|
261
|
+
resp = client.post(
|
|
262
|
+
"/test",
|
|
263
|
+
json={"name": "x"},
|
|
264
|
+
headers={"X-Request-ID": "trace-py-crash-002"},
|
|
265
|
+
)
|
|
266
|
+
assert resp.status_code == 500
|
|
267
|
+
|
|
268
|
+
error_logs = [
|
|
269
|
+
json.loads(l)
|
|
270
|
+
for l in log_lines
|
|
271
|
+
if '"level": "error"' in l and "Unexpected error" in l
|
|
272
|
+
]
|
|
273
|
+
assert len(error_logs) > 0, "expected error log from scoped logger"
|
|
274
|
+
assert error_logs[0]["trace_id"] == "trace-py-crash-002"
|
|
275
|
+
|
|
276
|
+
def test_does_not_throw_when_logger_unavailable(self) -> None:
|
|
277
|
+
"""When no logging middleware is present, catch blocks must not crash."""
|
|
278
|
+
app = Starlette(
|
|
279
|
+
routes=[Route("/test", usecase_handler(FailingUseCase), methods=["POST"])],
|
|
280
|
+
)
|
|
281
|
+
client = TestClient(app, raise_server_exceptions=False)
|
|
282
|
+
resp = client.post("/test", json={"name": "x"})
|
|
283
|
+
assert resp.status_code == 409
|
|
284
|
+
assert resp.json()["message"] == "Already exists"
|
|
File without changes
|
|
File without changes
|
{macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/macss_modular_api.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/macss_modular_api.egg-info/requires.txt
RENAMED
|
File without changes
|
{macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/macss_modular_api.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/modular_api/core/health/health_check.py
RENAMED
|
File without changes
|
{macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/modular_api/core/health/health_handler.py
RENAMED
|
File without changes
|
{macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/modular_api/core/health/health_service.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/modular_api/core/metrics/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/modular_api/core/metrics/metric_registry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{macss_modular_api-0.4.4 → macss_modular_api-0.4.6}/src/modular_api/core/use_case_exception.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|