Files
aaPanel/class/public/aaModel/model.py
T
Jack 8eadc94c98 Update to 7.32.0
1. Added: Mail Server - Add Domain with Domain management center to Automatic DNS record & apply SSL
2. Added: Mail Server - DNS record with One-click Parsing
3. Added: Mail Server - Compatible with Domain management center to apply for renewal SSL
4. Added: Mail Server - Mail Marketing - Add Template supports Upload HTML
5. Added: bt default command supports displaying separate IPV4, IPV6 and bound domain info
6. Added: Settings - Panel SSL supports using Domains SSL Certificate
7. Added: PHP Project - Automatic Add Record & SSL with progress display
8. Fixed: Mail Server quota limit issue
9. Fixed: WP Toolkit incorrect cache directory issue
10. Optimized: Enhanced domain validation in the Domain module
11. Fixed: Issue with automatic domain resolution prompts in Mail Server editing
12. Fixed: "Internal Server Error" occurring in certain scenarios
2025-04-15 17:11:22 +08:00

280 lines
9.6 KiB
Python

# coding: utf-8
import copy
from typing import Self, Generator, Optional, Dict, Any
from .fields import aaField, COMPARE
from .manager import aaManager
__all__ = ["aaModel"]
from public.exceptions import HintException
def generate_table_name(class_name: str) -> str:
"""
驼峰名转表名
"""
return ''.join(['_' + c.lower() if c.isupper() else c for c in class_name]).lstrip('_')
class aaMetaClass(type):
__abstract__: bool
__db_name__: str
__table_name__: str
__fields__: dict
__primary_key__: str
__serializes__: dict
__index_keys__: list
# __foreign_keys__: object
def __new__(cls, name, bases, attrs):
if attrs.get("__abstract__") is True:
return super().__new__(cls, name, bases, attrs)
attrs.update({"__abstract__": False})
new_class = super().__new__(cls, name, bases, attrs)
cls.__fields_process(obj=new_class, name=name, attrs=attrs)
cls.__database_process(obj=new_class, name=name, attrs=attrs)
return new_class
def __setattr__(cls, key, value):
if key == '__abstract__':
raise AttributeError("can't set attribute '__abstract__'")
return super().__setattr__(key, value)
@classmethod
def __fields_process(cls, obj: "aaMetaClass", name: str, attrs: dict):
pk = ""
fields = {}
for k, v in attrs.items():
if isinstance(v, aaField):
if k in fields:
raise HintException(f"model {name} field '{k}' is already defined")
if k in COMPARE:
raise HintException(f"model {name} field '{k}' is compare field, please change the name")
if k.startswith("_"):
raise HintException(f"model {name} field '{k}' is not support start with '_'")
fields[k] = v
if v.primary_key:
if pk:
raise HintException(f"model {name} can only have one primary key")
else:
pk = k
if not pk:
raise HintException(f"sth wrong with {name}'s primary key, please check the model")
setattr(obj, "__primary_key__", pk)
setattr(obj, "__fields__", fields)
setattr(obj, "__serializes__", cls.__get_serialized(fields))
@classmethod
def __database_process(cls, obj: "aaMetaClass", name: str, attrs: dict):
db_name, tb_name, idx = "default", generate_table_name(name), []
meta = attrs.get("_Meta")
if meta:
if hasattr(meta, "db_name"):
db_name = meta.db_name
if hasattr(meta, "table_name"):
tb_name = meta.table_name
if hasattr(meta, "index"):
idx = meta.index
setattr(obj, "__db_name__", db_name)
setattr(obj, "__table_name__", tb_name)
setattr(obj, "__index_keys__", idx)
@staticmethod
def __get_serialized(fields: Dict[str, aaField]) -> Dict[str, aaField]:
return dict(filter(
lambda x: x[1].serialized is not None, {**fields}.items()
))
class aaCusModel(metaclass=aaMetaClass):
__abstract__ = True
objects = aaManager()
def __init__(self, **kwargs):
if self.__abstract__ is True:
raise RuntimeError(f'{self.__class__.__name__} class can not be init')
self._field_filter = kwargs.pop("_field_filter", None)
for f, v in self._generate_init(kwargs):
setattr(self, f.field_name, v)
def _generate_init(self, val_data: dict) -> Generator:
for name, field in {**self.__class__.__fields__}.items():
val = val_data.pop(name) if name in val_data else field.get_default_val()
if field.primary_key is True and val == 0:
continue # skip default id val
if field.primary_key is True and val != 0:
try:
val = int(val)
except Exception:
pass
yield field, val
if val_data: # other field
# raise AttributeError(f"model '{self.__class__.__name__}' has no field {val_data}")
pass
class aaModel(aaCusModel):
"""
基础模型
:example:
class MyTestModel(aaModel):
id = IntField(primary_key=True)
name = StrField(ps="名字")
status = BoolField(default=True, ps="状态")
float_number = FloatField(default=0.05, ps="浮点")
class _Meta:
db_name = "default" 默认为 default.db 文件
table_name = "my_table" 默认为类名驼峰转表名 my_test_model
index = [("status", 1)] 索引
"""
__abstract__: bool = True
__destroyed: bool = False
id: int = None
def __repr__(self):
return f"<'{self.__class__.__name__}' Model Object, {self.as_dict()}>"
@staticmethod
def check_destroyed(func):
def wrapper(self, *args, **kwargs):
if getattr(self, "__destroyed", False):
raise RuntimeError(f"Cannot call {func.__name__}() on destroyed object")
return func(self, *args, **kwargs)
return wrapper
@classmethod
@check_destroyed
def _output(cls, data: dict, _field_filter=None) -> dict:
if not _field_filter:
return {
k: v if not cls.__serializes__.get(k) else cls.__serializes__.get(k).serialized(v, False) for k, v in
data.items()
}
else:
return {
k: v if not cls.__serializes__.get(k) else cls.__serializes__.get(k).serialized(v, False) for k, v in
data.items() if k in _field_filter
}
@classmethod
@check_destroyed
def _serialized_data(cls, data: Optional[dict | list], _field_filter=None) -> Optional[dict | list]:
if not data or not hasattr(cls, "__serializes__"):
return data
if isinstance(data, list):
return [cls._output(d, _field_filter) for d in data]
elif isinstance(data, dict):
return cls._output(data, _field_filter)
else:
return data
@check_destroyed
def _validate(self, raise_exp: bool = True) -> Optional[Dict[str, Any]]:
"""
模型验证
"""
body = {}
for f, cur_val in self._generate_init(copy.deepcopy(self.__dict__)):
try:
f.model_check_type(target=cur_val, raise_exp=True)
# 1, dynamic generated
if hasattr(f, "dynamic") and f.dynamic is True:
cur_val = f._dynamic(f, cur_val)
setattr(self, f.field_name, cur_val)
# 2, serialized
body[f.field_name] = f.serialized(cur_val, True) if f.serialized else cur_val
except TypeError as t:
if raise_exp:
raise t
else:
return {}
except Exception as e:
raise e
return body
def _before_save(self) -> bool:
# override
return True
def _after_save(self) -> None:
# override
pass
@check_destroyed
def save(self, raise_exp: bool = True) -> Optional[Self]:
"""
模型数据, 不存在则 保存 , 存在则 更新
:raise_exp 抛异常
:return: model object 字段类型异常等问题返回 None
"""
if self.__class__.__abstract__ is True:
raise RuntimeError(f'{self.__class__.__name__} class can not be save')
try:
cls = self.__class__
validate = self._validate(raise_exp=raise_exp)
primary_key = cls.__primary_key__
if validate and primary_key:
if primary_key in validate:
target_id = validate.pop(primary_key)
exist = cls.objects._query.where(f"{primary_key}=?", (target_id,)).exists()
if exist: # update
res = cls.objects._query.where(f"{primary_key}=?", (target_id,)).update(validate)
if res == 1:
self._after_save()
return self
else:
if raise_exp:
raise HintException("update error")
else:
return None
else:
validate[primary_key] = target_id
# save
if not self._before_save():
return None
new_id = cls.objects._query.insert(validate)
self._after_save()
return cls(**{primary_key: new_id, **self.__dict__})
return None
except TypeError as t:
if raise_exp:
raise t
else:
return None
except Exception as e:
raise HintException(e)
@check_destroyed
def delete(self) -> int:
try:
self.__class__.objects._query.where(
f"{self.__class__.__primary_key__}=?", (self.id,)
).delete()
setattr(self, "__destroyed", True)
except Exception as e:
print(e)
return 0
return 1
@check_destroyed
def as_dict(self) -> dict:
"""
转字典
"""
result = {}
field_filter = getattr(self, "_field_filter", None)
for k, v in self.__dict__.items():
if k.startswith("_"):
continue
if field_filter is not None and k not in field_filter:
continue
result[k] = v
return result