Initial commit

This commit is contained in:
shahzeydyfah
2024-02-15 00:17:51 +03:30
commit 4b0396466d
111 changed files with 6296 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
from .engine import *
from .filter import *
from .input import *
from .types import *
+112
View File
@@ -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
+160
View File
@@ -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
+159
View File
@@ -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
+23
View File
@@ -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]