macss-modular-api 0.4.2__tar.gz → 0.4.4__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.2/src/macss_modular_api.egg-info → macss_modular_api-0.4.4}/PKG-INFO +1 -1
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/pyproject.toml +1 -1
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4/src/macss_modular_api.egg-info}/PKG-INFO +1 -1
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/src/modular_api/core/module_builder.py +13 -32
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/src/modular_api/core/registry.py +2 -2
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/src/modular_api/core/usecase.py +7 -15
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/src/modular_api/core/usecase_handler.py +3 -3
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/src/modular_api/openapi/openapi.py +4 -3
- macss_modular_api-0.4.4/src/modular_api/openapi/swagger_docs.py +48 -0
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/tests/test_fromjson_validation.py +2 -10
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/tests/test_modular_api.py +3 -11
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/tests/test_module_builder.py +2 -14
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/tests/test_schema_conformance.py +8 -8
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/tests/test_usecase.py +8 -16
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/tests/test_usecase_handler.py +6 -38
- macss_modular_api-0.4.2/src/modular_api/openapi/swagger_docs.py +0 -228
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/LICENSE +0 -0
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/README.md +0 -0
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/setup.cfg +0 -0
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/src/macss_modular_api.egg-info/SOURCES.txt +0 -0
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/src/macss_modular_api.egg-info/dependency_links.txt +0 -0
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/src/macss_modular_api.egg-info/requires.txt +0 -0
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/src/macss_modular_api.egg-info/top_level.txt +0 -0
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/src/modular_api/__init__.py +0 -0
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/src/modular_api/core/__init__.py +0 -0
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/src/modular_api/core/health/__init__.py +0 -0
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/src/modular_api/core/health/health_check.py +0 -0
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/src/modular_api/core/health/health_handler.py +0 -0
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/src/modular_api/core/health/health_service.py +0 -0
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/src/modular_api/core/logger/__init__.py +0 -0
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/src/modular_api/core/logger/logger.py +0 -0
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/src/modular_api/core/logger/logging_middleware.py +0 -0
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/src/modular_api/core/metrics/__init__.py +0 -0
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/src/modular_api/core/metrics/metric.py +0 -0
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/src/modular_api/core/metrics/metric_registry.py +0 -0
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/src/modular_api/core/metrics/metrics_middleware.py +0 -0
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/src/modular_api/core/modular_api.py +0 -0
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/src/modular_api/core/use_case_exception.py +0 -0
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/src/modular_api/middlewares/__init__.py +0 -0
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/src/modular_api/middlewares/cors.py +0 -0
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/src/modular_api/openapi/__init__.py +0 -0
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/src/modular_api/py.typed +0 -0
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/tests/test_auto_schema.py +0 -0
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/tests/test_field_example.py +0 -0
- {macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/tests/test_use_case_exception.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: macss-modular-api
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4
|
|
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
|
|
@@ -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.4"
|
|
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"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: macss-modular-api
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4
|
|
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
|
|
@@ -91,7 +91,7 @@ class ModuleBuilder:
|
|
|
91
91
|
|
|
92
92
|
def usecase(
|
|
93
93
|
self,
|
|
94
|
-
|
|
94
|
+
command: str,
|
|
95
95
|
factory: UseCaseFactory,
|
|
96
96
|
*,
|
|
97
97
|
method: str = "POST",
|
|
@@ -100,15 +100,19 @@ class ModuleBuilder:
|
|
|
100
100
|
) -> ModuleBuilder:
|
|
101
101
|
"""Register a use case as an HTTP endpoint on this module.
|
|
102
102
|
|
|
103
|
+
``command`` becomes the route segment and the OpenAPI operationId root.
|
|
104
|
+
Convention: command, class name, and file name share the same root.
|
|
105
|
+
Example: command ``'hello-world'`` → class ``HelloWorld`` → file ``hello_world.py``.
|
|
106
|
+
|
|
103
107
|
Returns ``self`` for fluent chaining.
|
|
104
108
|
"""
|
|
105
|
-
|
|
109
|
+
clean_command = command.strip().lstrip("/")
|
|
106
110
|
method_upper = method.upper()
|
|
107
|
-
full_path = f"{self._normalize_base(self._base_path)}/{self._module_name}/{
|
|
111
|
+
full_path = f"{self._normalize_base(self._base_path)}/{self._module_name}/{clean_command}"
|
|
108
112
|
|
|
109
113
|
# Mount endpoint on internal route list
|
|
110
114
|
self._routes.append(
|
|
111
|
-
Route(f"/{
|
|
115
|
+
Route(f"/{clean_command}", endpoint=usecase_handler(factory), methods=[method_upper]),
|
|
112
116
|
)
|
|
113
117
|
|
|
114
118
|
# Capture schemas via a dummy factory call (fail-safe)
|
|
@@ -116,15 +120,15 @@ class ModuleBuilder:
|
|
|
116
120
|
|
|
117
121
|
# Register metadata for OpenAPI generation
|
|
118
122
|
doc = UseCaseDocMeta(
|
|
119
|
-
summary=summary or f"Use case {
|
|
120
|
-
description=description or f"Auto-generated documentation for {
|
|
123
|
+
summary=summary or f"Use case {clean_command} in module {self._module_name}",
|
|
124
|
+
description=description or f"Auto-generated documentation for {clean_command}",
|
|
121
125
|
tags=[self._module_name],
|
|
122
126
|
)
|
|
123
127
|
|
|
124
128
|
api_registry.routes.append(
|
|
125
129
|
UseCaseRegistration(
|
|
126
130
|
module=self._module_name,
|
|
127
|
-
|
|
131
|
+
command=clean_command,
|
|
128
132
|
method=method_upper,
|
|
129
133
|
path=full_path,
|
|
130
134
|
factory=factory,
|
|
@@ -145,16 +149,12 @@ class ModuleBuilder:
|
|
|
145
149
|
def _extract_schemas(factory: UseCaseFactory) -> dict[str, dict[str, Any]]:
|
|
146
150
|
"""Capture Input and Output schemas from a UseCase factory.
|
|
147
151
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
Inspects the factory's type hints to find the Input/Output classes.
|
|
151
|
-
2. Fallback: instantiate via ``factory({})`` and call ``to_schema()``
|
|
152
|
-
on the resulting UseCase's input/output (legacy pattern).
|
|
152
|
+
Uses class-level extraction via ``to_schema()`` classmethod on BaseModel DTOs.
|
|
153
|
+
Inspects the factory's type hints to find the Input/Output classes.
|
|
153
154
|
"""
|
|
154
155
|
input_schema: dict[str, Any] = {}
|
|
155
156
|
output_schema: dict[str, Any] = {}
|
|
156
157
|
|
|
157
|
-
# --- Strategy 1: class-level extraction from type hints ---
|
|
158
158
|
return_hint = _get_return_type_hint(factory)
|
|
159
159
|
|
|
160
160
|
if return_hint is not None:
|
|
@@ -172,25 +172,6 @@ class ModuleBuilder:
|
|
|
172
172
|
except Exception:
|
|
173
173
|
pass
|
|
174
174
|
|
|
175
|
-
if input_schema or output_schema:
|
|
176
|
-
return {"input": input_schema, "output": output_schema}
|
|
177
|
-
|
|
178
|
-
# --- Strategy 2: legacy factory({}) fallback ---
|
|
179
|
-
try:
|
|
180
|
-
instance = factory({})
|
|
181
|
-
except Exception:
|
|
182
|
-
return {"input": input_schema, "output": output_schema}
|
|
183
|
-
|
|
184
|
-
try:
|
|
185
|
-
input_schema = instance.input.to_schema()
|
|
186
|
-
except Exception:
|
|
187
|
-
pass
|
|
188
|
-
|
|
189
|
-
try:
|
|
190
|
-
output_schema = instance.output.to_schema()
|
|
191
|
-
except Exception:
|
|
192
|
-
pass
|
|
193
|
-
|
|
194
175
|
return {"input": input_schema, "output": output_schema}
|
|
195
176
|
|
|
196
177
|
@staticmethod
|
|
@@ -38,9 +38,9 @@ class UseCaseRegistration:
|
|
|
38
38
|
"""
|
|
39
39
|
|
|
40
40
|
module: str
|
|
41
|
-
|
|
41
|
+
command: str # route segment and operationId root, e.g. 'hello-world'
|
|
42
42
|
method: str # uppercase: "POST" | "GET" | "PUT" | "PATCH" | "DELETE"
|
|
43
|
-
path: str # e.g. "/api/
|
|
43
|
+
path: str # e.g. "/api/v1/greetings/hello-world"
|
|
44
44
|
factory: UseCaseFactory
|
|
45
45
|
schemas: dict[str, dict[str, Any]] = field(default_factory=dict)
|
|
46
46
|
doc: UseCaseDocMeta | None = None
|
|
@@ -172,8 +172,11 @@ O = TypeVar("O", bound=Output)
|
|
|
172
172
|
class UseCase(ABC, Generic[I, O]):
|
|
173
173
|
"""Contract for business logic units.
|
|
174
174
|
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
Lifecycle (handled by the framework):
|
|
176
|
+
1. ``from_json(json)`` — static factory, builds the use case
|
|
177
|
+
2. ``validate()`` — return error string or ``None``
|
|
178
|
+
3. ``execute()`` — run business logic, return Output
|
|
179
|
+
4. handler serializes — ``output.to_json()`` → HTTP response
|
|
177
180
|
"""
|
|
178
181
|
|
|
179
182
|
# Request-scoped logger injected by the framework before execute().
|
|
@@ -186,23 +189,12 @@ class UseCase(ABC, Generic[I, O]):
|
|
|
186
189
|
"""Input DTO — set in constructor."""
|
|
187
190
|
...
|
|
188
191
|
|
|
189
|
-
@property
|
|
190
|
-
@abstractmethod
|
|
191
|
-
def output(self) -> O:
|
|
192
|
-
"""Output DTO — set in ``execute()``."""
|
|
193
|
-
...
|
|
194
|
-
|
|
195
192
|
@abstractmethod
|
|
196
193
|
def validate(self) -> str | None:
|
|
197
194
|
"""Validate the input. Return an error message or ``None``."""
|
|
198
195
|
...
|
|
199
196
|
|
|
200
197
|
@abstractmethod
|
|
201
|
-
async def execute(self) ->
|
|
202
|
-
"""Run the business logic and
|
|
203
|
-
...
|
|
204
|
-
|
|
205
|
-
@abstractmethod
|
|
206
|
-
def to_json(self) -> dict[str, object]:
|
|
207
|
-
"""Serialize the output for the HTTP response body."""
|
|
198
|
+
async def execute(self) -> O:
|
|
199
|
+
"""Run the business logic and return the Output."""
|
|
208
200
|
...
|
|
@@ -104,12 +104,12 @@ def usecase_handler(factory: UseCaseFactory) -> Any:
|
|
|
104
104
|
)
|
|
105
105
|
|
|
106
106
|
# 4. Execute
|
|
107
|
-
await use_case.execute()
|
|
107
|
+
output = await use_case.execute()
|
|
108
108
|
|
|
109
109
|
# 5. Respond
|
|
110
110
|
return Response(
|
|
111
|
-
content=json.dumps(
|
|
112
|
-
status_code=
|
|
111
|
+
content=json.dumps(output.to_json()),
|
|
112
|
+
status_code=output.status_code,
|
|
113
113
|
media_type=_JSON_CONTENT_TYPE,
|
|
114
114
|
)
|
|
115
115
|
|
|
@@ -89,8 +89,9 @@ def _add_route_to_spec(
|
|
|
89
89
|
component_schemas: dict[str, Any],
|
|
90
90
|
) -> None:
|
|
91
91
|
"""Append a single route's path entry, schemas, and operation to the spec."""
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
safe_command = route.command.replace("-", "_")
|
|
93
|
+
input_ref_name = f"{route.module}_{safe_command}_Input"
|
|
94
|
+
output_ref_name = f"{route.module}_{safe_command}_Output"
|
|
94
95
|
|
|
95
96
|
# Store schemas in components
|
|
96
97
|
component_schemas[input_ref_name] = route.schemas.get("input", {"type": "object"})
|
|
@@ -102,7 +103,7 @@ def _add_route_to_spec(
|
|
|
102
103
|
description = route.doc.description if route.doc else None
|
|
103
104
|
|
|
104
105
|
operation: dict[str, Any] = {
|
|
105
|
-
"operationId": f"{route.module}_{
|
|
106
|
+
"operationId": f"{route.module}_{safe_command}_{method_lower}",
|
|
106
107
|
"tags": tags,
|
|
107
108
|
"responses": {
|
|
108
109
|
"200": {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Docs UI handler — serves the ``@macss/docs-ui`` widget from CDN.
|
|
2
|
+
|
|
3
|
+
Loads ``@macss/docs-ui`` from jsdelivr CDN, which wraps Swagger UI with
|
|
4
|
+
system-aware dark mode. The local ``/openapi.json`` endpoint provides
|
|
5
|
+
the spec. Styling, dark mode, and Swagger UI loading are handled
|
|
6
|
+
entirely by docs-ui — no inline CSS or JS in this template.
|
|
7
|
+
|
|
8
|
+
See: https://github.com/macss-dev/modular_api/tree/main/docs-ui
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from starlette.requests import Request
|
|
14
|
+
from starlette.responses import HTMLResponse
|
|
15
|
+
|
|
16
|
+
_DOCS_UI_CDN = "https://cdn.jsdelivr.net/npm/@macss/docs-ui@0.1/dist"
|
|
17
|
+
|
|
18
|
+
# {title} is the only placeholder — str.replace() is a literal match,
|
|
19
|
+
# so JavaScript braces pass through unaffected.
|
|
20
|
+
_DOCS_UI_HTML_TEMPLATE = """\
|
|
21
|
+
<!DOCTYPE html>
|
|
22
|
+
<html>
|
|
23
|
+
<head>
|
|
24
|
+
<title>{title} — API Reference</title>
|
|
25
|
+
<meta charset="utf-8" />
|
|
26
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
27
|
+
</head>
|
|
28
|
+
<body>
|
|
29
|
+
<div id="swagger-ui"></div>
|
|
30
|
+
<script src=""" + f'"{_DOCS_UI_CDN}/docs-ui.js"' + """></script>
|
|
31
|
+
<script>DocsUI.init({ specUrl: "/openapi.json" })</script>
|
|
32
|
+
</body>
|
|
33
|
+
</html>"""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def swagger_docs_handler(*, title: str) -> object:
|
|
37
|
+
"""Return a Starlette endpoint that serves a docs-ui HTML page.
|
|
38
|
+
|
|
39
|
+
Usage::
|
|
40
|
+
|
|
41
|
+
Route("/docs", endpoint=swagger_docs_handler(title="My API"))
|
|
42
|
+
"""
|
|
43
|
+
html = _DOCS_UI_HTML_TEMPLATE.replace("{title}", title)
|
|
44
|
+
|
|
45
|
+
async def _endpoint(request: Request) -> HTMLResponse:
|
|
46
|
+
return HTMLResponse(html)
|
|
47
|
+
|
|
48
|
+
return _endpoint
|
|
@@ -41,26 +41,18 @@ class StrictUseCase(UseCase[StrictInput, StrictOutput]):
|
|
|
41
41
|
|
|
42
42
|
def __init__(self, data: dict) -> None:
|
|
43
43
|
self._input = StrictInput.from_json(data)
|
|
44
|
-
self._output = StrictOutput()
|
|
45
44
|
|
|
46
45
|
@property
|
|
47
46
|
def input(self) -> StrictInput:
|
|
48
47
|
return self._input
|
|
49
48
|
|
|
50
|
-
@property
|
|
51
|
-
def output(self) -> StrictOutput:
|
|
52
|
-
return self._output
|
|
53
|
-
|
|
54
49
|
def validate(self) -> str | None:
|
|
55
50
|
if not self.input.name:
|
|
56
51
|
return "name is required"
|
|
57
52
|
return None
|
|
58
53
|
|
|
59
|
-
async def execute(self) ->
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def to_json(self) -> dict:
|
|
63
|
-
return self._output.to_json()
|
|
54
|
+
async def execute(self) -> StrictOutput:
|
|
55
|
+
return StrictOutput(greeting=f"Hi {self.input.name}, age {self.input.age}")
|
|
64
56
|
|
|
65
57
|
|
|
66
58
|
def _app():
|
|
@@ -32,24 +32,16 @@ class _PingOutput(Output):
|
|
|
32
32
|
class _PingUseCase(UseCase[_PingInput, _PingOutput]):
|
|
33
33
|
def __init__(self, inp: _PingInput) -> None:
|
|
34
34
|
self._input = inp
|
|
35
|
-
self._output = _PingOutput()
|
|
36
35
|
|
|
37
36
|
@property
|
|
38
37
|
def input(self) -> _PingInput:
|
|
39
38
|
return self._input
|
|
40
39
|
|
|
41
|
-
@property
|
|
42
|
-
def output(self) -> _PingOutput:
|
|
43
|
-
return self._output
|
|
44
|
-
|
|
45
40
|
def validate(self) -> str | None:
|
|
46
41
|
return None
|
|
47
42
|
|
|
48
|
-
async def execute(self) ->
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def to_json(self) -> dict:
|
|
52
|
-
return self.output.to_json()
|
|
43
|
+
async def execute(self) -> _PingOutput:
|
|
44
|
+
return _PingOutput()
|
|
53
45
|
|
|
54
46
|
@staticmethod
|
|
55
47
|
def from_json(json: dict) -> _PingUseCase:
|
|
@@ -213,7 +205,7 @@ class TestAutoMountedEndpoints:
|
|
|
213
205
|
response = client.get("/docs")
|
|
214
206
|
assert response.status_code == 200
|
|
215
207
|
assert "text/html" in response.headers["content-type"]
|
|
216
|
-
assert "
|
|
208
|
+
assert "@macss/docs-ui" in response.text
|
|
217
209
|
assert "Test API" in response.text
|
|
218
210
|
|
|
219
211
|
|
|
@@ -34,20 +34,11 @@ class StubUseCase(UseCase[StubInput, StubOutput]):
|
|
|
34
34
|
|
|
35
35
|
def __init__(self, input_dto: StubInput) -> None:
|
|
36
36
|
self._input = input_dto
|
|
37
|
-
self._output = StubOutput(message="")
|
|
38
37
|
|
|
39
38
|
@property
|
|
40
39
|
def input(self) -> StubInput:
|
|
41
40
|
return self._input
|
|
42
41
|
|
|
43
|
-
@property
|
|
44
|
-
def output(self) -> StubOutput:
|
|
45
|
-
return self._output
|
|
46
|
-
|
|
47
|
-
@output.setter
|
|
48
|
-
def output(self, value: StubOutput) -> None:
|
|
49
|
-
self._output = value
|
|
50
|
-
|
|
51
42
|
@classmethod
|
|
52
43
|
def from_json(cls, json: dict) -> StubUseCase:
|
|
53
44
|
return cls(StubInput.from_json(json))
|
|
@@ -57,11 +48,8 @@ class StubUseCase(UseCase[StubInput, StubOutput]):
|
|
|
57
48
|
return "name is required"
|
|
58
49
|
return None
|
|
59
50
|
|
|
60
|
-
async def execute(self) ->
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def to_json(self) -> dict:
|
|
64
|
-
return self.output.to_json()
|
|
51
|
+
async def execute(self) -> StubOutput:
|
|
52
|
+
return StubOutput(message=f"Hello, {self.input.name}!")
|
|
65
53
|
|
|
66
54
|
|
|
67
55
|
# ── Tests ─────────────────────────────────────────────────────────────────
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Conformance tests:
|
|
1
|
+
"""Conformance tests: HelloWorldInput/HelloWorldOutput schemas match shared fixtures."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -10,7 +10,7 @@ import sys
|
|
|
10
10
|
_EXAMPLE_DIR = Path(__file__).resolve().parent.parent / "example"
|
|
11
11
|
sys.path.insert(0, str(_EXAMPLE_DIR))
|
|
12
12
|
|
|
13
|
-
from
|
|
13
|
+
from modules.greetings.usecases.hello_world import HelloWorldInput, HelloWorldOutput # type: ignore[import-untyped]
|
|
14
14
|
|
|
15
15
|
_FIXTURES = Path(__file__).resolve().parent.parent.parent / "tests" / "fixtures"
|
|
16
16
|
|
|
@@ -25,26 +25,26 @@ class TestSchemaConformance:
|
|
|
25
25
|
def test_hello_input_schema_matches_fixture(self) -> None:
|
|
26
26
|
fixture = _load_fixture("hello_input_schema.json")
|
|
27
27
|
# to_schema() is now a classmethod — call on class, not instance
|
|
28
|
-
assert
|
|
28
|
+
assert HelloWorldInput.to_schema() == fixture
|
|
29
29
|
|
|
30
30
|
def test_hello_output_schema_matches_fixture(self) -> None:
|
|
31
31
|
fixture = _load_fixture("hello_output_schema.json")
|
|
32
|
-
assert
|
|
32
|
+
assert HelloWorldOutput.to_schema() == fixture
|
|
33
33
|
|
|
34
34
|
def test_hello_input_from_json(self) -> None:
|
|
35
35
|
"""from_json populates fields correctly."""
|
|
36
|
-
instance =
|
|
36
|
+
instance = HelloWorldInput.from_json({"name": "Carlos"})
|
|
37
37
|
assert instance.name == "Carlos"
|
|
38
38
|
|
|
39
39
|
def test_hello_input_to_json(self) -> None:
|
|
40
40
|
"""to_json serializes correctly."""
|
|
41
|
-
instance =
|
|
41
|
+
instance = HelloWorldInput(name="Carlos")
|
|
42
42
|
assert instance.to_json() == {"name": "Carlos"}
|
|
43
43
|
|
|
44
44
|
def test_hello_output_from_json(self) -> None:
|
|
45
|
-
instance =
|
|
45
|
+
instance = HelloWorldOutput.from_json({"message": "Hello!"})
|
|
46
46
|
assert instance.message == "Hello!"
|
|
47
47
|
|
|
48
48
|
def test_hello_output_to_json(self) -> None:
|
|
49
|
-
instance =
|
|
49
|
+
instance = HelloWorldOutput(message="Hello!")
|
|
50
50
|
assert instance.to_json() == {"message": "Hello!"}
|
|
@@ -32,16 +32,11 @@ class SumUseCase(UseCase[SumInput, SumOutput]):
|
|
|
32
32
|
|
|
33
33
|
def __init__(self, input_dto: SumInput) -> None:
|
|
34
34
|
self._input = input_dto
|
|
35
|
-
self._output = SumOutput(resultado=0)
|
|
36
35
|
|
|
37
36
|
@property
|
|
38
37
|
def input(self) -> SumInput:
|
|
39
38
|
return self._input
|
|
40
39
|
|
|
41
|
-
@property
|
|
42
|
-
def output(self) -> SumOutput:
|
|
43
|
-
return self._output
|
|
44
|
-
|
|
45
40
|
def validate(self) -> str | None:
|
|
46
41
|
if self._input.a is None:
|
|
47
42
|
return "a is required"
|
|
@@ -49,13 +44,10 @@ class SumUseCase(UseCase[SumInput, SumOutput]):
|
|
|
49
44
|
return "b is required"
|
|
50
45
|
return None
|
|
51
46
|
|
|
52
|
-
async def execute(self) ->
|
|
47
|
+
async def execute(self) -> SumOutput:
|
|
53
48
|
a = self._input.a or 0
|
|
54
49
|
b = self._input.b or 0
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def to_json(self) -> dict[str, object]:
|
|
58
|
-
return self._output.to_json()
|
|
50
|
+
return SumOutput(resultado=a + b)
|
|
59
51
|
|
|
60
52
|
|
|
61
53
|
# ── Input tests ─────────────────────────────────────────────────────
|
|
@@ -116,16 +108,16 @@ class TestUseCase:
|
|
|
116
108
|
uc = SumUseCase(SumInput(a=5, b=None))
|
|
117
109
|
assert uc.validate() is not None
|
|
118
110
|
|
|
119
|
-
async def
|
|
111
|
+
async def test_execute_returns_output(self) -> None:
|
|
120
112
|
uc = SumUseCase(SumInput(a=3, b=4))
|
|
121
113
|
assert uc.validate() is None
|
|
122
|
-
await uc.execute()
|
|
123
|
-
assert
|
|
114
|
+
output = await uc.execute()
|
|
115
|
+
assert output.resultado == 7
|
|
124
116
|
|
|
125
|
-
async def
|
|
117
|
+
async def test_execute_output_serializes(self) -> None:
|
|
126
118
|
uc = SumUseCase(SumInput(a=10, b=20))
|
|
127
|
-
await uc.execute()
|
|
128
|
-
assert
|
|
119
|
+
output = await uc.execute()
|
|
120
|
+
assert output.to_json() == {"resultado": 30}
|
|
129
121
|
|
|
130
122
|
def test_logger_defaults_to_none(self) -> None:
|
|
131
123
|
uc = SumUseCase(SumInput(a=1, b=2))
|
|
@@ -32,26 +32,18 @@ class GreetUseCase(UseCase[GreetInput, GreetOutput]):
|
|
|
32
32
|
|
|
33
33
|
def __init__(self, data: dict) -> None:
|
|
34
34
|
self._input = GreetInput.from_json(data)
|
|
35
|
-
self._output = GreetOutput()
|
|
36
35
|
|
|
37
36
|
@property
|
|
38
37
|
def input(self) -> GreetInput:
|
|
39
38
|
return self._input
|
|
40
39
|
|
|
41
|
-
@property
|
|
42
|
-
def output(self) -> GreetOutput:
|
|
43
|
-
return self._output
|
|
44
|
-
|
|
45
40
|
def validate(self) -> str | None:
|
|
46
41
|
if not self._input.name:
|
|
47
42
|
return "name is required"
|
|
48
43
|
return None
|
|
49
44
|
|
|
50
|
-
async def execute(self) ->
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def to_json(self) -> dict:
|
|
54
|
-
return self._output.to_json()
|
|
45
|
+
async def execute(self) -> GreetOutput:
|
|
46
|
+
return GreetOutput(greeting=f"Hello, {self._input.name}!")
|
|
55
47
|
|
|
56
48
|
|
|
57
49
|
class FailingUseCase(UseCase[GreetInput, GreetOutput]):
|
|
@@ -59,54 +51,38 @@ class FailingUseCase(UseCase[GreetInput, GreetOutput]):
|
|
|
59
51
|
|
|
60
52
|
def __init__(self, data: dict) -> None:
|
|
61
53
|
self._input = GreetInput.from_json(data)
|
|
62
|
-
self._output = GreetOutput()
|
|
63
54
|
|
|
64
55
|
@property
|
|
65
56
|
def input(self) -> GreetInput:
|
|
66
57
|
return self._input
|
|
67
58
|
|
|
68
|
-
@property
|
|
69
|
-
def output(self) -> GreetOutput:
|
|
70
|
-
return self._output
|
|
71
|
-
|
|
72
59
|
def validate(self) -> str | None:
|
|
73
60
|
return None
|
|
74
61
|
|
|
75
|
-
async def execute(self) ->
|
|
62
|
+
async def execute(self) -> GreetOutput:
|
|
76
63
|
raise UseCaseException(
|
|
77
64
|
status_code=409,
|
|
78
65
|
message="Already exists",
|
|
79
66
|
error_code="CONFLICT",
|
|
80
67
|
)
|
|
81
68
|
|
|
82
|
-
def to_json(self) -> dict:
|
|
83
|
-
return self._output.to_json()
|
|
84
|
-
|
|
85
69
|
|
|
86
70
|
class CrashingUseCase(UseCase[GreetInput, GreetOutput]):
|
|
87
71
|
"""Raises an unexpected error during execute."""
|
|
88
72
|
|
|
89
73
|
def __init__(self, data: dict) -> None:
|
|
90
74
|
self._input = GreetInput(name="x")
|
|
91
|
-
self._output = GreetOutput()
|
|
92
75
|
|
|
93
76
|
@property
|
|
94
77
|
def input(self) -> GreetInput:
|
|
95
78
|
return self._input
|
|
96
79
|
|
|
97
|
-
@property
|
|
98
|
-
def output(self) -> GreetOutput:
|
|
99
|
-
return self._output
|
|
100
|
-
|
|
101
80
|
def validate(self) -> str | None:
|
|
102
81
|
return None
|
|
103
82
|
|
|
104
|
-
async def execute(self) ->
|
|
83
|
+
async def execute(self) -> GreetOutput:
|
|
105
84
|
raise RuntimeError("disk on fire")
|
|
106
85
|
|
|
107
|
-
def to_json(self) -> dict:
|
|
108
|
-
return self._output.to_json()
|
|
109
|
-
|
|
110
86
|
|
|
111
87
|
# ── Helpers ───────────────────────────────────────────────────────────────
|
|
112
88
|
|
|
@@ -201,25 +177,17 @@ class TestUseCaseLifecycle:
|
|
|
201
177
|
class SpyUseCase(UseCase[GreetInput, GreetOutput]):
|
|
202
178
|
def __init__(self, data: dict) -> None:
|
|
203
179
|
self._input = GreetInput(name="x")
|
|
204
|
-
self._output = GreetOutput()
|
|
205
180
|
|
|
206
181
|
@property
|
|
207
182
|
def input(self) -> GreetInput:
|
|
208
183
|
return self._input
|
|
209
184
|
|
|
210
|
-
@property
|
|
211
|
-
def output(self) -> GreetOutput:
|
|
212
|
-
return self._output
|
|
213
|
-
|
|
214
185
|
def validate(self) -> str | None:
|
|
215
186
|
return None
|
|
216
187
|
|
|
217
|
-
async def execute(self) ->
|
|
188
|
+
async def execute(self) -> GreetOutput:
|
|
218
189
|
captured_loggers.append(self.logger)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
def to_json(self) -> dict:
|
|
222
|
-
return self._output.to_json()
|
|
190
|
+
return GreetOutput(greeting="ok")
|
|
223
191
|
|
|
224
192
|
# Middleware that fakes a logger in request.state
|
|
225
193
|
from starlette.middleware import Middleware
|
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
"""Swagger UI docs handler — serves a self-contained interactive API explorer.
|
|
2
|
-
|
|
3
|
-
Loads swagger-ui-dist@5 (CSS + JS bundle) from the jsdelivr CDN and
|
|
4
|
-
points the UI at the local ``/openapi.json`` endpoint. No server-side
|
|
5
|
-
dependencies; no npm packages required. Replaces the previous Scalar
|
|
6
|
-
widget as specified by PRD-003.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
|
|
11
|
-
from starlette.requests import Request
|
|
12
|
-
from starlette.responses import HTMLResponse
|
|
13
|
-
|
|
14
|
-
# Canonical Swagger UI HTML per PRD-003.
|
|
15
|
-
# Uses {title} as a plain str.replace() placeholder — Python's replace()
|
|
16
|
-
# is a literal match, so JavaScript braces pass through unaffected.
|
|
17
|
-
_SWAGGER_UI_HTML_TEMPLATE = """\
|
|
18
|
-
<!DOCTYPE html>
|
|
19
|
-
<html>
|
|
20
|
-
<head>
|
|
21
|
-
<title>{title} — API Reference</title>
|
|
22
|
-
<meta charset="utf-8" />
|
|
23
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
24
|
-
<link
|
|
25
|
-
rel="stylesheet"
|
|
26
|
-
href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css"
|
|
27
|
-
/>
|
|
28
|
-
<style>
|
|
29
|
-
/* ── Light mode baseline (Swagger UI default) ───────────── */
|
|
30
|
-
:root {
|
|
31
|
-
--bg-primary: #ffffff;
|
|
32
|
-
--bg-secondary: #f7f7f7;
|
|
33
|
-
--bg-block: #ffffff;
|
|
34
|
-
--border-color: #e8e8e8;
|
|
35
|
-
--text-primary: #3b4151;
|
|
36
|
-
--text-muted: #6b7280;
|
|
37
|
-
--input-bg: #ffffff;
|
|
38
|
-
--input-text: #3b4151;
|
|
39
|
-
--input-border: #d9d9d9;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/* ── Dark mode overrides ───────────────────────────── */
|
|
43
|
-
@media (prefers-color-scheme: dark) {
|
|
44
|
-
:root {
|
|
45
|
-
--bg-primary: #1a1a1a;
|
|
46
|
-
--bg-secondary: #222222;
|
|
47
|
-
--bg-block: #2a2a2a;
|
|
48
|
-
--border-color: #3a3a3a;
|
|
49
|
-
--text-primary: #e0e0e0;
|
|
50
|
-
--text-muted: #a0a0a0;
|
|
51
|
-
--input-bg: #2a2a2a;
|
|
52
|
-
--input-text: #e0e0e0;
|
|
53
|
-
--input-border: #444444;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
body {
|
|
57
|
-
background-color: var(--bg-primary);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/* ── General text ──────────────────────────────── */
|
|
61
|
-
.swagger-ui,
|
|
62
|
-
.swagger-ui .info .title,
|
|
63
|
-
.swagger-ui .info p,
|
|
64
|
-
.swagger-ui .info li,
|
|
65
|
-
.swagger-ui .info a,
|
|
66
|
-
.swagger-ui .opblock-tag,
|
|
67
|
-
.swagger-ui .opblock-tag small,
|
|
68
|
-
.swagger-ui table thead tr td,
|
|
69
|
-
.swagger-ui table thead tr th,
|
|
70
|
-
.swagger-ui .response-col_status,
|
|
71
|
-
.swagger-ui .response-col_description,
|
|
72
|
-
.swagger-ui .parameter__name,
|
|
73
|
-
.swagger-ui .parameter__type,
|
|
74
|
-
.swagger-ui .parameter__in,
|
|
75
|
-
.swagger-ui .tab li,
|
|
76
|
-
.swagger-ui .model-title,
|
|
77
|
-
.swagger-ui .model,
|
|
78
|
-
.swagger-ui .prop-type,
|
|
79
|
-
.swagger-ui .prop-format {
|
|
80
|
-
color: var(--text-primary);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/* ── Backgrounds ───────────────────────────────── */
|
|
84
|
-
.swagger-ui .wrapper,
|
|
85
|
-
.swagger-ui .scheme-container,
|
|
86
|
-
.swagger-ui section.models,
|
|
87
|
-
.swagger-ui section.models .model-container,
|
|
88
|
-
.swagger-ui .model-box {
|
|
89
|
-
background: var(--bg-secondary);
|
|
90
|
-
border-color: var(--border-color);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/* ── Operation blocks ────────────────────────────── */
|
|
94
|
-
.swagger-ui .opblock {
|
|
95
|
-
background: var(--bg-block);
|
|
96
|
-
border-color: var(--border-color);
|
|
97
|
-
box-shadow: none;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
.swagger-ui .opblock .opblock-summary {
|
|
101
|
-
background: var(--bg-block);
|
|
102
|
-
border-color: var(--border-color);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
.swagger-ui .opblock .opblock-section-header {
|
|
106
|
-
background: var(--bg-secondary);
|
|
107
|
-
border-color: var(--border-color);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
.swagger-ui .opblock .opblock-section-header h4 {
|
|
111
|
-
color: var(--text-primary);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/* HTTP method accent colors preserved in dark mode */
|
|
115
|
-
.swagger-ui .opblock.opblock-post { border-color: #49cc90; }
|
|
116
|
-
.swagger-ui .opblock.opblock-get { border-color: #61affe; }
|
|
117
|
-
.swagger-ui .opblock.opblock-put { border-color: #fca130; }
|
|
118
|
-
.swagger-ui .opblock.opblock-delete { border-color: #f93e3e; }
|
|
119
|
-
.swagger-ui .opblock.opblock-patch { border-color: #50e3c2; }
|
|
120
|
-
|
|
121
|
-
.swagger-ui .opblock.opblock-post .opblock-summary-method { background: #49cc90; }
|
|
122
|
-
.swagger-ui .opblock.opblock-get .opblock-summary-method { background: #61affe; }
|
|
123
|
-
.swagger-ui .opblock.opblock-put .opblock-summary-method { background: #fca130; }
|
|
124
|
-
.swagger-ui .opblock.opblock-delete .opblock-summary-method { background: #f93e3e; }
|
|
125
|
-
.swagger-ui .opblock.opblock-patch .opblock-summary-method { background: #50e3c2; }
|
|
126
|
-
|
|
127
|
-
/* ── Inputs and controls ─────────────────────────── */
|
|
128
|
-
.swagger-ui input[type=text],
|
|
129
|
-
.swagger-ui input[type=password],
|
|
130
|
-
.swagger-ui input[type=search],
|
|
131
|
-
.swagger-ui input[type=email],
|
|
132
|
-
.swagger-ui textarea,
|
|
133
|
-
.swagger-ui select {
|
|
134
|
-
background: var(--input-bg);
|
|
135
|
-
color: var(--input-text);
|
|
136
|
-
border-color: var(--input-border);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/* ── Buttons ───────────────────────────────────── */
|
|
140
|
-
.swagger-ui .btn {
|
|
141
|
-
background: var(--bg-block);
|
|
142
|
-
color: var(--text-primary);
|
|
143
|
-
border-color: var(--border-color);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
.swagger-ui .btn.execute {
|
|
147
|
-
background: #4a90e2;
|
|
148
|
-
color: #ffffff;
|
|
149
|
-
border-color: #4a90e2;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
.swagger-ui .btn.authorize {
|
|
153
|
-
color: #49cc90;
|
|
154
|
-
border-color: #49cc90;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/* ── Response area ─────────────────────────────── */
|
|
158
|
-
.swagger-ui .responses-inner,
|
|
159
|
-
.swagger-ui .response-col_description__inner {
|
|
160
|
-
background: var(--bg-secondary);
|
|
161
|
-
color: var(--text-primary);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
.swagger-ui .highlight-code,
|
|
165
|
-
.swagger-ui .microlight {
|
|
166
|
-
background: #111111 !important;
|
|
167
|
-
color: #e0e0e0 !important;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/* ── Topbar ────────────────────────────────────── */
|
|
171
|
-
.swagger-ui .topbar {
|
|
172
|
-
background-color: #111111;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/* ── Expand/collapse arrows ──────────────────────── */
|
|
176
|
-
.swagger-ui .expand-methods svg,
|
|
177
|
-
.swagger-ui .expand-operation svg {
|
|
178
|
-
fill: var(--text-muted);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/* ── Filter input ──────────────────────────────── */
|
|
182
|
-
.swagger-ui .filter .operation-filter-input {
|
|
183
|
-
background: var(--input-bg);
|
|
184
|
-
color: var(--input-text);
|
|
185
|
-
border-color: var(--input-border);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/* ── Scrollbar (webkit) ──────────────────────────── */
|
|
189
|
-
::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
190
|
-
::-webkit-scrollbar-track { background: var(--bg-primary); }
|
|
191
|
-
::-webkit-scrollbar-thumb { background: #444444; border-radius: 4px; }
|
|
192
|
-
::-webkit-scrollbar-thumb:hover { background: #555555; }
|
|
193
|
-
}
|
|
194
|
-
</style>
|
|
195
|
-
</head>
|
|
196
|
-
<body>
|
|
197
|
-
<div id="swagger-ui"></div>
|
|
198
|
-
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js">
|
|
199
|
-
</script>
|
|
200
|
-
<script>
|
|
201
|
-
SwaggerUIBundle({
|
|
202
|
-
url: "/openapi.json",
|
|
203
|
-
dom_id: "#swagger-ui",
|
|
204
|
-
presets: [
|
|
205
|
-
SwaggerUIBundle.presets.apis,
|
|
206
|
-
SwaggerUIBundle.SwaggerUIStandalonePreset
|
|
207
|
-
],
|
|
208
|
-
layout: "BaseLayout",
|
|
209
|
-
deepLinking: true
|
|
210
|
-
})
|
|
211
|
-
</script>
|
|
212
|
-
</body>
|
|
213
|
-
</html>"""
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
def swagger_docs_handler(*, title: str) -> object:
|
|
217
|
-
"""Return a Starlette endpoint that serves a Swagger UI HTML page.
|
|
218
|
-
|
|
219
|
-
Usage::
|
|
220
|
-
|
|
221
|
-
Route("/docs", endpoint=swagger_docs_handler(title="My API"))
|
|
222
|
-
"""
|
|
223
|
-
html = _SWAGGER_UI_HTML_TEMPLATE.replace("{title}", title)
|
|
224
|
-
|
|
225
|
-
async def _endpoint(request: Request) -> HTMLResponse:
|
|
226
|
-
return HTMLResponse(html)
|
|
227
|
-
|
|
228
|
-
return _endpoint
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/src/macss_modular_api.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/src/macss_modular_api.egg-info/requires.txt
RENAMED
|
File without changes
|
{macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/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.2 → macss_modular_api-0.4.4}/src/modular_api/core/health/health_check.py
RENAMED
|
File without changes
|
{macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/src/modular_api/core/health/health_handler.py
RENAMED
|
File without changes
|
{macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/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.2 → macss_modular_api-0.4.4}/src/modular_api/core/metrics/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/src/modular_api/core/metrics/metric_registry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{macss_modular_api-0.4.2 → macss_modular_api-0.4.4}/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
|