Initial commit
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
from .engine import *
|
||||
from .filter import *
|
||||
from .input import *
|
||||
from .types import *
|
||||
@@ -0,0 +1,112 @@
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import func
|
||||
|
||||
from core.models.base import ORDER_BY_CREATED_AT
|
||||
from core.utils.table_engine.filter import TableEngineFilter
|
||||
from core.utils.table_engine.input import TableEngineInput
|
||||
|
||||
|
||||
class TableEngine:
|
||||
object: any
|
||||
input: TableEngineInput
|
||||
is_sql_server: bool
|
||||
is_deleted: bool
|
||||
model: any
|
||||
joins: list = None
|
||||
filter: TableEngineFilter
|
||||
sort: dict = ORDER_BY_CREATED_AT
|
||||
extra_filter: list
|
||||
|
||||
def __init__(self, _object, _input: dict):
|
||||
self.object = _object
|
||||
_input = _input or {}
|
||||
self.input = TableEngineInput(**_input)
|
||||
self.input.get_limit()
|
||||
self.filter = TableEngineFilter(self.input)
|
||||
self.extra_filter = []
|
||||
|
||||
def soft_deleted(self):
|
||||
self.is_deleted = True
|
||||
self.input.is_deleted = True
|
||||
return self
|
||||
|
||||
def sql_server(self):
|
||||
self.is_sql_server = True
|
||||
return self
|
||||
|
||||
def text_search_cols(self, cols: list):
|
||||
self.input.text_search_cols(cols)
|
||||
return self
|
||||
|
||||
def set_limit(self, limit: int):
|
||||
self.input.p.s = limit
|
||||
return self
|
||||
|
||||
def set_model(self, model: BaseModel.__class__):
|
||||
self.model = model
|
||||
return self
|
||||
|
||||
def join(self, joins):
|
||||
self.joins = joins
|
||||
return self
|
||||
|
||||
def sort_by(self, sort_by: dict | str):
|
||||
self.sort = sort_by
|
||||
return self
|
||||
|
||||
def _get_sql_order(self):
|
||||
if isinstance(self.sort, dict):
|
||||
return self.model.Id.desc()
|
||||
return self.sort
|
||||
|
||||
def add_filter(self, filters):
|
||||
self.extra_filter.append(filters)
|
||||
self.input.extra_filter.append(filters)
|
||||
return self
|
||||
|
||||
def get_query(self):
|
||||
return self.input.dict()
|
||||
|
||||
async def exec(self):
|
||||
if hasattr(self, 'is_sql_server') and self.is_sql_server:
|
||||
return await self._exec_sql_server()
|
||||
else:
|
||||
return await self._exec_mongo()
|
||||
|
||||
async def _exec_mongo(self):
|
||||
cursor = self.object.find(
|
||||
self.filter.get()).sort(
|
||||
**self.sort).limit(
|
||||
self.input.get_limit()).skip(self.input.skip())
|
||||
return [self.model(**document) async for document in cursor]
|
||||
|
||||
async def _exec_sql_server(self):
|
||||
query = self.object \
|
||||
.query(self.model)
|
||||
|
||||
if self.joins:
|
||||
for j in self.joins:
|
||||
if isinstance(j, list):
|
||||
query = query.join(*j)
|
||||
else:
|
||||
query = query.join(j)
|
||||
|
||||
query = self.filter.get_sql_server(
|
||||
self.model, query, self.extra_filter)
|
||||
|
||||
count = query.count()
|
||||
|
||||
query = query \
|
||||
.order_by(self._get_sql_order()) \
|
||||
.offset(self.input.skip()) \
|
||||
.limit(self.input.get_limit())
|
||||
|
||||
return query.all(), count
|
||||
|
||||
async def count(self):
|
||||
if hasattr(self, 'is_sql_server') and self.is_sql_server:
|
||||
count = self.object.query(func.count(self.model.Id))
|
||||
else:
|
||||
count = await self.object.count_documents(
|
||||
self.filter.get())
|
||||
return count
|
||||
@@ -0,0 +1,160 @@
|
||||
__all__ = ['TableEngineFilter']
|
||||
|
||||
from uuid import UUID
|
||||
|
||||
from sqlalchemy import or_
|
||||
|
||||
from core.utils.table_engine.input import TableEngineInput, FilterItem
|
||||
from core.utils.utils import is_valid_uuid
|
||||
|
||||
|
||||
def filter_item_generator(item: FilterItem) -> dict:
|
||||
if is_valid_uuid(item.col):
|
||||
return {
|
||||
'data': {
|
||||
"$elemMatch": {
|
||||
'field_id': UUID(item.col),
|
||||
'value': {**get_expression_filter(item.op, cast_value(item.val, item.type))}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {item.col: {
|
||||
**get_expression_filter(item.op, cast_value(item.val, item.type))}}
|
||||
|
||||
|
||||
def cast_value(val: str, type: str) -> any:
|
||||
if type in ['NUMBER', 'STACK', 'FLOAT']:
|
||||
return float(val)
|
||||
if type == 'UUID':
|
||||
return UUID(str(val))
|
||||
return val
|
||||
|
||||
|
||||
def get_expression_filter(op: str, val: str) -> dict:
|
||||
_op = get_operator(op)
|
||||
match _op:
|
||||
case "sw":
|
||||
return {'$regex': f'^{val}'}
|
||||
case "ew":
|
||||
return {'$regex': f'{val}$'}
|
||||
case "n_ew":
|
||||
return {'$regex': f'(?<!{val})$'}
|
||||
case "n_sw":
|
||||
return {'$regex': f'^(?!{val})'}
|
||||
case "n_like":
|
||||
return {'$not': {'$regex': val}}
|
||||
case "bet":
|
||||
if type(val) is str:
|
||||
return {'$gte': val.split('-')[0], '$lte': val.split('-')[1]}
|
||||
return {'$gte': val[0], '$lte': val[1]}
|
||||
case "n_bet":
|
||||
if type(val) is str:
|
||||
return {'$gte': val.split('-')[0], '$lte': val.split('-')[1]}
|
||||
return {'$not': {'$gte': val[0], '$lte': val[1]}}
|
||||
case "exi":
|
||||
return {'$exists': True}
|
||||
case "n_exi":
|
||||
return {'$exists': False}
|
||||
case "in":
|
||||
return {'$exists': True}
|
||||
case "not_in":
|
||||
return {'$exists': False}
|
||||
return {_op: val}
|
||||
|
||||
|
||||
def get_operator(op: str) -> str:
|
||||
ops = {
|
||||
"eq": "$eq",
|
||||
"n_eq": "$ne",
|
||||
"gt": "$gt",
|
||||
"ge": "$ge",
|
||||
"lt": "$lt",
|
||||
"le": "$le",
|
||||
"like": "$regex",
|
||||
"n_like": "n_like",
|
||||
"sw": "sw",
|
||||
"ew": "ew",
|
||||
'n_ew': 'n_ew',
|
||||
'n_sw': 'n_sw',
|
||||
'exi': 'exi',
|
||||
"n_exi": "n_exi",
|
||||
'lte': '$lte',
|
||||
'gte': '$gte',
|
||||
'in': 'in',
|
||||
'n_in': 'not_in',
|
||||
'bet': 'bet',
|
||||
'n_bet': 'n_bet',
|
||||
}
|
||||
|
||||
return ops[op] if op in ops else '$eq'
|
||||
|
||||
|
||||
def getAndOr(op: str) -> str:
|
||||
return {"and": "$and", "or": '$or'}.get(op, '$and')
|
||||
|
||||
|
||||
class TableEngineFilter:
|
||||
stack: list
|
||||
input: TableEngineInput
|
||||
|
||||
def __init__(self, _input: TableEngineInput):
|
||||
self.stack = []
|
||||
self.input = _input
|
||||
|
||||
def add(self, filter_item: dict):
|
||||
self.stack.append(filter_item)
|
||||
|
||||
def query_generator(self):
|
||||
if not self.input.has_filter():
|
||||
return []
|
||||
|
||||
filters = self.input.filters()
|
||||
query = []
|
||||
for filter_scope in filters:
|
||||
query.append({getAndOr(filter_scope.op): [
|
||||
filter_item_generator(item) for item in filter_scope.items]})
|
||||
return query
|
||||
|
||||
def get(self):
|
||||
f = self.stack
|
||||
query = self.query_generator()
|
||||
if query:
|
||||
f.append(*query)
|
||||
if self.input.has_text_search():
|
||||
items = {}
|
||||
for col in self.input.get_search_cols():
|
||||
items[col] = {'$regex': self.input.get_text_search()}
|
||||
f.append(items)
|
||||
if hasattr(self.input, 'is_deleted') and self.input.is_deleted:
|
||||
f.append({'deleted_at': {'$exists': False}})
|
||||
if len(self.input.extra_filter) > 0:
|
||||
f.extend(self.input.extra_filter)
|
||||
if not f:
|
||||
return {}
|
||||
return {'$and': f}
|
||||
|
||||
def get_sql_server(self, model, query, filters):
|
||||
"""
|
||||
text search filter
|
||||
"""
|
||||
text_search_stack = []
|
||||
fi = []
|
||||
if self.input.has_text_search():
|
||||
for col in self.input.get_search_cols():
|
||||
if isinstance(col, str):
|
||||
text_search_stack.append(getattr(model, col).like(
|
||||
f'%{self.input.get_text_search()}%'))
|
||||
|
||||
if text_search_stack:
|
||||
fi.append(or_(*text_search_stack))
|
||||
|
||||
if self.input.is_deleted:
|
||||
fi.append(getattr(model, 'IsDeleted') == 0)
|
||||
|
||||
if filters:
|
||||
for fil in filters:
|
||||
fi.append(fil)
|
||||
|
||||
query = query.filter(*fi)
|
||||
return query
|
||||
@@ -0,0 +1,159 @@
|
||||
from enum import Enum
|
||||
from typing import List, Optional, Any
|
||||
|
||||
import pydantic
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Cr(str, Enum):
|
||||
OR = 'or'
|
||||
AND = 'and'
|
||||
|
||||
|
||||
class FilterItem(BaseModel):
|
||||
col: Optional[str]
|
||||
cr: Optional[Cr]
|
||||
id: Optional[str]
|
||||
op: Optional[str]
|
||||
type: Optional[str]
|
||||
val: Optional[Any]
|
||||
|
||||
@staticmethod
|
||||
def ValidItem(item: dict):
|
||||
return item.get('col') and \
|
||||
item.get('id') and \
|
||||
item.get('op') and \
|
||||
item.get('type')
|
||||
|
||||
@staticmethod
|
||||
def ValidValue(item: dict):
|
||||
return len(str(item.get('val', '')).strip()) > 0
|
||||
|
||||
|
||||
class TableEngineInput(BaseModel):
|
||||
|
||||
class FilterScope(BaseModel):
|
||||
id: str
|
||||
items: Optional[List[FilterItem]]
|
||||
op: Optional[Cr]
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
if kwargs.get('items'):
|
||||
self.items = [
|
||||
FilterItem(
|
||||
**item) for item in filter(
|
||||
lambda i: FilterItem.ValidItem(i),
|
||||
kwargs.get(
|
||||
'items',
|
||||
[]))]
|
||||
self.op = Cr(kwargs.get('op'))
|
||||
self.id = str(kwargs.get('id'))
|
||||
|
||||
@staticmethod
|
||||
def ValidScope(item: dict):
|
||||
return item.get('id') and item.get('items') and len(
|
||||
item.get('items')) > 0 and item.get('op')
|
||||
|
||||
@staticmethod
|
||||
def CleanItems(item):
|
||||
clone = item.dict()
|
||||
clone['items'] = list(filter(
|
||||
lambda i: FilterItem.ValidValue(i), clone.get(
|
||||
'items', [])))
|
||||
return TableEngineInput.FilterScope(**clone)
|
||||
|
||||
class Page(BaseModel):
|
||||
c: int = 1
|
||||
s: int = 5
|
||||
|
||||
class SearchModel(BaseModel):
|
||||
text: Optional[str]
|
||||
cols: Optional[List[Any]]
|
||||
|
||||
class SortModel(BaseModel):
|
||||
field: Optional[str]
|
||||
direction: Optional[str]
|
||||
|
||||
class Config:
|
||||
extra = pydantic.Extra.ignore
|
||||
anystr_strip_whitespace = True # strip whitespaces from strings
|
||||
underscore_attrs_are_private = False
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
f: Optional[List[FilterScope]]
|
||||
extra_filter: list = []
|
||||
o: Optional[SortModel]
|
||||
p: Optional[Page] = Page()
|
||||
s: Optional[SearchModel]
|
||||
is_deleted: Optional[bool]
|
||||
|
||||
def limit(self):
|
||||
return self.p.s
|
||||
|
||||
def order(self):
|
||||
return self.p.o
|
||||
|
||||
def filters(self) -> List[FilterScope]:
|
||||
return self.f
|
||||
|
||||
def has_filter(self) -> bool:
|
||||
return self.f is not None and \
|
||||
len(self.f) > 0 and \
|
||||
self.f[0].items is not None and \
|
||||
len(self.f[0].items) > 0
|
||||
|
||||
def has_extra_filter(self) -> bool:
|
||||
return self.extra_filter is not None and \
|
||||
len(self.extra_filter) > 0
|
||||
|
||||
def scopes(self):
|
||||
return self.s
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
_input = {**{'p': {'c': 1, 's': 5},
|
||||
'f': [], 's': {}, 'o': {}}, **(kwargs or {})}
|
||||
self.p = TableEngineInput.Page(**_input.get('p'))
|
||||
|
||||
if _input.get('f'):
|
||||
self.f = [
|
||||
TableEngineInput.FilterScope(
|
||||
**f) for f in filter(
|
||||
lambda fs:TableEngineInput.FilterScope.ValidScope(fs),
|
||||
_input.get('f'))]
|
||||
self.f = list(map(
|
||||
lambda fs: TableEngineInput.FilterScope.CleanItems(fs),
|
||||
self.f))
|
||||
|
||||
if _input.get('s'):
|
||||
self.s = TableEngineInput.SearchModel(**_input.get('s'))
|
||||
|
||||
def skip(self):
|
||||
return self.page() * self.p.s
|
||||
|
||||
def page(self):
|
||||
return self.p.c - 1
|
||||
|
||||
def virtual_page(self):
|
||||
return self.page() + 1
|
||||
|
||||
def get_limit(self):
|
||||
return self.p.s if self.p else 20
|
||||
|
||||
def text_search_cols(self, cols: List[str]):
|
||||
if self.s:
|
||||
self.s.cols = cols
|
||||
|
||||
def has_text_search(self) -> bool:
|
||||
return self.s is not None and self.s.text is not None and self.s.cols is not None and len(
|
||||
self.s.cols) > 0 and len(self.s.text.strip()) > 0
|
||||
|
||||
def add_filter(self, _filter):
|
||||
self.extra_filter.append(_filter)
|
||||
|
||||
def get_search_cols(self) -> List[str]:
|
||||
return self.s.cols if self.s else []
|
||||
|
||||
def get_text_search(self) -> str:
|
||||
return self.s.text.strip() if self.s else None
|
||||
@@ -0,0 +1,23 @@
|
||||
from typing import Generic, TypeVar, List, Optional
|
||||
|
||||
from pydantic.generics import GenericModel
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
class TableEngineInputType(GenericModel, Generic[T]):
|
||||
f: Optional[List]
|
||||
o: Optional[dict] = {}
|
||||
p: Optional[dict] = {}
|
||||
s: Optional[dict] = {}
|
||||
|
||||
|
||||
class TableEngineExport(GenericModel, Generic[T]):
|
||||
rows: Optional[List[T]]
|
||||
total: Optional[int]
|
||||
page: Optional[int]
|
||||
te: Optional[TableEngineInputType]
|
||||
|
||||
|
||||
class TableEngineRequestType(GenericModel, Generic[T]):
|
||||
te: Optional[TableEngineInputType]
|
||||
Reference in New Issue
Block a user