hframe 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.
hframe/__init__.py ADDED
@@ -0,0 +1,108 @@
1
+ """
2
+ hframe — 分层架构后端框架
3
+
4
+ 架构链路:View(可选) → Handler → Service → Repository → DB
5
+
6
+ 核心暴露:
7
+ - Config: 全局配置
8
+ - GenericHandler: HTTP 无关的业务编排层(级联/事务/权限)
9
+ - GenericService: 单实体生命周期(before/do/after 钩子)
10
+ - BaseRepository: 数据访问唯一出口
11
+
12
+ 工具暴露:
13
+ - util.crypto: md5, crypt, make_id
14
+ - util.data: build_tree, copy_dict, check_data_type
15
+ - util.format: to_snake_case, to_camel_case, json_dumps, Serialize
16
+ - util.transform: is_integer, set_default_int, trans_str_to_arr
17
+ - util.yaml_util: read_yaml_file, write_yaml_file
18
+ - util.auth: UserService(登录/注册/身份)
19
+ - util.verify: CodeService(验证码)
20
+ """
21
+
22
+ __version__ = "0.1.0"
23
+
24
+ # ========== 核心配置 ==========
25
+ from .config import Config
26
+
27
+ # ========== 类型化异常 ==========
28
+ from .exceptions import (
29
+ HFrameException,
30
+ ValidationException,
31
+ UniqueValidationException,
32
+ NotFoundException,
33
+ UnauthorizedException,
34
+ PermissionException,
35
+ ServiceException,
36
+ ConflictException,
37
+ ForbiddenOperationException,
38
+ RepositoryException,
39
+ InfrastructureException,
40
+ is_exception,
41
+ get_exception_code,
42
+ )
43
+
44
+ # ========== 日志 ==========
45
+ from .logging import LogUtil
46
+
47
+ # ========== Repository 层 ==========
48
+ from .repository import BaseRepository, VersionRepository, ListFilters, Filter, FilterOp
49
+
50
+ # ========== Service 层 ==========
51
+ from .service import GenericService, ServiceConfig, ServiceHooks, CrudRequest, VersionFieldMapping
52
+
53
+ # ========== Handler 层(核心暴露) ==========
54
+ from .handler import (
55
+ GenericHandler,
56
+ HandlerConfig,
57
+ HandlerHooks,
58
+ CascadeRelation,
59
+ ReferenceRelation,
60
+ ChildRefRelation,
61
+ GetRequest,
62
+ MapRequest,
63
+ RequestFactory,
64
+ )
65
+
66
+ # ========== 工具层 ==========
67
+ from .util import UserService, CodeService
68
+ from .util import crypto, data, format, transform, yaml_util
69
+
70
+ # ========== 注册表 ==========
71
+ from .registry import Registry, EntityConfig
72
+
73
+ # ========== Django View 层(可选) ==========
74
+ try:
75
+ from .view import (
76
+ BaseView, ModuleView, ListView, DetailView, EditView, DeleteView,
77
+ H5BaseView, H5ModuleView, H5ListView, H5DetailView, H5EditView, H5DeleteView, H5Mixin,
78
+ )
79
+ except ImportError:
80
+ pass
81
+
82
+ __all__ = [
83
+ # 版本
84
+ '__version__',
85
+ # 配置
86
+ 'Config',
87
+ # 异常
88
+ 'HFrameException', 'ValidationException', 'UniqueValidationException',
89
+ 'NotFoundException', 'UnauthorizedException', 'PermissionException',
90
+ 'ServiceException', 'ConflictException', 'ForbiddenOperationException',
91
+ 'RepositoryException', 'InfrastructureException',
92
+ 'is_exception', 'get_exception_code',
93
+ # 日志
94
+ 'LogUtil',
95
+ # Repository
96
+ 'BaseRepository', 'VersionRepository', 'ListFilters', 'Filter', 'FilterOp',
97
+ # Service
98
+ 'GenericService', 'ServiceConfig', 'ServiceHooks', 'CrudRequest', 'VersionFieldMapping',
99
+ # Handler(核心)
100
+ 'GenericHandler', 'HandlerConfig', 'HandlerHooks',
101
+ 'CascadeRelation', 'ReferenceRelation', 'ChildRefRelation',
102
+ 'GetRequest', 'MapRequest', 'RequestFactory',
103
+ # 注册表
104
+ 'Registry', 'EntityConfig',
105
+ # 工具
106
+ 'UserService', 'CodeService',
107
+ 'crypto', 'data', 'format', 'transform', 'yaml_util',
108
+ ]
hframe/config.py ADDED
@@ -0,0 +1,107 @@
1
+ """
2
+ hframe 配置模块
3
+
4
+ 集中管理所有配置:数据库、缓存、应用基础信息等。
5
+ 支持从 YAML 文件加载并深度合并默认配置。
6
+ """
7
+
8
+ import os
9
+ import yaml
10
+ from typing import Any, Dict
11
+
12
+
13
+ # 默认配置(可作为开发环境的兜底)
14
+ _DEFAULTS: Dict[str, Any] = {
15
+ "BASE_INFO": {
16
+ "secret_key": "py_crux_default_secret",
17
+ "datetime_format": "%Y-%m-%d %H:%M:%S",
18
+ "auth_token_expire_time": 3600,
19
+ "admin_role_id": -1,
20
+ "check_page_permission": 0,
21
+ "user_tables": {},
22
+ },
23
+ "MYSQL_INFO": {
24
+ "host": "localhost",
25
+ "port": 3306,
26
+ "user": "root",
27
+ "password": "",
28
+ "database": "test",
29
+ "charset": "utf8mb4",
30
+ },
31
+ "REDIS_INFO": {
32
+ "host": "localhost",
33
+ "port": 6379,
34
+ "password": "",
35
+ "db": 0,
36
+ },
37
+ "SSH_INFO": {},
38
+ "WECHAT_INFO": {},
39
+ }
40
+
41
+
42
+ def _deep_merge(source: Dict, dest: Dict) -> Dict:
43
+ """深度合并字典,source 覆盖 dest"""
44
+ for key, value in source.items():
45
+ if key in dest and isinstance(value, dict) and isinstance(dest[key], dict):
46
+ _deep_merge(value, dest[key])
47
+ else:
48
+ dest[key] = value
49
+ return dest
50
+
51
+
52
+ class Config:
53
+ """
54
+ 全局配置容器
55
+
56
+ 使用方式:
57
+ Config.BASE_INFO['secret_key']
58
+ Config.MYSQL_INFO['host']
59
+
60
+ 初始化:
61
+ Config.load('config.yaml') # 从 YAML 加载并深度合并默认值
62
+ """
63
+
64
+ BASE_INFO: Dict[str, Any] = dict(_DEFAULTS["BASE_INFO"])
65
+ MYSQL_INFO: Dict[str, Any] = dict(_DEFAULTS["MYSQL_INFO"])
66
+ REDIS_INFO: Dict[str, Any] = dict(_DEFAULTS["REDIS_INFO"])
67
+ SSH_INFO: Dict[str, Any] = dict(_DEFAULTS["SSH_INFO"])
68
+ WECHAT_INFO: Dict[str, Any] = dict(_DEFAULTS["WECHAT_INFO"])
69
+
70
+ @classmethod
71
+ def load(cls, file_path: str = "config.yaml"):
72
+ """
73
+ 从 YAML 文件加载配置,与默认值深度合并
74
+
75
+ Args:
76
+ file_path: YAML 配置文件路径,支持相对路径和绝对路径
77
+ """
78
+ if not os.path.exists(file_path):
79
+ return
80
+
81
+ with open(file_path, 'r', encoding='utf-8') as f:
82
+ user_config = yaml.safe_load(f) or {}
83
+
84
+ # 深度合并:用户配置覆盖默认值
85
+ for section in ("BASE_INFO", "MYSQL_INFO", "REDIS_INFO", "SSH_INFO", "WECHAT_INFO"):
86
+ if section in user_config:
87
+ _deep_merge(user_config[section], cls.__dict__[section])
88
+
89
+ @classmethod
90
+ def reset(cls):
91
+ """重置为默认配置"""
92
+ cls.BASE_INFO = dict(_DEFAULTS["BASE_INFO"])
93
+ cls.MYSQL_INFO = dict(_DEFAULTS["MYSQL_INFO"])
94
+ cls.REDIS_INFO = dict(_DEFAULTS["REDIS_INFO"])
95
+ cls.SSH_INFO = dict(_DEFAULTS["SSH_INFO"])
96
+ cls.WECHAT_INFO = dict(_DEFAULTS["WECHAT_INFO"])
97
+
98
+ @classmethod
99
+ def get(cls, key: str, default: Any = None) -> Any:
100
+ """快捷取值:Config.get('BASE_INFO.secret_key')"""
101
+ parts = key.split('.')
102
+ obj = cls
103
+ for part in parts:
104
+ obj = getattr(obj, part, None) if hasattr(obj, part) else (obj.get(part) if isinstance(obj, dict) else None)
105
+ if obj is None:
106
+ return default
107
+ return obj
hframe/exceptions.py ADDED
@@ -0,0 +1,126 @@
1
+ """
2
+ hframe 类型化异常体系
3
+
4
+ 借鉴 Go 项目哨兵错误模式,用 Python 的异常类继承体系实现。
5
+ 每个异常类型携带业务码和消息,支持精确匹配(类比 errors.Is())。
6
+ """
7
+
8
+
9
+ class HFrameException(Exception):
10
+ """框架基础异常"""
11
+
12
+ def __init__(self, message: str = "", code: int = 500):
13
+ self.message = message
14
+ self.code = code
15
+ super().__init__(message)
16
+
17
+ def __repr__(self):
18
+ return f"{self.__class__.__name__}(code={self.code}, message={self.message})"
19
+
20
+
21
+ # ============================================================
22
+ # 校验类异常(客户端可修正)
23
+ # ============================================================
24
+
25
+ class ValidationException(HFrameException):
26
+ """数据校验失败"""
27
+
28
+ def __init__(self, message: str = "数据校验失败"):
29
+ super().__init__(message=message, code=400)
30
+
31
+
32
+ class UniqueValidationException(HFrameException):
33
+ """唯一性校验失败"""
34
+
35
+ def __init__(self, message: str = "唯一性校验失败"):
36
+ super().__init__(message=message, code=409)
37
+
38
+
39
+ class NotFoundException(HFrameException):
40
+ """资源不存在"""
41
+
42
+ def __init__(self, message: str = "资源不存在"):
43
+ super().__init__(message=message, code=404)
44
+
45
+
46
+ # ============================================================
47
+ # 权限/认证类异常
48
+ # ============================================================
49
+
50
+ class UnauthorizedException(HFrameException):
51
+ """未认证(需要登录)"""
52
+
53
+ def __init__(self, message: str = "未认证"):
54
+ super().__init__(message=message, code=401)
55
+
56
+
57
+ class PermissionException(HFrameException):
58
+ """无权限(认证了但权限不足)"""
59
+
60
+ def __init__(self, message: str = "权限不足"):
61
+ super().__init__(message=message, code=403)
62
+
63
+
64
+ # ============================================================
65
+ # 业务类异常
66
+ # ============================================================
67
+
68
+ class ServiceException(HFrameException):
69
+ """Service 层业务异常(通用)"""
70
+
71
+ def __init__(self, message: str = "服务异常"):
72
+ super().__init__(message=message, code=500)
73
+
74
+
75
+ class ConflictException(HFrameException):
76
+ """状态冲突(如版本已发布)"""
77
+
78
+ def __init__(self, message: str = "状态冲突"):
79
+ super().__init__(message=message, code=409)
80
+
81
+
82
+ class ForbiddenOperationException(HFrameException):
83
+ """操作被禁止(业务规则限制)"""
84
+
85
+ def __init__(self, message: str = "操作被禁止"):
86
+ super().__init__(message=message, code=403)
87
+
88
+
89
+ # ============================================================
90
+ # 基础设施类异常
91
+ # ============================================================
92
+
93
+ class RepositoryException(HFrameException):
94
+ """Repository 层数据访问异常"""
95
+
96
+ def __init__(self, message: str = "数据访问异常"):
97
+ super().__init__(message=message, code=500)
98
+
99
+
100
+ class InfrastructureException(HFrameException):
101
+ """基础设施异常(DB连接、Redis连接等)"""
102
+
103
+ def __init__(self, message: str = "基础设施异常"):
104
+ super().__init__(message=message, code=500)
105
+
106
+
107
+ # ============================================================
108
+ # 辅助:异常匹配函数(类比 Go 的 errors.Is)
109
+ # ============================================================
110
+
111
+ def is_exception(exc: Exception, target_type: type) -> bool:
112
+ """
113
+ 判断异常是否为指定类型(含子类)
114
+
115
+ 用法:
116
+ if is_exception(e, NotFoundException):
117
+ # 处理未找到的情况
118
+ """
119
+ return isinstance(exc, target_type)
120
+
121
+
122
+ def get_exception_code(exc: Exception) -> int:
123
+ """获取异常的业务码(非 HFrameException 返回 500)"""
124
+ if isinstance(exc, HFrameException):
125
+ return exc.code
126
+ return 500
@@ -0,0 +1,13 @@
1
+ """Handler 层 — HTTP 无关的业务编排层"""
2
+
3
+ from .config import HandlerConfig, CascadeRelation, ReferenceRelation, ChildRefRelation
4
+ from .generic import GenericHandler
5
+ from .hooks import HandlerHooks
6
+ from .request import GetRequest, MapRequest, RequestFactory
7
+
8
+ __all__ = [
9
+ 'HandlerConfig', 'CascadeRelation', 'ReferenceRelation', 'ChildRefRelation',
10
+ 'GenericHandler',
11
+ 'HandlerHooks',
12
+ 'GetRequest', 'MapRequest', 'RequestFactory',
13
+ ]
@@ -0,0 +1,108 @@
1
+ """
2
+ Handler 配置
3
+
4
+ 定义 Handler 层的配置项:级联关系、请求工厂、认证/权限钩子等。
5
+ 借鉴 Go 项目 HandlerConfig[M] 的设计,去掉泛型和 gin 绑定。
6
+ """
7
+
8
+ from typing import Any, Callable, Dict, List, Optional, Tuple
9
+
10
+
11
+ class CascadeRelation:
12
+ """
13
+ 级联关系声明(向下:父→子)
14
+
15
+ 配置后,Handler 在 Create/Delete/Get/List 时自动处理子表联动。
16
+ """
17
+
18
+ def __init__(self, *,
19
+ child_table: str,
20
+ parent_fk_field: str,
21
+ child_fk_field: str,
22
+ child_columns: List[str] = None,
23
+ show_name: str = "",
24
+ on_delete: str = "cascade"): # cascade | soft_delete | restrict
25
+ self.child_table = child_table
26
+ self.parent_fk_field = parent_fk_field # 父表中指向子表的FK字段名
27
+ self.child_fk_field = child_fk_field # 子表中指向父表的FK字段名
28
+ self.child_columns = child_columns or ["id", "name"]
29
+ self.show_name = show_name or child_table
30
+ self.on_delete = on_delete
31
+
32
+
33
+ class ReferenceRelation:
34
+ """
35
+ 向上级联声明(子→父)
36
+
37
+ 配置后,Get/List 查询时自动解析逻辑外键字段(如 site_ulid→site),
38
+ 并将解析结果挂到返回 dict 的对应键下。
39
+ """
40
+
41
+ def __init__(self, *,
42
+ parent_table: str,
43
+ fk_field: str, # 本实体中的外键字段名
44
+ parent_pk_field: str = "id",
45
+ parent_columns: List[str] = None,
46
+ show_name: str = "",
47
+ parent_service_name: str = ""):
48
+ self.parent_table = parent_table
49
+ self.fk_field = fk_field
50
+ self.parent_pk_field = parent_pk_field
51
+ self.parent_columns = parent_columns or ["id", "name"]
52
+ self.show_name = show_name or parent_table
53
+ self.parent_service_name = parent_service_name
54
+
55
+
56
+ class ChildRefRelation:
57
+ """
58
+ 向下引用声明(父→子 FK 列表)
59
+
60
+ 配置后,Get/List 查询时自动将 FK 列表(如 tag_ulids: [1,2,3])
61
+ 批量解析为完整对象列表。
62
+ 与 CascadeRelation 不同:仅关联已有实体,不参与级联创建/删除/更新。
63
+ """
64
+
65
+ def __init__(self, *,
66
+ child_table: str,
67
+ fk_list_field: str, # 父实体中存放子ID列表的字段名
68
+ child_pk_field: str = "id",
69
+ child_columns: List[str] = None,
70
+ show_name: str = "",
71
+ child_service_name: str = ""):
72
+ self.child_table = child_table
73
+ self.fk_list_field = fk_list_field
74
+ self.child_pk_field = child_pk_field
75
+ self.child_columns = child_columns or ["id", "name"]
76
+ self.show_name = show_name or child_table
77
+ self.child_service_name = child_service_name
78
+
79
+
80
+ class HandlerConfig:
81
+ """
82
+ Handler 配置
83
+
84
+ 每个实体类型创建一个 Handler 实例时附带一份配置。
85
+ """
86
+
87
+ def __init__(self, *,
88
+ entity_name: str = "",
89
+ cascades: List[CascadeRelation] = None,
90
+ references: List[ReferenceRelation] = None,
91
+ child_refs: List[ChildRefRelation] = None,
92
+ auth: Callable = None,
93
+ perm: Callable = None):
94
+ """
95
+ Args:
96
+ entity_name: 实体名(用于日志和错误信息)
97
+ cascades: 级联关系列表
98
+ references: 向上级联引用列表
99
+ child_refs: 向下子引用列表
100
+ auth: 认证钩子 Callable,接收请求上下文返回 user_info
101
+ perm: 权限校验钩子 Callable,接收 (user_info, resource, action) 返回 bool
102
+ """
103
+ self.entity_name = entity_name
104
+ self.cascades = cascades or []
105
+ self.references = references or []
106
+ self.child_refs = child_refs or []
107
+ self.auth = auth
108
+ self.perm = perm