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.
- fastv-1.0.0/PKG-INFO +93 -0
- fastv-1.0.0/README.md +45 -0
- fastv-1.0.0/fastv/__init__.py +396 -0
- fastv-1.0.0/fastv/auth/__init__.py +24 -0
- fastv-1.0.0/fastv/auth/auth_manager.py +122 -0
- fastv-1.0.0/fastv/auth/guard.py +127 -0
- fastv-1.0.0/fastv/auth/jwt_manager.py +80 -0
- fastv-1.0.0/fastv/auth/password_hasher.py +46 -0
- fastv-1.0.0/fastv/cache/__init__.py +3 -0
- fastv-1.0.0/fastv/cache/manager.py +273 -0
- fastv-1.0.0/fastv/config/__init__.py +120 -0
- fastv-1.0.0/fastv/console/__init__.py +6 -0
- fastv-1.0.0/fastv/console/application.py +75 -0
- fastv-1.0.0/fastv/console/caller.py +71 -0
- fastv-1.0.0/fastv/console/command.py +115 -0
- fastv-1.0.0/fastv/console/commands/__init__.py +1 -0
- fastv-1.0.0/fastv/console/commands/about.py +32 -0
- fastv-1.0.0/fastv/console/commands/cache_clear.py +46 -0
- fastv-1.0.0/fastv/console/commands/list_command.py +33 -0
- fastv-1.0.0/fastv/console/commands/make_command.py +43 -0
- fastv-1.0.0/fastv/console/commands/make_controller.py +72 -0
- fastv-1.0.0/fastv/console/commands/make_migration.py +30 -0
- fastv-1.0.0/fastv/console/commands/make_model.py +69 -0
- fastv-1.0.0/fastv/console/commands/migrate.py +28 -0
- fastv-1.0.0/fastv/console/commands/migrate_refresh.py +32 -0
- fastv-1.0.0/fastv/console/commands/migrate_rollback.py +29 -0
- fastv-1.0.0/fastv/console/commands/migrate_status.py +80 -0
- fastv-1.0.0/fastv/console/commands/start_server.py +79 -0
- fastv-1.0.0/fastv/console/output.py +165 -0
- fastv-1.0.0/fastv/console/registrar.py +114 -0
- fastv-1.0.0/fastv/console/signature_parser.py +115 -0
- fastv-1.0.0/fastv/container/__init__.py +3 -0
- fastv-1.0.0/fastv/container/container.py +151 -0
- fastv-1.0.0/fastv/controllers.py +122 -0
- fastv-1.0.0/fastv/database/__init__.py +14 -0
- fastv-1.0.0/fastv/database/builder.py +497 -0
- fastv-1.0.0/fastv/database/connection.py +221 -0
- fastv-1.0.0/fastv/database/migration.py +43 -0
- fastv-1.0.0/fastv/database/model.py +447 -0
- fastv-1.0.0/fastv/events/__init__.py +3 -0
- fastv-1.0.0/fastv/events/dispatcher.py +87 -0
- fastv-1.0.0/fastv/exceptions/__init__.py +93 -0
- fastv-1.0.0/fastv/exceptions/handler.py +181 -0
- fastv-1.0.0/fastv/http/__init__.py +4 -0
- fastv-1.0.0/fastv/http/middleware/__init__.py +3 -0
- fastv-1.0.0/fastv/http/middleware/pipeline.py +53 -0
- fastv-1.0.0/fastv/http/request.py +151 -0
- fastv-1.0.0/fastv/http/response.py +67 -0
- fastv-1.0.0/fastv/log/__init__.py +3 -0
- fastv-1.0.0/fastv/log/logger.py +146 -0
- fastv-1.0.0/fastv/queue/__init__.py +31 -0
- fastv-1.0.0/fastv/queue/asynctasq_integration.py +230 -0
- fastv-1.0.0/fastv/redis/__init__.py +10 -0
- fastv-1.0.0/fastv/redis/client.py +903 -0
- fastv-1.0.0/fastv/routing/__init__.py +3 -0
- fastv-1.0.0/fastv/routing/router.py +346 -0
- fastv-1.0.0/fastv/schedule/__init__.py +5 -0
- fastv-1.0.0/fastv/schedule/commands/__init__.py +1 -0
- fastv-1.0.0/fastv/schedule/commands/schedule_serve.py +105 -0
- fastv-1.0.0/fastv/schedule/runner.py +140 -0
- fastv-1.0.0/fastv/schedule/scheduler.py +256 -0
- fastv-1.0.0/fastv/support/__init__.py +8 -0
- fastv-1.0.0/fastv/support/helpers.py +48 -0
- fastv-1.0.0/fastv/support/str.py +46 -0
- fastv-1.0.0/fastv/validation/__init__.py +3 -0
- fastv-1.0.0/fastv/validation/validator.py +147 -0
- fastv-1.0.0/fastv.egg-info/PKG-INFO +93 -0
- fastv-1.0.0/fastv.egg-info/SOURCES.txt +72 -0
- fastv-1.0.0/fastv.egg-info/dependency_links.txt +1 -0
- fastv-1.0.0/fastv.egg-info/entry_points.txt +2 -0
- fastv-1.0.0/fastv.egg-info/requires.txt +37 -0
- fastv-1.0.0/fastv.egg-info/top_level.txt +1 -0
- fastv-1.0.0/pyproject.toml +119 -0
- 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
|