tiferet-openapi 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Great Strength Systems
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,197 @@
1
+ Metadata-Version: 2.4
2
+ Name: tiferet-openapi
3
+ Version: 0.1.0
4
+ Summary: A shared OpenAPI abstraction layer for the Tiferet Framework.
5
+ Author-email: "Andrew Shatz, Great Strength Systems" <andrew@greatstrength.me>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/greatstrength/tiferet-openapi
8
+ Project-URL: Repository, https://github.com/greatstrength/tiferet-openapi
9
+ Project-URL: Download, https://github.com/greatstrength/tiferet-openapi
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: tiferet>=2.0.0b1
14
+ Provides-Extra: test
15
+ Requires-Dist: pytest>=8.3.3; extra == "test"
16
+ Requires-Dist: pytest_env>=1.1.5; extra == "test"
17
+ Dynamic: license-file
18
+
19
+ # Tiferet OpenAPI — A Shared OpenAPI Abstraction Layer for the Tiferet Framework
20
+
21
+ ## Introduction
22
+
23
+ Tiferet OpenAPI provides the shared abstraction layer that both [tiferet-flask](https://github.com/greatstrength/tiferet-flask) and [tiferet-fast](https://github.com/greatstrength/tiferet-fast) depend on for OpenAPI-style API development. It extracts the common domain objects, service interfaces, domain events, mappers, YAML-backed repository, and context classes that were previously duplicated across both framework adapters.
24
+
25
+ By unifying these components into a single package, tiferet-openapi eliminates code duplication, ensures behavioral consistency between Flask and FastAPI adapters, and provides a clean foundation for building new framework adapters.
26
+
27
+ ## Installation
28
+
29
+ ### From PyPI
30
+
31
+ ```bash
32
+ pip install tiferet-openapi
33
+ ```
34
+
35
+ ### For Development
36
+
37
+ ```bash
38
+ git clone https://github.com/greatstrength/tiferet-openapi.git
39
+ cd tiferet-openapi
40
+ python3.10 -m venv .venv
41
+ source .venv/bin/activate
42
+ pip install -e ".[test]"
43
+ ```
44
+
45
+ ## Architecture
46
+
47
+ Tiferet OpenAPI follows the Tiferet framework's layered Domain-Driven Design architecture:
48
+
49
+ ```
50
+ tiferet_openapi/
51
+ ├── __init__.py — Version and public exports
52
+ ├── domain/ — ApiRoute, ApiRouter (DomainObject, Pydantic v2)
53
+ ├── interfaces/ — OpenApiService (Service ABC)
54
+ ├── events/ — GetRouters, GetRoute, GetStatusCode (DomainEvent)
55
+ ├── mappers/ — Aggregates and TransferObjects for YAML round-trip
56
+ ├── repos/ — OpenApiYamlRepository (YamlLoader-backed OpenApiService)
57
+ └── contexts/ — OpenApiContext (AppInterfaceContext), OpenApiRequestContext
58
+ ```
59
+
60
+ ### Domain Objects
61
+
62
+ `ApiRoute` and `ApiRouter` are read-only Pydantic v2 domain models that represent API routing configuration:
63
+
64
+ - **`ApiRoute`** — An individual route with `id`, `endpoint` (format: `router_name.route_id`), `path`, `methods`, and `status_code`.
65
+ - **`ApiRouter`** — A named group of routes with an optional URL `prefix`.
66
+
67
+ ### Service Interface
68
+
69
+ `OpenApiService` is the abstract contract for API configuration access:
70
+
71
+ - `get_routers()` — Retrieve all configured routers.
72
+ - `get_route(route_id, router_name=None)` — Look up a single route.
73
+ - `get_status_code(error_code)` — Map an error code to an HTTP status code.
74
+
75
+ ### Domain Events
76
+
77
+ Three domain events encapsulate the service operations for use in the feature workflow:
78
+
79
+ - **`GetRouters`** — Retrieves all routers via the injected `OpenApiService`.
80
+ - **`GetRoute`** — Parses a dotted endpoint string (e.g., `calc.add`) and retrieves the matching route.
81
+ - **`GetStatusCode`** — Looks up the HTTP status code for a given error code.
82
+
83
+ ### Mappers
84
+
85
+ Aggregates and TransferObjects bridge YAML configuration and runtime domain objects:
86
+
87
+ - **`ApiRouteAggregate`**, **`ApiRouterAggregate`** — Mutable aggregates with route management methods.
88
+ - **`ApiRouteYamlObject`**, **`ApiRouterYamlObject`** — YAML serialization with `_ROLES`-based role control, `map()` for aggregate construction, `from_model()` for reverse mapping.
89
+
90
+ ### Repository
91
+
92
+ `OpenApiYamlRepository` is the YAML-backed implementation of `OpenApiService`. It accepts a parameterized `root_key` (defaults to `"openapi"`) enabling compatibility with multiple YAML formats:
93
+
94
+ - `root_key="openapi"` — unified `openapi.yml` format
95
+ - `root_key="flask"` — legacy `flask.yml` format
96
+ - `root_key="fast"` — legacy `fast.yml` format
97
+
98
+ ### Contexts
99
+
100
+ - **`OpenApiContext(AppInterfaceContext)`** — Shared API context that receives `DomainEvent` instances for route and status code lookup. Provides `parse_request`, `handle_error` (with HTTP status code resolution), and `handle_response` (returning `(response, status_code)` tuples).
101
+ - **`OpenApiRequestContext(RequestContext)`** — Pydantic-aware request context that serializes `BaseModel` results via `model_dump()`, with support for lists, dicts, `None`, and primitives.
102
+
103
+ ## YAML Configuration Format
104
+
105
+ The repository reads configuration from a YAML file with the following structure:
106
+
107
+ ```yaml
108
+ openapi: # root_key (can be 'flask', 'fast', or any custom key)
109
+ routers:
110
+ calc:
111
+ prefix: /calc
112
+ routes:
113
+ add:
114
+ path: /add
115
+ methods:
116
+ - POST
117
+ status_code: 200
118
+ subtract:
119
+ path: /subtract
120
+ methods:
121
+ - POST
122
+ status_code: 200
123
+ health:
124
+ routes:
125
+ ping:
126
+ path: /ping
127
+ methods:
128
+ - GET
129
+ status_code: 200
130
+ errors:
131
+ INVALID_INPUT: 400
132
+ DIVISION_BY_ZERO: 422
133
+ NOT_FOUND: 404
134
+ ```
135
+
136
+ ## Usage
137
+
138
+ Tiferet OpenAPI is consumed by framework-specific adapters. Here's how the shared components integrate:
139
+
140
+ ### In tiferet-flask / tiferet-fast
141
+
142
+ Framework adapters extend `OpenApiContext` and use `OpenApiYamlRepository` as their configuration backend:
143
+
144
+ ```python
145
+ # Framework adapter context (e.g., FlaskApiContext)
146
+ from tiferet_openapi import OpenApiContext, OpenApiRequestContext
147
+
148
+ class FlaskApiContext(OpenApiContext):
149
+ # Inherits parse_request, handle_error, handle_response
150
+ # Adds Flask-specific builder logic
151
+ pass
152
+ ```
153
+
154
+ ### Direct Repository Usage
155
+
156
+ ```python
157
+ from tiferet_openapi import OpenApiYamlRepository
158
+
159
+ # Load configuration
160
+ repo = OpenApiYamlRepository(
161
+ openapi_yaml_file='app/configs/openapi.yml',
162
+ root_key='openapi',
163
+ )
164
+
165
+ # Retrieve all routers
166
+ routers = repo.get_routers()
167
+ for router in routers:
168
+ print(f"{router.name}: {router.prefix}")
169
+ for route in router.routes:
170
+ print(f" {route.endpoint} -> {route.path} [{', '.join(route.methods)}]")
171
+
172
+ # Look up a specific route
173
+ route = repo.get_route('add', router_name='calc')
174
+ print(f"Route: {route.endpoint}, Status: {route.status_code}")
175
+
176
+ # Map error code to HTTP status
177
+ status = repo.get_status_code('INVALID_INPUT') # Returns 400
178
+ status = repo.get_status_code('UNKNOWN') # Returns 500 (default)
179
+ ```
180
+
181
+ ## Testing
182
+
183
+ Run the test suite:
184
+
185
+ ```bash
186
+ pytest tiferet_openapi/ -v
187
+ ```
188
+
189
+ Tests are co-located in `<package>/tests/` directories:
190
+ - **Domain/mapper tests** use direct Pydantic constructors.
191
+ - **Event tests** use `DomainEvent.handle()` with mocked `OpenApiService`.
192
+ - **Repo tests** are integration tests using `tmp_path` with real YAML files.
193
+ - **Context tests** use `mock.Mock(spec=DomainEvent)` for event dependencies.
194
+
195
+ ## License
196
+
197
+ MIT — see [LICENSE](LICENSE) for details.
@@ -0,0 +1,179 @@
1
+ # Tiferet OpenAPI — A Shared OpenAPI Abstraction Layer for the Tiferet Framework
2
+
3
+ ## Introduction
4
+
5
+ Tiferet OpenAPI provides the shared abstraction layer that both [tiferet-flask](https://github.com/greatstrength/tiferet-flask) and [tiferet-fast](https://github.com/greatstrength/tiferet-fast) depend on for OpenAPI-style API development. It extracts the common domain objects, service interfaces, domain events, mappers, YAML-backed repository, and context classes that were previously duplicated across both framework adapters.
6
+
7
+ By unifying these components into a single package, tiferet-openapi eliminates code duplication, ensures behavioral consistency between Flask and FastAPI adapters, and provides a clean foundation for building new framework adapters.
8
+
9
+ ## Installation
10
+
11
+ ### From PyPI
12
+
13
+ ```bash
14
+ pip install tiferet-openapi
15
+ ```
16
+
17
+ ### For Development
18
+
19
+ ```bash
20
+ git clone https://github.com/greatstrength/tiferet-openapi.git
21
+ cd tiferet-openapi
22
+ python3.10 -m venv .venv
23
+ source .venv/bin/activate
24
+ pip install -e ".[test]"
25
+ ```
26
+
27
+ ## Architecture
28
+
29
+ Tiferet OpenAPI follows the Tiferet framework's layered Domain-Driven Design architecture:
30
+
31
+ ```
32
+ tiferet_openapi/
33
+ ├── __init__.py — Version and public exports
34
+ ├── domain/ — ApiRoute, ApiRouter (DomainObject, Pydantic v2)
35
+ ├── interfaces/ — OpenApiService (Service ABC)
36
+ ├── events/ — GetRouters, GetRoute, GetStatusCode (DomainEvent)
37
+ ├── mappers/ — Aggregates and TransferObjects for YAML round-trip
38
+ ├── repos/ — OpenApiYamlRepository (YamlLoader-backed OpenApiService)
39
+ └── contexts/ — OpenApiContext (AppInterfaceContext), OpenApiRequestContext
40
+ ```
41
+
42
+ ### Domain Objects
43
+
44
+ `ApiRoute` and `ApiRouter` are read-only Pydantic v2 domain models that represent API routing configuration:
45
+
46
+ - **`ApiRoute`** — An individual route with `id`, `endpoint` (format: `router_name.route_id`), `path`, `methods`, and `status_code`.
47
+ - **`ApiRouter`** — A named group of routes with an optional URL `prefix`.
48
+
49
+ ### Service Interface
50
+
51
+ `OpenApiService` is the abstract contract for API configuration access:
52
+
53
+ - `get_routers()` — Retrieve all configured routers.
54
+ - `get_route(route_id, router_name=None)` — Look up a single route.
55
+ - `get_status_code(error_code)` — Map an error code to an HTTP status code.
56
+
57
+ ### Domain Events
58
+
59
+ Three domain events encapsulate the service operations for use in the feature workflow:
60
+
61
+ - **`GetRouters`** — Retrieves all routers via the injected `OpenApiService`.
62
+ - **`GetRoute`** — Parses a dotted endpoint string (e.g., `calc.add`) and retrieves the matching route.
63
+ - **`GetStatusCode`** — Looks up the HTTP status code for a given error code.
64
+
65
+ ### Mappers
66
+
67
+ Aggregates and TransferObjects bridge YAML configuration and runtime domain objects:
68
+
69
+ - **`ApiRouteAggregate`**, **`ApiRouterAggregate`** — Mutable aggregates with route management methods.
70
+ - **`ApiRouteYamlObject`**, **`ApiRouterYamlObject`** — YAML serialization with `_ROLES`-based role control, `map()` for aggregate construction, `from_model()` for reverse mapping.
71
+
72
+ ### Repository
73
+
74
+ `OpenApiYamlRepository` is the YAML-backed implementation of `OpenApiService`. It accepts a parameterized `root_key` (defaults to `"openapi"`) enabling compatibility with multiple YAML formats:
75
+
76
+ - `root_key="openapi"` — unified `openapi.yml` format
77
+ - `root_key="flask"` — legacy `flask.yml` format
78
+ - `root_key="fast"` — legacy `fast.yml` format
79
+
80
+ ### Contexts
81
+
82
+ - **`OpenApiContext(AppInterfaceContext)`** — Shared API context that receives `DomainEvent` instances for route and status code lookup. Provides `parse_request`, `handle_error` (with HTTP status code resolution), and `handle_response` (returning `(response, status_code)` tuples).
83
+ - **`OpenApiRequestContext(RequestContext)`** — Pydantic-aware request context that serializes `BaseModel` results via `model_dump()`, with support for lists, dicts, `None`, and primitives.
84
+
85
+ ## YAML Configuration Format
86
+
87
+ The repository reads configuration from a YAML file with the following structure:
88
+
89
+ ```yaml
90
+ openapi: # root_key (can be 'flask', 'fast', or any custom key)
91
+ routers:
92
+ calc:
93
+ prefix: /calc
94
+ routes:
95
+ add:
96
+ path: /add
97
+ methods:
98
+ - POST
99
+ status_code: 200
100
+ subtract:
101
+ path: /subtract
102
+ methods:
103
+ - POST
104
+ status_code: 200
105
+ health:
106
+ routes:
107
+ ping:
108
+ path: /ping
109
+ methods:
110
+ - GET
111
+ status_code: 200
112
+ errors:
113
+ INVALID_INPUT: 400
114
+ DIVISION_BY_ZERO: 422
115
+ NOT_FOUND: 404
116
+ ```
117
+
118
+ ## Usage
119
+
120
+ Tiferet OpenAPI is consumed by framework-specific adapters. Here's how the shared components integrate:
121
+
122
+ ### In tiferet-flask / tiferet-fast
123
+
124
+ Framework adapters extend `OpenApiContext` and use `OpenApiYamlRepository` as their configuration backend:
125
+
126
+ ```python
127
+ # Framework adapter context (e.g., FlaskApiContext)
128
+ from tiferet_openapi import OpenApiContext, OpenApiRequestContext
129
+
130
+ class FlaskApiContext(OpenApiContext):
131
+ # Inherits parse_request, handle_error, handle_response
132
+ # Adds Flask-specific builder logic
133
+ pass
134
+ ```
135
+
136
+ ### Direct Repository Usage
137
+
138
+ ```python
139
+ from tiferet_openapi import OpenApiYamlRepository
140
+
141
+ # Load configuration
142
+ repo = OpenApiYamlRepository(
143
+ openapi_yaml_file='app/configs/openapi.yml',
144
+ root_key='openapi',
145
+ )
146
+
147
+ # Retrieve all routers
148
+ routers = repo.get_routers()
149
+ for router in routers:
150
+ print(f"{router.name}: {router.prefix}")
151
+ for route in router.routes:
152
+ print(f" {route.endpoint} -> {route.path} [{', '.join(route.methods)}]")
153
+
154
+ # Look up a specific route
155
+ route = repo.get_route('add', router_name='calc')
156
+ print(f"Route: {route.endpoint}, Status: {route.status_code}")
157
+
158
+ # Map error code to HTTP status
159
+ status = repo.get_status_code('INVALID_INPUT') # Returns 400
160
+ status = repo.get_status_code('UNKNOWN') # Returns 500 (default)
161
+ ```
162
+
163
+ ## Testing
164
+
165
+ Run the test suite:
166
+
167
+ ```bash
168
+ pytest tiferet_openapi/ -v
169
+ ```
170
+
171
+ Tests are co-located in `<package>/tests/` directories:
172
+ - **Domain/mapper tests** use direct Pydantic constructors.
173
+ - **Event tests** use `DomainEvent.handle()` with mocked `OpenApiService`.
174
+ - **Repo tests** are integration tests using `tmp_path` with real YAML files.
175
+ - **Context tests** use `mock.Mock(spec=DomainEvent)` for event dependencies.
176
+
177
+ ## License
178
+
179
+ MIT — see [LICENSE](LICENSE) for details.
@@ -0,0 +1,43 @@
1
+ [project]
2
+ name = "tiferet-openapi"
3
+ dynamic = ["version"] # Mark version as dynamic
4
+ description = "A shared OpenAPI abstraction layer for the Tiferet Framework."
5
+ authors = [
6
+ { name = "Andrew Shatz, Great Strength Systems", email = "andrew@greatstrength.me" }
7
+ ]
8
+ license = { text = "MIT" }
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ "tiferet>=2.0.0b1"
13
+ ]
14
+
15
+ [project.urls]
16
+ Homepage = "https://github.com/greatstrength/tiferet-openapi"
17
+ Repository = "https://github.com/greatstrength/tiferet-openapi"
18
+ Download = "https://github.com/greatstrength/tiferet-openapi"
19
+
20
+ [project.optional-dependencies]
21
+ test = [
22
+ "pytest>=8.3.3",
23
+ "pytest_env>=1.1.5"
24
+ ]
25
+
26
+ [build-system]
27
+ requires = ["setuptools>=61.0", "wheel"]
28
+ build-backend = "setuptools.build_meta"
29
+
30
+ [tool.setuptools.packages.find]
31
+ where = ["."]
32
+ include = [
33
+ "tiferet_openapi",
34
+ "tiferet_openapi.contexts",
35
+ "tiferet_openapi.domain",
36
+ "tiferet_openapi.events",
37
+ "tiferet_openapi.interfaces",
38
+ "tiferet_openapi.mappers",
39
+ "tiferet_openapi.repos"
40
+ ]
41
+
42
+ [tool.setuptools.dynamic]
43
+ version = { attr = "tiferet_openapi.__version__" }
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,26 @@
1
+ # *** exports
2
+
3
+ # ** app
4
+ # Export the main domain objects, interfaces, events, mappers, repos, and contexts.
5
+ # Use a try-except block to avoid import errors on build systems.
6
+ try:
7
+ from .domain import ApiRoute, ApiRouter
8
+ from .interfaces import OpenApiService
9
+ from .events import GetRouters, GetRoute, GetStatusCode
10
+ from .mappers import (
11
+ ApiRouteAggregate,
12
+ ApiRouterAggregate,
13
+ ApiRouteYamlObject,
14
+ ApiRouterYamlObject,
15
+ )
16
+ from .repos import OpenApiYamlRepository
17
+ from .contexts import OpenApiContext, OpenApiRequestContext
18
+ except Exception as e:
19
+ import os, sys
20
+ # Only print warning if TIFERET_SILENT_IMPORTS is not set to a truthy value
21
+ if not os.getenv('TIFERET_SILENT_IMPORTS'):
22
+ print(f"Warning: Failed to import Tiferet OpenAPI modules: {e}", file=sys.stderr)
23
+ pass
24
+
25
+ # *** version
26
+ __version__ = "0.1.0"
@@ -0,0 +1,7 @@
1
+ '''Tiferet OpenAPI Contexts'''
2
+
3
+ # *** exports
4
+
5
+ # ** app
6
+ from .openapi import OpenApiContext
7
+ from .request import OpenApiRequestContext
@@ -0,0 +1,140 @@
1
+ '''Tiferet OpenAPI Context'''
2
+
3
+ # *** imports
4
+
5
+ # ** core
6
+ from typing import Any, Callable
7
+
8
+ # ** infra
9
+ from tiferet import TiferetError
10
+ from tiferet.assets.exceptions import TiferetAPIError
11
+ from tiferet.contexts import (
12
+ AppInterfaceContext,
13
+ FeatureContext,
14
+ ErrorContext,
15
+ LoggingContext,
16
+ )
17
+ from tiferet.events import DomainEvent
18
+
19
+ # ** app
20
+ from .request import OpenApiRequestContext
21
+
22
+
23
+ # *** contexts
24
+
25
+ # ** context: open_api_context
26
+ class OpenApiContext(AppInterfaceContext):
27
+ '''
28
+ A shared API context for managing OpenAPI interactions within the Tiferet framework.
29
+ '''
30
+
31
+ # * attribute: get_route_handler
32
+ get_route_handler: Callable
33
+
34
+ # * attribute: get_status_code_handler
35
+ get_status_code_handler: Callable
36
+
37
+ # * init
38
+ def __init__(self,
39
+ interface_id: str,
40
+ features: FeatureContext,
41
+ errors: ErrorContext,
42
+ logging: LoggingContext,
43
+ get_route_evt: DomainEvent,
44
+ get_status_code_evt: DomainEvent,
45
+ ):
46
+ '''
47
+ Initialize the OpenAPI context.
48
+
49
+ :param interface_id: The interface ID.
50
+ :type interface_id: str
51
+ :param features: The feature context.
52
+ :type features: FeatureContext
53
+ :param errors: The error context.
54
+ :type errors: ErrorContext
55
+ :param logging: The logging context.
56
+ :type logging: LoggingContext
57
+ :param get_route_evt: The domain event for retrieving a route.
58
+ :type get_route_evt: DomainEvent
59
+ :param get_status_code_evt: The domain event for retrieving a status code.
60
+ :type get_status_code_evt: DomainEvent
61
+ '''
62
+
63
+ # Call the parent constructor.
64
+ super().__init__(interface_id, features, errors, logging)
65
+
66
+ # Set the domain event handlers.
67
+ self.get_route_handler = get_route_evt.execute
68
+ self.get_status_code_handler = get_status_code_evt.execute
69
+
70
+ # * method: parse_request
71
+ def parse_request(self, headers: dict = {}, data: dict = {}, feature_id: str = None, **kwargs) -> OpenApiRequestContext:
72
+ '''
73
+ Parse the incoming request and return an OpenApiRequestContext instance.
74
+
75
+ :param headers: The request headers.
76
+ :type headers: dict
77
+ :param data: The request data.
78
+ :type data: dict
79
+ :param feature_id: The feature ID.
80
+ :type feature_id: str
81
+ :param kwargs: Additional keyword arguments.
82
+ :type kwargs: dict
83
+ :return: An OpenApiRequestContext instance.
84
+ :rtype: OpenApiRequestContext
85
+ '''
86
+
87
+ # Return an OpenApiRequestContext instance.
88
+ return OpenApiRequestContext(
89
+ headers=headers,
90
+ data=data,
91
+ feature_id=feature_id,
92
+ )
93
+
94
+ # * method: handle_error
95
+ def handle_error(self, error: Exception, **kwargs) -> Any:
96
+ '''
97
+ Handle the error and raise TiferetAPIError with status_code.
98
+
99
+ :param error: The error to handle.
100
+ :type error: Exception
101
+ :param kwargs: Additional keyword arguments.
102
+ :type kwargs: dict
103
+ :return: The error response.
104
+ :rtype: Any
105
+ '''
106
+
107
+ # Get the status code via event if it's a TiferetError.
108
+ if isinstance(error, TiferetError):
109
+ status_code = self.get_status_code_handler(error_code=error.error_code)
110
+ else:
111
+ status_code = 500
112
+
113
+ # Delegate formatting to parent (which raises TiferetAPIError).
114
+ try:
115
+ return super().handle_error(error, **kwargs)
116
+ except TiferetAPIError as api_error:
117
+ api_error.status_code = status_code
118
+ raise
119
+
120
+ # * method: handle_response
121
+ def handle_response(self, request: OpenApiRequestContext, **kwargs) -> Any:
122
+ '''
123
+ Handle the response from the request context.
124
+
125
+ :param request: The request context.
126
+ :type request: OpenApiRequestContext
127
+ :param kwargs: Additional keyword arguments.
128
+ :type kwargs: dict
129
+ :return: The response and status code.
130
+ :rtype: Any
131
+ '''
132
+
133
+ # Handle the response from the request context.
134
+ response = super().handle_response(request, **kwargs)
135
+
136
+ # Retrieve the route by the request feature id.
137
+ route = self.get_route_handler(endpoint=request.feature_id)
138
+
139
+ # Return the result with the specified status code.
140
+ return response, route.status_code if route else 200
@@ -0,0 +1,72 @@
1
+ '''Tiferet OpenAPI Request Context'''
2
+
3
+ # *** imports
4
+
5
+ # ** core
6
+ from typing import Any
7
+
8
+ # ** infra
9
+ from pydantic import BaseModel
10
+ from tiferet.contexts.request import RequestContext
11
+
12
+
13
+ # *** contexts
14
+
15
+ # ** context: open_api_request_context
16
+ class OpenApiRequestContext(RequestContext):
17
+ '''
18
+ A context for handling OpenAPI request data and responses with Pydantic model serialization.
19
+ '''
20
+
21
+ # * method: handle_response
22
+ def handle_response(self) -> Any:
23
+ '''
24
+ Handle the response for the OpenAPI request context.
25
+
26
+ :return: The response.
27
+ :rtype: Any
28
+ '''
29
+
30
+ # Set the result using the set_result method to ensure proper formatting.
31
+ self.set_result(self.result)
32
+
33
+ # Handle the response using the parent method.
34
+ return super().handle_response()
35
+
36
+ # * method: set_result
37
+ def set_result(self, result: Any, data_key: str = None):
38
+ '''
39
+ Set the result of the request context.
40
+
41
+ :param result: The result to set.
42
+ :type result: Any
43
+ :param data_key: The key in the request data to set the result to.
44
+ If provided, the raw result is stored for downstream commands.
45
+ If None, the result is serialized for the final response.
46
+ :type data_key: str
47
+ '''
48
+
49
+ # If a data key is provided, delegate to the parent to store the raw result.
50
+ if data_key:
51
+ super().set_result(result, data_key=data_key)
52
+ return
53
+
54
+ # If the response is None, return an empty response.
55
+ if result is None:
56
+ self.result = ''
57
+
58
+ # Convert the response to a dictionary if it's a BaseModel.
59
+ elif isinstance(result, BaseModel):
60
+ self.result = result.model_dump()
61
+
62
+ # If the response is a list containing BaseModel instances, convert each to a dictionary.
63
+ elif isinstance(result, list) and all(isinstance(item, BaseModel) for item in result):
64
+ self.result = [item.model_dump() for item in result]
65
+
66
+ # If the response is a dict containing BaseModel instances, convert each to a dictionary.
67
+ elif isinstance(result, dict) and all(isinstance(value, BaseModel) for value in result.values()):
68
+ self.result = {key: value.model_dump() for key, value in result.items()}
69
+
70
+ # Otherwise, set the result directly.
71
+ else:
72
+ self.result = result