toms-fast 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. toms_fast-0.2.1.dist-info/METADATA +467 -0
  2. toms_fast-0.2.1.dist-info/RECORD +60 -0
  3. toms_fast-0.2.1.dist-info/WHEEL +4 -0
  4. toms_fast-0.2.1.dist-info/entry_points.txt +2 -0
  5. tomskit/__init__.py +0 -0
  6. tomskit/celery/README.md +693 -0
  7. tomskit/celery/__init__.py +4 -0
  8. tomskit/celery/celery.py +306 -0
  9. tomskit/celery/config.py +377 -0
  10. tomskit/cli/__init__.py +207 -0
  11. tomskit/cli/__main__.py +8 -0
  12. tomskit/cli/scaffold.py +123 -0
  13. tomskit/cli/templates/__init__.py +42 -0
  14. tomskit/cli/templates/base.py +348 -0
  15. tomskit/cli/templates/celery.py +101 -0
  16. tomskit/cli/templates/extensions.py +213 -0
  17. tomskit/cli/templates/fastapi.py +400 -0
  18. tomskit/cli/templates/migrations.py +281 -0
  19. tomskit/cli/templates_config.py +122 -0
  20. tomskit/logger/README.md +466 -0
  21. tomskit/logger/__init__.py +4 -0
  22. tomskit/logger/config.py +106 -0
  23. tomskit/logger/logger.py +290 -0
  24. tomskit/py.typed +0 -0
  25. tomskit/redis/README.md +462 -0
  26. tomskit/redis/__init__.py +6 -0
  27. tomskit/redis/config.py +85 -0
  28. tomskit/redis/redis_pool.py +87 -0
  29. tomskit/redis/redis_sync.py +66 -0
  30. tomskit/server/__init__.py +47 -0
  31. tomskit/server/config.py +117 -0
  32. tomskit/server/exceptions.py +412 -0
  33. tomskit/server/middleware.py +371 -0
  34. tomskit/server/parser.py +312 -0
  35. tomskit/server/resource.py +464 -0
  36. tomskit/server/server.py +276 -0
  37. tomskit/server/type.py +263 -0
  38. tomskit/sqlalchemy/README.md +590 -0
  39. tomskit/sqlalchemy/__init__.py +20 -0
  40. tomskit/sqlalchemy/config.py +125 -0
  41. tomskit/sqlalchemy/database.py +125 -0
  42. tomskit/sqlalchemy/pagination.py +359 -0
  43. tomskit/sqlalchemy/property.py +19 -0
  44. tomskit/sqlalchemy/sqlalchemy.py +131 -0
  45. tomskit/sqlalchemy/types.py +32 -0
  46. tomskit/task/README.md +67 -0
  47. tomskit/task/__init__.py +4 -0
  48. tomskit/task/task_manager.py +124 -0
  49. tomskit/tools/README.md +63 -0
  50. tomskit/tools/__init__.py +18 -0
  51. tomskit/tools/config.py +70 -0
  52. tomskit/tools/warnings.py +37 -0
  53. tomskit/tools/woker.py +81 -0
  54. tomskit/utils/README.md +666 -0
  55. tomskit/utils/README_SERIALIZER.md +644 -0
  56. tomskit/utils/__init__.py +35 -0
  57. tomskit/utils/fields.py +434 -0
  58. tomskit/utils/marshal_utils.py +137 -0
  59. tomskit/utils/response_utils.py +13 -0
  60. tomskit/utils/serializers.py +447 -0
@@ -0,0 +1,47 @@
1
+ from tomskit.server.exceptions import (
2
+ APIException,
3
+ ServiceException,
4
+ raise_api_error,
5
+ format_validation_errors,
6
+ )
7
+ from tomskit.server.server import FastApp, FastModule, current_app
8
+ from tomskit.server.resource import (
9
+ Resource,
10
+ ResourceRouter,
11
+ ResourceRegistry,
12
+ api_doc,
13
+ register_resource,
14
+ )
15
+ from tomskit.server.parser import RequestParser
16
+ from tomskit.server.type import Boolean, IntRange, StrLen, DatetimeString, PhoneNumber, EmailStr, UUIDType
17
+ from tomskit.server.middleware import (
18
+ RequestIDMiddleware,
19
+ ResourceCleanupMiddleware,
20
+ CleanupStrategy,
21
+ )
22
+
23
+ __all__ = [
24
+ 'raise_api_error',
25
+ 'APIException',
26
+ 'ServiceException',
27
+ 'format_validation_errors',
28
+ 'current_app',
29
+ 'Resource',
30
+ 'ResourceRouter',
31
+ 'ResourceRegistry',
32
+ 'api_doc',
33
+ 'register_resource',
34
+ 'FastApp',
35
+ 'FastModule',
36
+ 'RequestParser',
37
+ 'Boolean',
38
+ 'IntRange',
39
+ 'StrLen',
40
+ 'DatetimeString',
41
+ 'PhoneNumber',
42
+ 'EmailStr',
43
+ 'UUIDType',
44
+ 'RequestIDMiddleware',
45
+ 'ResourceCleanupMiddleware',
46
+ 'CleanupStrategy',
47
+ ]
@@ -0,0 +1,117 @@
1
+
2
+ import typing as t
3
+
4
+
5
+ class Config(dict):
6
+ """Works exactly like a dict but provides ways to fill it from files
7
+ or special dictionaries. There are two common patterns to populate the
8
+ config.
9
+
10
+ Either you can fill the config from a config file::
11
+
12
+ app.config.from_pyfile('yourconfig.cfg')
13
+
14
+ Or alternatively you can define the configuration options in the
15
+ module that calls :meth:`from_object` or provide an import path to
16
+ a module that should be loaded. It is also possible to tell it to
17
+ use the same module and with that provide the configuration values
18
+ just before the call::
19
+
20
+ DEBUG = True
21
+ SECRET_KEY = 'development key'
22
+ app.config.from_object(__name__)
23
+
24
+ In both cases (loading from any Python file or loading from modules),
25
+ only uppercase keys are added to the config. This makes it possible to use
26
+ lowercase values in the config file for temporary values that are not added
27
+ to the config or to define the config keys in the same file that implements
28
+ the application.
29
+
30
+ Probably the most interesting way to load configurations is from an
31
+ environment variable pointing to a file::
32
+
33
+ app.config.from_envvar('YOURAPPLICATION_SETTINGS')
34
+
35
+ In this case before launching the application you have to set this
36
+ environment variable to the file you want to use. On Linux and OS X
37
+ use the export statement::
38
+
39
+ export YOURAPPLICATION_SETTINGS='/path/to/config/file'
40
+
41
+ On windows use `set` instead.
42
+
43
+ :param root_path: path to which files are read relative from. When the
44
+ config object is created by the application, this is
45
+ the application's :attr:`~flask.Flask.root_path`.
46
+ :param defaults: an optional dictionary of default values
47
+ """
48
+ def __init__(self,
49
+ defaults: dict[str, t.Any] | None = None,
50
+ ) -> None:
51
+ super().__init__(defaults or {})
52
+
53
+
54
+ def from_mapping(
55
+ self, mapping: t.Mapping[str, t.Any] | None = None, **kwargs: t.Any
56
+ ) -> bool:
57
+ """Updates the config like :meth:`update` ignoring items with
58
+ non-upper keys.
59
+
60
+ :return: Always returns ``True``.
61
+
62
+ .. versionadded:: 0.11
63
+ """
64
+ mappings: dict[str, t.Any] = {}
65
+ if mapping is not None:
66
+ mappings.update(mapping)
67
+ mappings.update(kwargs)
68
+ for key, value in mappings.items():
69
+ if key.isupper():
70
+ self[key] = value
71
+ return True
72
+
73
+ def get_namespace(
74
+ self, namespace: str, lowercase: bool = True, trim_namespace: bool = True
75
+ ) -> dict[str, t.Any]:
76
+ """Returns a dictionary containing a subset of configuration options
77
+ that match the specified namespace/prefix. Example usage::
78
+
79
+ app.config['IMAGE_STORE_TYPE'] = 'fs'
80
+ app.config['IMAGE_STORE_PATH'] = '/var/app/images'
81
+ app.config['IMAGE_STORE_BASE_URL'] = 'http://img.website.com'
82
+ image_store_config = app.config.get_namespace('IMAGE_STORE_')
83
+
84
+ The resulting dictionary `image_store_config` would look like::
85
+
86
+ {
87
+ 'type': 'fs',
88
+ 'path': '/var/app/images',
89
+ 'base_url': 'http://img.website.com'
90
+ }
91
+
92
+ This is often useful when configuration options map directly to
93
+ keyword arguments in functions or class constructors.
94
+
95
+ :param namespace: a configuration namespace
96
+ :param lowercase: a flag indicating if the keys of the resulting
97
+ dictionary should be lowercase
98
+ :param trim_namespace: a flag indicating if the keys of the resulting
99
+ dictionary should not include the namespace
100
+
101
+ .. versionadded:: 0.11
102
+ """
103
+ rv = {}
104
+ for k, v in self.items():
105
+ if not k.startswith(namespace):
106
+ continue
107
+ if trim_namespace:
108
+ key = k[len(namespace) :]
109
+ else:
110
+ key = k
111
+ if lowercase:
112
+ key = key.lower()
113
+ rv[key] = v
114
+ return rv
115
+
116
+ def __repr__(self) -> str:
117
+ return f"<{type(self).__name__} {dict.__repr__(self)}>"
@@ -0,0 +1,412 @@
1
+ """
2
+ Exception handling module.
3
+
4
+ Provides unified exception base classes for layered exception handling:
5
+ - ServiceException: Business layer exception base class (framework-agnostic)
6
+ - APIException: Framework layer exception base class (inherits from HTTPException)
7
+ - raise_api_error: Quick function to raise APIException
8
+ - format_validation_errors: Utility function to format Pydantic ValidationError
9
+ """
10
+
11
+ from typing import Optional, Any
12
+ from fastapi import HTTPException
13
+ from pydantic import ValidationError
14
+
15
+
16
+ # ============================================================================
17
+ # Exception Base Classes
18
+ # ============================================================================
19
+
20
+ class ServiceException(Exception):
21
+ """
22
+ Business exception base class.
23
+
24
+ Used in Service layer (business logic layer) to raise business exceptions.
25
+
26
+ Features:
27
+ - Framework-agnostic: No dependency on FastAPI/HTTP, can be used in any business layer
28
+ - Business semantics: Exception names reflect business meaning
29
+ - Business context: Contains detailed business-related information
30
+ - Serializable: Exception information can be converted to transferable format
31
+
32
+ Attributes:
33
+ code (str): Business error code, e.g., "USER_NOT_FOUND"
34
+ message (str): User-friendly error message
35
+ detail (dict): Detailed error information (business context)
36
+ original_exception (Exception, optional): Original exception (if any)
37
+
38
+ Example:
39
+ >>> raise ServiceException(
40
+ ... code="USER_NOT_FOUND",
41
+ ... message="User not found",
42
+ ... detail={"user_id": "123"}
43
+ ... )
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ code: str,
49
+ message: str,
50
+ detail: Optional[dict] = None,
51
+ original_exception: Optional[Exception] = None,
52
+ ):
53
+ """
54
+ Initialize business exception.
55
+
56
+ Args:
57
+ code: Business error code, e.g., "USER_NOT_FOUND"
58
+ message: User-friendly error message
59
+ detail: Detailed error information (business context), defaults to empty dict
60
+ original_exception: Original exception (if any), used for log tracking
61
+ """
62
+ self.code = code
63
+ self.message = message
64
+ self.detail = detail or {}
65
+ self.original_exception = original_exception
66
+ super().__init__(self.message)
67
+
68
+ def __repr__(self) -> str:
69
+ return f"{self.__class__.__name__}(code={self.code!r}, message={self.message!r})"
70
+
71
+
72
+ class APIException(HTTPException):
73
+ """
74
+ Framework unified exception base class.
75
+
76
+ Used in framework layer for unified exception handling, inherits from FastAPI's HTTPException.
77
+
78
+ Features:
79
+ - Inherits from HTTPException (FastAPI native)
80
+ - Contains unified response format
81
+ - Can be handled by global exception handlers
82
+
83
+ Attributes:
84
+ code (str): Error code, e.g., "user_not_found"
85
+ message (str): User-friendly error message
86
+ status_code (int): HTTP status code
87
+ detail (dict): Detailed error information
88
+ original_exception (Exception, optional): Original Service exception (optional, for logging)
89
+
90
+ Response format:
91
+ {
92
+ "code": "user_not_found",
93
+ "message": "User not found",
94
+ "status": 404
95
+ }
96
+
97
+ If detail is provided, it will be merged to the top level:
98
+ {
99
+ "code": "user_not_found",
100
+ "message": "User not found",
101
+ "status": 404,
102
+ "user_id": "123", # content from detail
103
+ ...
104
+ }
105
+
106
+ Example:
107
+ >>> raise APIException(
108
+ ... code="user_not_found",
109
+ ... message="User not found",
110
+ ... status_code=404,
111
+ ... detail={"user_id": "123"}
112
+ ... )
113
+ """
114
+
115
+ def __init__(
116
+ self,
117
+ code: str,
118
+ message: str,
119
+ status_code: int = 400,
120
+ detail: Optional[dict] = None,
121
+ original_exception: Optional[Exception] = None,
122
+ ):
123
+ """
124
+ Initialize framework exception.
125
+
126
+ Args:
127
+ code: Error code, e.g., "user_not_found"
128
+ message: User-friendly error message
129
+ status_code: HTTP status code, defaults to 400
130
+ detail: Detailed error information, defaults to None (if provided, merged to response top level)
131
+ original_exception: Original Service exception (optional, for log tracking)
132
+ """
133
+ self.code = code
134
+ self.message = message
135
+ self.original_exception = original_exception
136
+
137
+ # HTTPException's detail parameter is used for response content
138
+ # Format to unified structure: code, message, status at top level
139
+ # If detail is provided, merge to top level (instead of nesting in detail field)
140
+ if detail:
141
+ response_detail = {
142
+ "code": code,
143
+ "message": message,
144
+ "status": status_code,
145
+ **detail, # Merge detail directly using dict unpacking (more efficient)
146
+ }
147
+ else:
148
+ response_detail = {
149
+ "code": code,
150
+ "message": message,
151
+ "status": status_code,
152
+ }
153
+
154
+ super().__init__(status_code=status_code, detail=response_detail)
155
+
156
+ def __repr__(self) -> str:
157
+ return (
158
+ f"{self.__class__.__name__}"
159
+ f"(code={self.code!r}, message={self.message!r}, status_code={self.status_code})"
160
+ )
161
+
162
+
163
+ # ============================================================================
164
+ # Quick Exception Raising
165
+ # ============================================================================
166
+
167
+ # Default error codes and messages for common HTTP status codes
168
+ _DEFAULT_ERROR_MESSAGES = {
169
+ 400: ("bad_request", "Bad Request"),
170
+ 401: ("unauthorized", "Unauthorized"),
171
+ 403: ("forbidden", "Forbidden"),
172
+ 404: ("not_found", "Not Found"),
173
+ 409: ("conflict", "Conflict"),
174
+ 422: ("validation_error", "Validation Error"),
175
+ 500: ("internal_server_error", "Internal Server Error"),
176
+ 502: ("bad_gateway", "Bad Gateway"),
177
+ 503: ("service_unavailable", "Service Unavailable"),
178
+ 504: ("gateway_timeout", "Gateway Timeout"),
179
+ }
180
+
181
+
182
+ def raise_api_error(
183
+ status_code: int,
184
+ code: Optional[str] = None,
185
+ message: Optional[str] = None,
186
+ detail: Optional[dict] = None,
187
+ ) -> None:
188
+ """
189
+ Quickly raise APIException.
190
+
191
+ Uses unified response format with code, message, and status fields.
192
+
193
+ Args:
194
+ status_code: HTTP status code
195
+ code: Error code (optional), auto-generated from status_code if not provided
196
+ message: Error message (optional), auto-generated from status_code if not provided
197
+ detail: Detailed error information dict (optional), merged to response top level
198
+
199
+ Example:
200
+ >>> # Basic usage (using default code and message)
201
+ >>> raise_api_error(404)
202
+
203
+ >>> # Specify code and message
204
+ >>> raise_api_error(404, code="page_not_found", message="Page not found")
205
+
206
+ >>> # Add detail information (merged to response top level)
207
+ >>> raise_api_error(
208
+ ... status_code=404,
209
+ ... code="user_not_found",
210
+ ... message="User not found",
211
+ ... detail={"user_id": "123", "field": "id"}
212
+ ... )
213
+
214
+ Response format:
215
+ {
216
+ "code": "not_found",
217
+ "message": "Not Found",
218
+ "status": 404
219
+ }
220
+
221
+ If detail is provided:
222
+ {
223
+ "code": "user_not_found",
224
+ "message": "User not found",
225
+ "status": 404,
226
+ "user_id": "123",
227
+ "field": "id"
228
+ }
229
+ """
230
+ # Get default code and message based on status_code
231
+ default_code, default_message = _DEFAULT_ERROR_MESSAGES.get(
232
+ status_code, (f"http_{status_code}", f"HTTP {status_code} Error")
233
+ )
234
+
235
+ # Determine final code and message
236
+ final_code = code if code is not None else default_code
237
+ final_message = message if message is not None else default_message
238
+
239
+ # Raise APIException
240
+ raise APIException(
241
+ code=final_code,
242
+ message=final_message,
243
+ status_code=status_code,
244
+ detail=detail,
245
+ )
246
+
247
+
248
+ # ============================================================================
249
+ # ValidationError Formatting Utilities
250
+ # ============================================================================
251
+
252
+ # Sensitive field list (input values for these fields should not be returned in response)
253
+ # Pre-compute lowercase versions for efficient matching
254
+ _SENSITIVE_FIELDS = {"password", "token", "secret", "api_key", "access_token", "refresh_token"}
255
+ _SENSITIVE_FIELDS_LOWER = {field.lower() for field in _SENSITIVE_FIELDS}
256
+
257
+
258
+ def _get_field_path(loc: tuple[Any, ...]) -> str:
259
+ """
260
+ Convert field location tuple to path string.
261
+
262
+ Args:
263
+ loc: Field location tuple, e.g., ("user", "profile", "name") or ("items", 0, "name")
264
+
265
+ Returns:
266
+ Field path string, e.g., "user.profile.name" or "items.0.name"
267
+
268
+ Example:
269
+ >>> _get_field_path(("email",))
270
+ "email"
271
+ >>> _get_field_path(("user", "profile", "name"))
272
+ "user.profile.name"
273
+ >>> _get_field_path(("items", 0, "name"))
274
+ "items.0.name"
275
+ """
276
+ return ".".join(str(x) for x in loc)
277
+
278
+
279
+ def _should_include_input(field_path: str) -> bool:
280
+ """
281
+ Determine whether input value should be included in response.
282
+
283
+ Sensitive field input values should never be returned to avoid leaking sensitive information.
284
+ However, validation errors for sensitive fields (field, message, type) are still included
285
+ to notify users of validation issues.
286
+
287
+ Args:
288
+ field_path: Field path, e.g., "user.password" or "email"
289
+
290
+ Returns:
291
+ True if input value should be included (non-sensitive fields), False otherwise (sensitive fields)
292
+
293
+ Note:
294
+ - This function only determines whether to include the input VALUE
295
+ - Field, message, and type are always included regardless of sensitivity
296
+ - For sensitive fields: field/message/type are included, but input value is never included
297
+ """
298
+ field_lower = field_path.lower()
299
+ # Check if any sensitive field keyword appears in the field path
300
+ return not any(sensitive in field_lower for sensitive in _SENSITIVE_FIELDS_LOWER)
301
+
302
+
303
+ def _format_single_error(error: Any) -> dict[str, Any]:
304
+ """
305
+ Format a single field error.
306
+
307
+ Args:
308
+ error: Pydantic error dict containing loc, msg, type, input fields
309
+
310
+ Returns:
311
+ Formatted error dict containing field, message, type, input (if applicable)
312
+
313
+ Note:
314
+ - All fields (including sensitive fields) will include field, message, type
315
+ to ensure users are notified of validation errors
316
+ - For sensitive fields: input value is never included (even if present)
317
+ - For non-sensitive fields: input value is included only if present (not None)
318
+ """
319
+ # Pydantic's ErrorDetails can be accessed as dict or object
320
+ # Try dict access first (faster), fallback to getattr
321
+ if isinstance(error, dict):
322
+ loc = error.get("loc", ())
323
+ msg = error.get("msg", "")
324
+ error_type = error.get("type", "")
325
+ input_value = error.get("input")
326
+ else:
327
+ loc = getattr(error, "loc", ())
328
+ msg = getattr(error, "msg", "")
329
+ error_type = getattr(error, "type", "")
330
+ input_value = getattr(error, "input", None)
331
+
332
+ field_path = _get_field_path(loc)
333
+ error_dict: dict[str, Any] = {
334
+ "field": field_path,
335
+ "message": msg,
336
+ "type": error_type,
337
+ }
338
+
339
+ # Only include input value for non-sensitive fields
340
+ # For sensitive fields, we never include input value to avoid leaking sensitive information
341
+ # However, field/message/type are always included so users are notified of validation errors
342
+ if _should_include_input(field_path) and input_value is not None:
343
+ error_dict["input"] = input_value
344
+
345
+ return error_dict
346
+
347
+
348
+ def format_validation_errors(validation_error: ValidationError) -> dict[str, Any]:
349
+ """
350
+ Format Pydantic ValidationError to unified error format.
351
+
352
+ Convert Pydantic's ValidationError to detailed field error list,
353
+ used in exception handlers to generate unified error responses.
354
+
355
+ Args:
356
+ validation_error: Pydantic ValidationError instance
357
+
358
+ Returns:
359
+ Dict containing errors list:
360
+ {
361
+ "errors": [
362
+ {
363
+ "field": "email",
364
+ "message": "value is not a valid email address",
365
+ "type": "value_error.email",
366
+ "input": "invalid-email"
367
+ },
368
+ {
369
+ "field": "age",
370
+ "message": "field required",
371
+ "type": "missing",
372
+ "input": null
373
+ },
374
+ ...
375
+ ]
376
+ }
377
+
378
+ Example:
379
+ >>> from pydantic import BaseModel, ValidationError
380
+ >>>
381
+ >>> class User(BaseModel):
382
+ ... email: str
383
+ ... age: int
384
+ >>>
385
+ >>> try:
386
+ ... User(email="invalid", age="not-int")
387
+ ... except ValidationError as e:
388
+ ... errors = format_validation_errors(e)
389
+ ... print(errors)
390
+ {
391
+ "errors": [
392
+ {
393
+ "field": "email",
394
+ "message": "value is not a valid email address",
395
+ "type": "value_error.email",
396
+ "input": "invalid"
397
+ },
398
+ {
399
+ "field": "age",
400
+ "message": "value is not a valid integer",
401
+ "type": "type_error.integer",
402
+ "input": "not-int"
403
+ }
404
+ ]
405
+ }
406
+
407
+ Note:
408
+ - Sensitive fields (e.g., password, token) input values are not included in response
409
+ - Supports nested field paths, e.g., "user.profile.name"
410
+ - Supports array indices, e.g., "items.0.name"
411
+ """
412
+ return {"errors": [_format_single_error(error) for error in validation_error.errors()]}