Files
Django/gvsdsdk/model_base.py
2026-06-17 04:10:52 +08:00

484 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import typing
from typing import Any, Dict, List, Optional, Tuple, Type, Union
from django.db import models
from django.db.models import Q
from .fluent import FluentQuery
class classproperty:
def __init__(self, func):
self.fget = func
def __get__(self, obj, objtype=None):
return self.fget(objtype if objtype is not None else type(obj))
ABBREVIATIONS = {
'uuid': 'UUID',
'id': 'ID',
'ip': 'IP',
'url': 'URL',
'uri': 'URI',
'api': 'API',
'html': 'HTML',
'css': 'CSS',
'js': 'JS',
'json': 'JSON',
'xml': 'XML',
'sql': 'SQL',
'http': 'HTTP',
'https': 'HTTPS',
'smtp': 'SMTP',
'ssl': 'SSL',
'tls': 'TLS',
'ssh': 'SSH',
'ftp': 'FTP',
'2fa': '2FA',
'elt': 'ELT',
'sve': 'SVE',
't4': 'T4',
'mc': 'MC',
'crb': 'CRB',
'ccgo': 'CCGO',
'ugm': 'UGM',
'gerp': 'GERP',
'abac': 'ABAC',
'rbac': 'RBAC',
'smtm': 'SMTM',
'pwd': 'PWD',
}
def snake_to_pascal(name):
if '_' not in name:
return name[0].upper() + name[1:] if name else name
parts = name.split('_')
result = []
for p in parts:
lower = p.lower()
if lower in ABBREVIATIONS:
result.append(ABBREVIATIONS[lower])
else:
result.append(p.capitalize())
return ''.join(result)
def pascal_to_snake(name):
import re
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
s2 = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1)
parts = s2.lower().split('_')
return '_'.join(parts)
class _InvertedFieldExpression:
def __init__(self, field_expr):
self._field_expr = field_expr
def startswith(self, value):
return ~Q(**{f'{self._field_expr._django_name}__startswith': value})
def contains(self, value):
return ~Q(**{f'{self._field_expr._django_name}__contains': value})
def like(self, pattern):
clean = pattern.strip('%')
return ~Q(**{f'{self._field_expr._django_name}__icontains': clean})
def in_(self, values):
return ~Q(**{f'{self._field_expr._django_name}__in': values})
def is_(self, other):
if other is None:
return ~Q(**{f'{self._field_expr._django_name}__isnull': True})
return ~Q(**{self._field_expr._django_name: other})
class FieldExpression:
_resolve_cache = {}
def __init__(self, model_class, field_name):
self.model_class = model_class
self.field_name = field_name
self._django_name = self._resolve_django_name()
def _resolve_django_name(self):
cache_key = (self.model_class.__name__, self.field_name)
if cache_key in FieldExpression._resolve_cache:
return FieldExpression._resolve_cache[cache_key]
if hasattr(self.model_class, '_meta'):
for f in self.model_class._meta.local_fields:
if f.name == self.field_name:
FieldExpression._resolve_cache[cache_key] = f.name
return f.name
FieldExpression._resolve_cache[cache_key] = self.field_name
return self.field_name
def __eq__(self, other):
if isinstance(other, FieldExpression):
return NotImplemented
if other is None:
return Q(**{f'{self._django_name}__isnull': True})
return Q(**{self._django_name: other})
def __ne__(self, other):
if isinstance(other, FieldExpression):
return NotImplemented
return ~Q(**{self._django_name: other})
def __gt__(self, other):
return Q(**{f'{self._django_name}__gt': other})
def __ge__(self, other):
return Q(**{f'{self._django_name}__gte': other})
def __lt__(self, other):
return Q(**{f'{self._django_name}__lt': other})
def __le__(self, other):
return Q(**{f'{self._django_name}__lte': other})
def __getitem__(self, key):
return _JsonKeyExpression(self._django_name, key)
def __hash__(self):
return hash((self.model_class.__name__, self.field_name))
def __and__(self, other):
if isinstance(other, Q):
return Q(self) & other
return NotImplemented
def __or__(self, other):
if isinstance(other, Q):
return Q(self) | other
return NotImplemented
def __invert__(self):
return _InvertedFieldExpression(self)
def is_(self, other):
if other is None:
return Q(**{f'{self._django_name}__isnull': True})
return Q(**{self._django_name: other})
def is_not(self, other):
if other is None:
return Q(**{f'{self._django_name}__isnull': False})
return ~Q(**{self._django_name: other})
def like(self, pattern):
clean = pattern.strip('%')
return Q(**{f'{self._django_name}__icontains': clean})
def contains(self, value):
return Q(**{f'{self._django_name}__contains': value})
def ilike(self, pattern):
return self.like(pattern)
def startswith(self, value):
return Q(**{f'{self._django_name}__startswith': value})
def istartswith(self, value):
return Q(**{f'{self._django_name}__istartswith': value})
def endswith(self, value):
return Q(**{f'{self._django_name}__endswith': value})
def iendswith(self, value):
return Q(**{f'{self._django_name}__iendswith': value})
def in_(self, values):
if hasattr(values, '_qs'):
return Q(**{f'{self._django_name}__in': values._qs.values(self._django_name)})
return Q(**{f'{self._django_name}__in': values})
def between(self, low, high):
return Q(**{f'{self._django_name}__gte': low}) & Q(**{f'{self._django_name}__lte': high})
def regex(self, pattern):
return Q(**{f'{self._django_name}__regex': pattern})
def iregex(self, pattern):
return Q(**{f'{self._django_name}__iregex': pattern})
def desc(self):
return f'-{self._django_name}'
def asc(self):
return self._django_name
def __repr__(self):
return f'FieldExpr({self.model_class.__name__}.{self.field_name})'
def __str__(self):
return self._django_name
class _JsonKeyExpression:
def __init__(self, field_name, key):
self.field_name = field_name
self.key = key
def _lookup(self, suffix=''):
if suffix:
return f'{self.field_name}__{self.key}__{suffix}'
return f'{self.field_name}__{self.key}'
def cast(self, type_hint):
return _CastJsonExpression(self.field_name, self.key, type_hint)
def __eq__(self, other):
return Q(**{self._lookup(): other})
def __ne__(self, other):
return ~Q(**{self._lookup(): other})
def __gt__(self, other):
return Q(**{self._lookup('gt'): other})
def __ge__(self, other):
return Q(**{self._lookup('gte'): other})
def __lt__(self, other):
return Q(**{self._lookup('lt'): other})
def __le__(self, other):
return Q(**{self._lookup('lte'): other})
def between(self, low, high):
return Q(**{self._lookup('gte'): low}) & Q(**{self._lookup('lte'): high})
def __repr__(self):
return f'JsonKeyExpr({self.field_name}[{self.key!r}])'
class _CastJsonExpression:
def __init__(self, field_name, key, type_hint):
self.field_name = field_name
self.key = key
self.type_hint = type_hint
def _lookup(self, suffix=''):
if suffix:
return f'{self.field_name}__{self.key}__{suffix}'
return f'{self.field_name}__{self.key}'
def between(self, low, high):
return Q(**{self._lookup('gte'): low}) & Q(**{self._lookup('lte'): high})
def __eq__(self, other):
return Q(**{self._lookup(): other})
def __ne__(self, other):
return ~Q(**{self._lookup(): other})
def __gt__(self, other):
return Q(**{self._lookup('gt'): other})
def __ge__(self, other):
return Q(**{self._lookup('gte'): other})
def __lt__(self, other):
return Q(**{self._lookup('lt'): other})
def __le__(self, other):
return Q(**{self._lookup('lte'): other})
def __repr__(self):
type_name = getattr(self.type_hint, '__name__', str(self.type_hint))
return f'CastJsonExpr({self.field_name}[{self.key!r}]->{type_name})'
class ForeignKeyIdDescriptor:
"""FK 字段描述符:类级别访问返回 FieldExpression实例级别访问/设置委托给原始 Django 字段。"""
def __init__(self, fk_name):
self.fk_name = fk_name
self.id_attr = fk_name + '_id'
self._original_field = None # 延迟绑定原始 Django 字段
def _get_original_field(self, owner):
if self._original_field is None:
try:
self._original_field = owner._meta.get_field(self.fk_name)
except Exception:
pass
return self._original_field
def __get__(self, obj, objtype=None):
if obj is None:
return FieldExpression(objtype, self.id_attr)
return getattr(obj, self.id_attr)
def __set__(self, obj, value):
# 直接写入实例 __dict__避免触发描述符递归
obj.__dict__[self.id_attr] = value
class FieldExpressionDescriptor:
"""普通字段描述符:类级别访问返回 FieldExpression实例级别访问/设置委托给原始 Django 字段。"""
def __init__(self, field_name):
self.field_name = field_name
self._original_field = None # 延迟绑定原始 Django 字段
def _get_original_field(self, owner):
if self._original_field is None:
try:
self._original_field = owner._meta.get_field(self.field_name)
except Exception:
pass
return self._original_field
def __get__(self, obj, objtype=None):
if obj is None:
return FieldExpression(objtype, self.field_name)
# 实例级别:从 __dict__ 获取,或让 Django 字段描述符处理
if self.field_name in obj.__dict__:
return obj.__dict__[self.field_name]
# 委托给原始 Django 字段
orig = self._get_original_field(objtype)
if orig is not None:
return orig.__get__(obj, objtype)
return getattr(obj, self.field_name)
def __set__(self, obj, value):
# 直接写入实例 __dict__避免触发描述符递归
obj.__dict__[self.field_name] = value
class QModelBase(models.base.ModelBase):
def __new__(mcs, name, bases, namespace, **kwargs):
# 不再强制设置 db_column —— Django 会自动处理
# 动态设置 managed子服务器可设 GVSDSDK_MANAGE_TABLES=True 让 gvsdsdk 管理表
meta = namespace.get('Meta')
if meta and getattr(meta, 'app_label', None) == 'gvsdsdk':
try:
from django.conf import settings
if getattr(settings, 'GVSDSDK_MANAGE_TABLES', False):
meta.managed = True
except Exception:
pass
cls = super().__new__(mcs, name, bases, namespace, **kwargs)
# 模型创建后,包装字段描述符以支持类级别 FieldExpression
if not cls._meta.abstract:
_wrap_field_descriptors(cls)
return cls
def _wrap_field_descriptors(cls: Type) -> None:
"""包装 Django 字段描述符,使类级别访问返回 FieldExpression
实例级别完全委托给 Django 原生描述符。
Django 的字段描述符DeferredAttribute / ForwardManyToOneDescriptor
- __get__(None, cls) → 返回字段对象本身(类级别)
- __get__(instance, cls) → 返回字段值(实例级别)
我们包装后:
- __get__(None, cls) → 返回 FieldExpression类级别用于链式 OOP
- __get__(instance, cls) → 委托给原生描述符(实例级别,正常行为)
"""
from django.db.models.query_utils import DeferredAttribute
from django.db.models.fields.related_descriptors import (
ForwardManyToOneDescriptor,
ForwardOneToOneDescriptor,
)
for field in cls._meta.local_fields:
fname = field.name
if fname.startswith('_'):
continue
original_desc = cls.__dict__.get(fname)
if original_desc is None:
continue
# 只包装已知的 Django 描述符类型
if isinstance(original_desc, (DeferredAttribute, ForwardManyToOneDescriptor,
ForwardOneToOneDescriptor)):
wrapper = _FieldExpressionWrapper(original_desc, cls, fname)
setattr(cls, fname, wrapper)
class _FieldExpressionWrapper:
"""包装 Django 字段描述符,类级别返回 FieldExpression实例级别委托给原生描述符。"""
def __init__(self, original_descriptor, model_class, field_name):
self._original = original_descriptor
self._model = model_class
self._field_name = field_name
self._has_set = hasattr(original_descriptor, '__set__')
def __get__(self, obj, objtype=None):
if obj is None:
# 类级别访问:返回 FieldExpression
return FieldExpression(self._model, self._field_name)
# 实例级别访问:委托给原生 Django 描述符
return self._original.__get__(obj, objtype)
def __set__(self, obj, value):
if self._has_set:
# FK/O2O 描述符有 __set__
self._original.__set__(obj, value)
else:
# 普通字段DeferredAttribute没有 __set__直接写 __dict__
obj.__dict__[self._field_name] = value
class QModel(models.Model, metaclass=QModelBase):
"""QModel 兼容层基类,提供 SQLAlchemy 风格的链式 OOP 查询接口。
所有继承 QModel 的模型自动获得:
- ``Model.query`` : 返回 FluentQuery支持链式查询
- ``Model.field_name`` : 类级别访问返回 FieldExpression支持表达式查询
- ``Model.field_name`` : 实例级别访问返回字段值(与普通 Django Model 一致)
用法示例::
# 链式查询
orders = Dingdan.query.filter(zhuangtai=8).order_by('-create_time').to_list()
# 字段表达式
Dingdan.query.filter(Dingdan.jine > 100, Dingdan.zhuangtai.in_([1,2,3]))
# 聚合
Dingdan.query.aggregate(total=Sum('jine'))
# values / annotate
Dingdan.query.values('zhuangtai').annotate(cnt=Count('id'))
# 分页
Dingdan.query.paginate(page=1, per_page=20)
"""
class Meta:
abstract = True
@classproperty
def query(cls) -> FluentQuery:
"""返回 FluentQuery 兼容层查询构建器,支持链式 OOP 查询。"""
return FluentQuery(cls)
@classmethod
def __class_getitem__(cls, item) -> FluentQuery:
"""支持 Model[condition] 语法,返回 filter 后的 FluentQuery。
用法::
Dingdan[Dingdan.zhuangtai == 8].order_by('-create_time').to_list()
"""
if isinstance(item, Q):
return cls.query.filter(item)
if isinstance(item, dict):
return cls.query.filter(**item)
return cls.query.filter(item)