Compare commits

...
10 Commits
19 changed files with 618 additions and 26 deletions
+27
View File
@@ -0,0 +1,27 @@
"""
مدل ORM برای جدول const در MySQL.
"""
from sqlalchemy import Column, Integer, String, Text
from core.service.mysql_orm import Base
class Const(Base):
"""مدل جدول const"""
__tablename__ = "const"
id = Column(Integer, primary_key=True, index=True)
key = Column(String(255), nullable=False, index=True)
title_fa = Column(String(255), nullable=True)
value = Column(Text, nullable=True)
def __repr__(self):
return f"<Const(id={self.id}, key='{self.key}', title_fa='{self.title_fa}')>"
def to_dict(self):
"""تبدیل به dict برای JSON response"""
return {
"id": self.id,
"key": self.key,
"title_fa": self.title_fa,
"value": self.value,
}
+2
View File
@@ -48,3 +48,5 @@ class RegisterStatusModel(BaseModel):
amount: Optional[AmountStatus] = Field(description="مبالغ")
related_to_number_start:Optional[int] = Field(description="شماره صورت وضعیت مرتبط")
related_to_number_end:Optional[int] = Field(description="شماره صورت وضعیت مرتبط")
date_at: Optional[datetime.datetime] = Field(description='تاریخ پرداخت')
amount_ّfinancial: Optional[int] = Field(description='مبلغ صورت وضعیت در قسمت مالی')
+34
View File
@@ -0,0 +1,34 @@
"""
Repository برای جدول const در MySQL - برای استفاده در خروجی اکسل.
"""
from typing import List, Dict, Optional
from sqlalchemy import select
from core.service.mysql_orm import get_mysql_session
from app.models.mysql_const import Const
class MySQLConstRepository:
"""Repository برای خواندن دیتا از جدول const"""
async def get_by_keys(self, keys: List[str]) -> Dict[int, str]:
"""
گرفتن const ها با لیست key ها و برگرداندن dict به صورت {id: title_fa}
مثال: get_by_keys(['nature', 'type']) -> {2: 'طبیعت 2', 9: 'نوع 9'}
"""
async with get_mysql_session() as session:
result = await session.execute(
select(Const).where(Const.key.in_(keys))
)
consts = result.scalars().all()
return {c.id: c.title_fa or c.value or str(c.id) for c in consts}
async def get_by_key(self, key: str) -> Dict[int, str]:
"""گرفتن const ها با یک key و برگرداندن dict به صورت {id: title_fa}"""
return await self.get_by_keys([key])
async def get_all_consts_dict(self) -> Dict[int, str]:
"""گرفتن همه const ها به صورت {id: title_fa} - برای کش کردن"""
async with get_mysql_session() as session:
result = await session.execute(select(Const))
consts = result.scalars().all()
return {c.id: c.title_fa or c.value or str(c.id) for c in consts}
+4 -2
View File
@@ -261,8 +261,10 @@ class ProjectRepository(BaseRepository[ProjectModel]):
_set = ProjectModel(
sts=1,
counter=i['id'],
design=Design(**i)
).init_id().cu_at()
design=Design(**i),
created_at=i['created_at'],
updated_at=i['updated_at'],
).init_id()
obj.append(_set.mongo_dict())
await use_collection(TaskEnv.CLN_PROJECT).insert_many(obj)
+54
View File
@@ -19,3 +19,57 @@ class RegisterStatusRepository(BaseRepository):
async def get_list(self,project_id:uuid.UUID):
list_status = await use_collection(TaskEnv.CLN_AUDIT_STATUS).find({"project_id":project_id}).to_list(None)
return list_status
async def create(self, coding: RegisterStatusModel):
result = await super().create(coding)
price = await use_collection(TaskEnv.CLN_PROJECT_PRICE_REPORT).find_one({'project_id': coding.project_id})
if price and coding.amount_ّfinancial:
if coding.sts ==1:
await use_collection(TaskEnv.CLN_PROJECT_PRICE_REPORT).update_one(
{'project_id': coding.project_id},
{"$inc": {'amount_status': coding.amount_ّfinancial}}
)
elif coding.sts == 2:
await use_collection(TaskEnv.CLN_PROJECT_PRICE_REPORT).update_one(
{'project_id': coding.project_id},
{"$inc": {'amount_adjust': coding.amount_ّfinancial}}
)
return result
async def update(self, _id: uuid.UUID, _update: RegisterStatusModel):
_old_dic = await use_collection(TaskEnv.CLN_AUDIT_STATUS).find_one({'_id':_id})
change = 0
result = await super().update(_id, _update)
price = await use_collection(TaskEnv.CLN_PROJECT_PRICE_REPORT).find_one({'project_id': _update.project_id})
if price:
_old = RegisterStatusModel(**_old_dic)
if 'amount_ّfinancial' not in _old_dic and _update.amount_ّfinancial:
change = _update.amount_ّfinancial
if _old.amount_ّfinancial != _update.amount_ّfinancial:
change = _update.amount_ّfinancial - _old.amount_ّfinancial
if change != 0 :
if _old.sts == 1:
await use_collection(TaskEnv.CLN_PROJECT_PRICE_REPORT).update_one(
{'project_id': _update.project_id},
{"$inc": {'amount_status': change}}
)
elif _old.sts == 2:
await use_collection(TaskEnv.CLN_PROJECT_PRICE_REPORT).update_one(
{'project_id': _update.project_id},
{"$inc": {'amount_adjust': change}}
)
return result
async def delete(self, _id: uuid.UUID):
_old_dic = await use_collection(TaskEnv.CLN_AUDIT_STATUS).find_one({'_id': _id})
await super().delete(_id)
price = await use_collection(TaskEnv.CLN_PROJECT_PRICE_REPORT).find_one({'project_id': _old_dic['project_id']})
if price and _old_dic['sts'] == 1 and 'amount_status' in price:
await use_collection(TaskEnv.CLN_PROJECT_PRICE_REPORT).update_one(
{'project_id': _old_dic['project_id']},
{"$inc": {'amount_status': -(_old_dic['amount_ّfinancial'])}}
)
if price and _old_dic['sts'] == 2 and 'amount_adjust' in price:
await use_collection(TaskEnv.CLN_PROJECT_PRICE_REPORT).update_one(
{'project_id': _old_dic['project_id']},
{"$inc": {'amount_adjust': -(_old_dic['amount_ّfinancial'])}}
)
return True
+2 -1
View File
@@ -18,4 +18,5 @@ from .financial_status import *
from .financial_adjust import *
from .delivery import *
from .report import *
from .excel import *
from .excel import *
from .download import *
+48
View File
@@ -0,0 +1,48 @@
"""
دانلود فایل اکسل تولیدشده از خروجی جدول (Table Engine).
فایل‌ها در پوشه temp پروژه ذخیره می‌شوند و از طریق این endpoint قابل دانلود هستند.
"""
import os
import re
from fastapi import APIRouter, HTTPException
from starlette.responses import FileResponse
from config import conf
from core.app import app
from core.utils.router import generate_router
router = generate_router("download", auth_dependencies=False, version="v2")
_tag_base = ["download"]
_pre = conf.server.prefix_url.replace("v1", "v2")
def _excel_temp_dir():
return os.path.join(os.getcwd(), "temp")
def _safe_excel_filename(name: str) -> bool:
"""فقط نام فایل اکسلی با فرمت uuid.xlsx (۳۲ کاراکتر hex + .xlsx) مجاز است."""
return bool(re.match(r"^[a-f0-9]{32}\.xlsx$", name))
@router.get("/excel/{filename}", tags=_tag_base)
async def download_excel(filename: str):
"""
دانلود فایل اکسل با نام بازگردانده‌شده از API لیست (فیلد path).
مثال: بعد از درخواست خروجی اکسل، در پاسخ path مثلاً «abc123...xlsx» می‌آید؛
برای دانلود: GET /v2/download/excel/abc123....xlsx
"""
if not _safe_excel_filename(filename):
raise HTTPException(status_code=400, detail="نام فایل نامعتبر است.")
file_path = os.path.join(_excel_temp_dir(), filename)
if not os.path.isfile(file_path):
raise HTTPException(status_code=404, detail="فایل یافت نشد یا منقضی شده است.")
return FileResponse(
file_path,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
filename=filename,
)
app.include_router(router)
+2 -2
View File
@@ -5,8 +5,8 @@ from core.service.mongo_db import use_collection
def get_auth_db():
return conf.server.is_prod and conf.database.get('mongo').production.db or conf.database.get(
'mongo').dev.db
return conf.server.is_prod and conf.database.get('mongo_auth').production.db or conf.database.get(
'mongo_auth').dev.db
def use_auth_collection(collection: str) -> Collection:
+15
View File
@@ -81,3 +81,18 @@ database:
version: 4
password: 'sample'
user: 'local'
# MySQL (read-only) - جدا از database تا مونگو و بقیه بدون تغییر بمانند
mysql:
dev:
host: '172.16.1.194'
port: 3309
user: 'root'
password: '2TjwtsnYAs'
db: 'doc'
production:
host: '172.16.1.194'
port: 3309
user: 'root'
password: '2TjwtsnYAs'
db: 'doc'
+38 -4
View File
@@ -1,10 +1,10 @@
import os
from pathlib import Path
from typing import Optional, List, Union
from typing import Optional, List, Union, Dict, Any
import pydantic
import yaml
from pydantic import BaseModel
from pydantic import BaseModel, validator
def build_path(path: Union[Path, str], path_is_absolute: bool = False) -> Path:
@@ -73,6 +73,19 @@ class DatabaseModeConfig(BaseModel):
production: Optional[DatabaseConfig]
class MySQLConfig(BaseModel):
host: str
port: int
user: str
password: str
db: str
class MySQLModeConfig(BaseModel):
dev: Optional[MySQLConfig]
production: Optional[MySQLConfig]
class DependencyConfig(BaseModel):
middleware: Optional[List[str]]
routes: Optional[List[str]]
@@ -115,12 +128,33 @@ class CoreConfig(pydantic.BaseModel):
server: Optional[ServerConfig]
auth: Optional[AuthConfig]
dependency: Optional[DependencyConfig]
database: dict[str, Union[DatabaseConfig, DatabaseModeConfig]]
database: Dict[str, Union[DatabaseConfig, DatabaseModeConfig]]
mysql: Optional[MySQLModeConfig] = None
static_files: dict[str, str] = dict()
celery: Optional[CeleryTask]
@validator('database', pre=True)
def parse_database(cls, v: Any) -> Dict[str, Union[DatabaseConfig, DatabaseModeConfig]]:
if not isinstance(v, dict):
return v
result: Dict[str, Union[DatabaseConfig, DatabaseModeConfig]] = {}
for key, val in v.items():
if not isinstance(val, dict):
result[key] = val
continue
if 'dev' in val or 'production' in val:
dev = val.get('dev')
prod = val.get('production')
result[key] = DatabaseModeConfig(
dev=DatabaseConfig(**(dev or {})) if dev else None,
production=DatabaseConfig(**(prod or {})) if prod else None,
)
else:
result[key] = DatabaseConfig(**val)
return result
class Config:
arbitrary_types_allowed = False
arbitrary_types_allowed = True
# def load_modules_config(self):
# # import dynamic routes in the modules
+67
View File
@@ -0,0 +1,67 @@
"""
اتصال MySQL فقط-خواندنی (read-only).
از پول کانکشن استفاده می‌کند و در هر کانکشن SESSION را به حالت فقط-خواندنی تنظیم می‌کند.
"""
from contextlib import asynccontextmanager
from typing import Optional
import aiomysql
from core.utils.mysql import get_mysql_config
_pool: Optional[aiomysql.Pool] = None
_pool_connection_name: Optional[str] = None
async def get_mysql_pool(connection_name: str = None) -> aiomysql.Pool:
"""پول کانکشن MySQL را برمی‌گرداند؛ در اولین فراخوانی ساخته می‌شود."""
global _pool, _pool_connection_name
name = connection_name or "mysql"
if _pool is None or _pool_connection_name != name:
if _pool is not None:
_pool.close()
await _pool.wait_closed()
cfg = get_mysql_config(name)
_pool = await aiomysql.create_pool(
host=cfg["host"],
port=cfg["port"],
user=cfg["user"],
password=cfg["password"],
db=cfg["db"],
charset="utf8mb4",
autocommit=True,
minsize=1,
maxsize=10,
)
_pool_connection_name = name
return _pool
@asynccontextmanager
async def get_mysql_connection(connection_name: str = None):
"""
کانکشن فقط-خواندنی از پول می‌گیرد.
در ابتدا SESSION را به حالت فقط-خواندنی تنظیم می‌کند (برای اطمینان بیشتر).
استفاده:
async with get_mysql_connection() as conn:
async with conn.cursor(aiomysql.DictCursor) as cur:
await cur.execute("SELECT * FROM t LIMIT 10")
rows = await cur.fetchall()
"""
pool = await get_mysql_pool(connection_name)
conn = await pool.acquire()
try:
async with conn.cursor() as cur:
await cur.execute("SET SESSION TRANSACTION READ ONLY")
yield conn
finally:
pool.release(conn)
async def close_mysql_pool():
"""بستن پول (مثلاً در shutdown اپ)."""
global _pool
if _pool is not None:
_pool.close()
await _pool.wait_closed()
_pool = None
+81
View File
@@ -0,0 +1,81 @@
"""
SQLAlchemy async setup برای MySQL (فقط خواندنی).
از asyncmy به عنوان async driver استفاده می‌کند.
"""
from contextlib import asynccontextmanager
from typing import Optional
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import declarative_base, sessionmaker
from core.utils.mysql import get_mysql_config
# Base class برای تمام مدل‌های ORM
Base = declarative_base()
_engine = None
_async_session_maker = None
def get_mysql_url(connection_name: str = None) -> str:
"""ساخت URL برای SQLAlchemy از تنظیمات MySQL"""
cfg = get_mysql_config(connection_name)
return f"mysql+asyncmy://{cfg['user']}:{cfg['password']}@{cfg['host']}:{cfg['port']}/{cfg['db']}?charset=utf8mb4"
def get_async_engine(connection_name: str = None):
"""برگرداندن async engine (در اولین فراخوانی ساخته می‌شود)"""
global _engine
if _engine is None:
url = get_mysql_url(connection_name)
_engine = create_async_engine(
url,
echo=False, # برای debug می‌تونی True کنی
pool_pre_ping=True, # چک کردن کانکشن قبل از استفاده
pool_size=5,
max_overflow=10,
)
return _engine
def get_async_session_maker(connection_name: str = None):
"""برگرداندن async sessionmaker"""
global _async_session_maker
if _async_session_maker is None:
engine = get_async_engine(connection_name)
_async_session_maker = sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False,
autocommit=False,
autoflush=False,
)
return _async_session_maker
@asynccontextmanager
async def get_mysql_session(connection_name: str = None):
"""
Context manager برای گرفتن async session (فقط خواندنی).
استفاده:
async with get_mysql_session() as session:
result = await session.execute(select(User).where(User.id == 1))
user = result.scalar_one_or_none()
"""
session_maker = get_async_session_maker(connection_name)
async with session_maker() as session:
# تنظیم session به حالت فقط-خواندنی
await session.execute("SET SESSION TRANSACTION READ ONLY")
try:
yield session
finally:
await session.rollback() # برای read-only همیشه rollback می‌کنیم
async def close_mysql_engine():
"""بستن engine (مثلاً در shutdown اپ)."""
global _engine, _async_session_maker
if _engine is not None:
await _engine.dispose()
_engine = None
_async_session_maker = None
+25
View File
@@ -0,0 +1,25 @@
"""
تنظیمات اتصال MySQL (فقط خواندنی).
از بخش جداگانه mysql در config.yml (هم‌سطح با database) استفاده می‌کند.
"""
from config import conf
def get_mysql_config(connection_name: str = None):
"""
بر اساس محیط (dev/production) تنظیمات MySQL را برمی‌گرداند.
Returns: dict با کلیدهای host, port, user, password, db
"""
if not conf.mysql:
raise ValueError("تنظیمات mysql در config.yml یافت نشد.")
env = "production" if conf.server.is_prod else "dev"
mode = getattr(conf.mysql, env, None) or getattr(conf.mysql, "dev", None)
if not mode:
raise ValueError(f"تنظیمات mysql.{env} در config.yml یافت نشد.")
return {
"host": getattr(mode, "host", "127.0.0.1"),
"port": int(getattr(mode, "port", 3306)),
"user": getattr(mode, "user", ""),
"password": getattr(mode, "password", ""),
"db": getattr(mode, "db", ""),
}
+1
View File
@@ -10,6 +10,7 @@ class TableEngineInputType(GenericModel, Generic[T]):
o: Optional[dict] = {}
p: Optional[dict] = {}
s: Optional[dict] = {}
e: Optional[dict] = {}
class TableEngineExport(GenericModel, Generic[T]):
+3
View File
@@ -62,6 +62,9 @@ class NewTableEngine:
if self.return_query:
query = await self.exe()
return query
if self.input.e and self.input.e.ot:
query = await self.exe()
return query
rows, count = await self.exe()
self.input.extra_filter = None
del self.engine
+186 -16
View File
@@ -2,6 +2,13 @@ import datetime
import uuid
from typing import Union, List
from uuid import UUID
import os
import xlsxwriter
import tempfile
import string
import copy
import jdatetime
# import dateutil.parser
from celery import shared_task
@@ -9,6 +16,8 @@ from celery import shared_task
from .AbstractEngine import AbstractEngineClass
from ..utils.Models import FilterItem, FilterScope, OutPutType
from ..utils.excel_value_maps import EXCEL_VALUE_MAP_REGISTRY
from app.repositories.mysql_const_repo import MySQLConstRepository
# def zero_time_data(date, is_max=False):
@@ -31,25 +40,26 @@ class MongoEngine(AbstractEngineClass):
rows = []
stack = []
filters = {}
_const_cache: dict = None # کش const ها از MySQL
async def exe(self):
_f = self.filters = self.set_filters()
print("_f ====>", _f)
if self.unselect:
tmp = self.query_class.find(_f, self.unselect)
else:
tmp = self.query_class.find(_f)
sort_by = self.set_sort()
if isinstance(sort_by, dict):
tmp = tmp.sort(**sort_by)
else:
tmp = tmp.sort(sort_by)
self.rows = tmp.limit(
self.input.get_limit()).skip(self.input.skip())
if self.input.e and self.input.e.ot:
self.input.e.d = self.generate_output(self.query_class, _f)
res = await tmp.to_list(None)
self.input.e.d = await self.generate_output(res)
return {'query': _f, 'sort': self.set_sort(), 'path': self.input.e.d}
self.rows = tmp.limit(self.input.get_limit()).skip(self.input.skip())
if self.return_query:
return {'query':_f, 'sort':self.set_sort(), 'limit':self.input.get_limit(), 'page':self.input.skip()}
return [self.model(**document) async for document in self.rows], await self.count()
@@ -249,15 +259,11 @@ class MongoEngine(AbstractEngineClass):
def set_joins(self):
pass
def generate_output(self, query, _f):
print(_f.__dict__)
return ''
async def generate_output(self, _f):
print(self.input.e.ot)
if self.input.e.ot == OutPutType.EXEL:
file_name = str(uuid.uuid4().hex) + '.xlsx'
return _f.dict()
MongoEngine.output_generator(str(query).replace('false', '0'), params,
str(self.query_class.bind.url), file_name=file_name, cols=self.input.e.c)
return file_name
return await self.output_generator(_f, file_name=file_name, cols=self.input.e.c)
elif self.input.e.ot == OutPutType.JSON:
pass
elif self.input.e.ot == OutPutType.PDF:
@@ -265,7 +271,171 @@ class MongoEngine(AbstractEngineClass):
else:
raise Exception("Output type not supported.")
@staticmethod
@shared_task(name='output_generator_task_mongo')
def output_generator(query, params, url, file_name, cols):
pass
def _excel_temp_dir(self):
"""پوشه temp پروژه برای ذخیره فایل‌های اکسل (همان مسیری که برای دانلود سرو می‌شود)."""
temp_dir = os.path.join(os.getcwd(), 'temp')
os.makedirs(temp_dir, exist_ok=True)
return temp_dir
def _expand_headers_with_id_fields(self, cols: dict) -> list:
"""
اگر design.nature در فیلدها باشد، design.nature_id هم اضافه می‌شود.
اگر design.budget در فیلدها باشد، design.budget_id هم اضافه می‌شود.
"""
headers = list(cols.keys())
expanded = []
pair_rules = [
('design.nature', 'design.nature_id', 'نام ماهیت'),
('design.budget', 'design.budget_id', 'نام دسته بودجه'),
]
for h in headers:
expanded.append(h)
for field, id_field, default_title in pair_rules:
if h == field and id_field not in headers and id_field not in expanded:
expanded.append(id_field)
cols[id_field] = cols.get(field, default_title) + ' (نام)'
break
return expanded
async def output_generator(self, query, file_name, cols):
datas = query
file_dir = os.path.join(self._excel_temp_dir(), file_name)
head_cols = list(str(string.ascii_uppercase))
headers = self._expand_headers_with_id_fields(cols)
workbook = xlsxwriter.Workbook(file_dir)
worksheet = workbook.add_worksheet()
# نوشتن هدرها
bold_format = workbook.add_format({'bold': True})
# نوشتن هدرها
for col_index, header in enumerate(headers):
worksheet.write(0, col_index, cols[header], bold_format)
# ساختن value_maps از vk (نام مپ) با استفاده از رجیستری
value_maps = self._build_value_maps()
# بارگذاری const ها برای فیلدهای خاص
const_maps = await self._load_const_maps(headers)
for row_index, item in enumerate(datas, start=1):
for col_index, col in enumerate(headers):
val = self.get_nested_value(item, col)
val = self._apply_value_map(col, val, value_maps)
val = await self._apply_const_map(col, val, const_maps)
worksheet.write(row_index, col_index, val)
workbook.close()
return file_name
def _build_value_maps(self):
"""اگر کاربر در e.vk برای ستونی کلید مپ داده (مثلاً design_sts)، مپ از رجیستری برمی‌گردد."""
value_maps = {}
vk = getattr(self.input.e, "vk", None) or {}
if not isinstance(vk, dict):
return value_maps
for col_key, map_name in vk.items():
if map_name and map_name in EXCEL_VALUE_MAP_REGISTRY:
value_maps[col_key] = EXCEL_VALUE_MAP_REGISTRY[map_name]
return value_maps
def _apply_value_map(self, col_key: str, val, value_maps: dict):
"""اگر برای این ستون مپ تعریف شده، مقدار را به متن متناظر تبدیل می‌کند."""
if not value_maps or col_key not in value_maps:
return val
mapping = value_maps[col_key]
if not isinstance(mapping, dict):
return val
if val is None:
return mapping.get(None, val)
key = val if isinstance(val, (int, float)) else (int(val) if isinstance(val, str) and str(val).strip().isdigit() else val)
if key in mapping:
return mapping[key]
if isinstance(key, int) and str(key) in mapping:
return mapping[str(key)]
return val
async def _load_const_maps(self, headers: List[str]) -> dict:
"""
شناسایی فیلدهای خاص (مثلاً nature_id, type_id, budget_id) و بارگذاری const ها از MySQL.
Returns: dict به صورت {col_key: {id: title_fa}} مثلاً {"design.nature_id": {2: "طبیعت 2", ...}}
"""
# لیست فیلدهای خاص که باید از const خوانده شوند
const_field_names = ['nature_id', 'type_id', 'budget_id'] # می‌تونی اضافه کنی
const_maps = {}
const_keys_to_load = set()
const_column_map = {} # {col_key: const_key} مثلاً {"design.nature_id": "nature"}
# شناسایی ستون‌هایی که باید const بخونن
for col_key in headers:
col_name = col_key.split('.')[-1] # آخرین بخش (nature_id)
if col_name in const_field_names:
const_key = col_name.replace('_id', '') # nature_id -> nature
const_keys_to_load.add(const_key)
const_column_map[col_key] = const_key
# بارگذاری const ها از MySQL
if const_keys_to_load:
repo = MySQLConstRepository()
for col_key, const_key in const_column_map.items():
consts_for_key = await repo.get_by_key(const_key)
if consts_for_key:
const_maps[col_key] = consts_for_key
return const_maps
async def _apply_const_map(self, col_key: str, val, const_maps: dict):
"""اگر این ستون در const_maps باشد، id را به title_fa تبدیل می‌کند."""
if not const_maps or col_key not in const_maps:
return val
mapping = const_maps[col_key]
if not isinstance(mapping, dict):
return val
if val is None:
return mapping.get(None, val)
# تبدیل val به int برای جستجو در mapping
key = val if isinstance(val, (int, float)) else (int(val) if isinstance(val, str) and str(val).strip().isdigit() else val)
if key in mapping:
return mapping[key]
if isinstance(key, int) and str(key) in mapping:
return mapping[str(key)]
return val
def get_nested_value(self, data, path, default='-'):
"""
دریافت مقدار از دیکشنری تو در تو با مسیر dot notation
مثال: path = "design.title"
"""
if not isinstance(path, str):
return default
keys = path.split('.')
val = data
for key in keys:
if isinstance(val, dict):
val = val.get(key, default)
else:
return default
if val is None:
return default
# تبدیل datetime و UUID به رشته برای اکسل
if isinstance(val, datetime.datetime):
return self.create_date(val)
if isinstance(val, uuid.UUID):
return str(val)
return val
def create_date(self, date):
shamsi_dt = jdatetime.datetime.fromgregorian(datetime=date)
return shamsi_dt.strftime("%Y/%m/%d - %H:%M:%S")
+2 -1
View File
@@ -35,7 +35,7 @@ class Cr(str, Enum):
class OutPutType(str, Enum):
JSON = 'json'
EXEL = 'exel'
EXEL = 'excel'
PDF = 'pdf'
@@ -81,6 +81,7 @@ class ExportModel(BaseModel):
ot: Optional[OutPutType]
c: Optional[dict]
d: Optional[str]
vk: Optional[dict] = None
class SortModel(BaseModel):
+25
View File
@@ -0,0 +1,25 @@
"""
رجیستری مپ‌های ازپیش‌تعریف‌شده برای خروجی اکسل.
کلاینت فقط کلید (مثلاً design_sts) را در e.vk می‌فرستد تا به‌جای عدد، متن در اکسل بیاید.
"""
EXCEL_VALUE_MAP_REGISTRY = {
"design_sts": {
1: "شروع نشده",
2: "جاری",
3: "متوقف",
4: "خاتمه یافته",
},
"monitor_sts": {
1: "شروع نشده",
2: "جاری",
3: "متوقف",
4: "تحویل موقت",
5: "خاتمه یافته",
},
"sts": {
1: "جاری",
2: "حذف شده",
},
}
+2
View File
@@ -1,5 +1,7 @@
aiofiles==22.1.0
aiomysql==0.2.0
aioredis==2.0.1
asyncmy==0.2.9
amqp==5.1.1
annotated-types==0.6.0
anyio==3.6.2