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,464 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Callable
5
+
6
+ from fastapi import APIRouter
7
+
8
+ from tomskit.server.server import FastModule
9
+
10
+
11
+ @dataclass
12
+ class ResourceInfo:
13
+ """Resource registration information"""
14
+ resource_cls: type
15
+ path: str
16
+ tags: list[str]
17
+
18
+
19
+ class ResourceRegistry:
20
+ """
21
+ Global resource registry, organized by module name.
22
+
23
+ Used to store all Resources registered via the @register_resource decorator,
24
+ supporting deferred registration, where resources are registered to their
25
+ corresponding FastModule instances at application startup.
26
+ """
27
+ _modules: dict[str, list[ResourceInfo]] = {}
28
+
29
+ @classmethod
30
+ def register(
31
+ cls,
32
+ resource_cls: type,
33
+ module_name: str,
34
+ path: str,
35
+ tags: list[str] | None = None,
36
+ ):
37
+ """
38
+ Register a Resource to the specified module.
39
+
40
+ Args:
41
+ resource_cls: Resource class
42
+ module_name: Module name, must match FastModule.name
43
+ path: Default path for the resource
44
+ tags: Default tags list for the resource
45
+ """
46
+ if module_name not in cls._modules:
47
+ cls._modules[module_name] = []
48
+ cls._modules[module_name].append(
49
+ ResourceInfo(
50
+ resource_cls=resource_cls,
51
+ path=path,
52
+ tags=tags or []
53
+ )
54
+ )
55
+
56
+ @classmethod
57
+ def get_module_resources(cls, module_name: str) -> list[ResourceInfo]:
58
+ """
59
+ Get all Resources for the specified module.
60
+
61
+ Args:
62
+ module_name: Module name
63
+
64
+ Returns:
65
+ List of ResourceInfo for the module
66
+ """
67
+ return cls._modules.get(module_name, [])
68
+
69
+ @classmethod
70
+ def clear_module(cls, module_name: str):
71
+ """
72
+ Clear registrations for the specified module (mainly for testing).
73
+
74
+ Args:
75
+ module_name: Module name
76
+ """
77
+ if module_name in cls._modules:
78
+ del cls._modules[module_name]
79
+
80
+ @classmethod
81
+ def clear_all(cls):
82
+ """Clear all registrations (mainly for testing)"""
83
+ cls._modules.clear()
84
+
85
+
86
+ def register_resource(
87
+ module: str,
88
+ path: str,
89
+ tags: list[str] | None = None,
90
+ ):
91
+ """
92
+ Decorator: Register a Resource to the specified module.
93
+
94
+ Resources marked with this decorator will be automatically registered
95
+ when FastModule.auto_register_resources() is called.
96
+
97
+ Args:
98
+ module: Module name, must match FastModule.name
99
+ path: Default path for the resource
100
+ tags: Default tags list for the resource
101
+
102
+ Example:
103
+ @register_resource(module="files", path="/files", tags=["File Management"])
104
+ class FileResource(Resource):
105
+ @api_doc(summary="Upload file", response_model=FileResponse)
106
+ async def post(self, request: Request):
107
+ ...
108
+ """
109
+ def decorator(cls):
110
+ ResourceRegistry.register(cls, module, path, tags=tags)
111
+ return cls
112
+ return decorator
113
+
114
+
115
+ def _normalize_responses(responses: dict[int | str, str | dict[str, Any]]) -> dict[int | str, dict[str, Any]]:
116
+ """
117
+ Normalize response format, supporting simplified string format.
118
+
119
+ Converts simplified format:
120
+ {200: "Success", 400: "Bad Request"}
121
+ to FastAPI expected format:
122
+ {200: {"description": "Success"}, 400: {"description": "Bad Request"}}
123
+ """
124
+ normalized: dict[int | str, dict[str, Any]] = {}
125
+ for status_code, value in responses.items():
126
+ if isinstance(value, str):
127
+ normalized[status_code] = {"description": value}
128
+ elif isinstance(value, dict):
129
+ normalized[status_code] = value
130
+ else:
131
+ normalized[status_code] = {"description": str(value)}
132
+ return normalized
133
+
134
+
135
+ def api_doc(
136
+ summary: str | None = None,
137
+ description: str | None = None,
138
+ response_description: str | None = None,
139
+ deprecated: bool = False,
140
+ operation_id: str | None = None,
141
+ response_model: Any = None,
142
+ status_code: int | None = None,
143
+ responses: dict[int | str, str | dict[str, Any]] | None = None,
144
+ path: str | None = None,
145
+ ):
146
+ """
147
+ Decorator for setting API documentation information and path for Resource class methods.
148
+
149
+ Important rules:
150
+ - The same parameter cannot be used repeatedly in multiple @api_doc decorators
151
+ (except responses which can be merged)
152
+ - response_model: Used to specify the BaseModel/Pydantic model for FastAPI API responses
153
+ - path: Method-level path, if provided will override the default path from registration
154
+ - responses can be merged across multiple decorators
155
+ - Other parameters (summary, description, response_model, operation_id, path, etc.)
156
+ will raise an exception if used repeatedly
157
+ - tags should be set in @register_resource, not in @api_doc
158
+
159
+ Example:
160
+ from pydantic import BaseModel
161
+
162
+ class UserResponse(BaseModel):
163
+ id: int
164
+ name: str
165
+
166
+ @register_resource(module="users", path="/users", tags=["User Management"])
167
+ class UserResource(Resource):
168
+ # Method 1: Using simplified string format
169
+ @api_doc(
170
+ summary="Get user list",
171
+ description="Get all users with pagination",
172
+ response_model=list[UserResponse],
173
+ responses={
174
+ 200: "Success",
175
+ 400: "Bad request",
176
+ 404: "Not found"
177
+ }
178
+ )
179
+ async def get(self, request: Request):
180
+ ...
181
+
182
+ # Method 2: Using full dictionary format
183
+ @api_doc(
184
+ summary="Create user",
185
+ response_model=UserResponse,
186
+ responses={
187
+ 201: {"description": "User created successfully"},
188
+ 400: {"description": "Invalid input data"},
189
+ 409: {"description": "User already exists", "content": {...}}
190
+ }
191
+ )
192
+ async def post(self, request: Request):
193
+ ...
194
+
195
+ # Method 3: Setting multiple times, responses will be automatically merged
196
+ @api_doc(responses={200: "Success"})
197
+ @api_doc(responses={404: "Not found"})
198
+ @api_doc(responses={500: "Internal server error"})
199
+ @api_doc(
200
+ path="/users/{user_id}", # Override default path
201
+ summary="Get user details",
202
+ response_model=UserResponse
203
+ )
204
+ async def get(self, request: Request):
205
+ ...
206
+ """
207
+ def decorator(func: Callable) -> Callable:
208
+ # Check for non-mergeable parameters that cannot be repeated
209
+ non_mergeable_params = {
210
+ 'summary': summary,
211
+ 'description': description,
212
+ 'response_description': response_description,
213
+ 'response_model': response_model,
214
+ 'status_code': status_code,
215
+ 'operation_id': operation_id,
216
+ 'path': path,
217
+ }
218
+
219
+ for param_name, param_value in non_mergeable_params.items():
220
+ if param_value is not None:
221
+ attr_name = f'_api_{param_name}'
222
+ if hasattr(func, attr_name):
223
+ raise ValueError(
224
+ f"Parameter '{param_name}' has already been set in a previous @api_doc decorator. "
225
+ f"It cannot be used repeatedly in multiple decorators. Please merge into a single @api_doc."
226
+ )
227
+
228
+ # Set parameters
229
+ if summary is not None:
230
+ setattr(func, '_api_summary', summary)
231
+ if description is not None:
232
+ setattr(func, '_api_description', description)
233
+ if response_description is not None:
234
+ setattr(func, '_api_response_description', response_description)
235
+ if response_model is not None:
236
+ setattr(func, '_api_response_model', response_model)
237
+ if status_code is not None:
238
+ setattr(func, '_api_status_code', status_code)
239
+ if operation_id is not None:
240
+ setattr(func, '_api_operation_id', operation_id)
241
+ if path is not None:
242
+ setattr(func, '_api_path', path)
243
+
244
+ # Merge responses: merge dictionaries instead of overwriting
245
+ if responses is not None:
246
+ existing_responses = getattr(func, '_api_responses', None)
247
+ if existing_responses:
248
+ merged_responses = dict(existing_responses)
249
+ normalized_new = _normalize_responses(responses)
250
+ merged_responses.update(normalized_new)
251
+ setattr(func, '_api_responses', merged_responses)
252
+ else:
253
+ normalized_responses = _normalize_responses(responses)
254
+ setattr(func, '_api_responses', normalized_responses)
255
+
256
+ if deprecated:
257
+ setattr(func, '_api_deprecated', True)
258
+
259
+ # Mark method as decorated by api_doc
260
+ setattr(func, '_api_doc_decorated', True)
261
+
262
+ return func
263
+ return decorator
264
+
265
+
266
+ class Resource:
267
+ """
268
+ RESTful API resource base class.
269
+
270
+ A Resource represents a resource entity, containing CRUD operations for that resource.
271
+ All methods share the same path (specified during registration), but can be overridden
272
+ via @api_doc(path=...).
273
+
274
+ Attributes:
275
+ decorators (List[Callable]): Class-level decorator list, applied to all methods.
276
+ methods (List[str]): List of supported HTTP methods.
277
+ """
278
+ decorators: list[Callable] | None = None
279
+ methods: list[str] = ["get", "post", "put", "delete", "patch"] # Only standard RESTful methods
280
+
281
+ def __init__(self, router: ResourceRouter):
282
+ self.router = router
283
+ self.app: FastModule = self.router.router_app
284
+
285
+ def __init_subclass__(cls):
286
+ """
287
+ Called when a subclass is initialized, ensuring methods are properly decorated.
288
+ """
289
+ # If subclass hasn't set decorators, initialize as empty list
290
+ if not hasattr(cls, 'decorators') or cls.decorators is None:
291
+ cls.decorators = []
292
+ super().__init_subclass__()
293
+ cls._decorate_methods()
294
+
295
+ @classmethod
296
+ def _decorate_methods(cls):
297
+ """
298
+ Apply decorators to methods in the subclass.
299
+ """
300
+ for method_name in cls.methods:
301
+ method = getattr(cls, method_name, None)
302
+ if method:
303
+ decorated = cls.apply_decorators(method)
304
+ setattr(cls, method_name, decorated)
305
+
306
+ @classmethod
307
+ def apply_decorators(cls, func: Callable) -> Callable:
308
+ """
309
+ Apply decorators to the specified method.
310
+
311
+ Args:
312
+ func (Callable): Method to be decorated.
313
+
314
+ Returns:
315
+ Callable: Decorated method.
316
+ """
317
+ if cls.decorators:
318
+ for decorator in reversed(cls.decorators):
319
+ func = decorator(func)
320
+ return func
321
+
322
+
323
+ class ResourceRouter(APIRouter):
324
+ """
325
+ RESTful resource router for registering and managing Resources.
326
+
327
+ Inherits from FastAPI's APIRouter, specifically designed for handling Resource class registration.
328
+ """
329
+
330
+ def __init__(self, app: FastModule, *args, **kwargs):
331
+ """
332
+ Initialize ResourceRouter.
333
+
334
+ Args:
335
+ app (FastModule): FastModule instance, must be provided.
336
+ *args, **kwargs: Other parameters passed to APIRouter.
337
+ """
338
+ self.router_app = app
339
+ if self.router_app is None:
340
+ raise ValueError("The 'app' parameter must be provided.")
341
+
342
+ super().__init__(*args, **kwargs)
343
+ self._default_dependencies = kwargs.get('dependencies', [])
344
+ # Cache route names for efficient duplicate checking
345
+ self._route_names: set[str] = set()
346
+
347
+ def check_name_duplicate(self, name: str) -> bool:
348
+ """Check if route name already exists"""
349
+ return name in self._route_names
350
+
351
+ def add_resource(
352
+ self,
353
+ resource_cls: type[Resource],
354
+ path: str,
355
+ tags: list[str] | None = None,
356
+ ):
357
+ """
358
+ Add Resource to the router.
359
+
360
+ Args:
361
+ resource_cls (Type[Resource]): Resource class, must inherit from Resource.
362
+ path (str): Default path for the resource. All methods use this path by default,
363
+ unless a method specifies a different path via @api_doc(path=...).
364
+ tags (List[str] | None): Default tags list. Applied to all methods,
365
+ unless a method specifies different tags via @api_doc(tags=...).
366
+ """
367
+ resource_instance = resource_cls(router=self)
368
+
369
+ # Use tags passed during registration (Resource class no longer has tags attribute)
370
+ class_tags = tags or []
371
+
372
+ # Standard HTTP methods set for efficient validation
373
+ VALID_HTTP_METHODS = {'get', 'post', 'put', 'delete', 'patch'}
374
+
375
+ # Get all methods decorated with @api_doc
376
+ # Check class methods directly instead of using dir() for better performance
377
+ methods_to_register = []
378
+ for attr_name in resource_cls.methods:
379
+ # Validate method name early
380
+ method_name_lower = attr_name.lower()
381
+ if method_name_lower not in VALID_HTTP_METHODS:
382
+ raise ValueError(
383
+ f"Method name '{attr_name}' is not a standard HTTP method. "
384
+ f"Only 'get', 'post', 'put', 'delete', 'patch' are supported. "
385
+ f"Non-standard method names like 'get_detail' are not allowed."
386
+ )
387
+
388
+ class_method = getattr(resource_cls, attr_name, None)
389
+ if class_method and callable(class_method) and hasattr(class_method, '_api_doc_decorated'):
390
+ # Get the method from instance to ensure it has the correct bound state
391
+ instance_method = getattr(resource_instance, attr_name, None)
392
+ if instance_method:
393
+ # Store original method name, lowercase method name, class method, and instance method
394
+ methods_to_register.append((attr_name, method_name_lower, class_method, instance_method))
395
+
396
+ if not methods_to_register:
397
+ raise ValueError(
398
+ f"Resource {resource_cls.__name__} has no methods decorated with @api_doc. "
399
+ f"Please add @api_doc decorator to at least one method."
400
+ )
401
+
402
+ # Register each method
403
+ for original_method_name, method_name_lower, class_method, instance_method in methods_to_register:
404
+ # Apply class-level decorators to instance method
405
+ decorated_method = resource_cls.apply_decorators(instance_method)
406
+
407
+ # Batch get all method-level documentation attributes from class method
408
+ # (attributes are set on the class method by @api_doc decorator)
409
+ method_path = getattr(class_method, '_api_path', None) or path
410
+ method_summary = getattr(class_method, '_api_summary', None)
411
+ method_description = getattr(class_method, '_api_description', None)
412
+ method_response_model = getattr(class_method, '_api_response_model', None)
413
+ method_status_code = getattr(class_method, '_api_status_code', None)
414
+ method_responses = getattr(class_method, '_api_responses', None)
415
+ method_response_description = getattr(class_method, '_api_response_description', None)
416
+ method_deprecated = getattr(class_method, '_api_deprecated', False)
417
+ method_operation_id = getattr(class_method, '_api_operation_id', None)
418
+
419
+ # Use tags from register_resource only, not from api_doc
420
+ method_tags = class_tags
421
+
422
+ # Determine HTTP method (already validated and lowercased)
423
+ http_method = method_name_lower.upper()
424
+
425
+ # Generate route name using class name and original method name
426
+ route_name = f"{resource_cls.__name__}_{original_method_name}"
427
+ if self.check_name_duplicate(route_name):
428
+ raise ValueError(f"Resource route name {route_name} already exists")
429
+ # Add to cache
430
+ self._route_names.add(route_name)
431
+
432
+ # Build route parameters
433
+ # Note: FastAPI's APIRouter.add_api_route will automatically prepend
434
+ # the router's prefix to the path. So we pass the path without prefix here.
435
+ route_kwargs: dict[str, Any] = {
436
+ "path": method_path,
437
+ "endpoint": decorated_method,
438
+ "methods": [http_method],
439
+ "name": route_name,
440
+ "dependencies": self._default_dependencies,
441
+ }
442
+
443
+ # Add documentation-related parameters (only if they have values)
444
+ if method_tags:
445
+ route_kwargs["tags"] = method_tags
446
+ if method_summary:
447
+ route_kwargs["summary"] = method_summary
448
+ if method_description:
449
+ route_kwargs["description"] = method_description
450
+ if method_response_model:
451
+ route_kwargs["response_model"] = method_response_model
452
+ if method_status_code:
453
+ route_kwargs["status_code"] = method_status_code
454
+ if method_responses:
455
+ # method_responses is already normalized in api_doc decorator
456
+ route_kwargs["responses"] = method_responses
457
+ if method_response_description:
458
+ route_kwargs["response_description"] = method_response_description
459
+ if method_deprecated:
460
+ route_kwargs["deprecated"] = True
461
+ if method_operation_id:
462
+ route_kwargs["operation_id"] = method_operation_id
463
+
464
+ self.add_api_route(**route_kwargs)