fastv 1.0.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.
Files changed (74) hide show
  1. fastv-1.0.0/PKG-INFO +93 -0
  2. fastv-1.0.0/README.md +45 -0
  3. fastv-1.0.0/fastv/__init__.py +396 -0
  4. fastv-1.0.0/fastv/auth/__init__.py +24 -0
  5. fastv-1.0.0/fastv/auth/auth_manager.py +122 -0
  6. fastv-1.0.0/fastv/auth/guard.py +127 -0
  7. fastv-1.0.0/fastv/auth/jwt_manager.py +80 -0
  8. fastv-1.0.0/fastv/auth/password_hasher.py +46 -0
  9. fastv-1.0.0/fastv/cache/__init__.py +3 -0
  10. fastv-1.0.0/fastv/cache/manager.py +273 -0
  11. fastv-1.0.0/fastv/config/__init__.py +120 -0
  12. fastv-1.0.0/fastv/console/__init__.py +6 -0
  13. fastv-1.0.0/fastv/console/application.py +75 -0
  14. fastv-1.0.0/fastv/console/caller.py +71 -0
  15. fastv-1.0.0/fastv/console/command.py +115 -0
  16. fastv-1.0.0/fastv/console/commands/__init__.py +1 -0
  17. fastv-1.0.0/fastv/console/commands/about.py +32 -0
  18. fastv-1.0.0/fastv/console/commands/cache_clear.py +46 -0
  19. fastv-1.0.0/fastv/console/commands/list_command.py +33 -0
  20. fastv-1.0.0/fastv/console/commands/make_command.py +43 -0
  21. fastv-1.0.0/fastv/console/commands/make_controller.py +72 -0
  22. fastv-1.0.0/fastv/console/commands/make_migration.py +30 -0
  23. fastv-1.0.0/fastv/console/commands/make_model.py +69 -0
  24. fastv-1.0.0/fastv/console/commands/migrate.py +28 -0
  25. fastv-1.0.0/fastv/console/commands/migrate_refresh.py +32 -0
  26. fastv-1.0.0/fastv/console/commands/migrate_rollback.py +29 -0
  27. fastv-1.0.0/fastv/console/commands/migrate_status.py +80 -0
  28. fastv-1.0.0/fastv/console/commands/start_server.py +79 -0
  29. fastv-1.0.0/fastv/console/output.py +165 -0
  30. fastv-1.0.0/fastv/console/registrar.py +114 -0
  31. fastv-1.0.0/fastv/console/signature_parser.py +115 -0
  32. fastv-1.0.0/fastv/container/__init__.py +3 -0
  33. fastv-1.0.0/fastv/container/container.py +151 -0
  34. fastv-1.0.0/fastv/controllers.py +122 -0
  35. fastv-1.0.0/fastv/database/__init__.py +14 -0
  36. fastv-1.0.0/fastv/database/builder.py +497 -0
  37. fastv-1.0.0/fastv/database/connection.py +221 -0
  38. fastv-1.0.0/fastv/database/migration.py +43 -0
  39. fastv-1.0.0/fastv/database/model.py +447 -0
  40. fastv-1.0.0/fastv/events/__init__.py +3 -0
  41. fastv-1.0.0/fastv/events/dispatcher.py +87 -0
  42. fastv-1.0.0/fastv/exceptions/__init__.py +93 -0
  43. fastv-1.0.0/fastv/exceptions/handler.py +181 -0
  44. fastv-1.0.0/fastv/http/__init__.py +4 -0
  45. fastv-1.0.0/fastv/http/middleware/__init__.py +3 -0
  46. fastv-1.0.0/fastv/http/middleware/pipeline.py +53 -0
  47. fastv-1.0.0/fastv/http/request.py +151 -0
  48. fastv-1.0.0/fastv/http/response.py +67 -0
  49. fastv-1.0.0/fastv/log/__init__.py +3 -0
  50. fastv-1.0.0/fastv/log/logger.py +146 -0
  51. fastv-1.0.0/fastv/queue/__init__.py +31 -0
  52. fastv-1.0.0/fastv/queue/asynctasq_integration.py +230 -0
  53. fastv-1.0.0/fastv/redis/__init__.py +10 -0
  54. fastv-1.0.0/fastv/redis/client.py +903 -0
  55. fastv-1.0.0/fastv/routing/__init__.py +3 -0
  56. fastv-1.0.0/fastv/routing/router.py +346 -0
  57. fastv-1.0.0/fastv/schedule/__init__.py +5 -0
  58. fastv-1.0.0/fastv/schedule/commands/__init__.py +1 -0
  59. fastv-1.0.0/fastv/schedule/commands/schedule_serve.py +105 -0
  60. fastv-1.0.0/fastv/schedule/runner.py +140 -0
  61. fastv-1.0.0/fastv/schedule/scheduler.py +256 -0
  62. fastv-1.0.0/fastv/support/__init__.py +8 -0
  63. fastv-1.0.0/fastv/support/helpers.py +48 -0
  64. fastv-1.0.0/fastv/support/str.py +46 -0
  65. fastv-1.0.0/fastv/validation/__init__.py +3 -0
  66. fastv-1.0.0/fastv/validation/validator.py +147 -0
  67. fastv-1.0.0/fastv.egg-info/PKG-INFO +93 -0
  68. fastv-1.0.0/fastv.egg-info/SOURCES.txt +72 -0
  69. fastv-1.0.0/fastv.egg-info/dependency_links.txt +1 -0
  70. fastv-1.0.0/fastv.egg-info/entry_points.txt +2 -0
  71. fastv-1.0.0/fastv.egg-info/requires.txt +37 -0
  72. fastv-1.0.0/fastv.egg-info/top_level.txt +1 -0
  73. fastv-1.0.0/pyproject.toml +119 -0
  74. fastv-1.0.0/setup.cfg +4 -0
fastv-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastv
3
+ Version: 1.0.0
4
+ Summary: Laravel-style FastAPI framework
5
+ Author-email: FastV Team <support@fastv.dev>
6
+ License-Expression: MIT
7
+ Keywords: fastapi,laravel,web-framework,async,orm,cli
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Framework :: FastAPI
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
+ Requires-Python: >=3.12
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: fastapi>=0.139.0
17
+ Requires-Dist: uvicorn>=0.51.0
18
+ Requires-Dist: pydantic>=2.13.0
19
+ Requires-Dist: sqlalchemy>=2.0.0
20
+ Requires-Dist: python-dotenv>=1.2.0
21
+ Requires-Dist: click>=8.4.0
22
+ Requires-Dist: rich>=14.0.0
23
+ Requires-Dist: redis>=5.0
24
+ Requires-Dist: apscheduler>=3.10.0
25
+ Requires-Dist: alembic>=1.13.0
26
+ Requires-Dist: PyJWT>=2.8.0
27
+ Requires-Dist: passlib[bcrypt]>=1.7.4
28
+ Requires-Dist: bcrypt<4.1.0,>=4.0.0
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=8.0; extra == "dev"
31
+ Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
32
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
33
+ Requires-Dist: httpx>=0.28.0; extra == "dev"
34
+ Requires-Dist: ruff>=0.8.0; extra == "dev"
35
+ Provides-Extra: mysql
36
+ Requires-Dist: aiomysql>=0.2.0; extra == "mysql"
37
+ Requires-Dist: pymysql>=1.0; extra == "mysql"
38
+ Requires-Dist: cryptography>=3.0; extra == "mysql"
39
+ Provides-Extra: postgres
40
+ Requires-Dist: asyncpg>=0.30.0; extra == "postgres"
41
+ Requires-Dist: psycopg2-binary>=2.9; extra == "postgres"
42
+ Provides-Extra: sqlite
43
+ Requires-Dist: aiosqlite>=0.20.0; extra == "sqlite"
44
+ Provides-Extra: queue
45
+ Requires-Dist: asynctasq>=1.7.0; extra == "queue"
46
+ Requires-Dist: msgspec>=0.18.0; extra == "queue"
47
+ Requires-Dist: pydantic-settings>=2.0; extra == "queue"
48
+
49
+ # FastV — Laravel-style FastAPI Framework
50
+
51
+ A full-stack web framework built on FastAPI, bringing Laravel's architectural patterns to Python.
52
+
53
+ ## Features
54
+
55
+ - **Service Container (IoC)** — Dependency injection with auto-wiring
56
+ - **Eloquent-style ORM** — SQLAlchemy 2.0 with chainable QueryBuilder
57
+ - **Artisan-style CLI** — Code generators, migrations, and more
58
+ - **JWT Authentication** — Guards, password hashing, token management
59
+ - **Cache System** — Array, File, and Redis drivers
60
+ - **Event Dispatcher** — Pub/sub with wildcard support
61
+ - **Task Queue** — AsyncTasQ integration
62
+ - **Scheduler** — APScheduler with Laravel-style fluent API
63
+ - **Middleware Pipeline** — Chainable HTTP middleware
64
+
65
+ ## Installation
66
+
67
+ ```bash
68
+ pip install fastv
69
+ ```
70
+
71
+ ## Quick Start
72
+
73
+ ```bash
74
+ # Create a new project
75
+ mkdir myapp && cd myapp
76
+
77
+ # Initialize (create config, bootstrap, etc.)
78
+ # Then start the server
79
+ fastv start
80
+ ```
81
+
82
+ ## Requirements
83
+
84
+ - Python 3.12+
85
+ - FastAPI 0.139+
86
+
87
+ ## Documentation
88
+
89
+ See `docs/technical-documentation.md` for detailed framework documentation.
90
+
91
+ ## License
92
+
93
+ MIT
fastv-1.0.0/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # FastV — Laravel-style FastAPI Framework
2
+
3
+ A full-stack web framework built on FastAPI, bringing Laravel's architectural patterns to Python.
4
+
5
+ ## Features
6
+
7
+ - **Service Container (IoC)** — Dependency injection with auto-wiring
8
+ - **Eloquent-style ORM** — SQLAlchemy 2.0 with chainable QueryBuilder
9
+ - **Artisan-style CLI** — Code generators, migrations, and more
10
+ - **JWT Authentication** — Guards, password hashing, token management
11
+ - **Cache System** — Array, File, and Redis drivers
12
+ - **Event Dispatcher** — Pub/sub with wildcard support
13
+ - **Task Queue** — AsyncTasQ integration
14
+ - **Scheduler** — APScheduler with Laravel-style fluent API
15
+ - **Middleware Pipeline** — Chainable HTTP middleware
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ pip install fastv
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ ```bash
26
+ # Create a new project
27
+ mkdir myapp && cd myapp
28
+
29
+ # Initialize (create config, bootstrap, etc.)
30
+ # Then start the server
31
+ fastv start
32
+ ```
33
+
34
+ ## Requirements
35
+
36
+ - Python 3.12+
37
+ - FastAPI 0.139+
38
+
39
+ ## Documentation
40
+
41
+ See `docs/technical-documentation.md` for detailed framework documentation.
42
+
43
+ ## License
44
+
45
+ MIT
@@ -0,0 +1,396 @@
1
+ """
2
+ Application - Framework core entry point.
3
+ """
4
+
5
+ import importlib
6
+ import os
7
+ import sys
8
+ from contextlib import asynccontextmanager
9
+ from typing import AsyncIterator, Callable, Coroutine, List
10
+
11
+ from fastapi import FastAPI
12
+
13
+ from fastv.config import Config
14
+ from fastv.container.container import Container
15
+ from fastv.support.helpers import set_application
16
+
17
+
18
+ class Application(Container):
19
+ _instance = None
20
+
21
+ def __new__(cls, base_dir=None):
22
+ if cls._instance is None:
23
+ cls._instance = super().__new__(cls)
24
+ cls._instance._initialized = False
25
+ return cls._instance
26
+
27
+ def __init__(self, base_dir=None):
28
+ if getattr(self, "_initialized", False):
29
+ return
30
+ super().__init__()
31
+ self.base_dir = base_dir or os.getcwd()
32
+ self._shutdown_callbacks: List[Callable[[], Coroutine]] = []
33
+ self._startup_callbacks: List[Callable[[], Coroutine]] = []
34
+ self.app = FastAPI(title="FastAPI Application")
35
+ self.config = Config(self.base_dir)
36
+ self.router = None
37
+ self._bootstrapped = False
38
+ self._initialized = True
39
+ set_application(self)
40
+
41
+ def _on_startup(self, callback: Callable[[], Coroutine]):
42
+ """注册 startup 回调(替代 @app.on_event("startup"))"""
43
+ self._startup_callbacks.append(callback)
44
+
45
+ def _on_shutdown(self, callback: Callable[[], Coroutine]):
46
+ """注册 shutdown 回调(替代 @app.on_event("shutdown"))"""
47
+ self._shutdown_callbacks.append(callback)
48
+
49
+ @asynccontextmanager
50
+ async def _lifespan(self, app: FastAPI) -> AsyncIterator[None]:
51
+ """FastAPI lifespan 上下文管理器"""
52
+ # 启动
53
+ for cb in self._startup_callbacks:
54
+ try:
55
+ await cb()
56
+ except Exception as e:
57
+ print(f"[Lifespan] startup callback failed: {e}")
58
+ yield
59
+ # 关闭
60
+ for cb in self._shutdown_callbacks:
61
+ try:
62
+ await cb()
63
+ except Exception as e:
64
+ print(f"[Lifespan] shutdown callback failed: {e}")
65
+
66
+ def bootstrap(self):
67
+ if self._bootstrapped:
68
+ return self.app
69
+ self._load_config()
70
+ self._register_services()
71
+ self._register_exception_handler()
72
+ self._init_redis()
73
+ self._init_cache()
74
+ self._init_logger()
75
+ self._init_database()
76
+ self._init_auth()
77
+ self._init_asynctasq()
78
+ self._discover_routes()
79
+ self._discover_middleware()
80
+ self._discover_events()
81
+ self._discover_commands()
82
+ # 注册 lifespan(替代 on_event)
83
+ self.app.router.lifespan_context = self._lifespan
84
+ self._bootstrapped = True
85
+ return self.app
86
+
87
+ def _load_config(self):
88
+ config_dir = os.path.join(self.base_dir, "config")
89
+ if os.path.isdir(config_dir):
90
+ self.config.load_from_dir(config_dir)
91
+ self.instance("config", self.config)
92
+
93
+ def _register_services(self):
94
+ from fastv.events.dispatcher import Dispatcher
95
+ from fastv.http.middleware.pipeline import Pipeline
96
+ from fastv.routing.router import Router
97
+ self.singleton("router", lambda: Router())
98
+ self.singleton("pipeline", lambda: Pipeline())
99
+ self.singleton("events", lambda: Dispatcher())
100
+ self.instance("app", self.app)
101
+
102
+ def _register_exception_handler(self):
103
+ from fastv.exceptions.handler import register_exception_handler
104
+ exc_config = self.config.get("exception", {})
105
+ register_exception_handler(self.app, exc_config)
106
+
107
+ def _discover_routes(self):
108
+ route_dir = os.path.join(self.base_dir, "app")
109
+ if not os.path.isdir(route_dir):
110
+ return
111
+ self._add_app_to_path()
112
+ route_count = 0
113
+ for root, dirs, files in os.walk(route_dir):
114
+ if os.path.basename(root) == "route":
115
+ for f in files:
116
+ if f.endswith(".py") and not f.startswith("_"):
117
+ rel_path = os.path.relpath(os.path.join(root, f), self.base_dir)
118
+ module_name = rel_path.replace(os.sep, ".").replace(".py", "")
119
+ try:
120
+ module = importlib.import_module(module_name)
121
+ if hasattr(module, "register"):
122
+ module.register(self.app)
123
+ route_count += 1
124
+ from fastv.routing.router import Router as PkgRouter
125
+ for name in dir(module):
126
+ obj = getattr(module, name)
127
+ if isinstance(obj, PkgRouter):
128
+ obj.register(self.app)
129
+ route_count += 1
130
+ except ImportError as e:
131
+ print("[Route] Failed:", module_name, e)
132
+ if route_count > 0:
133
+ print("[Route] Discovered", route_count, "route file(s)")
134
+
135
+ def _discover_middleware(self):
136
+ mw_config = self.config.get("middleware")
137
+ if mw_config:
138
+ self._load_middleware_from_config(mw_config)
139
+ else:
140
+ self._auto_discover_middleware()
141
+
142
+ def _load_middleware_from_config(self, mw_config: dict):
143
+ mw_count = 0
144
+ self._add_app_to_path()
145
+
146
+ # 全局中间件
147
+ for mw_path in mw_config.get("global_middleware", []):
148
+ cls = self._import_class(mw_path)
149
+ if cls:
150
+ self.app.middleware("http")(self._mw_adapter(cls))
151
+ mw_count += 1
152
+
153
+ # 模块中间件
154
+ for module_name, mw_list in mw_config.get("modules", {}).items():
155
+ for mw_path in mw_list:
156
+ cls = self._import_class(mw_path)
157
+ if cls:
158
+ self.app.middleware("http")(self._mw_adapter(cls))
159
+ mw_count += 1
160
+
161
+ if mw_count > 0:
162
+ print(f"[Middleware] Registered {mw_count} middleware(s) from config")
163
+
164
+ def _auto_discover_middleware(self):
165
+ app_dir = os.path.join(self.base_dir, "app")
166
+ if not os.path.isdir(app_dir):
167
+ return
168
+ self._add_app_to_path()
169
+ mw_count = 0
170
+ for root, dirs, files in os.walk(app_dir):
171
+ if os.path.basename(root) == "middleware":
172
+ for f in files:
173
+ if f.endswith(".py") and not f.startswith("_"):
174
+ rel_path = os.path.relpath(os.path.join(root, f), self.base_dir)
175
+ module_name = rel_path.replace(os.sep, ".").replace(".py", "")
176
+ try:
177
+ module = importlib.import_module(module_name)
178
+ for name in dir(module):
179
+ cls = getattr(module, name)
180
+ if isinstance(cls, type) and hasattr(cls, "__call__"):
181
+ self.app.middleware("http")(self._mw_adapter(cls))
182
+ mw_count += 1
183
+ except ImportError as e:
184
+ print("[Middleware] Failed:", module_name, e)
185
+ if mw_count > 0:
186
+ print(f"[Middleware] Auto-discovered {mw_count} middleware(s)")
187
+
188
+ @staticmethod
189
+ def _import_class(dotted_path: str):
190
+ module_name, class_name = dotted_path.rsplit(".", 1)
191
+ try:
192
+ module = importlib.import_module(module_name)
193
+ return getattr(module, class_name, None)
194
+ except ImportError as e:
195
+ print(f"[Middleware] Failed to import {dotted_path}: {e}")
196
+ return None
197
+
198
+ def _discover_events(self):
199
+ event_dir = os.path.join(self.base_dir, "app", "event")
200
+ if not os.path.isdir(event_dir):
201
+ return
202
+ self._add_app_to_path()
203
+ listener_count = 0
204
+ for f in os.listdir(event_dir):
205
+ if f.endswith(".py") and not f.startswith("_"):
206
+ modname = "app.event." + f[:-3]
207
+ try:
208
+ module = importlib.import_module(modname)
209
+ dispatcher = self.make("events")
210
+ if hasattr(module, "register"):
211
+ module.register(dispatcher)
212
+ listener_count += 1
213
+ for name in dir(module):
214
+ cls = getattr(module, name)
215
+ if isinstance(cls, type) and hasattr(cls, "subscribe"):
216
+ cls().subscribe(dispatcher)
217
+ listener_count += 1
218
+ except ImportError as e:
219
+ print("[Event] Failed:", modname, e)
220
+ if listener_count > 0:
221
+ print("[Event] Registered", listener_count, "event listener(s)")
222
+
223
+ def _init_database(self):
224
+ default = self.config.get("database.default", "sqlite")
225
+ connections = self.config.get("database.connections", {})
226
+ if not connections:
227
+ return
228
+ conn_config = connections.get(default, {})
229
+ if not conn_config:
230
+ return
231
+ from fastv.database.connection import ConnectionManager, get_db
232
+ db = get_db()
233
+ url = ConnectionManager.build_url(conn_config)
234
+ echo = self.config.get("database.debug", False)
235
+
236
+ # 连接池配置(可在 config/database.php 中覆盖)
237
+ pool_kwargs = {
238
+ "pool_size": conn_config.get("pool_size", 10),
239
+ "max_overflow": conn_config.get("max_overflow", 20),
240
+ "pool_recycle": conn_config.get("pool_recycle", 3600),
241
+ "pool_pre_ping": conn_config.get("pool_pre_ping", True),
242
+ "connect_timeout": conn_config.get("connect_timeout", 10),
243
+ }
244
+ try:
245
+ db.configure(url, echo=echo, **pool_kwargs)
246
+ self.instance("db", db)
247
+ print(f"[Database] Connected ({default}) pool_size={pool_kwargs['pool_size']}")
248
+
249
+ self._on_shutdown(_make_close_db(db))
250
+ except Exception as e:
251
+ print(f"[Database] Skip ({e})")
252
+ return
253
+
254
+ def _init_auth(self):
255
+ """初始化认证服务"""
256
+ auth_config = self.config.get("auth", {})
257
+ if not auth_config:
258
+ return
259
+
260
+ from fastv.auth import AuthManager, set_auth
261
+ auth = AuthManager(auth_config)
262
+ self.instance("auth", auth)
263
+ set_auth(auth)
264
+ print("[Auth] JWT authentication initialized")
265
+
266
+ def _discover_commands(self):
267
+ """Deprecated: CLI command discovery is now handled by ArtisanApplication via fastv.py."""
268
+ pass
269
+
270
+ def _add_app_to_path(self):
271
+ app_path = os.path.join(self.base_dir, "app")
272
+ if app_path not in sys.path:
273
+ sys.path.insert(0, app_path)
274
+ if self.base_dir not in sys.path:
275
+ sys.path.insert(0, self.base_dir)
276
+
277
+
278
+ def _init_redis(self):
279
+ """初始化 Redis 连接"""
280
+ redis_config = self.config.get("redis.redis", {})
281
+ if not redis_config:
282
+ return
283
+ from fastv.redis import get_redis
284
+ rm = get_redis()
285
+ rm.configure(redis_config)
286
+ conn = rm.connection("default")
287
+
288
+ self.instance("redis", rm)
289
+
290
+ if conn.is_connected:
291
+ import asyncio
292
+ loop = asyncio.get_event_loop()
293
+ if loop.is_running():
294
+ print("[Redis] Connected")
295
+ else:
296
+ try:
297
+ pong = loop.run_until_complete(conn.ping())
298
+ if pong:
299
+ print("[Redis] Connected")
300
+ else:
301
+ print("[Redis] Connected but server unreachable")
302
+ except Exception as e:
303
+ print(f"[Redis] Connected but ping failed ({e})")
304
+
305
+ self._on_shutdown(_make_close_redis(rm))
306
+ else:
307
+ print("[Redis] Package not installed (pip install redis), using fallback")
308
+
309
+ def _init_cache(self):
310
+ """初始化缓存"""
311
+ default = self.config.get("cache.default", "file")
312
+ stores = self.config.get("cache.stores", {})
313
+ if not stores:
314
+ stores = {"array": {"driver": "array"}}
315
+ default = "array"
316
+ redis_mgr = None
317
+ if self.has("redis"):
318
+ redis_mgr = self.make("redis")
319
+ from fastv.cache import get_cache
320
+ cm = get_cache()
321
+ cm.configure(default, stores, redis_mgr)
322
+ self.instance("cache", cm)
323
+ print(f"[Cache] Driver: {default}")
324
+
325
+ def _init_logger(self):
326
+ """初始化日志"""
327
+ default = self.config.get("logging.default", "daily")
328
+ channels = self.config.get("logging.channels", {})
329
+ level = self.config.get("logging.level", "debug")
330
+ if not channels:
331
+ channels = {"daily": {"driver": "daily", "path": "storage/logs/app.log", "level": "debug", "days": 14}}
332
+ from fastv.log import get_logger
333
+ lm = get_logger()
334
+ lm.configure(default, channels, level)
335
+ self.instance("log", lm)
336
+ lm.info(f"[Logger] Channel: {default}, Level: {level}")
337
+ print(f"[Logger] Channel: {default}")
338
+
339
+ def _init_asynctasq(self):
340
+ """初始化 AsyncTasQ 任务队列"""
341
+ queue_cfg = self.config.get("queue.asynctasq", {})
342
+ if not queue_cfg:
343
+ return
344
+ redis_cfg = self.config.get("redis.redis.default", {})
345
+ db_name = self.config.get("database.default", "sqlite")
346
+ db_cfg = self.config.get(f"database.connections.{db_name}", {})
347
+ try:
348
+ from fastv.queue import register_lifecycle
349
+ register_lifecycle(self, queue_cfg, redis_cfg, db_cfg)
350
+ except ImportError:
351
+ print("[TaskQueue] asynctasq not installed, skipping")
352
+ except Exception as e:
353
+ print(f"[TaskQueue] Init failed ({e}), task dispatch will be unavailable")
354
+
355
+ async def __call__(self, scope, receive, send):
356
+ await self.app(scope, receive, send)
357
+
358
+ @staticmethod
359
+ def _mw_adapter(mw_class):
360
+ async def middleware_func(request, call_next):
361
+ instance = mw_class()
362
+ if hasattr(instance, "__call__"):
363
+ return await instance(request, call_next)
364
+ if hasattr(instance, "handle"):
365
+ return await instance.handle(request, call_next)
366
+ return await call_next(request)
367
+ return middleware_func
368
+
369
+
370
+ def _make_close_db(db):
371
+ """创建关闭数据库连接的回调"""
372
+ async def _close():
373
+ await db.close()
374
+ print("[Database] Disconnected")
375
+ return _close
376
+
377
+
378
+ def _make_close_redis(rm):
379
+ """创建关闭 Redis 连接的回调"""
380
+ async def _close():
381
+ await rm.close_all()
382
+ print("[Redis] Disconnected")
383
+ return _close
384
+
385
+
386
+ def get_app():
387
+ return Application._instance
388
+
389
+
390
+ def cli():
391
+ """CLI entry point — Artisan-style command interface."""
392
+ from fastv.console.application import ArtisanApplication
393
+ app = ArtisanApplication()
394
+ app.discover()
395
+ app.create_cli()()
396
+
@@ -0,0 +1,24 @@
1
+ """
2
+ Auth 认证模块
3
+
4
+ 提供 JWT 认证功能:
5
+ - JwtManager: JWT token 生成和验证
6
+ - PasswordHasher: 密码哈希和验证
7
+ - JwtGuard: JWT 认证守卫
8
+ - AuthManager: 认证管理器
9
+ - get_auth(): 获取全局 Auth 实例
10
+ """
11
+
12
+ from .auth_manager import AuthManager, get_auth, set_auth
13
+ from .guard import JwtGuard
14
+ from .jwt_manager import JwtManager
15
+ from .password_hasher import PasswordHasher
16
+
17
+ __all__ = [
18
+ "get_auth",
19
+ "set_auth",
20
+ "JwtManager",
21
+ "PasswordHasher",
22
+ "JwtGuard",
23
+ "AuthManager",
24
+ ]
@@ -0,0 +1,122 @@
1
+ """
2
+ Auth Manager - 认证管理器
3
+
4
+ 管理多个认证守卫(类似 Laravel AuthManager):
5
+ - guard(): 获取指定守卫
6
+ - check(): 检查是否已认证
7
+ - user(): 获取当前用户
8
+ - id(): 获取当前用户 ID
9
+ """
10
+
11
+ import importlib
12
+ from typing import Optional
13
+
14
+ from fastv.auth.guard import JwtGuard
15
+ from fastv.auth.jwt_manager import JwtManager
16
+
17
+
18
+ class AuthManager:
19
+ """认证管理器"""
20
+
21
+ def __init__(self, config: dict):
22
+ self.config = config
23
+ self._guards = {}
24
+ self._current_guard_name = config.get("guards", {}).get("default", "jwt")
25
+
26
+ def guard(self, name: Optional[str] = None) -> JwtGuard:
27
+ """
28
+ 获取指定守卫
29
+
30
+ Args:
31
+ name: 守卫名称,默认使用配置中的 default
32
+
33
+ Returns:
34
+ JwtGuard 实例
35
+ """
36
+ guard_name = name or self._current_guard_name
37
+
38
+ if guard_name not in self._guards:
39
+ self._guards[guard_name] = self._create_guard(guard_name)
40
+
41
+ return self._guards[guard_name]
42
+
43
+ def _create_guard(self, name: str) -> JwtGuard:
44
+ """创建守卫实例"""
45
+ guards_config = self.config.get("guards", {}).get("guards", {})
46
+ guard_config = guards_config.get(name, {})
47
+
48
+ driver = guard_config.get("driver", "jwt")
49
+ provider_name = guard_config.get("provider", "users")
50
+
51
+ if driver != "jwt":
52
+ raise ValueError(f"Unsupported guard driver: {driver}")
53
+
54
+ # 获取用户提供者配置
55
+ providers_config = self.config.get("guards", {}).get("providers", {})
56
+ provider_config = providers_config.get(provider_name, {})
57
+
58
+ provider_driver = provider_config.get("driver", "eloquent")
59
+ model_path = provider_config.get("model", "app.model.user.User")
60
+
61
+ if provider_driver != "eloquent":
62
+ raise ValueError(f"Unsupported provider driver: {provider_driver}")
63
+
64
+ # 动态加载用户模型
65
+ user_model = self._load_model(model_path)
66
+
67
+ # 创建 JWT 管理器
68
+ jwt_manager = JwtManager(self.config)
69
+
70
+ return JwtGuard(user_model, jwt_manager)
71
+
72
+ @staticmethod
73
+ def _load_model(model_path: str):
74
+ """动态加载模型类"""
75
+ module_path, class_name = model_path.rsplit(".", 1)
76
+ module = importlib.import_module(module_path)
77
+ return getattr(module, class_name)
78
+
79
+ def check(self) -> bool:
80
+ """检查是否已认证"""
81
+ return self.guard().check()
82
+
83
+ def user(self) -> Optional[object]:
84
+ """获取当前用户"""
85
+ return self.guard().user()
86
+
87
+ def id(self) -> Optional[int]:
88
+ """获取当前用户 ID"""
89
+ return self.guard().id()
90
+
91
+ def logout(self):
92
+ """登出"""
93
+ self.guard().logout()
94
+
95
+
96
+ # 全局 Auth 实例(类似 Laravel Auth facade)
97
+ _auth_instance: Optional[AuthManager] = None
98
+
99
+
100
+ def get_auth() -> AuthManager:
101
+ """获取全局 Auth 实例"""
102
+ global _auth_instance
103
+ if _auth_instance is None:
104
+ # 从 Application 容器获取
105
+ try:
106
+ from fastv import get_app
107
+ app = get_app()
108
+ if app and app.has("auth"):
109
+ _auth_instance = app.make("auth")
110
+ except Exception:
111
+ pass
112
+
113
+ if _auth_instance is None:
114
+ raise RuntimeError("Auth not initialized. Call Application._init_auth() first.")
115
+
116
+ return _auth_instance
117
+
118
+
119
+ def set_auth(auth: AuthManager):
120
+ """设置全局 Auth 实例"""
121
+ global _auth_instance
122
+ _auth_instance = auth