faster-app 0.1.0__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.
faster_api/__init__.py ADDED
@@ -0,0 +1,58 @@
1
+ """
2
+ Faster API - 一个轻量级的 Python Web 框架
3
+
4
+ 提供了以下核心功能:
5
+ - 自动发现和加载模块 (DiscoverBase)
6
+ - 数据库模型基类 (UUIDModel, DateTimeModel, StatusModel, ScopeModel)
7
+ - 命令行工具基类 (CommandBase)
8
+ - 路由管理 (ApiResponse)
9
+ - 数据库连接管理 (tortoise_init)
10
+ """
11
+
12
+ __version__ = "0.1.0"
13
+ __author__ = "peizhenfei"
14
+ __email__ = "peizhenfei@hotmail.com"
15
+
16
+ # 导出主要的类和函数
17
+ from faster_api.base import DiscoverBase
18
+ from faster_api.models.base import (
19
+ UUIDModel,
20
+ DateTimeModel,
21
+ StatusModel,
22
+ ScopeModel,
23
+ SyncTimeModel,
24
+ SyncCrontabModel,
25
+ )
26
+ from faster_api.commands.base import CommandBase, with_db_init
27
+ from faster_api.routes.base import ApiResponse
28
+ from faster_api.db import tortoise_init
29
+
30
+ # 导出发现器
31
+ from faster_api.models.discover import ModelDiscover
32
+ from faster_api.commands.discover import CommandDiscover
33
+ from faster_api.routes.discover import RoutesDiscover
34
+
35
+ # 导出配置
36
+ from faster_api.settings.builtins.settings import DefaultSettings
37
+
38
+ __all__ = [
39
+ # 基础类
40
+ "DiscoverBase",
41
+ "CommandBase",
42
+ "with_db_init",
43
+ "ApiResponse",
44
+ "tortoise_init",
45
+ # 模型基类
46
+ "UUIDModel",
47
+ "DateTimeModel",
48
+ "StatusModel",
49
+ "ScopeModel",
50
+ "SyncTimeModel",
51
+ "SyncCrontabModel",
52
+ # 发现器
53
+ "ModelDiscover",
54
+ "CommandDiscover",
55
+ "RoutesDiscover",
56
+ # 配置
57
+ "DefaultSettings",
58
+ ]
faster_api/base.py ADDED
@@ -0,0 +1,129 @@
1
+ import os
2
+ import importlib.util
3
+ import inspect
4
+ from typing import Dict, List
5
+
6
+
7
+ class DiscoverBase(object):
8
+ INSTANCE_TYPE = None
9
+ TARGETS: List[Dict[str, str]] = []
10
+
11
+ def discover(self) -> List[type]:
12
+ """
13
+ 自动扫描 TARGETS 中的目录和文件,
14
+ 导出所有的实例
15
+ """
16
+ instances = []
17
+
18
+ # 扫描 TARGETS 中的目录和文件
19
+ for target in self.TARGETS:
20
+ instances.extend(
21
+ self.scan(
22
+ directory=target.get("directory"),
23
+ filename=target.get("filename"),
24
+ skip_files=target.get("skip_files"),
25
+ skip_dirs=target.get("skip_dirs"),
26
+ )
27
+ )
28
+ # print(instances)
29
+ return instances
30
+
31
+ def walk(
32
+ self,
33
+ directory: str,
34
+ filename: str = None,
35
+ skip_files: List[str] = [],
36
+ skip_dirs: List[str] = [],
37
+ ) -> List[str]:
38
+ """
39
+ 遍历目录下的所有文件
40
+ """
41
+ results = []
42
+ if not os.path.exists(directory) or not os.path.isdir(directory):
43
+ return results
44
+
45
+ for root, dirs, files in os.walk(directory):
46
+ # 过滤掉需要跳过的目录,直接修改 dirs 列表来影响 os.walk 的遍历
47
+ dirs[:] = [d for d in dirs if d not in skip_dirs]
48
+
49
+ for file in files:
50
+ if filename is None or file == filename:
51
+ if file in skip_files:
52
+ continue
53
+ # 只处理 .py 文件
54
+ if file.endswith(".py"):
55
+ results.append(os.path.join(root, file))
56
+ return results
57
+
58
+ def scan(
59
+ self,
60
+ directory: str,
61
+ filename: str = None,
62
+ skip_files: List[str] = [],
63
+ skip_dirs: List[str] = [],
64
+ ) -> List[type]:
65
+ """
66
+ 通用扫描方法
67
+
68
+ Args:
69
+ directory: 要扫描的目录路径
70
+ filename: 要扫描的具体文件名,如果为 None 则扫描目录下所有 .py 文件
71
+ skip_files: 要跳过的文件列表
72
+ skip_dirs: 要跳过的目录列表
73
+ Returns:
74
+ 扫描到的所有实例列表
75
+ """
76
+ instances = []
77
+
78
+ files = self.walk(directory, filename, skip_files, skip_dirs)
79
+
80
+ for file in files:
81
+ instances.extend(
82
+ self.import_and_extract_instances(file, file.split("/")[-1][:-3])
83
+ )
84
+
85
+ return instances
86
+
87
+ def import_and_extract_instances(
88
+ self, file_path: str, module_name: str
89
+ ) -> List[type]:
90
+ """
91
+ 导入模块并提取实例
92
+
93
+ Args:
94
+ file_path: 文件路径
95
+ module_name: 模块名称
96
+
97
+ Returns:
98
+ 提取到的实例列表
99
+ """
100
+ instances = []
101
+
102
+ try:
103
+ # 动态导入模块
104
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
105
+ if spec is None or spec.loader is None:
106
+ return instances
107
+
108
+ module = importlib.util.module_from_spec(spec)
109
+ spec.loader.exec_module(module)
110
+
111
+ # 查找模块中所有的类并实例化
112
+ for _, obj in inspect.getmembers(module):
113
+ if (
114
+ inspect.isclass(obj)
115
+ and issubclass(obj, self.INSTANCE_TYPE)
116
+ and obj != self.INSTANCE_TYPE
117
+ ):
118
+ try:
119
+ # 实例化命令类
120
+ instance = obj()
121
+ instances.append(instance)
122
+ except Exception as e:
123
+ print(f"Warning: Failed to instantiate {obj.__name__}: {e}")
124
+
125
+ except Exception as e:
126
+ # 静默跳过导入失败的模块,避免阻断整个发现过程
127
+ print(f"Warning: Failed to import instances from {module_name}: {e}")
128
+
129
+ return instances
@@ -0,0 +1,14 @@
1
+ """
2
+ 命令行工具模块
3
+
4
+ 提供了基于 Fire 的命令行工具基类和自动发现功能
5
+ """
6
+
7
+ from .base import CommandBase, with_db_init
8
+ from .discover import CommandDiscover
9
+
10
+ __all__ = [
11
+ "CommandBase",
12
+ "with_db_init",
13
+ "CommandDiscover",
14
+ ]
@@ -0,0 +1,85 @@
1
+ """
2
+ 命令基类, 使用 fire 库管理子命令
3
+ """
4
+
5
+ import inspect
6
+ from functools import wraps
7
+ from tortoise import Tortoise
8
+ from faster_api.db import tortoise_init
9
+
10
+
11
+ def with_db_init(func):
12
+ """装饰器:为异步方法自动初始化和关闭数据库连接"""
13
+
14
+ @wraps(func)
15
+ async def wrapper(*args, **kwargs):
16
+ # 初始化数据库连接
17
+ await tortoise_init()
18
+ try:
19
+ # 执行原方法
20
+ result = await func(*args, **kwargs)
21
+ return result
22
+ finally:
23
+ # 关闭数据库连接
24
+ if Tortoise._inited:
25
+ await Tortoise.close_connections()
26
+
27
+ return wrapper
28
+
29
+
30
+ class CommandBase(object):
31
+ """命令基类"""
32
+
33
+ # 默认要去掉的后缀列表
34
+ DEFAULT_SUFFIXES = [
35
+ "Command",
36
+ "Commands",
37
+ "Handler",
38
+ "Handlers",
39
+ "Operations",
40
+ "Operation",
41
+ ]
42
+
43
+ def __getattribute__(self, name):
44
+ """自动为异步方法添加数据库初始化装饰器"""
45
+ attr = object.__getattribute__(self, name)
46
+
47
+ # 如果是方法且是异步的,自动添加数据库初始化装饰器
48
+ if (
49
+ inspect.iscoroutinefunction(attr)
50
+ and not name.startswith("_")
51
+ and not hasattr(attr, "_db_wrapped")
52
+ ):
53
+ wrapped_attr = with_db_init(attr)
54
+ wrapped_attr._db_wrapped = True # 标记已包装,避免重复包装
55
+ return wrapped_attr
56
+
57
+ return attr
58
+
59
+ @classmethod
60
+ def get_command_name(cls, class_name: str = None, suffixes: list = None) -> str:
61
+ """
62
+ 自动去除类名中的常见后缀,生成简洁的命令名
63
+
64
+ Args:
65
+ class_name: 类名,如果不提供则使用当前类的名称
66
+ suffixes: 要去除的后缀列表,如果不提供则使用默认后缀
67
+
68
+ Returns:
69
+ 去除后缀后的命令名(小写)
70
+ """
71
+ if class_name is None:
72
+ class_name = cls.__name__
73
+
74
+ if suffixes is None:
75
+ suffixes = cls.DEFAULT_SUFFIXES
76
+
77
+ # 按照后缀长度从长到短排序,优先匹配较长的后缀
78
+ sorted_suffixes = sorted(suffixes, key=len, reverse=True)
79
+
80
+ for suffix in sorted_suffixes:
81
+ if class_name.endswith(suffix):
82
+ return class_name[: -len(suffix)].lower()
83
+
84
+ # 如果没有匹配的后缀,直接返回小写的类名
85
+ return class_name.lower()
@@ -0,0 +1,3 @@
1
+ """
2
+ 内置命令模块
3
+ """
@@ -0,0 +1,137 @@
1
+ """系统内置命令"""
2
+
3
+ import os
4
+ import shutil
5
+ from faster_api.commands.base import CommandBase
6
+ from rich.console import Console
7
+ from faster_api.settings.builtins.settings import DefaultSettings
8
+ from faster_api.models.discover import ModelDiscover
9
+ from aerich import Command
10
+
11
+ console = Console()
12
+
13
+
14
+ class DBOperations(CommandBase):
15
+ """数据库操作命令 - 使用Aerich管理数据库迁移"""
16
+
17
+ def __init__(self, fake: bool = False):
18
+ self.fake = fake
19
+ self.command = Command(
20
+ tortoise_config=self._get_tortoise_config(), app="aerich"
21
+ )
22
+
23
+ def _get_tortoise_config(self):
24
+ """获取Tortoise ORM配置"""
25
+ apps_models = ModelDiscover().discover()
26
+ # print("--->", apps_models) # 注释掉调试输出
27
+ configs = DefaultSettings()
28
+ tortoise_config = configs.TORTOISE_ORM.copy()
29
+
30
+ # 清空原有的apps配置
31
+ tortoise_config["apps"] = {}
32
+
33
+ # 为每个app创建配置
34
+ for app_name, models in apps_models.items():
35
+ tortoise_config["apps"][app_name] = {
36
+ "models": models,
37
+ "default_connection": "development" if configs.DEBUG else "production",
38
+ }
39
+
40
+ # 添加aerich模型到一个单独的app中
41
+ tortoise_config["apps"]["aerich"] = {
42
+ "models": ["aerich.models"],
43
+ "default_connection": "development" if configs.DEBUG else "production",
44
+ }
45
+
46
+ return tortoise_config
47
+
48
+ async def init_db(self):
49
+ """初始化数据库(首次创建表)"""
50
+ try:
51
+ await self.command.init_db(safe=True)
52
+ console.print("✅ 数据库初始化成功")
53
+ except Exception as e:
54
+ console.print(f"❌ 数据库初始化失败: {e}")
55
+ finally:
56
+ await self.command.close()
57
+
58
+ async def migrate(self):
59
+ """执行数据库迁移"""
60
+ try:
61
+ await self.command.init()
62
+ await self.command.migrate()
63
+ console.print("✅ 数据库迁移执行成功")
64
+ except Exception as e:
65
+ console.print(f"❌ 数据库迁移执行失败: {e}")
66
+ finally:
67
+ await self.command.close()
68
+
69
+ async def upgrade(self):
70
+ """执行数据库迁移"""
71
+ try:
72
+ await self.command.init()
73
+ await self.command.upgrade(fake=self.fake)
74
+ console.print("✅ 数据库迁移执行成功")
75
+ except Exception as e:
76
+ console.print(f"❌ 数据库迁移执行失败: {e}")
77
+ finally:
78
+ await self.command.close()
79
+
80
+ async def downgrade(self, version: int = -1):
81
+ """回滚数据库迁移"""
82
+ try:
83
+ await self.command.init()
84
+ await self.command.downgrade(version=version, delete=True, fake=self.fake)
85
+ console.print("✅ 数据库回滚成功")
86
+ except Exception as e:
87
+ console.print(f"❌ 数据库回滚失败: {e}")
88
+ finally:
89
+ await self.command.close()
90
+
91
+ async def history(self):
92
+ """查看迁移历史"""
93
+ try:
94
+ await self.command.init()
95
+ history = await self.command.history()
96
+ console.print("✅ 迁移历史:")
97
+ for record in history:
98
+ console.print(f" - {record}")
99
+
100
+ except Exception as e:
101
+ console.print(f"❌ 查看迁移历史失败: {e}")
102
+ finally:
103
+ await self.command.close()
104
+
105
+ async def heads(self):
106
+ """查看当前迁移头部"""
107
+ try:
108
+ await self.command.init()
109
+ heads = await self.command.heads()
110
+ console.print("✅ 当前迁移头部:")
111
+ for record in heads:
112
+ console.print(f" - {record}")
113
+ except Exception as e:
114
+ console.print(f"❌ 查看当前迁移头部失败: {e}")
115
+ finally:
116
+ await self.command.close()
117
+
118
+ async def dev_clean(self):
119
+ """清理开发环境数据
120
+ 1. 移除 sqlite 数据库文件
121
+ 2. 移除 aerich 迁移记录
122
+ """
123
+ try:
124
+ # 删除数据库文件
125
+ db_file = f"{configs.DB_DATABASE}.db"
126
+ if os.path.exists(db_file):
127
+ os.remove(db_file)
128
+ console.print(f"✅ 已删除数据库文件: {db_file}")
129
+
130
+ # 递归删除 migrations 目录
131
+ if os.path.exists("migrations"):
132
+ shutil.rmtree("migrations")
133
+ console.print("✅ 已删除迁移目录: migrations")
134
+
135
+ console.print("✅ 开发环境数据清理成功")
136
+ except Exception as e:
137
+ console.print(f"❌ 清理开发环境数据失败: {e}")
@@ -0,0 +1,87 @@
1
+ from contextlib import asynccontextmanager
2
+ from fastapi import FastAPI
3
+ from tortoise import Tortoise
4
+ from faster_api.settings.builtins.settings import DefaultSettings
5
+ from rich.console import Console
6
+ from starlette.staticfiles import StaticFiles
7
+ from faster_api.commands.base import CommandBase
8
+ from faster_api.db import tortoise_init
9
+ import uvicorn
10
+ import threading
11
+
12
+ from faster_api.routes.discover import RoutesDiscover
13
+
14
+ console = Console()
15
+
16
+
17
+ @asynccontextmanager
18
+ async def lifespan(app: FastAPI):
19
+ await tortoise_init()
20
+ yield
21
+ await Tortoise.close_connections()
22
+
23
+
24
+ class FastAPIAppSingleton:
25
+ """线程安全的FastAPI应用单例类"""
26
+
27
+ _instance = None
28
+ _lock = threading.Lock()
29
+
30
+ def __new__(cls):
31
+ if cls._instance is None:
32
+ with cls._lock:
33
+ # 双重检查锁定模式
34
+ if cls._instance is None:
35
+ cls._instance = cls._create_app()
36
+ return cls._instance
37
+
38
+ @classmethod
39
+ def _create_app(cls):
40
+ """创建FastAPI应用实例"""
41
+ # 创建FastAPI应用实例
42
+ configs = DefaultSettings()
43
+ app = FastAPI(
44
+ title=configs.PROJECT_NAME,
45
+ version=configs.VERSION,
46
+ debug=configs.DEBUG,
47
+ lifespan=lifespan,
48
+ docs_url=None,
49
+ redoc_url=None,
50
+ )
51
+
52
+ # 添加静态文件服务器
53
+ try:
54
+ import os
55
+
56
+ static_dir = os.path.join(os.path.dirname(__file__), "..", "..", "statics")
57
+ app.mount("/static", StaticFiles(directory=static_dir), name="static")
58
+ except Exception as e:
59
+ console.print(f"静态文件服务器启动失败: {e}")
60
+
61
+ # 添加路由
62
+ routes = RoutesDiscover().discover()
63
+ for route in routes:
64
+ app.include_router(route)
65
+
66
+ return app
67
+
68
+
69
+ app = FastAPIAppSingleton()
70
+
71
+
72
+ class FastApiOperations(CommandBase):
73
+ def __init__(self, host: str = None, port: int = None):
74
+ configs = DefaultSettings()
75
+ self.host = host or configs.HOST
76
+ self.port = port or configs.PORT
77
+ self.configs = configs
78
+
79
+ def start(self):
80
+ # 启动服务
81
+ reload = True if self.configs.DEBUG else False
82
+ uvicorn.run(
83
+ "faster_api.commands.builtins.fastapi:app",
84
+ host=self.host,
85
+ port=self.port,
86
+ reload=reload,
87
+ )
@@ -0,0 +1,24 @@
1
+ """
2
+ 自动发现 apps 目录下的 commands 模块和内置命令
3
+ """
4
+
5
+ from faster_api.commands.base import CommandBase
6
+ from faster_api.base import DiscoverBase
7
+
8
+
9
+ class CommandDiscover(DiscoverBase):
10
+ INSTANCE_TYPE = CommandBase
11
+ TARGETS = [
12
+ {
13
+ "directory": "apps",
14
+ "filename": "commands.py",
15
+ "skip_dirs": ["__pycache__"],
16
+ "skip_files": [],
17
+ },
18
+ {
19
+ "directory": "commands/builtins",
20
+ "filename": None,
21
+ "skip_dirs": ["__pycache__"],
22
+ "skip_files": [],
23
+ },
24
+ ]
faster_api/db.py ADDED
@@ -0,0 +1,32 @@
1
+ from tortoise import Tortoise
2
+ from faster_api.models.discover import ModelDiscover
3
+
4
+
5
+ async def tortoise_init(tortoise_config: dict = None):
6
+ """
7
+ 初始化Tortoise ORM
8
+
9
+ Args:
10
+ tortoise_config: Tortoise ORM 配置字典,如果不提供则使用默认配置
11
+ """
12
+ if not Tortoise._inited:
13
+ if tortoise_config is None:
14
+ # 提供一个默认的配置示例
15
+ tortoise_config = {
16
+ "connections": {"default": "sqlite://db.sqlite3"},
17
+ "apps": {},
18
+ "use_tz": False,
19
+ "timezone": "UTC",
20
+ }
21
+
22
+ # 自动发现模型并添加到配置中
23
+ apps_models = ModelDiscover().discover()
24
+
25
+ # 为每个app创建配置
26
+ for app_name, models in apps_models.items():
27
+ tortoise_config["apps"][app_name] = {
28
+ "models": models,
29
+ "default_connection": "default",
30
+ }
31
+
32
+ await Tortoise.init(config=tortoise_config)
@@ -0,0 +1,21 @@
1
+ """
2
+ 数据库模型模块
3
+
4
+ 提供了基于 Tortoise ORM 的模型基类和自动发现功能
5
+ """
6
+
7
+ from .base import (
8
+ UUIDModel,
9
+ DateTimeModel,
10
+ StatusModel,
11
+ ScopeModel,
12
+ )
13
+ from .discover import ModelDiscover
14
+
15
+ __all__ = [
16
+ "UUIDModel",
17
+ "DateTimeModel",
18
+ "StatusModel",
19
+ "ScopeModel",
20
+ "ModelDiscover",
21
+ ]