Coverage for seedboxsync/front/apis/resources.py: 97%
30 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-26 17:46 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-26 17:46 +0000
1#
2# Copyright (C) 2015-2026 Guillaume Kulakowski <guillaume@kulakowski.fr>
3#
4# For the full copyright and license information, please view the LICENSE
5# file that was distributed with this source code.
6#
7"""SeedboxSync front RestXResource module."""
9from datetime import datetime
10from typing import Any
11import uuid
12from flask_restx import Model, Namespace, OrderedModel, Resource as RestXResource, fields
15class Resource(RestXResource): # type: ignore[misc]
16 """
17 Base Resource class extending Flask-RestX Resource.
19 Provides utility methods for:
20 - enforcing limits on query parameters
21 - building consistent API response envelopes
22 - generating envelope models for Swagger documentation
23 """
25 def set_limit(self, limit: int) -> int:
26 """
27 Clamp the limit value within the allowed range [5, 1000].
29 Args:
30 limit (int): The requested limit value.
32 Returns:
33 int: The clamped limit within [5, 1000].
34 """
35 if limit < 5:
36 return 5
37 if limit > 1000:
38 return 1000
39 return limit
41 def build_envelope(
42 self,
43 data: Any,
44 *,
45 type: str = "about:blank", # noqa: A002
46 status_code: int = 200,
47 message: str | None = None,
48 data_total: int | None = None,
49 ) -> dict[str, Any]:
50 """
51 Build a standard API response envelope.
53 Args:
54 data (Any): The response payload.
55 type (str): The resource type or endpoint identifier (default: 'about:blank').
56 status_code (int): HTTP status code (default: 200).
57 message (str): Send message in place of data.
58 data_total (int | None): Optional total number of items if the result is paginated.
60 Returns:
61 dict[str, Any]: Structured API response containing metadata and payload.
62 """
63 return {
64 "type": type,
65 "success": True,
66 "status": status_code,
67 "timestamp": datetime.now().astimezone().isoformat(),
68 "traceId": str(uuid.uuid4()),
69 **({"data": data} if data is not None else {}),
70 **({"data_total": data_total} if data_total is not None else {}),
71 **({"message": message} if message is not None else {}),
72 "data": data,
73 }
75 @staticmethod
76 def build_envelope_model(
77 api: Namespace,
78 name: str,
79 *,
80 nested_model: Model | OrderedModel | None = None,
81 as_list: bool = True,
82 as_message: bool = False,
83 ) -> Model | OrderedModel:
84 """
85 Build a Flask-RestX model for a standard API response envelope.
87 This is used for Swagger documentation. You can generate an envelope
88 for a single resource or for a list of resources.
90 Args:
91 api (Namespace): The Flask-RestX namespace.
92 name (str): The name of the nested resource model.
93 nested_model (Model | OrderedModel): The Flask-RestX model representing the resource.
94 as_list (bool): If True, the 'data' field will be a list of nested resources.
95 as_message (bool): The response is a message only.
97 Returns:
98 Model | OrderedModel: A new Flask-RestX model representing the envelope.
99 """
100 if as_message is False:
101 if as_list:
102 data_field = fields.List(
103 fields.Nested(nested_model),
104 required=True,
105 description=f"List of {name} objects",
106 )
107 else:
108 data_field = fields.Nested(nested_model, required=True, description=f"The {name} object")
109 data_total = fields.Integer(required=False, description=f"Total of {name} object")
110 message_field = None
111 else:
112 data_field = None
113 data_total = None
114 message_field = fields.String(required=False, description="Response message", example="All is OK")
116 return api.model(
117 f"Envelope[{name}]",
118 {
119 "type": fields.String(
120 required=True,
121 description="Resource type or endpoint identifier",
122 example=name,
123 ),
124 "success": fields.Boolean(
125 required=True,
126 description="Indicates if the request succeeded",
127 example=True,
128 ),
129 "status": fields.Integer(required=True, description="HTTP status code", example=200),
130 "timestamp": fields.DateTime(required=True, description="Timestamp of the response"),
131 "traceId": fields.String(
132 required=True,
133 description="Unique trace identifier",
134 example="0a8ab95e-a463-424e-bc6d-505503bf200d",
135 ),
136 **({"data": data_field} if data_field is not None else {}),
137 **({"data_total": data_total} if data_total is not None else {}),
138 **({"message": message_field} if message_field is not None else {}),
139 },
140 )
143class DateTimeOrZero(fields.DateTime): # type: ignore[misc]
144 """Format date-time values while preserving the legacy zero value."""
146 def format(self, value: int | datetime) -> Any:
147 """
148 Format a date-time value or return zero unchanged.
150 Args:
151 value (int | datetime): Date-time value or the legacy zero sentinel.
153 Returns:
154 Any: Formatted date-time value or zero.
155 """
156 if value == 0:
157 return 0
158 return super().format(value)