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,276 @@
1
+
2
+ import os
3
+ import warnings
4
+ from contextvars import ContextVar
5
+ from typing import Any, Optional
6
+
7
+ from fastapi import FastAPI
8
+ from starlette.types import ASGIApp
9
+
10
+ from tomskit.server.config import Config
11
+
12
+
13
+ class CurrentApp:
14
+ __app_ctx: ContextVar[Optional["FastApp"]] = ContextVar('tomskit.current_app.context', default=None)
15
+
16
+ def set_app(self, app_instance: "FastApp"):
17
+ self.__app_ctx.set(app_instance)
18
+
19
+ def _get_app(self) -> "FastApp":
20
+ """Get the current application instance, raising error if not set."""
21
+ if (app := self.__app_ctx.get()) is None:
22
+ raise RuntimeError("No application instance is currently set")
23
+ return app
24
+
25
+ @property
26
+ def app(self) -> "FastApp":
27
+ """Get the current application instance."""
28
+ return self._get_app()
29
+
30
+ def __call__(self) -> "FastApp":
31
+ """Get the current application instance when called."""
32
+ return self._get_app()
33
+
34
+ @property
35
+ def config(self):
36
+ """Get the configuration from the current application instance."""
37
+ return self._get_app().config
38
+
39
+ @property
40
+ def root_path(self):
41
+ """Get the root path from the current application instance."""
42
+ return self._get_app().root_path
43
+
44
+ def reset_app(self):
45
+ """Reset the current application instance."""
46
+ self.__app_ctx.set(None)
47
+ # Also reset FastApp singleton instance for testing
48
+ FastApp.reset_instance()
49
+
50
+ current_app = CurrentApp()
51
+
52
+ class FastApp(FastAPI):
53
+ """
54
+ A custom FastAPI instance that disables default documentation paths.
55
+
56
+ This class creates a custom FastAPI instance that disables the default
57
+ documentation paths (/docs, /redoc, /openapi) to enhance security and
58
+ reduce unnecessary exposure in production environments.
59
+
60
+ This class also adds configuration support and can initialize the
61
+ current_app context variable for global access to the application instance.
62
+ """
63
+
64
+ _instance: Optional["FastApp"] = None
65
+
66
+ def __new__(cls, *args, **kwargs):
67
+ if cls._instance is None:
68
+ cls._instance = super().__new__(cls)
69
+ cls._instance._is_initialized = False
70
+ return cls._instance
71
+
72
+ @classmethod
73
+ def reset_instance(cls):
74
+ """Reset the singleton instance. Useful for testing."""
75
+ cls._instance = None
76
+
77
+ def __init__(self, *args, **kwargs):
78
+ if not self._is_initialized:
79
+ # Only disable docs if not explicitly provided by user
80
+ if 'docs_url' not in kwargs:
81
+ kwargs['docs_url'] = None
82
+ if 'redoc_url' not in kwargs:
83
+ kwargs['redoc_url'] = None
84
+ if 'openapi_url' not in kwargs:
85
+ kwargs['openapi_url'] = None
86
+ super().__init__(*args, **kwargs)
87
+ self.config = Config()
88
+ current_app.set_app(self)
89
+ self._is_initialized = True
90
+ else:
91
+ # Even if already initialized, ensure current_app is set
92
+ # This handles cases where reset_app() was called but instance still exists
93
+ current_app.set_app(self)
94
+
95
+ def mount(self, path: str, app: ASGIApp, name: Optional[str] = None):
96
+ """Mount an ASGI application at the specified path."""
97
+ if hasattr(app, "config"):
98
+ app.config = self.config
99
+ super().mount(path, app, name)
100
+
101
+ def set_environ(self):
102
+ """Set environment variables from configuration."""
103
+ for key, value in self.config.items():
104
+ if isinstance(value, str):
105
+ os.environ[key] = value
106
+ elif isinstance(value, (int, float, bool)):
107
+ os.environ[key] = str(value)
108
+ elif value is None:
109
+ os.environ[key] = ""
110
+
111
+ def set_app_root_path(self, app_file: str):
112
+ """Set the application root path based on the app file location."""
113
+ self.app_root_path = os.path.dirname(os.path.abspath(app_file))
114
+
115
+ def add_exception_handler(self, exc_class_or_status_code, handler):
116
+ """Add an exception handler to the application."""
117
+ super().add_exception_handler(exc_class_or_status_code, handler)
118
+
119
+ class FastModule(FastAPI):
120
+ """
121
+ FastAPI sub-application module class.
122
+
123
+ Used to create independent sub-application modules, where each module
124
+ can have its own routes, middleware, and configuration. Supports
125
+ automatic Resource registration to simplify module management.
126
+ """
127
+
128
+ def __init__(self, name: str, *args, **kwargs):
129
+ """
130
+ Initialize FastModule.
131
+
132
+ Args:
133
+ name: Module name used to identify the module. Must match the
134
+ module name in @register_resource(module=...).
135
+ *args, **kwargs: Additional arguments passed to FastAPI.
136
+ """
137
+ # Only disable docs if not explicitly provided by user
138
+ if 'docs_url' not in kwargs:
139
+ kwargs['docs_url'] = None
140
+ if 'redoc_url' not in kwargs:
141
+ kwargs['redoc_url'] = None
142
+ if 'openapi_url' not in kwargs:
143
+ kwargs['openapi_url'] = None
144
+
145
+ super().__init__(*args, **kwargs)
146
+
147
+ self.module_name = name
148
+ self._router: Optional[Any] = None # Store the unique ResourceRouter instance
149
+
150
+ # Get configuration from current_app
151
+ try:
152
+ self.config = current_app.config
153
+ except RuntimeError:
154
+ raise RuntimeError(
155
+ "FastApp instance must be created and set as current_app before creating FastModule"
156
+ )
157
+
158
+ def create_router(self, prefix: str = "", **kwargs):
159
+ """
160
+ Create a ResourceRouter and associate it with this module.
161
+
162
+ A FastModule can only have one ResourceRouter. If a router has
163
+ already been created, calling this method again will raise a ValueError.
164
+
165
+ Args:
166
+ prefix: Route prefix.
167
+ **kwargs: Additional arguments passed to ResourceRouter.
168
+
169
+ Returns:
170
+ ResourceRouter: The created ResourceRouter instance.
171
+
172
+ Example:
173
+ router = module.create_router(prefix="/api/v1")
174
+ """
175
+ from tomskit.server.resource import ResourceRouter
176
+
177
+ if self._router is not None:
178
+ raise ValueError(
179
+ f"FastModule '{self.module_name}' already has a router. "
180
+ "A FastModule can only have one ResourceRouter. "
181
+ "If you need multiple routers, create multiple FastModule instances."
182
+ )
183
+
184
+ router = ResourceRouter(app=self, prefix=prefix, **kwargs)
185
+ self._router = router
186
+ return router
187
+
188
+ def auto_register_resources(self):
189
+ """
190
+ Automatically register all Resources marked for this module.
191
+
192
+ This method searches ResourceRegistry for all Resources marked with
193
+ the current module name and automatically registers them to this
194
+ module's ResourceRouter.
195
+
196
+ If the router does not exist, it will be automatically created
197
+ (using default prefix="").
198
+
199
+ Example:
200
+ module = FastModule(name="files")
201
+ module.auto_register_resources() # Auto-register all Resources marked for "files" module
202
+
203
+ # Or create router first, then auto-register
204
+ module = FastModule(name="files")
205
+ router = module.create_router(prefix="/api/v1")
206
+ module.auto_register_resources() # Register to the created router
207
+ """
208
+ from tomskit.server.resource import ResourceRegistry
209
+
210
+ # Get or create router
211
+ if self._router is None:
212
+ self._router = self.create_router()
213
+
214
+ # Get all Resources for this module from registry
215
+ resources = ResourceRegistry.get_module_resources(self.module_name)
216
+
217
+ if not resources:
218
+ # If no Resources are registered, issue a warning but don't error (allows manual registration)
219
+ warnings.warn(
220
+ f"Module '{self.module_name}' has no Resources registered via @register_resource. "
221
+ f"Ensure Resource classes use @register_resource(module='{self.module_name}', ...) decorator.",
222
+ UserWarning,
223
+ )
224
+ return
225
+
226
+ # Register all Resources
227
+ for resource_info in resources:
228
+ self._router.add_resource(
229
+ resource_cls=resource_info.resource_cls,
230
+ path=resource_info.path,
231
+ tags=resource_info.tags if resource_info.tags else None,
232
+ )
233
+
234
+ # Include router after all resources are registered
235
+ # This ensures router has routes before being included
236
+ self.include_router(self._router)
237
+
238
+ def setup_cors(
239
+ self,
240
+ allow_origins: Optional[list[str]] = None,
241
+ allow_credentials: bool = True,
242
+ allow_methods: Optional[list[str]] = None,
243
+ allow_headers: Optional[list[str]] = None,
244
+ expose_headers: Optional[list[str]] = None,
245
+ ):
246
+ """
247
+ Configure CORS middleware.
248
+
249
+ Args:
250
+ allow_origins: List of allowed origins for cross-origin requests.
251
+ allow_credentials: Whether to allow credentials in cross-origin requests.
252
+ allow_methods: List of allowed HTTP methods.
253
+ allow_headers: List of allowed request headers.
254
+ expose_headers: List of response headers exposed to clients.
255
+
256
+ Example:
257
+ module.setup_cors(
258
+ allow_origins=["http://localhost:3000"],
259
+ allow_credentials=True,
260
+ allow_methods=["GET", "POST", "PUT", "DELETE"],
261
+ )
262
+ """
263
+ from fastapi.middleware.cors import CORSMiddleware
264
+
265
+ self.add_middleware(
266
+ CORSMiddleware,
267
+ allow_origins=allow_origins or [],
268
+ allow_credentials=allow_credentials,
269
+ allow_methods=allow_methods or ["GET", "PUT", "POST", "DELETE", "OPTIONS", "PATCH"],
270
+ allow_headers=allow_headers or ["Content-Type", "Authorization"],
271
+ expose_headers=expose_headers or [],
272
+ )
273
+
274
+ def add_exception_handler(self, exc_class_or_status_code, handler):
275
+ """Add an exception handler to the module."""
276
+ super().add_exception_handler(exc_class_or_status_code, handler)
tomskit/server/type.py ADDED
@@ -0,0 +1,263 @@
1
+ import uuid
2
+ from typing import Any, Dict, Type
3
+ from pydantic_core import core_schema
4
+ from datetime import datetime
5
+
6
+ def IntRange(minimum: int, maximum: int) -> Type[int]:
7
+ """
8
+ Factory for a constrained int subtype: value must be in [minimum, maximum].
9
+
10
+ Usage:
11
+ class User(BaseModel):
12
+ age: IntRange(18, 35)
13
+ """
14
+ if minimum > maximum:
15
+ raise ValueError(f"IntRange: minimum ({minimum}) cannot exceed maximum ({maximum})")
16
+
17
+ class IntRangeType(int):
18
+ @classmethod
19
+ def __get_pydantic_core_schema__(cls, source, handler) -> core_schema.CoreSchema:
20
+ # Use the built-in int_schema with ge/le
21
+ return core_schema.int_schema(strict=True, ge=minimum, le=maximum)
22
+
23
+ @classmethod
24
+ def __modify_json_schema__(cls, schema: Dict[str, Any]) -> None:
25
+ schema.update(
26
+ type="integer",
27
+ minimum=minimum,
28
+ maximum=maximum,
29
+ description=f"Integer in range [{minimum}, {maximum}]"
30
+ )
31
+
32
+ IntRangeType.__name__ = f"IntRange_{minimum}_{maximum}"
33
+ return IntRangeType
34
+
35
+ class Boolean:
36
+ """
37
+ Type that parses "true"/"false"/"1"/"0" into bool.
38
+ """
39
+
40
+ @staticmethod
41
+ def _parse_bool(v: Any) -> bool:
42
+ if isinstance(v, bool):
43
+ return v
44
+ if v is None or (isinstance(v, str) and not v):
45
+ raise ValueError("Boolean type must be non-null")
46
+ s = str(v).lower()
47
+ if s in ("true", "1"):
48
+ return True
49
+ if s in ("false", "0"):
50
+ return False
51
+ raise ValueError(f"Invalid literal for Boolean(): {v!r}")
52
+
53
+ @classmethod
54
+ def __get_pydantic_core_schema__(cls, source, handler) -> core_schema.CoreSchema:
55
+ return core_schema.union_schema([
56
+ core_schema.bool_schema(strict=True),
57
+ core_schema.chain_schema([
58
+ core_schema.str_schema(strict=False, coerce_numbers_to_str=True),
59
+ core_schema.no_info_plain_validator_function(cls._parse_bool),
60
+ ]),
61
+ ], mode='smart')
62
+
63
+ @classmethod
64
+ def __modify_json_schema__(cls, schema: Dict[str, Any]) -> None:
65
+ schema.update(
66
+ type="boolean",
67
+ description="Accepts true/false/1/0 (case-insensitive)"
68
+ )
69
+
70
+
71
+ class PhoneNumber(str):
72
+ """
73
+ Phone number type for Chinese mobile numbers (11 digits, starts with 1[3-9]).
74
+ """
75
+ @classmethod
76
+ def __get_pydantic_core_schema__(cls, source, handler) -> core_schema.CoreSchema:
77
+ return core_schema.str_schema(
78
+ strict=True,
79
+ pattern=r"^1[3-9]\d{9}$",
80
+ regex_engine="python-re"
81
+ )
82
+
83
+ @classmethod
84
+ def __modify_json_schema__(cls, schema: Dict[str, Any]) -> None:
85
+ schema.update(
86
+ type="string",
87
+ pattern=r"^1[3-9]\d{9}$",
88
+ description="Chinese mobile phone number"
89
+ )
90
+
91
+ class EmailStr(str):
92
+ """
93
+ Email address type.
94
+ """
95
+ @classmethod
96
+ def __get_pydantic_core_schema__(cls, source, handler) -> core_schema.CoreSchema:
97
+ return core_schema.str_schema(
98
+ strict=True,
99
+ pattern=r"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$",
100
+ regex_engine="python-re"
101
+ )
102
+
103
+ @classmethod
104
+ def __modify_json_schema__(cls, schema: Dict[str, Any]) -> None:
105
+ schema.update(
106
+ type="string",
107
+ format="email",
108
+ description="Email address"
109
+ )
110
+
111
+ def StrLen(max_length: int, min_length: int = 0) -> Type[str]:
112
+ """
113
+ Factory for a constrained string subtype: string length must be in [min_length, max_length].
114
+
115
+ Usage:
116
+ class User(BaseModel):
117
+ username: StrLen(3, 20) # between 3-20 characters
118
+ name: StrLen(1, 50) # between 1-50 characters
119
+ """
120
+ if min_length < 0:
121
+ raise ValueError(f"StrLen: min_length ({min_length}) cannot be negative")
122
+ if min_length > max_length:
123
+ raise ValueError(f"StrLen: min_length ({min_length}) cannot exceed max_length ({max_length})")
124
+
125
+ min_length = max_length if min_length == 0 else min_length
126
+
127
+ class StrLenType(str):
128
+ @classmethod
129
+ def __get_pydantic_core_schema__(cls, source, handler) -> core_schema.CoreSchema:
130
+ return core_schema.str_schema(
131
+ strict=True,
132
+ min_length=min_length,
133
+ max_length=max_length
134
+ )
135
+
136
+ @classmethod
137
+ def __modify_json_schema__(cls, schema: Dict[str, Any]) -> None:
138
+ schema.update(
139
+ type="string",
140
+ minLength=min_length,
141
+ maxLength=max_length,
142
+ description=f"String with length between {min_length} and {max_length} characters"
143
+ )
144
+
145
+ StrLenType.__name__ = f"StrLen_{min_length}_{max_length}"
146
+ return StrLenType
147
+
148
+ def DatetimeString(format: str, argument: str = "datetime") -> Type[str]:
149
+ """
150
+ Factory for datetime string type with custom format validation.
151
+
152
+ Usage:
153
+ class Event(BaseModel):
154
+ created_at: DatetimeString("%Y-%m-%d %H:%M:%S")
155
+ date_only: DatetimeString("%Y-%m-%d", "date")
156
+ """
157
+
158
+ class DatetimeStrType(str):
159
+ @classmethod
160
+ def __get_pydantic_core_schema__(cls, source, handler) -> core_schema.CoreSchema:
161
+ def validate_datetime(value: str) -> str:
162
+ try:
163
+ datetime.strptime(value, format)
164
+ return value
165
+ except ValueError:
166
+ raise ValueError(f"Invalid {argument}: '{value}', expected format: {format}")
167
+
168
+ return core_schema.chain_schema([
169
+ core_schema.str_schema(),
170
+ core_schema.no_info_plain_validator_function(validate_datetime)
171
+ ])
172
+
173
+ @classmethod
174
+ def __modify_json_schema__(cls, schema: Dict[str, Any]) -> None:
175
+ # 根据格式判断是日期还是日期时间
176
+ is_datetime = any(x in format for x in ["%H", "%M", "%S"])
177
+
178
+ schema.update(
179
+ type="string",
180
+ format="date-time" if is_datetime else "date",
181
+ description=f"{argument.title()} string in format: {format}",
182
+ example=datetime.now().strftime(format)
183
+ )
184
+
185
+ DatetimeStrType.__name__ = f"DatetimeString_{argument}"
186
+ return DatetimeStrType
187
+
188
+
189
+ class UUIDType(str):
190
+ """
191
+ UUID 类型验证器
192
+ 用于验证字符串是否为有效的 UUID 格式
193
+ """
194
+
195
+ @classmethod
196
+ def __get_pydantic_core_schema__(cls, source, handler) -> core_schema.CoreSchema:
197
+ def validate_uuid(value: Any) -> str:
198
+ """验证 UUID 格式"""
199
+ if value == "":
200
+ return value
201
+
202
+ try:
203
+ # 验证是否为有效的 UUID
204
+ uuid_obj = uuid.UUID(str(value))
205
+ # 返回标准化的 UUID 字符串
206
+ return str(uuid_obj)
207
+ except (ValueError, TypeError):
208
+ raise ValueError(f"{value} is not a valid UUID")
209
+
210
+ return core_schema.no_info_plain_validator_function(
211
+ validate_uuid
212
+ )
213
+
214
+ @classmethod
215
+ def __modify_json_schema__(cls, schema: Dict[str, Any]) -> None:
216
+ schema.update(
217
+ type="string",
218
+ format="uuid",
219
+ pattern="^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
220
+ description="A valid UUID string"
221
+ )
222
+
223
+ @classmethod
224
+ def is_valid_uuid(cls, value: str) -> bool:
225
+ """
226
+ 检查字符串是否为有效的 UUID
227
+
228
+ Args:
229
+ value: 待验证的字符串
230
+
231
+ Returns:
232
+ bool: 如果是有效的 UUID 返回 True,否则返回 False
233
+ """
234
+ if not value or value == "":
235
+ return False
236
+
237
+ try:
238
+ uuid.UUID(value)
239
+ return True
240
+ except (ValueError, TypeError):
241
+ return False
242
+
243
+ # 预定义的常用日期时间格式
244
+ class CommonDateFormats:
245
+ """常用的日期时间格式"""
246
+ ISO_DATE = "%Y-%m-%d" # 2023-12-25
247
+ ISO_DATETIME = "%Y-%m-%d %H:%M:%S" # 2023-12-25 14:30:00
248
+ ISO_DATETIME_MS = "%Y-%m-%d %H:%M:%S.%f" # 2023-12-25 14:30:00.123456
249
+ US_DATE = "%m/%d/%Y" # 12/25/2023
250
+ EU_DATE = "%d/%m/%Y" # 25/12/2023
251
+ TIME_24H = "%H:%M:%S" # 14:30:00
252
+ TIME_12H = "%I:%M:%S %p" # 02:30:00 PM
253
+
254
+
255
+ # 便捷的预定义类型
256
+ ISODate = DatetimeString(CommonDateFormats.ISO_DATE, "date")
257
+ ISODateTime = DatetimeString(CommonDateFormats.ISO_DATETIME, "datetime")
258
+ USDate = DatetimeString(CommonDateFormats.US_DATE, "date")
259
+ EUDate = DatetimeString(CommonDateFormats.EU_DATE, "date")
260
+ Time24H = DatetimeString(CommonDateFormats.TIME_24H, "time")
261
+ Time12H = DatetimeString(CommonDateFormats.TIME_12H, "time")
262
+
263
+