Compare commits
22 Commits
34ac2a0a69
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| eab801e41b | |||
| 7defcbdbd4 | |||
| ee18f16250 | |||
| c266814c96 | |||
| a7bda3d9f6 | |||
| be878717e5 | |||
| 2700538945 | |||
| 80a74ac8e6 | |||
| ef657c4939 | |||
| 36b3e056dc | |||
| 307e6573e3 | |||
| 82fcd6e8cb | |||
| 0e8c9077c9 | |||
| 9b109a7270 | |||
| c1196497d4 | |||
| 759a8d6478 | |||
| a579ae4145 | |||
| fcaa7fe177 | |||
| 281600c72d | |||
| 62aeebf079 | |||
| 83f3b55f49 | |||
| 90c0bae8f1 |
11
.env.example
11
.env.example
@ -5,3 +5,14 @@ PG_DATABASE=
|
|||||||
PG_HOST=
|
PG_HOST=
|
||||||
|
|
||||||
SECRET_KEY=
|
SECRET_KEY=
|
||||||
|
|
||||||
|
S3_URL=
|
||||||
|
S3_ACCESS_KEY=
|
||||||
|
S3_SECRET_ACCESS_KEY=
|
||||||
|
S3_REGION=
|
||||||
|
S3_BUCKET=
|
||||||
|
|
||||||
|
REDIS_PASSWORD=
|
||||||
|
REDIS_HOST=
|
||||||
|
REDIS_DB=
|
||||||
|
REDIS_URL=
|
||||||
|
|||||||
@ -12,3 +12,11 @@ PG_DATABASE = os.environ.get("PG_DATABASE")
|
|||||||
PG_HOST = os.environ.get("PG_HOST")
|
PG_HOST = os.environ.get("PG_HOST")
|
||||||
|
|
||||||
SECRET_KEY = os.environ.get("SECRET_KEY")
|
SECRET_KEY = os.environ.get("SECRET_KEY")
|
||||||
|
|
||||||
|
S3_URL = os.environ.get("S3_URL")
|
||||||
|
S3_ACCESS_KEY = os.environ.get("S3_ACCESS_KEY")
|
||||||
|
S3_SECRET_ACCESS_KEY = os.environ.get("S3_SECRET_ACCESS_KEY")
|
||||||
|
S3_REGION = os.environ.get("S3_REGION")
|
||||||
|
S3_BUCKET = os.environ.get("S3_BUCKET")
|
||||||
|
|
||||||
|
RABBITMQ_URL = os.environ.get("RABBITMQ_URL")
|
||||||
|
|||||||
4
core/__init__.py
Normal file
4
core/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from .lifespan import lifespan as lifespan
|
||||||
|
from .app_settings import settings as settings
|
||||||
|
from .middlewares import register_middlewares as register_middlewares
|
||||||
|
from .exceptions import register_exception_handlers as register_exception_handlers
|
||||||
11
core/app_settings.py
Normal file
11
core/app_settings.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from fastapi.responses import ORJSONResponse
|
||||||
|
|
||||||
|
|
||||||
|
class Settings:
|
||||||
|
ROOT_PATH = "/api"
|
||||||
|
DEFAULT_RESPONSE_CLASS = ORJSONResponse
|
||||||
|
ORIGINS = ["http://localhost:3000"]
|
||||||
|
STATIC_DIR = "static"
|
||||||
|
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
17
core/exceptions.py
Normal file
17
core/exceptions.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from fastapi import Request
|
||||||
|
from fastapi.applications import AppType
|
||||||
|
from starlette.responses import JSONResponse
|
||||||
|
|
||||||
|
from utils.exceptions import ObjectNotFoundException, ForbiddenException
|
||||||
|
|
||||||
|
|
||||||
|
def register_exception_handlers(app: AppType):
|
||||||
|
@app.exception_handler(ObjectNotFoundException)
|
||||||
|
async def not_found_exception_handler(
|
||||||
|
request: Request, exc: ObjectNotFoundException
|
||||||
|
):
|
||||||
|
return JSONResponse(status_code=404, content={"detail": exc.name})
|
||||||
|
|
||||||
|
@app.exception_handler(ForbiddenException)
|
||||||
|
async def forbidden_exception_handler(request: Request, exc: ForbiddenException):
|
||||||
|
return JSONResponse(status_code=403, content={"detail": exc.name})
|
||||||
16
core/lifespan.py
Normal file
16
core/lifespan.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import contextlib
|
||||||
|
|
||||||
|
from fastapi.applications import AppType
|
||||||
|
|
||||||
|
from task_management import broker
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.asynccontextmanager
|
||||||
|
async def lifespan(app: AppType):
|
||||||
|
if not broker.is_worker_process:
|
||||||
|
await broker.startup()
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
if not broker.is_worker_process:
|
||||||
|
await broker.shutdown()
|
||||||
19
core/middlewares.py
Normal file
19
core/middlewares.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from fastapi.applications import AppType
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.middleware.gzip import GZipMiddleware
|
||||||
|
|
||||||
|
from .app_settings import settings
|
||||||
|
|
||||||
|
|
||||||
|
def register_middlewares(app: AppType):
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=settings.ORIGINS,
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
app.add_middleware(
|
||||||
|
GZipMiddleware,
|
||||||
|
minimum_size=1_000,
|
||||||
|
)
|
||||||
0
external/__init__.py
vendored
Normal file
0
external/__init__.py
vendored
Normal file
1
external/s3_uploader/__init__.py
vendored
Normal file
1
external/s3_uploader/__init__.py
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .uploader import S3Uploader as S3Uploader
|
||||||
81
external/s3_uploader/uploader.py
vendored
Normal file
81
external/s3_uploader/uploader.py
vendored
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import uuid
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from aioboto3 import Session
|
||||||
|
from fastapi import UploadFile
|
||||||
|
|
||||||
|
from backend import config
|
||||||
|
from logger import logger_builder
|
||||||
|
|
||||||
|
|
||||||
|
class S3Uploader:
|
||||||
|
session: Session
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.session = Session()
|
||||||
|
|
||||||
|
def _get_client(self) -> int:
|
||||||
|
return self.session.client(
|
||||||
|
"s3",
|
||||||
|
endpoint_url=config.S3_URL,
|
||||||
|
aws_access_key_id=config.S3_ACCESS_KEY,
|
||||||
|
aws_secret_access_key=config.S3_SECRET_ACCESS_KEY,
|
||||||
|
region_name=config.S3_REGION,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _generate_s3_key() -> str:
|
||||||
|
return uuid.uuid4().hex
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_file_path_from_name(filename: str) -> str:
|
||||||
|
return f"{config.S3_URL}/{config.S3_BUCKET}/{filename}"
|
||||||
|
|
||||||
|
async def upload_from_bytes(
|
||||||
|
self,
|
||||||
|
image_bytes: bytes,
|
||||||
|
content_type: str,
|
||||||
|
extension: str,
|
||||||
|
unique_s3_key: Optional[str] = None,
|
||||||
|
) -> str:
|
||||||
|
logger = logger_builder.get_logger()
|
||||||
|
|
||||||
|
if unique_s3_key is None:
|
||||||
|
unique_s3_key = await self._generate_s3_key()
|
||||||
|
|
||||||
|
filename = unique_s3_key + "." + extension
|
||||||
|
file_url = self.get_file_path_from_name(filename)
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with self._get_client() as s3_client:
|
||||||
|
await s3_client.put_object(
|
||||||
|
Bucket=config.S3_BUCKET,
|
||||||
|
Key=filename,
|
||||||
|
Body=image_bytes,
|
||||||
|
ContentType=content_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"Successfully uploaded {filename} to S3")
|
||||||
|
return file_url
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error uploading image bytes: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def upload_from_upload_file_obj(self, upload_file: UploadFile) -> str:
|
||||||
|
file_bytes = upload_file.file.read()
|
||||||
|
extension = upload_file.filename.split(".")[-1]
|
||||||
|
file_url = await self.upload_from_bytes(
|
||||||
|
file_bytes, upload_file.content_type, extension
|
||||||
|
)
|
||||||
|
return file_url
|
||||||
|
|
||||||
|
async def delete_image(self, s3_key: str):
|
||||||
|
logger = logger_builder.get_logger()
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with self._get_client() as s3_client:
|
||||||
|
await s3_client.delete_object(Bucket=config.S3_BUCKET, Key=s3_key)
|
||||||
|
|
||||||
|
logger.info(f"Successfully deleted {s3_key} from S3")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error deleting image from S3: {e}")
|
||||||
@ -10,7 +10,7 @@ from logger.constants import (
|
|||||||
from logger.formatter import JsonFormatter
|
from logger.formatter import JsonFormatter
|
||||||
from logger.gunzip_rotating_file_handler import GunZipRotatingFileHandler
|
from logger.gunzip_rotating_file_handler import GunZipRotatingFileHandler
|
||||||
from logger.filters import LevelFilter, RequestIdFilter
|
from logger.filters import LevelFilter, RequestIdFilter
|
||||||
from core.singleton import Singleton
|
from utils.singleton import Singleton
|
||||||
|
|
||||||
|
|
||||||
class LoggerBuilder(metaclass=Singleton):
|
class LoggerBuilder(metaclass=Singleton):
|
||||||
|
|||||||
48
main.py
48
main.py
@ -1,40 +1,28 @@
|
|||||||
from fastapi import FastAPI, Request
|
import taskiq_fastapi
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.gzip import GZipMiddleware
|
|
||||||
from fastapi.responses import ORJSONResponse
|
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from starlette.responses import JSONResponse
|
|
||||||
|
|
||||||
import routers
|
import routers
|
||||||
|
from core import lifespan, settings, register_middlewares, register_exception_handlers
|
||||||
|
from task_management import broker
|
||||||
from utils.auto_include_routers import auto_include_routers
|
from utils.auto_include_routers import auto_include_routers
|
||||||
from utils.exceptions import ObjectNotFoundException
|
|
||||||
|
|
||||||
origins = ["http://localhost:3000"]
|
|
||||||
|
|
||||||
app = FastAPI(
|
def create_app() -> FastAPI:
|
||||||
|
app = FastAPI(
|
||||||
separate_input_output_schemas=True,
|
separate_input_output_schemas=True,
|
||||||
default_response_class=ORJSONResponse,
|
default_response_class=settings.DEFAULT_RESPONSE_CLASS,
|
||||||
root_path="/api",
|
root_path=settings.ROOT_PATH,
|
||||||
)
|
# lifespan=lifespan,
|
||||||
|
)
|
||||||
|
|
||||||
app.add_middleware(
|
register_middlewares(app)
|
||||||
CORSMiddleware,
|
register_exception_handlers(app)
|
||||||
allow_origins=origins,
|
auto_include_routers(app, routers, full_path=True)
|
||||||
allow_credentials=True,
|
app.mount("/static", StaticFiles(directory=settings.STATIC_DIR), name="static")
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
taskiq_fastapi.init(broker, "main:app")
|
||||||
)
|
return app
|
||||||
app.add_middleware(
|
|
||||||
GZipMiddleware,
|
|
||||||
minimum_size=1_000,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.exception_handler(ObjectNotFoundException)
|
app = create_app()
|
||||||
async def unicorn_exception_handler(request: Request, exc: ObjectNotFoundException):
|
|
||||||
return JSONResponse(status_code=404, content={"detail": exc.name})
|
|
||||||
|
|
||||||
|
|
||||||
auto_include_routers(app, routers, True)
|
|
||||||
|
|
||||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
|
||||||
|
|||||||
@ -1,13 +1,20 @@
|
|||||||
from sqlalchemy.orm import configure_mappers
|
from sqlalchemy.orm import configure_mappers
|
||||||
|
|
||||||
from modules.fulfillment_base.models import * # noqa: F401
|
from modules.fulfillment_base.models import * # noqa: F401
|
||||||
|
from .attr_select import (
|
||||||
|
AttributeOption as AttributeOption,
|
||||||
|
AttributeSelect as AttributeSelect,
|
||||||
|
)
|
||||||
|
from .attribute import (
|
||||||
|
AttributeType as AttributeType,
|
||||||
|
Attribute as Attribute,
|
||||||
|
AttributeValue as AttributeValue,
|
||||||
|
AttributeLabel as AttributeLabel,
|
||||||
|
module_attribute as module_attribute,
|
||||||
|
)
|
||||||
|
from .auth import User as User
|
||||||
from .base import BaseModel as BaseModel
|
from .base import BaseModel as BaseModel
|
||||||
from .board import Board as Board
|
from .board import Board as Board
|
||||||
from .built_in_module import ( # noqa: F401
|
|
||||||
BuiltInModule as BuiltInModule,
|
|
||||||
project_built_in_module as project_built_in_module,
|
|
||||||
built_in_module_dependencies as built_in_module_dependencies,
|
|
||||||
)
|
|
||||||
from .deal import Deal as Deal
|
from .deal import Deal as Deal
|
||||||
from .deal_group import DealGroup as DealGroup
|
from .deal_group import DealGroup as DealGroup
|
||||||
from .deal_tag import (
|
from .deal_tag import (
|
||||||
@ -15,6 +22,12 @@ from .deal_tag import (
|
|||||||
DealTagColor as DealTagColor,
|
DealTagColor as DealTagColor,
|
||||||
deals_deal_tags as deals_deal_tags,
|
deals_deal_tags as deals_deal_tags,
|
||||||
)
|
)
|
||||||
|
from .module import ( # noqa: F401
|
||||||
|
Module as Module,
|
||||||
|
ModuleTab as ModuleTab,
|
||||||
|
project_module as project_module,
|
||||||
|
module_dependencies as module_dependencies,
|
||||||
|
)
|
||||||
from .project import Project as Project
|
from .project import Project as Project
|
||||||
from .status import Status as Status, DealStatusHistory as DealStatusHistory
|
from .status import Status as Status, DealStatusHistory as DealStatusHistory
|
||||||
|
|
||||||
|
|||||||
43
models/attr_select.py
Normal file
43
models/attr_select.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from sqlalchemy import ForeignKey
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from models.base import BaseModel
|
||||||
|
from models.mixins import IdMixin, SoftDeleteMixin
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from models import Attribute
|
||||||
|
|
||||||
|
|
||||||
|
class AttributeSelect(BaseModel, IdMixin, SoftDeleteMixin):
|
||||||
|
__tablename__ = "attribute_selects"
|
||||||
|
|
||||||
|
name: Mapped[str] = mapped_column()
|
||||||
|
is_built_in: Mapped[bool] = mapped_column(
|
||||||
|
default=False,
|
||||||
|
comment="Если встроенный select, то запрещено редактировать пользователю",
|
||||||
|
)
|
||||||
|
|
||||||
|
options: Mapped[list["AttributeOption"]] = relationship(
|
||||||
|
back_populates="select",
|
||||||
|
lazy="noload",
|
||||||
|
)
|
||||||
|
|
||||||
|
attributes: Mapped[list["Attribute"]] = relationship(
|
||||||
|
back_populates="select",
|
||||||
|
lazy="noload",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AttributeOption(BaseModel, IdMixin, SoftDeleteMixin):
|
||||||
|
__tablename__ = "attribute_options"
|
||||||
|
|
||||||
|
name: Mapped[str] = mapped_column()
|
||||||
|
lexorank: Mapped[str] = mapped_column(comment="Ранг опции")
|
||||||
|
|
||||||
|
select_id: Mapped[int] = mapped_column(ForeignKey("attribute_selects.id"))
|
||||||
|
select: Mapped[AttributeSelect] = relationship(
|
||||||
|
back_populates="options",
|
||||||
|
lazy="noload",
|
||||||
|
)
|
||||||
139
models/attribute.py
Normal file
139
models/attribute.py
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
from sqlalchemy import ForeignKey, Table, Column
|
||||||
|
from sqlalchemy.dialects.postgresql import JSONB
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from models.base import BaseModel
|
||||||
|
from models.mixins import IdMixin, SoftDeleteMixin
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from models import Module, Deal, AttributeSelect, AttributeOption
|
||||||
|
|
||||||
|
module_attribute = Table(
|
||||||
|
"module_attribute",
|
||||||
|
BaseModel.metadata,
|
||||||
|
Column("module_id", ForeignKey("modules.id"), primary_key=True),
|
||||||
|
Column("attribute_id", ForeignKey("attributes.id"), primary_key=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AttributeType(BaseModel, IdMixin, SoftDeleteMixin):
|
||||||
|
__tablename__ = "attribute_types"
|
||||||
|
|
||||||
|
type: Mapped[str] = mapped_column(unique=True)
|
||||||
|
name: Mapped[str] = mapped_column(unique=True)
|
||||||
|
|
||||||
|
attributes: Mapped["Attribute"] = relationship(
|
||||||
|
back_populates="type",
|
||||||
|
lazy="noload",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Attribute(BaseModel, IdMixin, SoftDeleteMixin):
|
||||||
|
__tablename__ = "attributes"
|
||||||
|
|
||||||
|
label: Mapped[str] = mapped_column()
|
||||||
|
is_applicable_to_group: Mapped[bool] = mapped_column(
|
||||||
|
default=False,
|
||||||
|
comment="Применять ли изменения атрибута карточки ко всем карточкам в группе",
|
||||||
|
)
|
||||||
|
is_nullable: Mapped[bool] = mapped_column(default=False)
|
||||||
|
|
||||||
|
default_value: Mapped[Optional[dict[str, any]]] = mapped_column(JSONB)
|
||||||
|
default_option_id: Mapped[Optional[int]] = mapped_column(
|
||||||
|
ForeignKey("attribute_options.id")
|
||||||
|
)
|
||||||
|
default_option: Mapped[Optional["AttributeOption"]] = relationship(
|
||||||
|
backref="attributes", lazy="joined"
|
||||||
|
)
|
||||||
|
|
||||||
|
description: Mapped[str] = mapped_column(default="")
|
||||||
|
|
||||||
|
is_built_in: Mapped[bool] = mapped_column(default=False)
|
||||||
|
|
||||||
|
type_id: Mapped[int] = mapped_column(ForeignKey("attribute_types.id"))
|
||||||
|
type: Mapped[AttributeType] = relationship(
|
||||||
|
back_populates="attributes",
|
||||||
|
lazy="joined",
|
||||||
|
)
|
||||||
|
|
||||||
|
select_id: Mapped[Optional[int]] = mapped_column(ForeignKey("attribute_selects.id"))
|
||||||
|
select: Mapped[Optional["AttributeSelect"]] = relationship(
|
||||||
|
back_populates="attributes",
|
||||||
|
lazy="joined",
|
||||||
|
)
|
||||||
|
|
||||||
|
modules: Mapped[list["Module"]] = relationship(
|
||||||
|
secondary=module_attribute,
|
||||||
|
back_populates="attributes",
|
||||||
|
lazy="noload",
|
||||||
|
)
|
||||||
|
|
||||||
|
values: Mapped[list["AttributeValue"]] = relationship(
|
||||||
|
uselist=True,
|
||||||
|
back_populates="attribute",
|
||||||
|
lazy="noload",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AttributeValue(BaseModel):
|
||||||
|
__tablename__ = "attribute_values"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(
|
||||||
|
primary_key=True,
|
||||||
|
autoincrement=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
value: Mapped[Optional[dict[str, any]]] = mapped_column(JSONB)
|
||||||
|
|
||||||
|
deal_id: Mapped[int] = mapped_column(
|
||||||
|
ForeignKey("deals.id"),
|
||||||
|
primary_key=True,
|
||||||
|
)
|
||||||
|
deal: Mapped["Deal"] = relationship(
|
||||||
|
back_populates="attributes_values",
|
||||||
|
lazy="noload",
|
||||||
|
)
|
||||||
|
|
||||||
|
module_id: Mapped[int] = mapped_column(
|
||||||
|
ForeignKey("modules.id"),
|
||||||
|
primary_key=True,
|
||||||
|
)
|
||||||
|
module: Mapped["Module"] = relationship(
|
||||||
|
back_populates="attribute_values",
|
||||||
|
lazy="noload",
|
||||||
|
)
|
||||||
|
|
||||||
|
attribute_id: Mapped[int] = mapped_column(
|
||||||
|
ForeignKey("attributes.id"),
|
||||||
|
primary_key=True,
|
||||||
|
)
|
||||||
|
attribute: Mapped[Attribute] = relationship(
|
||||||
|
back_populates="values",
|
||||||
|
lazy="noload",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AttributeLabel(BaseModel):
|
||||||
|
__tablename__ = "attribute_labels"
|
||||||
|
|
||||||
|
label: Mapped[str] = mapped_column()
|
||||||
|
|
||||||
|
module_id: Mapped[int] = mapped_column(
|
||||||
|
ForeignKey("modules.id"),
|
||||||
|
primary_key=True,
|
||||||
|
)
|
||||||
|
module: Mapped["Module"] = relationship(
|
||||||
|
backref="attribute_labels",
|
||||||
|
lazy="noload",
|
||||||
|
)
|
||||||
|
|
||||||
|
attribute_id: Mapped[int] = mapped_column(
|
||||||
|
ForeignKey("attributes.id"),
|
||||||
|
primary_key=True,
|
||||||
|
)
|
||||||
|
attribute: Mapped[Attribute] = relationship(
|
||||||
|
backref="attribute_labels",
|
||||||
|
lazy="noload",
|
||||||
|
)
|
||||||
6
models/auth.py
Normal file
6
models/auth.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from models.base import BaseModel
|
||||||
|
from models.mixins import IdMixin, SoftDeleteMixin
|
||||||
|
|
||||||
|
|
||||||
|
class User(BaseModel, IdMixin, SoftDeleteMixin):
|
||||||
|
__tablename__ = "users"
|
||||||
@ -1,79 +0,0 @@
|
|||||||
import enum
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from sqlalchemy import Table, Column, ForeignKey
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
||||||
|
|
||||||
from models.base import BaseModel
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from models import Project
|
|
||||||
|
|
||||||
project_built_in_module = Table(
|
|
||||||
"project_built_in_module",
|
|
||||||
BaseModel.metadata,
|
|
||||||
Column("project_id", ForeignKey("projects.id"), primary_key=True),
|
|
||||||
Column("module_id", ForeignKey("built_in_modules.id"), primary_key=True),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
built_in_module_dependencies = Table(
|
|
||||||
"built_in_module_dependencies",
|
|
||||||
BaseModel.metadata,
|
|
||||||
Column("module_id", ForeignKey("built_in_modules.id"), primary_key=True),
|
|
||||||
Column("depends_on_id", ForeignKey("built_in_modules.id"), primary_key=True),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BuiltInModule(BaseModel):
|
|
||||||
__tablename__ = "built_in_modules"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
key: Mapped[str] = mapped_column(unique=True)
|
|
||||||
label: Mapped[str] = mapped_column()
|
|
||||||
description: Mapped[str] = mapped_column()
|
|
||||||
is_deleted: Mapped[bool] = mapped_column(default=False)
|
|
||||||
|
|
||||||
depends_on: Mapped[list["BuiltInModule"]] = relationship(
|
|
||||||
secondary=built_in_module_dependencies,
|
|
||||||
primaryjoin="BuiltInModule.id == built_in_module_dependencies.c.module_id",
|
|
||||||
secondaryjoin="BuiltInModule.id == built_in_module_dependencies.c.depends_on_id",
|
|
||||||
back_populates="depended_on_by",
|
|
||||||
lazy="immediate",
|
|
||||||
)
|
|
||||||
|
|
||||||
depended_on_by: Mapped[list["BuiltInModule"]] = relationship(
|
|
||||||
secondary="built_in_module_dependencies",
|
|
||||||
primaryjoin="BuiltInModule.id == built_in_module_dependencies.c.depends_on_id",
|
|
||||||
secondaryjoin="BuiltInModule.id == built_in_module_dependencies.c.module_id",
|
|
||||||
back_populates="depends_on",
|
|
||||||
lazy="noload",
|
|
||||||
)
|
|
||||||
|
|
||||||
projects: Mapped[list["Project"]] = relationship(
|
|
||||||
uselist=True,
|
|
||||||
secondary="project_built_in_module",
|
|
||||||
back_populates="built_in_modules",
|
|
||||||
lazy="noload",
|
|
||||||
)
|
|
||||||
|
|
||||||
tabs: Mapped[list["BuiltInModuleTab"]] = relationship(
|
|
||||||
lazy="immediate", backref="module", cascade="all, delete-orphan"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class DeviceType(enum.StrEnum):
|
|
||||||
MOBILE = "mobile"
|
|
||||||
DESKTOP = "desktop"
|
|
||||||
BOTH = "both"
|
|
||||||
|
|
||||||
|
|
||||||
class BuiltInModuleTab(BaseModel):
|
|
||||||
__tablename__ = "built_in_module_tab"
|
|
||||||
|
|
||||||
id: Mapped[int] = mapped_column(primary_key=True)
|
|
||||||
key: Mapped[str] = mapped_column(unique=True)
|
|
||||||
label: Mapped[str] = mapped_column()
|
|
||||||
icon_name: Mapped[str] = mapped_column()
|
|
||||||
module_id: Mapped[int] = mapped_column(ForeignKey("built_in_modules.id"))
|
|
||||||
device: Mapped[DeviceType] = mapped_column(default=DeviceType.BOTH)
|
|
||||||
@ -7,7 +7,14 @@ from models.base import BaseModel
|
|||||||
from models.mixins import SoftDeleteMixin, CreatedAtMixin, IdMixin
|
from models.mixins import SoftDeleteMixin, CreatedAtMixin, IdMixin
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from models import Status, Board, DealStatusHistory, DealGroup, DealTag
|
from models import (
|
||||||
|
Status,
|
||||||
|
Board,
|
||||||
|
DealStatusHistory,
|
||||||
|
DealGroup,
|
||||||
|
DealTag,
|
||||||
|
AttributeValue,
|
||||||
|
)
|
||||||
from modules.clients.models import Client
|
from modules.clients.models import Client
|
||||||
|
|
||||||
|
|
||||||
@ -49,6 +56,10 @@ class Deal(BaseModel, IdMixin, SoftDeleteMixin, CreatedAtMixin):
|
|||||||
secondaryjoin="and_(DealTag.id == deals_deal_tags.c.deal_tag_id, DealTag.is_deleted == False)",
|
secondaryjoin="and_(DealTag.id == deals_deal_tags.c.deal_tag_id, DealTag.is_deleted == False)",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
attributes_values: Mapped[list["AttributeValue"]] = relationship(
|
||||||
|
back_populates="deal",
|
||||||
|
)
|
||||||
|
|
||||||
# module client
|
# module client
|
||||||
client_id: Mapped[Optional[int]] = mapped_column(
|
client_id: Mapped[Optional[int]] = mapped_column(
|
||||||
ForeignKey("clients.id", ondelete="CASCADE"),
|
ForeignKey("clients.id", ondelete="CASCADE"),
|
||||||
|
|||||||
89
models/module.py
Normal file
89
models/module.py
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import enum
|
||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
from sqlalchemy import Table, Column, ForeignKey
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from models import AttributeValue, Attribute
|
||||||
|
from models.base import BaseModel
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from models import Project
|
||||||
|
|
||||||
|
project_module = Table(
|
||||||
|
"project_module",
|
||||||
|
BaseModel.metadata,
|
||||||
|
Column("project_id", ForeignKey("projects.id"), primary_key=True),
|
||||||
|
Column("module_id", ForeignKey("modules.id"), primary_key=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
module_dependencies = Table(
|
||||||
|
"module_dependencies",
|
||||||
|
BaseModel.metadata,
|
||||||
|
Column("module_id", ForeignKey("modules.id"), primary_key=True),
|
||||||
|
Column("depends_on_id", ForeignKey("modules.id"), primary_key=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Module(BaseModel):
|
||||||
|
__tablename__ = "modules"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
key: Mapped[str] = mapped_column(unique=True)
|
||||||
|
label: Mapped[str] = mapped_column()
|
||||||
|
description: Mapped[Optional[str]] = mapped_column()
|
||||||
|
is_deleted: Mapped[bool] = mapped_column(default=False)
|
||||||
|
is_built_in: Mapped[bool] = mapped_column(default=False, server_default="0")
|
||||||
|
|
||||||
|
depends_on: Mapped[list["Module"]] = relationship(
|
||||||
|
secondary=module_dependencies,
|
||||||
|
primaryjoin="Module.id == module_dependencies.c.module_id",
|
||||||
|
secondaryjoin="Module.id == module_dependencies.c.depends_on_id",
|
||||||
|
back_populates="depended_on_by",
|
||||||
|
lazy="immediate",
|
||||||
|
)
|
||||||
|
|
||||||
|
depended_on_by: Mapped[list["Module"]] = relationship(
|
||||||
|
secondary="module_dependencies",
|
||||||
|
primaryjoin="Module.id == module_dependencies.c.depends_on_id",
|
||||||
|
secondaryjoin="Module.id == module_dependencies.c.module_id",
|
||||||
|
back_populates="depends_on",
|
||||||
|
lazy="noload",
|
||||||
|
)
|
||||||
|
|
||||||
|
projects: Mapped[list["Project"]] = relationship(
|
||||||
|
uselist=True,
|
||||||
|
secondary="project_module",
|
||||||
|
back_populates="modules",
|
||||||
|
lazy="noload",
|
||||||
|
)
|
||||||
|
|
||||||
|
tabs: Mapped[list["ModuleTab"]] = relationship(
|
||||||
|
lazy="immediate", backref="module", cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
|
|
||||||
|
attributes: Mapped[list["Attribute"]] = relationship(
|
||||||
|
secondary="module_attribute", back_populates="modules", lazy="noload"
|
||||||
|
)
|
||||||
|
|
||||||
|
attribute_values: Mapped[list["AttributeValue"]] = relationship(
|
||||||
|
lazy="noload", back_populates="module"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceType(enum.StrEnum):
|
||||||
|
MOBILE = "mobile"
|
||||||
|
DESKTOP = "desktop"
|
||||||
|
BOTH = "both"
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleTab(BaseModel):
|
||||||
|
__tablename__ = "module_tab"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
key: Mapped[str] = mapped_column(unique=True)
|
||||||
|
label: Mapped[str] = mapped_column()
|
||||||
|
icon_name: Mapped[Optional[str]] = mapped_column()
|
||||||
|
module_id: Mapped[int] = mapped_column(ForeignKey("modules.id"))
|
||||||
|
device: Mapped[DeviceType] = mapped_column(default=DeviceType.BOTH)
|
||||||
@ -6,7 +6,7 @@ from models.base import BaseModel
|
|||||||
from models.mixins import SoftDeleteMixin, CreatedAtMixin, IdMixin
|
from models.mixins import SoftDeleteMixin, CreatedAtMixin, IdMixin
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from models import Board, BuiltInModule, DealTag
|
from models import Board, Module, DealTag
|
||||||
|
|
||||||
|
|
||||||
class Project(BaseModel, IdMixin, SoftDeleteMixin, CreatedAtMixin):
|
class Project(BaseModel, IdMixin, SoftDeleteMixin, CreatedAtMixin):
|
||||||
@ -19,11 +19,11 @@ class Project(BaseModel, IdMixin, SoftDeleteMixin, CreatedAtMixin):
|
|||||||
lazy="noload",
|
lazy="noload",
|
||||||
)
|
)
|
||||||
|
|
||||||
built_in_modules: Mapped[list["BuiltInModule"]] = relationship(
|
modules: Mapped[list["Module"]] = relationship(
|
||||||
secondary="project_built_in_module",
|
secondary="project_module",
|
||||||
back_populates="projects",
|
back_populates="projects",
|
||||||
lazy="selectin",
|
lazy="selectin",
|
||||||
order_by="asc(BuiltInModule.id)",
|
order_by="asc(Module.id)",
|
||||||
)
|
)
|
||||||
|
|
||||||
tags: Mapped[list["DealTag"]] = relationship(
|
tags: Mapped[list["DealTag"]] = relationship(
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
from reportlab.graphics.barcode import code128
|
from reportlab.graphics.barcode import code128
|
||||||
from reportlab.lib.units import mm
|
from reportlab.lib.units import mm
|
||||||
from reportlab.platypus import Spacer, PageBreak, Paragraph
|
from reportlab.platypus import Spacer, PageBreak, Paragraph
|
||||||
@ -24,7 +25,7 @@ class BarcodePdfGenerator(PDFGenerator):
|
|||||||
return None
|
return None
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def generate(
|
async def generate(
|
||||||
self, barcodes_data: list[BarcodeData | PdfBarcodeImageGenData]
|
self, barcodes_data: list[BarcodeData | PdfBarcodeImageGenData]
|
||||||
) -> BytesIO:
|
) -> BytesIO:
|
||||||
pdf_barcodes_gen_data: list[PdfBarcodeGenData | PdfBarcodeImageGenData] = []
|
pdf_barcodes_gen_data: list[PdfBarcodeGenData | PdfBarcodeImageGenData] = []
|
||||||
@ -52,9 +53,9 @@ class BarcodePdfGenerator(PDFGenerator):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return self._generate(pdf_barcodes_gen_data)
|
return await self._generate(pdf_barcodes_gen_data)
|
||||||
|
|
||||||
def _generate(
|
async def _generate(
|
||||||
self, barcodes_data: list[PdfBarcodeGenData | PdfBarcodeImageGenData]
|
self, barcodes_data: list[PdfBarcodeGenData | PdfBarcodeImageGenData]
|
||||||
) -> BytesIO:
|
) -> BytesIO:
|
||||||
pdf_maker = PdfMaker((self.page_width, self.page_height))
|
pdf_maker = PdfMaker((self.page_width, self.page_height))
|
||||||
@ -63,9 +64,10 @@ class BarcodePdfGenerator(PDFGenerator):
|
|||||||
|
|
||||||
for barcode_data in barcodes_data:
|
for barcode_data in barcodes_data:
|
||||||
if "barcode_value" in barcode_data:
|
if "barcode_value" in barcode_data:
|
||||||
pdf_files.append(self._generate_for_one_product(barcode_data))
|
result = self._generate_for_one_product(barcode_data)
|
||||||
else:
|
else:
|
||||||
pdf_files.append(self._generate_for_one_product_using_img(barcode_data))
|
result = await self._generate_for_one_product_using_img(barcode_data)
|
||||||
|
pdf_files.append(result)
|
||||||
pdf_files.append(self._generate_spacers())
|
pdf_files.append(self._generate_spacers())
|
||||||
|
|
||||||
for file in pdf_files[:-1]:
|
for file in pdf_files[:-1]:
|
||||||
@ -138,11 +140,18 @@ class BarcodePdfGenerator(PDFGenerator):
|
|||||||
buffer.seek(0)
|
buffer.seek(0)
|
||||||
return buffer
|
return buffer
|
||||||
|
|
||||||
def _generate_for_one_product_using_img(
|
async def _generate_for_one_product_using_img(
|
||||||
self, barcode_data: PdfBarcodeImageGenData
|
self, barcode_data: PdfBarcodeImageGenData
|
||||||
) -> BytesIO:
|
) -> BytesIO:
|
||||||
with open(barcode_data["barcode_image_url"], "rb") as pdf_file:
|
pdf_url = barcode_data["barcode_image_url"]
|
||||||
pdf_bytes = pdf_file.read()
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(pdf_url) as response:
|
||||||
|
if response.status != 200:
|
||||||
|
raise ValueError(
|
||||||
|
f"Failed to download PDF from {pdf_url} (status {response.status})"
|
||||||
|
)
|
||||||
|
pdf_bytes = await response.read()
|
||||||
|
|
||||||
pdf_maker = PdfMaker((self.page_width, self.page_height))
|
pdf_maker = PdfMaker((self.page_width, self.page_height))
|
||||||
for _ in range(barcode_data["num_duplicates"]):
|
for _ in range(barcode_data["num_duplicates"]):
|
||||||
|
|||||||
@ -33,4 +33,4 @@ class ProductBarcodeImage(BaseModel):
|
|||||||
)
|
)
|
||||||
product: Mapped["Product"] = relationship(back_populates="barcode_image")
|
product: Mapped["Product"] = relationship(back_populates="barcode_image")
|
||||||
|
|
||||||
filename: Mapped[str] = mapped_column()
|
image_url: Mapped[str] = mapped_column()
|
||||||
|
|||||||
@ -52,7 +52,6 @@ class Product(BaseModel, IdMixin, SoftDeleteMixin):
|
|||||||
barcode_image: Mapped["ProductBarcodeImage"] = relationship(
|
barcode_image: Mapped["ProductBarcodeImage"] = relationship(
|
||||||
back_populates="product",
|
back_populates="product",
|
||||||
lazy="joined",
|
lazy="joined",
|
||||||
uselist=False,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -26,6 +26,7 @@ class DealProductRepository(
|
|||||||
return (
|
return (
|
||||||
stmt.options(
|
stmt.options(
|
||||||
joinedload(DealProduct.product).selectinload(Product.barcodes),
|
joinedload(DealProduct.product).selectinload(Product.barcodes),
|
||||||
|
joinedload(DealProduct.product).joinedload(Product.client),
|
||||||
selectinload(DealProduct.product_services).joinedload(
|
selectinload(DealProduct.product_services).joinedload(
|
||||||
DealProductService.service
|
DealProductService.service
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
from sqlalchemy import or_, delete
|
from sqlalchemy import or_, delete
|
||||||
from sqlalchemy.orm import selectinload, joinedload
|
from sqlalchemy.orm import selectinload, joinedload
|
||||||
|
|
||||||
from modules.fulfillment_base.models import Product, ProductBarcode, BarcodeTemplate
|
from modules.fulfillment_base.models import Product, ProductBarcode, BarcodeTemplate, ProductBarcodeImage
|
||||||
|
from modules.fulfillment_base.models.product import ProductImage
|
||||||
from modules.fulfillment_base.schemas.product import (
|
from modules.fulfillment_base.schemas.product import (
|
||||||
CreateProductSchema,
|
CreateProductSchema,
|
||||||
UpdateProductSchema,
|
UpdateProductSchema,
|
||||||
@ -16,6 +17,7 @@ class ProductRepository(
|
|||||||
RepUpdateMixin[Product, UpdateProductSchema],
|
RepUpdateMixin[Product, UpdateProductSchema],
|
||||||
RepGetByIdMixin[Product],
|
RepGetByIdMixin[Product],
|
||||||
):
|
):
|
||||||
|
session: AsyncSession
|
||||||
entity_class = Product
|
entity_class = Product
|
||||||
entity_not_found_msg = "Товар не найден"
|
entity_not_found_msg = "Товар не найден"
|
||||||
|
|
||||||
@ -95,3 +97,36 @@ class ProductRepository(
|
|||||||
await self._update_barcodes(product, data.barcodes)
|
await self._update_barcodes(product, data.barcodes)
|
||||||
del data.barcodes
|
del data.barcodes
|
||||||
return await self._apply_update_data_to_model(product, data, True)
|
return await self._apply_update_data_to_model(product, data, True)
|
||||||
|
|
||||||
|
async def delete_images(self, product_images: list[ProductImage], with_commit: bool = False):
|
||||||
|
for img in product_images:
|
||||||
|
await self.session.delete(img)
|
||||||
|
if with_commit:
|
||||||
|
await self.session.commit()
|
||||||
|
else:
|
||||||
|
await self.session.flush()
|
||||||
|
|
||||||
|
async def delete_barcode_image(self, barcode_image: ProductBarcodeImage, with_commit: bool = False):
|
||||||
|
await self.session.delete(barcode_image)
|
||||||
|
if with_commit:
|
||||||
|
await self.session.commit()
|
||||||
|
else:
|
||||||
|
await self.session.flush()
|
||||||
|
|
||||||
|
async def create_image(self, product_id: int, image_url: str) -> ProductImage:
|
||||||
|
product_image = ProductImage(
|
||||||
|
product_id=product_id,
|
||||||
|
image_url=image_url,
|
||||||
|
)
|
||||||
|
self.session.add(product_image)
|
||||||
|
await self.session.commit()
|
||||||
|
return product_image
|
||||||
|
|
||||||
|
async def create_barcode_image(self, product_id: int, image_url: str) -> ProductBarcodeImage:
|
||||||
|
product_barcode_image = ProductBarcodeImage(
|
||||||
|
product_id=product_id,
|
||||||
|
image_url=image_url,
|
||||||
|
)
|
||||||
|
self.session.add(product_barcode_image)
|
||||||
|
await self.session.commit()
|
||||||
|
return product_barcode_image
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from pydantic import field_validator
|
from pydantic import field_validator, model_validator
|
||||||
|
|
||||||
from modules.fulfillment_base.models import ProductBarcode
|
from modules.fulfillment_base.models import ProductBarcode
|
||||||
from modules.fulfillment_base.schemas.barcode_template import BarcodeTemplateSchema
|
from modules.fulfillment_base.schemas.barcode_template import BarcodeTemplateSchema
|
||||||
@ -16,6 +16,11 @@ class ProductImageSchema(BaseSchema):
|
|||||||
image_url: str
|
image_url: str
|
||||||
|
|
||||||
|
|
||||||
|
class ProductBarcodeImageSchema(BaseSchema):
|
||||||
|
product_id: int
|
||||||
|
image_url: str
|
||||||
|
|
||||||
|
|
||||||
class CreateProductSchema(BaseSchema):
|
class CreateProductSchema(BaseSchema):
|
||||||
name: str
|
name: str
|
||||||
article: str
|
article: str
|
||||||
@ -27,10 +32,27 @@ class CreateProductSchema(BaseSchema):
|
|||||||
composition: Optional[str]
|
composition: Optional[str]
|
||||||
size: Optional[str]
|
size: Optional[str]
|
||||||
additional_info: Optional[str]
|
additional_info: Optional[str]
|
||||||
barcodes: list[str]
|
barcodes: list[str] = []
|
||||||
|
|
||||||
|
|
||||||
class ProductSchema(CreateProductSchema):
|
class BaseProductSchema(CreateProductSchema):
|
||||||
|
image_url: Optional[str] = None
|
||||||
|
images: Optional[list[ProductImageSchema]] = []
|
||||||
|
barcode_image_url: Optional[str] = None
|
||||||
|
barcode_image: Optional[ProductBarcodeImageSchema] = None
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def images_list_to_image_url(cls, values):
|
||||||
|
if values.images:
|
||||||
|
latest_image = values.images[-1]
|
||||||
|
values.image_url = latest_image.image_url
|
||||||
|
|
||||||
|
if values.barcode_image:
|
||||||
|
values.barcode_image_url = values.barcode_image.image_url
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
class ProductSchema(BaseProductSchema):
|
||||||
id: int
|
id: int
|
||||||
barcode_template: BarcodeTemplateSchema
|
barcode_template: BarcodeTemplateSchema
|
||||||
|
|
||||||
@ -75,6 +97,10 @@ class GetProductBarcodePdfRequest(BaseSchema):
|
|||||||
barcode: str
|
barcode: str
|
||||||
|
|
||||||
|
|
||||||
|
class GetDealBarcodesPdfRequest(BaseSchema):
|
||||||
|
deal_id: int
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
# region Response
|
# region Response
|
||||||
@ -93,6 +119,10 @@ class UpdateProductResponse(BaseResponse):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ProductUploadImageResponse(BaseResponse):
|
||||||
|
image_url: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class DeleteProductResponse(BaseResponse):
|
class DeleteProductResponse(BaseResponse):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -101,4 +131,16 @@ class GetProductBarcodePdfResponse(BasePdfResponse):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class GetDealBarcodesPdfResponse(BasePdfResponse):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BarcodeUploadImageResponse(BaseResponse):
|
||||||
|
image_url: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteBarcodeImageResponse(BaseResponse):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|||||||
@ -4,9 +4,16 @@ from io import BytesIO
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from modules.fulfillment_base.barcodes_pdf_gen import BarcodePdfGenerator, BarcodeData
|
from modules.fulfillment_base.barcodes_pdf_gen import BarcodePdfGenerator, BarcodeData
|
||||||
from modules.fulfillment_base.models import Product
|
from modules.fulfillment_base.barcodes_pdf_gen.types import PdfBarcodeImageGenData
|
||||||
from modules.fulfillment_base.repositories import ProductRepository
|
from modules.fulfillment_base.models import Product, DealProduct
|
||||||
from modules.fulfillment_base.schemas.product import GetProductBarcodePdfRequest
|
from modules.fulfillment_base.repositories import (
|
||||||
|
ProductRepository,
|
||||||
|
DealProductRepository,
|
||||||
|
)
|
||||||
|
from modules.fulfillment_base.schemas.product import (
|
||||||
|
GetProductBarcodePdfRequest,
|
||||||
|
GetDealBarcodesPdfRequest,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BarcodePrinterService:
|
class BarcodePrinterService:
|
||||||
@ -15,27 +22,75 @@ class BarcodePrinterService:
|
|||||||
def __init__(self, session: AsyncSession):
|
def __init__(self, session: AsyncSession):
|
||||||
self.session = session
|
self.session = session
|
||||||
|
|
||||||
async def generate_pdf(
|
async def generate_product_pdf(
|
||||||
self, request: GetProductBarcodePdfRequest
|
self, request: GetProductBarcodePdfRequest
|
||||||
) -> tuple[str, BytesIO]:
|
) -> tuple[str, BytesIO]:
|
||||||
product: Product = await ProductRepository(self.session).get_by_id(
|
product: Product = await ProductRepository(self.session).get_by_id(
|
||||||
request.product_id
|
request.product_id
|
||||||
)
|
)
|
||||||
|
if product.barcode_image:
|
||||||
|
barcode_data: PdfBarcodeImageGenData = {
|
||||||
|
"barcode_image_url": product.barcode_image.image_url,
|
||||||
|
"num_duplicates": request.quantity,
|
||||||
|
}
|
||||||
|
else:
|
||||||
barcode_data: BarcodeData = {
|
barcode_data: BarcodeData = {
|
||||||
"barcode": request.barcode,
|
"barcode": request.barcode,
|
||||||
"template": product.barcode_template,
|
"template": product.barcode_template,
|
||||||
"product": product,
|
"product": product,
|
||||||
"num_duplicates": request.quantity,
|
"num_duplicates": request.quantity,
|
||||||
}
|
}
|
||||||
|
|
||||||
filename = f"{product.id}_barcode.pdf"
|
filename = f"{product.id}_barcode.pdf"
|
||||||
|
|
||||||
size = product.barcode_template.size
|
size = product.barcode_template.size
|
||||||
generator = BarcodePdfGenerator(size.width, size.height)
|
generator = BarcodePdfGenerator(size.width, size.height)
|
||||||
return filename, generator.generate([barcode_data])
|
return filename, await generator.generate([barcode_data])
|
||||||
|
|
||||||
async def generate_base64(
|
async def generate_product_base64(
|
||||||
self, request: GetProductBarcodePdfRequest
|
self, request: GetProductBarcodePdfRequest
|
||||||
) -> tuple[str, str]:
|
) -> tuple[str, str]:
|
||||||
filename, pdf_buffer = await self.generate_pdf(request)
|
filename, pdf_buffer = await self.generate_product_pdf(request)
|
||||||
|
base64_string = base64.b64encode(pdf_buffer.read()).decode("utf-8")
|
||||||
|
return filename, base64_string
|
||||||
|
|
||||||
|
async def generate_deal_pdf(
|
||||||
|
self, request: GetDealBarcodesPdfRequest
|
||||||
|
) -> tuple[str, BytesIO]:
|
||||||
|
deal_product_repo = DealProductRepository(self.session)
|
||||||
|
deal_products: list[DealProduct] = await deal_product_repo.get_all(
|
||||||
|
request.deal_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(deal_products) == 0:
|
||||||
|
return "no_content.pdf", BytesIO()
|
||||||
|
|
||||||
|
barcodes_data: list[BarcodeData | PdfBarcodeImageGenData] = []
|
||||||
|
|
||||||
|
for deal_product in deal_products:
|
||||||
|
if deal_product.product.barcode_image:
|
||||||
|
barcode_data: PdfBarcodeImageGenData = {
|
||||||
|
"barcode_image_url": deal_product.product.barcode_image.image_url,
|
||||||
|
"num_duplicates": deal_product.quantity,
|
||||||
|
}
|
||||||
|
barcodes_data.append(barcode_data)
|
||||||
|
elif len(deal_product.product.barcodes) > 0:
|
||||||
|
barcode_data: BarcodeData = {
|
||||||
|
"barcode": deal_product.product.barcodes[0].barcode,
|
||||||
|
"template": deal_product.product.barcode_template,
|
||||||
|
"product": deal_product.product,
|
||||||
|
"num_duplicates": deal_product.quantity,
|
||||||
|
}
|
||||||
|
barcodes_data.append(barcode_data)
|
||||||
|
|
||||||
|
size = deal_products[0].product.barcode_template.size
|
||||||
|
generator = BarcodePdfGenerator(size.width, size.height)
|
||||||
|
filename = "deal_barcodes.pdf"
|
||||||
|
return filename, await generator.generate(barcodes_data)
|
||||||
|
|
||||||
|
async def generate_deal_base64(
|
||||||
|
self, request: GetDealBarcodesPdfRequest
|
||||||
|
) -> tuple[str, str]:
|
||||||
|
filename, pdf_buffer = await self.generate_deal_pdf(request)
|
||||||
base64_string = base64.b64encode(pdf_buffer.read()).decode("utf-8")
|
base64_string = base64.b64encode(pdf_buffer.read()).decode("utf-8")
|
||||||
return filename, base64_string
|
return filename, base64_string
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
import math
|
import math
|
||||||
|
|
||||||
|
from fastapi import UploadFile
|
||||||
|
|
||||||
|
from external.s3_uploader import S3Uploader
|
||||||
from modules.fulfillment_base.models import Product
|
from modules.fulfillment_base.models import Product
|
||||||
from modules.fulfillment_base.repositories import ProductRepository
|
from modules.fulfillment_base.repositories import ProductRepository
|
||||||
from modules.fulfillment_base.schemas.product import (
|
from modules.fulfillment_base.schemas.product import *
|
||||||
CreateProductRequest,
|
|
||||||
ProductSchema,
|
|
||||||
UpdateProductRequest, GetProductsResponse,
|
|
||||||
)
|
|
||||||
from schemas.base import PaginationSchema, PaginationInfoSchema
|
from schemas.base import PaginationSchema, PaginationInfoSchema
|
||||||
from services.mixins import *
|
from services.mixins import *
|
||||||
|
|
||||||
@ -46,5 +45,54 @@ class ProductService(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def is_soft_delete(self, product: ProductSchema) -> bool:
|
async def upload_image(
|
||||||
return True
|
self, product_id: int, upload_file: UploadFile
|
||||||
|
) -> ProductUploadImageResponse:
|
||||||
|
product: Product = await self.repository.get_by_id(product_id)
|
||||||
|
s3_uploader = S3Uploader()
|
||||||
|
|
||||||
|
if len(product.images) > 0:
|
||||||
|
for image in product.images:
|
||||||
|
s3_key = image.image_url.split("/")[-1]
|
||||||
|
await s3_uploader.delete_image(s3_key)
|
||||||
|
await self.repository.delete_images(product.images)
|
||||||
|
|
||||||
|
image_url = await s3_uploader.upload_from_upload_file_obj(upload_file)
|
||||||
|
await self.repository.create_image(product_id, image_url)
|
||||||
|
return ProductUploadImageResponse(
|
||||||
|
message="Изображение успешно загружено", image_url=image_url
|
||||||
|
)
|
||||||
|
|
||||||
|
async def upload_barcode_image(
|
||||||
|
self, product_id: int, upload_file: UploadFile
|
||||||
|
) -> BarcodeUploadImageResponse:
|
||||||
|
product: Product = await self.repository.get_by_id(product_id)
|
||||||
|
s3_uploader = S3Uploader()
|
||||||
|
|
||||||
|
if product.barcode_image:
|
||||||
|
s3_key = product.barcode_image.image_url.split("/")[-1]
|
||||||
|
await s3_uploader.delete_image(s3_key)
|
||||||
|
await self.repository.delete_barcode_image(product.barcode_image)
|
||||||
|
|
||||||
|
image_url = await s3_uploader.upload_from_upload_file_obj(upload_file)
|
||||||
|
await self.repository.create_barcode_image(product_id, image_url)
|
||||||
|
return BarcodeUploadImageResponse(
|
||||||
|
message="Изображение штрихкода успешно загружено", image_url=image_url
|
||||||
|
)
|
||||||
|
|
||||||
|
async def delete_barcode_image(self, product_id: int) -> DeleteBarcodeImageResponse:
|
||||||
|
product: Product = await self.repository.get_by_id(product_id)
|
||||||
|
|
||||||
|
if not product.barcode_image:
|
||||||
|
return DeleteBarcodeImageResponse(
|
||||||
|
message="У товара нет изображения штрихкода"
|
||||||
|
)
|
||||||
|
|
||||||
|
s3_uploader = S3Uploader()
|
||||||
|
s3_key = product.barcode_image.image_url.split("/")[-1]
|
||||||
|
await s3_uploader.delete_image(s3_key)
|
||||||
|
await self.repository.delete_barcode_image(product.barcode_image, True)
|
||||||
|
|
||||||
|
return DeleteBarcodeImageResponse(
|
||||||
|
message="Изображение штрихкода успешно удалено"
|
||||||
|
)
|
||||||
|
|||||||
@ -24,7 +24,12 @@ dependencies = [
|
|||||||
"reportlab>=4.4.4",
|
"reportlab>=4.4.4",
|
||||||
"pathlib>=1.0.1",
|
"pathlib>=1.0.1",
|
||||||
"starlette>=0.47.2",
|
"starlette>=0.47.2",
|
||||||
"lexorank>=1.0.1",
|
"python-multipart>=0.0.20",
|
||||||
|
"lexorank-py==0.1.1",
|
||||||
|
"taskiq>=0.11.19",
|
||||||
|
"taskiq-aio-pika>=0.4.4",
|
||||||
|
"taskiq-fastapi>=0.3.5",
|
||||||
|
"taskiq-postgresql>=0.4.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
|
from .attr_select import AttrSelectRepository as AttrSelectRepository
|
||||||
|
from .attribute import AttributeRepository as AttributeRepository
|
||||||
from .board import BoardRepository as BoardRepository
|
from .board import BoardRepository as BoardRepository
|
||||||
from .built_in_module import BuiltInModuleRepository as BuiltInModuleRepository
|
|
||||||
from .deal import DealRepository as DealRepository
|
from .deal import DealRepository as DealRepository
|
||||||
from .deal_group import DealGroupRepository as DealGroupRepository
|
from .deal_group import DealGroupRepository as DealGroupRepository
|
||||||
from .deal_tag import DealTagRepository as DealTagRepository
|
from .deal_tag import DealTagRepository as DealTagRepository
|
||||||
|
from .module import ModuleRepository as ModuleRepository
|
||||||
from .project import ProjectRepository as ProjectRepository
|
from .project import ProjectRepository as ProjectRepository
|
||||||
from .status import StatusRepository as StatusRepository
|
from .status import StatusRepository as StatusRepository
|
||||||
|
from .attr_option import AttrOptionRepository as AttrOptionRepository
|
||||||
|
|||||||
21
repositories/attr_option.py
Normal file
21
repositories/attr_option.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
from sqlalchemy import Select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from models import AttributeOption
|
||||||
|
from repositories.mixins import RepCrudMixin
|
||||||
|
from schemas.attr_option import CreateAttrOptionSchema, UpdateAttrOptionSchema
|
||||||
|
|
||||||
|
|
||||||
|
class AttrOptionRepository(
|
||||||
|
RepCrudMixin[AttributeOption, CreateAttrOptionSchema, UpdateAttrOptionSchema],
|
||||||
|
):
|
||||||
|
session: AsyncSession
|
||||||
|
entity_class = AttributeOption
|
||||||
|
entity_not_found_msg = "Опция не найдена"
|
||||||
|
|
||||||
|
def _process_get_all_stmt_with_args(self, stmt: Select, *args) -> Select:
|
||||||
|
select_id = args[0]
|
||||||
|
return stmt.where(
|
||||||
|
AttributeOption.select_id == select_id,
|
||||||
|
AttributeOption.is_deleted.is_(False),
|
||||||
|
).order_by(AttributeOption.id)
|
||||||
16
repositories/attr_select.py
Normal file
16
repositories/attr_select.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
from sqlalchemy import Select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from models import AttributeSelect
|
||||||
|
from repositories.mixins import RepCrudMixin
|
||||||
|
from schemas.attr_select import UpdateAttrSelectSchema, CreateAttrSelectSchema
|
||||||
|
|
||||||
|
|
||||||
|
class AttrSelectRepository(
|
||||||
|
RepCrudMixin[AttributeSelect, CreateAttrSelectSchema, UpdateAttrSelectSchema],
|
||||||
|
):
|
||||||
|
session: AsyncSession
|
||||||
|
entity_class = AttributeSelect
|
||||||
|
|
||||||
|
def _process_get_all_stmt(self, stmt: Select) -> Select:
|
||||||
|
return stmt.where(AttributeSelect.is_deleted.is_(False))
|
||||||
212
repositories/attribute.py
Normal file
212
repositories/attribute.py
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from sqlalchemy import and_
|
||||||
|
from sqlalchemy.orm import joinedload
|
||||||
|
|
||||||
|
from models import (
|
||||||
|
Attribute,
|
||||||
|
AttributeLabel,
|
||||||
|
AttributeType,
|
||||||
|
AttributeValue,
|
||||||
|
module_attribute,
|
||||||
|
Module,
|
||||||
|
Project,
|
||||||
|
)
|
||||||
|
from repositories.mixins import *
|
||||||
|
from schemas.attribute import (
|
||||||
|
CreateAttributeSchema,
|
||||||
|
UpdateAttributeSchema,
|
||||||
|
UpdateDealModuleAttributeSchema,
|
||||||
|
)
|
||||||
|
from utils.exceptions import ForbiddenException
|
||||||
|
|
||||||
|
|
||||||
|
class AttributeRepository(
|
||||||
|
RepCrudMixin[Attribute, CreateAttributeSchema, UpdateAttributeSchema]
|
||||||
|
):
|
||||||
|
session: AsyncSession
|
||||||
|
entity_class = Attribute
|
||||||
|
|
||||||
|
def _process_get_all_stmt(self, stmt: Select) -> Select:
|
||||||
|
return (
|
||||||
|
stmt.options(joinedload(Attribute.type))
|
||||||
|
.where(Attribute.is_deleted.is_(False))
|
||||||
|
.order_by(Attribute.is_built_in.desc(), Attribute.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _process_get_by_id_stmt(self, stmt: Select) -> Select:
|
||||||
|
return stmt.options(joinedload(Attribute.type))
|
||||||
|
|
||||||
|
async def update(self, attr: Attribute, data: UpdateAttributeSchema) -> Attribute:
|
||||||
|
return await self._apply_update_data_to_model(
|
||||||
|
attr, data, with_commit=True, set_if_value_is_not_none=False
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _before_delete(self, attribute: Attribute) -> None:
|
||||||
|
if attribute.is_built_in:
|
||||||
|
raise ForbiddenException("Нельзя менять встроенный атрибут")
|
||||||
|
|
||||||
|
async def _get_all_attributes_for_deal(
|
||||||
|
self, project_id
|
||||||
|
) -> list[tuple[Attribute, int]]:
|
||||||
|
stmt = (
|
||||||
|
select(Attribute, Module.id)
|
||||||
|
.join(Attribute.modules)
|
||||||
|
.join(Module.projects)
|
||||||
|
.where(
|
||||||
|
Module.is_deleted.is_(False),
|
||||||
|
Project.is_deleted.is_(False),
|
||||||
|
Project.id == project_id,
|
||||||
|
)
|
||||||
|
.distinct(Attribute.id, Module.id)
|
||||||
|
)
|
||||||
|
result = await self.session.execute(stmt)
|
||||||
|
return list(result.all())
|
||||||
|
|
||||||
|
async def create_attributes_for_new_deal(
|
||||||
|
self, deal_id: int, project_id: int
|
||||||
|
) -> None:
|
||||||
|
attributes = await self._get_all_attributes_for_deal(project_id)
|
||||||
|
for attribute, module_id in attributes:
|
||||||
|
def_val = (
|
||||||
|
attribute.default_option_id
|
||||||
|
if attribute.default_option_id is not None
|
||||||
|
else attribute.default_value
|
||||||
|
)
|
||||||
|
if def_val is None:
|
||||||
|
continue
|
||||||
|
value = AttributeValue(
|
||||||
|
attribute_id=attribute.id,
|
||||||
|
deal_id=deal_id,
|
||||||
|
module_id=module_id,
|
||||||
|
value=def_val,
|
||||||
|
)
|
||||||
|
self.session.add(value)
|
||||||
|
|
||||||
|
async def _get_attribute_module_label(
|
||||||
|
self, module_id: int, attribute_id: int
|
||||||
|
) -> Optional[AttributeLabel]:
|
||||||
|
stmt = select(AttributeLabel).where(
|
||||||
|
AttributeLabel.attribute_id == attribute_id,
|
||||||
|
AttributeLabel.module_id == module_id,
|
||||||
|
)
|
||||||
|
result = await self.session.execute(stmt)
|
||||||
|
row = result.one_or_none()
|
||||||
|
return row[0] if row else None
|
||||||
|
|
||||||
|
async def create_or_update_attribute_label(
|
||||||
|
self, module_id: int, attribute_id: int, label: str
|
||||||
|
):
|
||||||
|
attribute_label = await self._get_attribute_module_label(
|
||||||
|
module_id, attribute_id
|
||||||
|
)
|
||||||
|
if attribute_label:
|
||||||
|
attribute_label.label = label
|
||||||
|
else:
|
||||||
|
attribute_label = AttributeLabel(
|
||||||
|
module_id=module_id,
|
||||||
|
attribute_id=attribute_id,
|
||||||
|
label=label,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.session.add(attribute_label)
|
||||||
|
await self.session.commit()
|
||||||
|
|
||||||
|
async def get_attribute_types(self) -> list[AttributeType]:
|
||||||
|
stmt = select(AttributeType).where(AttributeType.is_deleted.is_(False))
|
||||||
|
result = await self.session.execute(stmt)
|
||||||
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
async def get_deal_module_attributes(
|
||||||
|
self, deal_id: int, module_id: int
|
||||||
|
) -> list[tuple[Attribute, AttributeValue, AttributeLabel]]:
|
||||||
|
stmt = (
|
||||||
|
select(
|
||||||
|
Attribute,
|
||||||
|
AttributeValue,
|
||||||
|
AttributeLabel,
|
||||||
|
)
|
||||||
|
.join(
|
||||||
|
module_attribute,
|
||||||
|
and_(
|
||||||
|
module_attribute.c.attribute_id == Attribute.id,
|
||||||
|
module_attribute.c.module_id == module_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
AttributeValue,
|
||||||
|
and_(
|
||||||
|
AttributeValue.attribute_id == Attribute.id,
|
||||||
|
AttributeValue.module_id == module_id,
|
||||||
|
AttributeValue.deal_id == deal_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
AttributeLabel,
|
||||||
|
and_(
|
||||||
|
AttributeLabel.attribute_id == Attribute.id,
|
||||||
|
AttributeLabel.module_id == module_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.where(Attribute.is_deleted.is_(False))
|
||||||
|
.options(joinedload(Attribute.select))
|
||||||
|
)
|
||||||
|
result = await self.session.execute(stmt)
|
||||||
|
return list(result.all())
|
||||||
|
|
||||||
|
async def _get_deals_attribute_values(
|
||||||
|
self, deal_ids: list[int], module_id: int
|
||||||
|
) -> list[AttributeValue]:
|
||||||
|
stmt = (
|
||||||
|
select(AttributeValue)
|
||||||
|
.join(Attribute, AttributeValue.attribute_id == Attribute.id)
|
||||||
|
.where(
|
||||||
|
AttributeValue.deal_id.in_(deal_ids),
|
||||||
|
AttributeValue.module_id == module_id,
|
||||||
|
Attribute.is_deleted.is_(False),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
result = await self.session.execute(stmt)
|
||||||
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
async def update_or_create_deals_attribute_values(
|
||||||
|
self,
|
||||||
|
main_deal_id: int,
|
||||||
|
group_deal_ids: list[int],
|
||||||
|
module_id: int,
|
||||||
|
attributes: list[UpdateDealModuleAttributeSchema],
|
||||||
|
):
|
||||||
|
old_deal_attribute_values: list[
|
||||||
|
AttributeValue
|
||||||
|
] = await self._get_deals_attribute_values(group_deal_ids, module_id)
|
||||||
|
|
||||||
|
dict_old_attrs: dict[int, dict[int, AttributeValue]] = defaultdict(dict)
|
||||||
|
|
||||||
|
for deal_attribute in old_deal_attribute_values:
|
||||||
|
dict_old_attrs[deal_attribute.deal_id][deal_attribute.attribute_id] = (
|
||||||
|
deal_attribute
|
||||||
|
)
|
||||||
|
|
||||||
|
for attribute in attributes:
|
||||||
|
if attribute.is_applicable_to_group:
|
||||||
|
deal_ids_to_apply = group_deal_ids
|
||||||
|
else:
|
||||||
|
deal_ids_to_apply = [main_deal_id]
|
||||||
|
|
||||||
|
for deal_id in deal_ids_to_apply:
|
||||||
|
if attribute.attribute_id in dict_old_attrs[deal_id]:
|
||||||
|
attribute_value = dict_old_attrs[deal_id][attribute.attribute_id]
|
||||||
|
attribute_value.value = attribute.value
|
||||||
|
else:
|
||||||
|
if attribute.value is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
attribute_value = AttributeValue(
|
||||||
|
attribute_id=attribute.attribute_id,
|
||||||
|
deal_id=deal_id,
|
||||||
|
module_id=module_id,
|
||||||
|
value=attribute.value,
|
||||||
|
)
|
||||||
|
self.session.add(attribute_value)
|
||||||
|
|
||||||
|
await self.session.commit()
|
||||||
@ -1,18 +0,0 @@
|
|||||||
from models import Board, BuiltInModule
|
|
||||||
from repositories.mixins import *
|
|
||||||
|
|
||||||
|
|
||||||
class BuiltInModuleRepository(
|
|
||||||
BaseRepository,
|
|
||||||
RepGetAllMixin[BuiltInModule],
|
|
||||||
):
|
|
||||||
entity_class = BuiltInModule
|
|
||||||
|
|
||||||
def _process_get_all_stmt_with_args(self, stmt: Select, *args) -> Select:
|
|
||||||
project_id = args[0]
|
|
||||||
return stmt.where(Board.project_id == project_id).order_by(Board.lexorank)
|
|
||||||
|
|
||||||
async def get_by_ids(self, ids: list[int]) -> list[BuiltInModule]:
|
|
||||||
stmt = select(BuiltInModule).where(BuiltInModule.id.in_(ids))
|
|
||||||
built_in_modules = await self.session.scalars(stmt)
|
|
||||||
return built_in_modules.all()
|
|
||||||
@ -8,6 +8,7 @@ from modules.fulfillment_base.models import (
|
|||||||
DealProductService,
|
DealProductService,
|
||||||
DealProduct,
|
DealProduct,
|
||||||
)
|
)
|
||||||
|
from repositories import AttributeRepository
|
||||||
from repositories.mixins import *
|
from repositories.mixins import *
|
||||||
from schemas.base import SortDir
|
from schemas.base import SortDir
|
||||||
from schemas.deal import UpdateDealSchema, CreateDealSchema
|
from schemas.deal import UpdateDealSchema, CreateDealSchema
|
||||||
@ -146,6 +147,15 @@ class DealRepository(
|
|||||||
result = await self.session.execute(stmt)
|
result = await self.session.execute(stmt)
|
||||||
return list(result.scalars().all())
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
async def _prepare_create(self, data: CreateDealSchema) -> dict:
|
||||||
|
dumped = data.model_dump()
|
||||||
|
del dumped["project_id"]
|
||||||
|
return dumped
|
||||||
|
|
||||||
|
async def _after_create(self, obj: Deal, data: CreateDealSchema) -> None:
|
||||||
|
attr_repo = AttributeRepository(self.session)
|
||||||
|
await attr_repo.create_attributes_for_new_deal(obj.id, data.project_id)
|
||||||
|
|
||||||
def _process_get_by_id_stmt(self, stmt: Select) -> Select:
|
def _process_get_by_id_stmt(self, stmt: Select) -> Select:
|
||||||
return stmt.options(joinedload(Deal.status), joinedload(Deal.board))
|
return stmt.options(joinedload(Deal.status), joinedload(Deal.board))
|
||||||
|
|
||||||
|
|||||||
@ -65,13 +65,14 @@ class RepUpdateMixin(Generic[EntityType, UpdateSchemaType], RepBaseMixin[EntityT
|
|||||||
data: UpdateSchemaType,
|
data: UpdateSchemaType,
|
||||||
with_commit: Optional[bool] = False,
|
with_commit: Optional[bool] = False,
|
||||||
fields: Optional[list[str]] = None,
|
fields: Optional[list[str]] = None,
|
||||||
|
set_if_value_is_not_none: Optional[bool] = True,
|
||||||
) -> EntityType:
|
) -> EntityType:
|
||||||
if fields is None:
|
if fields is None:
|
||||||
fields = data.model_dump().keys()
|
fields = data.model_dump().keys()
|
||||||
|
|
||||||
for field in fields:
|
for field in fields:
|
||||||
value = getattr(data, field)
|
value = getattr(data, field)
|
||||||
if value is not None:
|
if not set_if_value_is_not_none or value is not None:
|
||||||
setattr(model, field, value)
|
setattr(model, field, value)
|
||||||
|
|
||||||
if with_commit:
|
if with_commit:
|
||||||
|
|||||||
101
repositories/module.py
Normal file
101
repositories/module.py
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
from sqlalchemy import and_, or_
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
|
from models import Module, Attribute, AttributeLabel, module_attribute, ModuleTab
|
||||||
|
from models.module import DeviceType
|
||||||
|
from repositories.mixins import *
|
||||||
|
from schemas.module import UpdateModuleCommonInfoSchema, CreateModuleSchema
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleRepository(
|
||||||
|
RepCrudMixin[Module, CreateModuleSchema, UpdateModuleCommonInfoSchema]
|
||||||
|
):
|
||||||
|
entity_class = Module
|
||||||
|
|
||||||
|
def _process_get_by_id_stmt(self, stmt: Select) -> Select:
|
||||||
|
return stmt.options(selectinload(Module.attributes).joinedload(Attribute.type))
|
||||||
|
|
||||||
|
async def get_by_ids(self, ids: list[int]) -> list[Module]:
|
||||||
|
stmt = select(Module).where(Module.id.in_(ids))
|
||||||
|
modules = await self.session.scalars(stmt)
|
||||||
|
return modules.all()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_stmt_modules_with_tuples() -> Select:
|
||||||
|
return (
|
||||||
|
select(Module, Attribute, AttributeLabel)
|
||||||
|
.join(
|
||||||
|
module_attribute,
|
||||||
|
Module.id == module_attribute.c.module_id,
|
||||||
|
isouter=True,
|
||||||
|
)
|
||||||
|
.join(
|
||||||
|
Attribute, module_attribute.c.attribute_id == Attribute.id, isouter=True
|
||||||
|
)
|
||||||
|
.join(
|
||||||
|
AttributeLabel,
|
||||||
|
and_(
|
||||||
|
Module.id == AttributeLabel.module_id,
|
||||||
|
Attribute.id == AttributeLabel.attribute_id,
|
||||||
|
),
|
||||||
|
isouter=True,
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
Module.is_deleted.is_(False),
|
||||||
|
or_(Attribute.id.is_(None), Attribute.is_deleted.is_(False)),
|
||||||
|
)
|
||||||
|
.order_by(Module.id, Attribute.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_with_attributes_as_tuples(
|
||||||
|
self,
|
||||||
|
) -> list[tuple[Module, Attribute, AttributeLabel]]:
|
||||||
|
stmt = self._get_stmt_modules_with_tuples()
|
||||||
|
return (await self.session.execute(stmt)).unique().all()
|
||||||
|
|
||||||
|
async def get_with_attributes_as_tuple_by_id(
|
||||||
|
self, pk: int
|
||||||
|
) -> list[tuple[Module, Attribute, AttributeLabel]]:
|
||||||
|
stmt = self._get_stmt_modules_with_tuples()
|
||||||
|
stmt = stmt.where(Module.id == pk)
|
||||||
|
return (await self.session.execute(stmt)).unique().all()
|
||||||
|
|
||||||
|
async def _prepare_create(self, data: CreateSchemaType) -> dict:
|
||||||
|
dump = data.model_dump()
|
||||||
|
dump["key"] = str(uuid.uuid4())
|
||||||
|
return dump
|
||||||
|
|
||||||
|
async def _after_create(self, module: Module, _) -> None:
|
||||||
|
tab = ModuleTab(
|
||||||
|
key=module.key,
|
||||||
|
label=module.label,
|
||||||
|
icon_name=None,
|
||||||
|
module_id=module.id,
|
||||||
|
device=DeviceType.BOTH,
|
||||||
|
)
|
||||||
|
self.session.add(tab)
|
||||||
|
|
||||||
|
async def get_module_tabs_by_module_id(self, module_id: int) -> list[ModuleTab]:
|
||||||
|
stmt = select(ModuleTab).where(ModuleTab.module_id == module_id)
|
||||||
|
result = await self.session.scalars(stmt)
|
||||||
|
return list(result.all())
|
||||||
|
|
||||||
|
async def update(
|
||||||
|
self, module: Module, data: UpdateModuleCommonInfoSchema
|
||||||
|
) -> Module:
|
||||||
|
tabs = await self.get_module_tabs_by_module_id(module.id)
|
||||||
|
for tab in tabs:
|
||||||
|
tab.label = data.label
|
||||||
|
self.session.add(tab)
|
||||||
|
|
||||||
|
return await self._apply_update_data_to_model(module, data, True)
|
||||||
|
|
||||||
|
async def add_attribute_to_module(self, module: Module, attribute: Attribute):
|
||||||
|
module.attributes.append(attribute)
|
||||||
|
await self.session.commit()
|
||||||
|
|
||||||
|
async def delete_attribute_from_module(self, module: Module, attribute: Attribute):
|
||||||
|
module.attributes.remove(attribute)
|
||||||
|
await self.session.commit()
|
||||||
@ -2,7 +2,7 @@ from sqlalchemy.orm import selectinload
|
|||||||
|
|
||||||
from models import DealTag
|
from models import DealTag
|
||||||
from models.project import Project
|
from models.project import Project
|
||||||
from repositories.built_in_module import BuiltInModuleRepository
|
from repositories.module import ModuleRepository
|
||||||
from repositories.mixins import *
|
from repositories.mixins import *
|
||||||
from schemas.project import CreateProjectSchema, UpdateProjectSchema
|
from schemas.project import CreateProjectSchema, UpdateProjectSchema
|
||||||
|
|
||||||
@ -26,10 +26,10 @@ class ProjectRepository(
|
|||||||
return self._apply_options(stmt)
|
return self._apply_options(stmt)
|
||||||
|
|
||||||
async def update(self, project: Project, data: UpdateProjectSchema) -> Project:
|
async def update(self, project: Project, data: UpdateProjectSchema) -> Project:
|
||||||
if data.built_in_modules is not None:
|
if data.modules is not None:
|
||||||
built_in_modules = data.built_in_modules
|
modules = data.modules
|
||||||
module_ids = [module.id for module in built_in_modules]
|
module_ids = [module.id for module in modules]
|
||||||
data.built_in_modules = await BuiltInModuleRepository(
|
data.modules = await ModuleRepository(
|
||||||
self.session
|
self.session
|
||||||
).get_by_ids(module_ids)
|
).get_by_ids(module_ids)
|
||||||
|
|
||||||
|
|||||||
56
routers/crm/v1/attr_option.py
Normal file
56
routers/crm/v1/attr_option.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
from fastapi import APIRouter, Path
|
||||||
|
|
||||||
|
from backend.dependecies import SessionDependency
|
||||||
|
from schemas.attr_option import *
|
||||||
|
from services import AttrOptionService
|
||||||
|
|
||||||
|
router = APIRouter(tags=["attr_option"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/{selectId}",
|
||||||
|
response_model=GetAllAttrSelectOptionsResponse,
|
||||||
|
operation_id="get_attr_options",
|
||||||
|
)
|
||||||
|
async def get_attr_options(
|
||||||
|
session: SessionDependency,
|
||||||
|
select_id: int = Path(alias="selectId"),
|
||||||
|
):
|
||||||
|
return await AttrOptionService(session).get_all(select_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/",
|
||||||
|
response_model=CreateAttrOptionResponse,
|
||||||
|
operation_id="create_attr_option",
|
||||||
|
)
|
||||||
|
async def create_attr_select(
|
||||||
|
session: SessionDependency,
|
||||||
|
request: CreateAttrOptionRequest,
|
||||||
|
):
|
||||||
|
return await AttrOptionService(session).create(request)
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch(
|
||||||
|
"/{pk}",
|
||||||
|
response_model=UpdateAttrOptionResponse,
|
||||||
|
operation_id="update_attr_option",
|
||||||
|
)
|
||||||
|
async def update_attr_option(
|
||||||
|
session: SessionDependency,
|
||||||
|
request: UpdateAttrOptionRequest,
|
||||||
|
pk: int = Path(),
|
||||||
|
):
|
||||||
|
return await AttrOptionService(session).update(pk, request)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete(
|
||||||
|
"/{pk}",
|
||||||
|
response_model=DeleteAttrOptionResponse,
|
||||||
|
operation_id="delete_attr_option",
|
||||||
|
)
|
||||||
|
async def delete_attr_option(
|
||||||
|
session: SessionDependency,
|
||||||
|
pk: int = Path(),
|
||||||
|
):
|
||||||
|
return await AttrOptionService(session).delete(pk)
|
||||||
55
routers/crm/v1/attr_select.py
Normal file
55
routers/crm/v1/attr_select.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
from fastapi import APIRouter, Path
|
||||||
|
|
||||||
|
from backend.dependecies import SessionDependency
|
||||||
|
from schemas.attr_select import *
|
||||||
|
from services import AttrSelectService
|
||||||
|
|
||||||
|
router = APIRouter(tags=["attr_select"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/",
|
||||||
|
response_model=GetAllAttrSelectsResponse,
|
||||||
|
operation_id="get_attr_selects",
|
||||||
|
)
|
||||||
|
async def get_attr_selects(
|
||||||
|
session: SessionDependency,
|
||||||
|
):
|
||||||
|
return await AttrSelectService(session).get_all()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/",
|
||||||
|
response_model=CreateAttrSelectResponse,
|
||||||
|
operation_id="create_attr_select",
|
||||||
|
)
|
||||||
|
async def create_attr_select(
|
||||||
|
session: SessionDependency,
|
||||||
|
request: CreateAttrSelectRequest,
|
||||||
|
):
|
||||||
|
return await AttrSelectService(session).create(request)
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch(
|
||||||
|
"/{pk}",
|
||||||
|
response_model=UpdateAttrSelectResponse,
|
||||||
|
operation_id="update_attr_select",
|
||||||
|
)
|
||||||
|
async def update_attr_select(
|
||||||
|
session: SessionDependency,
|
||||||
|
request: UpdateAttrSelectRequest,
|
||||||
|
pk: int = Path(),
|
||||||
|
):
|
||||||
|
return await AttrSelectService(session).update(pk, request)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete(
|
||||||
|
"/{pk}",
|
||||||
|
response_model=DeleteAttrSelectResponse,
|
||||||
|
operation_id="delete_attr_select",
|
||||||
|
)
|
||||||
|
async def delete_attr_select(
|
||||||
|
session: SessionDependency,
|
||||||
|
pk: int = Path(),
|
||||||
|
):
|
||||||
|
return await AttrSelectService(session).delete(pk)
|
||||||
109
routers/crm/v1/attribute.py
Normal file
109
routers/crm/v1/attribute.py
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
from fastapi import APIRouter, Path
|
||||||
|
|
||||||
|
from backend.dependecies import SessionDependency
|
||||||
|
from schemas.attribute import *
|
||||||
|
from services import AttributeService
|
||||||
|
|
||||||
|
router = APIRouter(tags=["attribute"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/",
|
||||||
|
response_model=GetAllAttributesResponse,
|
||||||
|
operation_id="get_attributes",
|
||||||
|
)
|
||||||
|
async def get_attributes(
|
||||||
|
session: SessionDependency,
|
||||||
|
):
|
||||||
|
return await AttributeService(session).get_all()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/",
|
||||||
|
response_model=CreateAttributeResponse,
|
||||||
|
operation_id="create_attribute",
|
||||||
|
)
|
||||||
|
async def create_attribute(
|
||||||
|
session: SessionDependency,
|
||||||
|
request: CreateAttributeRequest,
|
||||||
|
):
|
||||||
|
return await AttributeService(session).create(request)
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch(
|
||||||
|
"/{pk}",
|
||||||
|
response_model=UpdateAttributeResponse,
|
||||||
|
operation_id="update_attribute",
|
||||||
|
)
|
||||||
|
async def update_attribute(
|
||||||
|
session: SessionDependency,
|
||||||
|
request: UpdateAttributeRequest,
|
||||||
|
pk: int = Path(),
|
||||||
|
):
|
||||||
|
return await AttributeService(session).update(pk, request)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete(
|
||||||
|
"/{pk}",
|
||||||
|
response_model=DeleteAttributeResponse,
|
||||||
|
operation_id="delete_attribute",
|
||||||
|
)
|
||||||
|
async def delete_attribute(
|
||||||
|
session: SessionDependency,
|
||||||
|
pk: int = Path(),
|
||||||
|
):
|
||||||
|
return await AttributeService(session).delete(pk)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/label",
|
||||||
|
response_model=UpdateAttributeLabelResponse,
|
||||||
|
operation_id="update_attribute_label",
|
||||||
|
)
|
||||||
|
async def update_attribute_label(
|
||||||
|
session: SessionDependency,
|
||||||
|
request: UpdateAttributeLabelRequest,
|
||||||
|
):
|
||||||
|
return await AttributeService(session).update_attribute_label(request)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/type",
|
||||||
|
response_model=GetAllAttributeTypesResponse,
|
||||||
|
operation_id="get_attribute_types",
|
||||||
|
)
|
||||||
|
async def get_attribute_types(
|
||||||
|
session: SessionDependency,
|
||||||
|
):
|
||||||
|
return await AttributeService(session).get_attribute_types()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/deal/{dealId}/module/{moduleId}",
|
||||||
|
response_model=GetDealModuleAttributesResponse,
|
||||||
|
operation_id="get_deal_module_attributes",
|
||||||
|
)
|
||||||
|
async def get_deal_module_attributes(
|
||||||
|
session: SessionDependency,
|
||||||
|
deal_id: int = Path(alias="dealId"),
|
||||||
|
module_id: int = Path(alias="moduleId"),
|
||||||
|
):
|
||||||
|
return await AttributeService(session).get_deal_module_attributes(
|
||||||
|
deal_id, module_id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/deal/{dealId}/module/{moduleId}",
|
||||||
|
response_model=UpdateDealModuleAttributesResponse,
|
||||||
|
operation_id="update_deal_module_attributes",
|
||||||
|
)
|
||||||
|
async def update_deal_module_attributes(
|
||||||
|
session: SessionDependency,
|
||||||
|
request: UpdateDealModuleAttributesRequest,
|
||||||
|
deal_id: int = Path(alias="dealId"),
|
||||||
|
module_id: int = Path(alias="moduleId"),
|
||||||
|
):
|
||||||
|
return await AttributeService(session).update_deal_module_attributes(
|
||||||
|
deal_id, module_id, request
|
||||||
|
)
|
||||||
@ -1,18 +1,104 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter, Path
|
||||||
|
|
||||||
from backend.dependecies import SessionDependency
|
from backend.dependecies import SessionDependency
|
||||||
from schemas.module import GetAllBuiltInModulesResponse
|
from schemas.module import *
|
||||||
from services.built_in_module import BuiltInModuleService
|
from services.module import ModuleService
|
||||||
|
|
||||||
router = APIRouter(tags=["modules"])
|
router = APIRouter(tags=["modules"])
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/built-in/",
|
"/",
|
||||||
response_model=GetAllBuiltInModulesResponse,
|
response_model=GetAllModulesResponse,
|
||||||
operation_id="get_built_in_modules",
|
operation_id="get_modules",
|
||||||
)
|
)
|
||||||
async def get_built_in_modules(
|
async def get_modules(
|
||||||
session: SessionDependency,
|
session: SessionDependency,
|
||||||
):
|
):
|
||||||
return await BuiltInModuleService(session).get_all()
|
return await ModuleService(session).get_all()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/with-attributes",
|
||||||
|
response_model=GetAllWithAttributesResponse,
|
||||||
|
operation_id="get_modules_with_attributes",
|
||||||
|
)
|
||||||
|
async def get_modules_with_attributes(
|
||||||
|
session: SessionDependency,
|
||||||
|
):
|
||||||
|
return await ModuleService(session).get_with_attributes()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/{pk}/with-attributes",
|
||||||
|
response_model=GetByIdWithAttributesResponse,
|
||||||
|
operation_id="get_module_with_attributes",
|
||||||
|
)
|
||||||
|
async def get_module_with_attributes(
|
||||||
|
session: SessionDependency,
|
||||||
|
pk: int = Path(),
|
||||||
|
):
|
||||||
|
return await ModuleService(session).get_by_id_with_attributes(pk)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/",
|
||||||
|
response_model=CreateModuleResponse,
|
||||||
|
operation_id="create_module",
|
||||||
|
)
|
||||||
|
async def create_module(
|
||||||
|
session: SessionDependency,
|
||||||
|
request: CreateModuleRequest,
|
||||||
|
):
|
||||||
|
return await ModuleService(session).create(request)
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch(
|
||||||
|
"/{pk}/common-info",
|
||||||
|
response_model=UpdateModuleCommonInfoResponse,
|
||||||
|
operation_id="update_module",
|
||||||
|
)
|
||||||
|
async def update_module_common_info(
|
||||||
|
session: SessionDependency,
|
||||||
|
request: UpdateModuleCommonInfoRequest,
|
||||||
|
pk: int = Path(),
|
||||||
|
):
|
||||||
|
return await ModuleService(session).update(pk, request)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete(
|
||||||
|
"/{pk}",
|
||||||
|
response_model=DeleteModuleResponse,
|
||||||
|
operation_id="delete_module",
|
||||||
|
)
|
||||||
|
async def delete_module(
|
||||||
|
session: SessionDependency,
|
||||||
|
pk: int = Path(),
|
||||||
|
):
|
||||||
|
return await ModuleService(session).delete(pk)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/{moduleId}/attribute/{attributeId}",
|
||||||
|
response_model=AddAttributeResponse,
|
||||||
|
operation_id="add_attribute_to_module",
|
||||||
|
)
|
||||||
|
async def add_attribute_to_module(
|
||||||
|
session: SessionDependency,
|
||||||
|
module_id: int = Path(alias="moduleId"),
|
||||||
|
attribute_id: int = Path(alias="attributeId"),
|
||||||
|
):
|
||||||
|
return await ModuleService(session).add_attribute(module_id, attribute_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete(
|
||||||
|
"/{moduleId}/attribute/{attributeId}",
|
||||||
|
response_model=DeleteAttributeResponse,
|
||||||
|
operation_id="remove_attribute_from_module",
|
||||||
|
)
|
||||||
|
async def remove_attribute_from_module(
|
||||||
|
session: SessionDependency,
|
||||||
|
module_id: int = Path(alias="moduleId"),
|
||||||
|
attribute_id: int = Path(alias="attributeId"),
|
||||||
|
):
|
||||||
|
return await ModuleService(session).delete_attribute(module_id, attribute_id)
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
from fastapi import APIRouter, Path, Query
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Path, Query, File, UploadFile
|
||||||
|
|
||||||
from backend.dependecies import SessionDependency, PaginationDependency
|
from backend.dependecies import SessionDependency, PaginationDependency
|
||||||
from modules.fulfillment_base.schemas.product import *
|
from modules.fulfillment_base.schemas.product import *
|
||||||
@ -46,6 +48,19 @@ async def update_product(
|
|||||||
return await ProductService(session).update(pk, request)
|
return await ProductService(session).update(pk, request)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"{pk}/images/upload",
|
||||||
|
response_model=ProductUploadImageResponse,
|
||||||
|
operation_id="upload_product_image",
|
||||||
|
)
|
||||||
|
async def upload_product_image(
|
||||||
|
session: SessionDependency,
|
||||||
|
upload_file: Annotated[UploadFile, File()],
|
||||||
|
pk: int = Path(),
|
||||||
|
):
|
||||||
|
return await ProductService(session).upload_image(pk, upload_file)
|
||||||
|
|
||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
"/{pk}",
|
"/{pk}",
|
||||||
response_model=DeleteProductResponse,
|
response_model=DeleteProductResponse,
|
||||||
@ -64,10 +79,52 @@ async def delete_product(
|
|||||||
response_model=GetProductBarcodePdfResponse,
|
response_model=GetProductBarcodePdfResponse,
|
||||||
)
|
)
|
||||||
async def get_product_barcode_pdf(
|
async def get_product_barcode_pdf(
|
||||||
request: GetProductBarcodePdfRequest, session: SessionDependency
|
session: SessionDependency,
|
||||||
|
request: GetProductBarcodePdfRequest,
|
||||||
):
|
):
|
||||||
service = BarcodePrinterService(session)
|
service = BarcodePrinterService(session)
|
||||||
filename, base64_string = await service.generate_base64(request)
|
filename, base64_string = await service.generate_product_base64(request)
|
||||||
return GetProductBarcodePdfResponse(
|
return GetProductBarcodePdfResponse(
|
||||||
base64_string=base64_string, filename=filename, mime_type="application/pdf"
|
base64_string=base64_string, filename=filename, mime_type="application/pdf"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/barcode/for-deal/get-pdf",
|
||||||
|
operation_id="get_deal_barcodes_pdf",
|
||||||
|
response_model=GetDealBarcodesPdfResponse,
|
||||||
|
)
|
||||||
|
async def get_deal_barcodes_pdf(
|
||||||
|
session: SessionDependency,
|
||||||
|
request: GetDealBarcodesPdfRequest,
|
||||||
|
):
|
||||||
|
service = BarcodePrinterService(session)
|
||||||
|
filename, base64_string = await service.generate_deal_base64(request)
|
||||||
|
return GetProductBarcodePdfResponse(
|
||||||
|
base64_string=base64_string, filename=filename, mime_type="application/pdf"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"{pk}/barcode/image/upload",
|
||||||
|
response_model=BarcodeUploadImageResponse,
|
||||||
|
operation_id="upload_product_barcode_image",
|
||||||
|
)
|
||||||
|
async def upload_product_barcode_image(
|
||||||
|
upload_file: UploadFile,
|
||||||
|
session: SessionDependency,
|
||||||
|
pk: int,
|
||||||
|
):
|
||||||
|
return await ProductService(session).upload_barcode_image(pk, upload_file)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete(
|
||||||
|
"{pk}/barcode/image",
|
||||||
|
response_model=DeleteBarcodeImageResponse,
|
||||||
|
operation_id="delete_product_barcode_image",
|
||||||
|
)
|
||||||
|
async def delete_product_barcode_image(
|
||||||
|
session: SessionDependency,
|
||||||
|
pk: int,
|
||||||
|
):
|
||||||
|
return await ProductService(session).delete_barcode_image(pk)
|
||||||
|
|||||||
60
schemas/attr_option.py
Normal file
60
schemas/attr_option.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from schemas.base import BaseSchema, BaseResponse
|
||||||
|
|
||||||
|
|
||||||
|
# region Entity
|
||||||
|
|
||||||
|
|
||||||
|
class AttrOptionSchema(BaseSchema):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
lexorank: str
|
||||||
|
|
||||||
|
|
||||||
|
class CreateAttrOptionSchema(BaseSchema):
|
||||||
|
name: str
|
||||||
|
lexorank: str
|
||||||
|
select_id: int
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateAttrOptionSchema(BaseSchema):
|
||||||
|
name: Optional[str] = None
|
||||||
|
lexorank: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
|
# region Request
|
||||||
|
|
||||||
|
|
||||||
|
class CreateAttrOptionRequest(BaseSchema):
|
||||||
|
entity: CreateAttrOptionSchema
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateAttrOptionRequest(BaseSchema):
|
||||||
|
entity: UpdateAttrOptionSchema
|
||||||
|
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
|
# region Response
|
||||||
|
|
||||||
|
|
||||||
|
class GetAllAttrSelectOptionsResponse(BaseSchema):
|
||||||
|
items: list[AttrOptionSchema]
|
||||||
|
|
||||||
|
|
||||||
|
class CreateAttrOptionResponse(BaseSchema):
|
||||||
|
entity: AttrOptionSchema
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateAttrOptionResponse(BaseResponse):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteAttrOptionResponse(BaseSchema):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# endregion
|
||||||
60
schemas/attr_select.py
Normal file
60
schemas/attr_select.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
from schemas.attr_option import AttrOptionSchema
|
||||||
|
from schemas.base import BaseSchema, BaseResponse
|
||||||
|
|
||||||
|
|
||||||
|
# region Entity
|
||||||
|
|
||||||
|
|
||||||
|
class AttrSelectSchema(BaseSchema):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
is_built_in: bool
|
||||||
|
|
||||||
|
|
||||||
|
class CreateAttrSelectSchema(BaseSchema):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateAttrSelectSchema(BaseSchema):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
class AttrSelectWithOptionsSchema(AttrSelectSchema):
|
||||||
|
options: list[AttrOptionSchema]
|
||||||
|
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
|
# region Request
|
||||||
|
|
||||||
|
|
||||||
|
class CreateAttrSelectRequest(BaseSchema):
|
||||||
|
entity: CreateAttrSelectSchema
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateAttrSelectRequest(BaseSchema):
|
||||||
|
entity: UpdateAttrSelectSchema
|
||||||
|
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
|
# region Response
|
||||||
|
|
||||||
|
|
||||||
|
class GetAllAttrSelectsResponse(BaseSchema):
|
||||||
|
items: list[AttrSelectSchema]
|
||||||
|
|
||||||
|
|
||||||
|
class CreateAttrSelectResponse(BaseResponse):
|
||||||
|
entity: AttrSelectSchema
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateAttrSelectResponse(BaseResponse):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteAttrSelectResponse(BaseResponse):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# endregion
|
||||||
127
schemas/attribute.py
Normal file
127
schemas/attribute.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
from typing import Optional, Any
|
||||||
|
|
||||||
|
from schemas.attr_select import AttrSelectSchema, AttrOptionSchema
|
||||||
|
from schemas.base import BaseSchema, BaseResponse
|
||||||
|
|
||||||
|
|
||||||
|
# region Entity
|
||||||
|
|
||||||
|
|
||||||
|
class AttributeTypeSchema(BaseSchema):
|
||||||
|
id: int
|
||||||
|
type: str
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
class CreateAttributeSchema(BaseSchema):
|
||||||
|
label: str
|
||||||
|
is_applicable_to_group: bool
|
||||||
|
is_nullable: bool
|
||||||
|
default_value: Optional[Any]
|
||||||
|
default_option_id: Optional[int] = None
|
||||||
|
description: str
|
||||||
|
type_id: int
|
||||||
|
select_id: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
class AttributeSchema(CreateAttributeSchema):
|
||||||
|
id: int
|
||||||
|
is_built_in: bool
|
||||||
|
type: AttributeTypeSchema
|
||||||
|
default_option: Optional[AttrOptionSchema] = None
|
||||||
|
select: Optional[AttrSelectSchema]
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateAttributeSchema(BaseSchema):
|
||||||
|
label: Optional[str] = None
|
||||||
|
is_applicable_to_group: Optional[bool] = None
|
||||||
|
is_nullable: Optional[bool] = None
|
||||||
|
default_value: Optional[Any] = None
|
||||||
|
default_option_id: Optional[int] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleAttributeSchema(AttributeSchema):
|
||||||
|
original_label: str
|
||||||
|
|
||||||
|
|
||||||
|
class DealModuleAttributeSchema(BaseSchema):
|
||||||
|
attribute_id: int
|
||||||
|
label: str
|
||||||
|
original_label: str
|
||||||
|
value: Optional[Any]
|
||||||
|
type: AttributeTypeSchema
|
||||||
|
select: Optional[AttrSelectSchema]
|
||||||
|
default_value: Any
|
||||||
|
description: str
|
||||||
|
is_applicable_to_group: bool
|
||||||
|
is_nullable: bool
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateDealModuleAttributeSchema(BaseSchema):
|
||||||
|
attribute_id: int
|
||||||
|
is_applicable_to_group: bool
|
||||||
|
value: Optional[Any] = None
|
||||||
|
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
|
# region Request
|
||||||
|
|
||||||
|
|
||||||
|
class CreateAttributeRequest(BaseSchema):
|
||||||
|
entity: CreateAttributeSchema
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateAttributeRequest(BaseSchema):
|
||||||
|
entity: UpdateAttributeSchema
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateAttributeLabelRequest(BaseSchema):
|
||||||
|
module_id: int
|
||||||
|
attribute_id: int
|
||||||
|
label: str
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateDealModuleAttributesRequest(BaseSchema):
|
||||||
|
attributes: list[UpdateDealModuleAttributeSchema]
|
||||||
|
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
|
# region Response
|
||||||
|
|
||||||
|
|
||||||
|
class GetAllAttributesResponse(BaseSchema):
|
||||||
|
items: list[AttributeSchema]
|
||||||
|
|
||||||
|
|
||||||
|
class CreateAttributeResponse(BaseResponse):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateAttributeResponse(BaseResponse):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteAttributeResponse(BaseResponse):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateAttributeLabelResponse(BaseResponse):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class GetAllAttributeTypesResponse(BaseSchema):
|
||||||
|
items: list[AttributeTypeSchema]
|
||||||
|
|
||||||
|
|
||||||
|
class GetDealModuleAttributesResponse(BaseSchema):
|
||||||
|
attributes: list[DealModuleAttributeSchema]
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateDealModuleAttributesResponse(BaseResponse):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# endregion
|
||||||
@ -30,6 +30,7 @@ class DealSchema(BaseSchema):
|
|||||||
|
|
||||||
class CreateDealSchema(BaseSchema):
|
class CreateDealSchema(BaseSchema):
|
||||||
name: str
|
name: str
|
||||||
|
project_id: int
|
||||||
board_id: int
|
board_id: int
|
||||||
lexorank: str
|
lexorank: str
|
||||||
status_id: int
|
status_id: int
|
||||||
|
|||||||
@ -1,23 +1,56 @@
|
|||||||
from schemas.base import BaseSchema
|
from typing import Optional
|
||||||
|
|
||||||
|
from schemas.attribute import ModuleAttributeSchema
|
||||||
|
from schemas.base import BaseSchema, BaseResponse
|
||||||
|
|
||||||
|
|
||||||
# region Entity
|
# region Entity
|
||||||
|
|
||||||
|
|
||||||
class BuiltInModuleTabSchema(BaseSchema):
|
class ModuleTabSchema(BaseSchema):
|
||||||
id: int
|
id: int
|
||||||
key: str
|
key: str
|
||||||
label: str
|
label: str
|
||||||
icon_name: str
|
icon_name: Optional[str]
|
||||||
device: str
|
device: str
|
||||||
|
|
||||||
|
|
||||||
class BuiltInModuleSchema(BaseSchema):
|
class ModuleSchema(BaseSchema):
|
||||||
id: int
|
id: int
|
||||||
key: str
|
key: str
|
||||||
label: str
|
label: str
|
||||||
description: str
|
description: Optional[str]
|
||||||
depends_on: list["BuiltInModuleSchema"]
|
is_built_in: bool
|
||||||
tabs: list[BuiltInModuleTabSchema]
|
depends_on: list["ModuleSchema"]
|
||||||
|
tabs: list[ModuleTabSchema]
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleWithAttributesSchema(ModuleSchema):
|
||||||
|
attributes: list[ModuleAttributeSchema]
|
||||||
|
|
||||||
|
|
||||||
|
class CreateModuleSchema(BaseSchema):
|
||||||
|
label: str
|
||||||
|
description: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateModuleCommonInfoSchema(BaseSchema):
|
||||||
|
label: str
|
||||||
|
description: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
|
# region Requests
|
||||||
|
|
||||||
|
|
||||||
|
class CreateModuleRequest(BaseSchema):
|
||||||
|
entity: CreateModuleSchema
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateModuleCommonInfoRequest(BaseSchema):
|
||||||
|
entity: UpdateModuleCommonInfoSchema
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
@ -25,8 +58,36 @@ class BuiltInModuleSchema(BaseSchema):
|
|||||||
# region Response
|
# region Response
|
||||||
|
|
||||||
|
|
||||||
class GetAllBuiltInModulesResponse(BaseSchema):
|
class GetAllModulesResponse(BaseSchema):
|
||||||
items: list[BuiltInModuleSchema]
|
items: list[ModuleSchema]
|
||||||
|
|
||||||
|
|
||||||
|
class GetAllWithAttributesResponse(BaseSchema):
|
||||||
|
items: list[ModuleWithAttributesSchema]
|
||||||
|
|
||||||
|
|
||||||
|
class GetByIdWithAttributesResponse(BaseSchema):
|
||||||
|
entity: ModuleWithAttributesSchema
|
||||||
|
|
||||||
|
|
||||||
|
class CreateModuleResponse(BaseResponse):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateModuleCommonInfoResponse(BaseResponse):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteModuleResponse(BaseResponse):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AddAttributeResponse(BaseResponse):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteAttributeResponse(BaseResponse):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|||||||
@ -2,7 +2,7 @@ from typing import Optional
|
|||||||
|
|
||||||
from schemas.base import BaseSchema, BaseResponse
|
from schemas.base import BaseSchema, BaseResponse
|
||||||
from schemas.deal_tag import DealTagSchema
|
from schemas.deal_tag import DealTagSchema
|
||||||
from schemas.module import BuiltInModuleSchema
|
from schemas.module import ModuleSchema
|
||||||
|
|
||||||
|
|
||||||
# region Entity
|
# region Entity
|
||||||
@ -11,7 +11,7 @@ from schemas.module import BuiltInModuleSchema
|
|||||||
class ProjectSchema(BaseSchema):
|
class ProjectSchema(BaseSchema):
|
||||||
id: int
|
id: int
|
||||||
name: str
|
name: str
|
||||||
built_in_modules: list[BuiltInModuleSchema]
|
modules: list[ModuleSchema]
|
||||||
tags: list[DealTagSchema]
|
tags: list[DealTagSchema]
|
||||||
|
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ class CreateProjectSchema(BaseSchema):
|
|||||||
|
|
||||||
class UpdateProjectSchema(BaseSchema):
|
class UpdateProjectSchema(BaseSchema):
|
||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
built_in_modules: list[BuiltInModuleSchema] = None
|
modules: list[ModuleSchema] = None
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
|
from .attr_select import AttrSelectService as AttrSelectService
|
||||||
|
from .attribute import AttributeService as AttributeService
|
||||||
from .board import BoardService as BoardService
|
from .board import BoardService as BoardService
|
||||||
from .deal import DealService as DealService
|
from .deal import DealService as DealService
|
||||||
from .project import ProjectService as ProjectService
|
|
||||||
from .status import StatusService as StatusService
|
|
||||||
from .deal_group import DealGroupService as DealGroupService
|
from .deal_group import DealGroupService as DealGroupService
|
||||||
from .deal_tag import DealTagService as DealTagService
|
from .deal_tag import DealTagService as DealTagService
|
||||||
|
from .project import ProjectService as ProjectService
|
||||||
|
from .status import StatusService as StatusService
|
||||||
|
from .attr_option import AttrOptionService as AttrOptionService
|
||||||
|
|||||||
27
services/attr_option.py
Normal file
27
services/attr_option.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from models import AttributeOption
|
||||||
|
from repositories import AttrOptionRepository
|
||||||
|
from schemas.attr_option import (
|
||||||
|
AttrOptionSchema,
|
||||||
|
CreateAttrOptionRequest,
|
||||||
|
UpdateAttrOptionRequest,
|
||||||
|
)
|
||||||
|
from services.mixins import ServiceCrudMixin
|
||||||
|
|
||||||
|
|
||||||
|
class AttrOptionService(
|
||||||
|
ServiceCrudMixin[
|
||||||
|
AttributeOption,
|
||||||
|
AttrOptionSchema,
|
||||||
|
CreateAttrOptionRequest,
|
||||||
|
UpdateAttrOptionRequest,
|
||||||
|
]
|
||||||
|
):
|
||||||
|
schema_class = AttrOptionSchema
|
||||||
|
entity_deleted_msg = "Опция успешно удалена"
|
||||||
|
entity_updated_msg = "Опция успешно обновлена"
|
||||||
|
entity_created_msg = "Опция успешно создана"
|
||||||
|
|
||||||
|
def __init__(self, session: AsyncSession):
|
||||||
|
self.repository = AttrOptionRepository(session)
|
||||||
24
services/attr_select.py
Normal file
24
services/attr_select.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from models import AttributeSelect
|
||||||
|
from repositories import AttrSelectRepository
|
||||||
|
from schemas.attr_select import (
|
||||||
|
AttrSelectSchema,
|
||||||
|
CreateAttrSelectRequest,
|
||||||
|
UpdateAttrSelectRequest,
|
||||||
|
)
|
||||||
|
from services.mixins import ServiceCrudMixin
|
||||||
|
|
||||||
|
|
||||||
|
class AttrSelectService(
|
||||||
|
ServiceCrudMixin[
|
||||||
|
AttributeSelect,
|
||||||
|
AttrSelectSchema,
|
||||||
|
CreateAttrSelectRequest,
|
||||||
|
UpdateAttrSelectRequest,
|
||||||
|
]
|
||||||
|
):
|
||||||
|
schema_class = AttrSelectSchema
|
||||||
|
|
||||||
|
def __init__(self, session: AsyncSession):
|
||||||
|
self.repository = AttrSelectRepository(session)
|
||||||
76
services/attribute.py
Normal file
76
services/attribute.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
from models import Attribute, AttributeValue, AttributeLabel
|
||||||
|
from repositories import AttributeRepository, DealRepository
|
||||||
|
from schemas.attribute import *
|
||||||
|
from services.mixins import *
|
||||||
|
|
||||||
|
|
||||||
|
class AttributeService(
|
||||||
|
ServiceCrudMixin[
|
||||||
|
Attribute, AttributeSchema, CreateAttributeRequest, UpdateAttributeRequest
|
||||||
|
]
|
||||||
|
):
|
||||||
|
schema_class = AttributeSchema
|
||||||
|
|
||||||
|
def __init__(self, session: AsyncSession):
|
||||||
|
self.repository = AttributeRepository(session)
|
||||||
|
|
||||||
|
async def update_attribute_label(
|
||||||
|
self, request: UpdateAttributeLabelRequest
|
||||||
|
) -> UpdateAttributeLabelResponse:
|
||||||
|
await self.repository.create_or_update_attribute_label(
|
||||||
|
request.module_id, request.attribute_id, request.label
|
||||||
|
)
|
||||||
|
return UpdateAttributeLabelResponse(
|
||||||
|
message="Название атрибута в модуле изменено"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_attribute_types(self) -> GetAllAttributeTypesResponse:
|
||||||
|
types = await self.repository.get_attribute_types()
|
||||||
|
return GetAllAttributeTypesResponse(
|
||||||
|
items=[AttributeTypeSchema.model_validate(t) for t in types]
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_deal_module_attributes(
|
||||||
|
self, deal_id: int, module_id: int
|
||||||
|
) -> GetDealModuleAttributesResponse:
|
||||||
|
deal_attributes: list[
|
||||||
|
tuple[Attribute, AttributeValue, AttributeLabel]
|
||||||
|
] = await self.repository.get_deal_module_attributes(deal_id, module_id)
|
||||||
|
|
||||||
|
attributes = []
|
||||||
|
for attr, attr_value, attr_label in deal_attributes:
|
||||||
|
select_schema = (
|
||||||
|
AttrSelectSchema.model_validate(attr.select) if attr.select else None
|
||||||
|
)
|
||||||
|
|
||||||
|
attribute = DealModuleAttributeSchema(
|
||||||
|
attribute_id=attr.id,
|
||||||
|
label=attr_label.label if attr_label else attr.label,
|
||||||
|
original_label=attr.label,
|
||||||
|
value=attr_value.value if attr_value else None,
|
||||||
|
type=AttributeTypeSchema.model_validate(attr.type),
|
||||||
|
select=select_schema,
|
||||||
|
default_value=attr.default_value,
|
||||||
|
description=attr.description,
|
||||||
|
is_applicable_to_group=attr.is_applicable_to_group,
|
||||||
|
is_nullable=attr.is_nullable,
|
||||||
|
)
|
||||||
|
attributes.append(attribute)
|
||||||
|
|
||||||
|
return GetDealModuleAttributesResponse(attributes=attributes)
|
||||||
|
|
||||||
|
async def update_deal_module_attributes(
|
||||||
|
self, deal_id: int, module_id: int, request: UpdateDealModuleAttributesRequest
|
||||||
|
) -> UpdateDealModuleAttributesResponse:
|
||||||
|
deal_repo = DealRepository(self.repository.session)
|
||||||
|
deal = await deal_repo.get_by_id(deal_id)
|
||||||
|
if deal.group_id:
|
||||||
|
deals = await deal_repo.get_by_group_id(deal.group_id)
|
||||||
|
else:
|
||||||
|
deals = [deal]
|
||||||
|
|
||||||
|
group_deal_ids = [d.id for d in deals]
|
||||||
|
await self.repository.update_or_create_deals_attribute_values(
|
||||||
|
deal_id, group_deal_ids, module_id, request.attributes
|
||||||
|
)
|
||||||
|
return UpdateDealModuleAttributesResponse(message="Успешно сохранено")
|
||||||
@ -1,11 +0,0 @@
|
|||||||
from models import BuiltInModule
|
|
||||||
from repositories import BuiltInModuleRepository
|
|
||||||
from schemas.module import BuiltInModuleSchema
|
|
||||||
from services.mixins import *
|
|
||||||
|
|
||||||
|
|
||||||
class BuiltInModuleService(ServiceGetAllMixin[BuiltInModule, BuiltInModuleSchema]):
|
|
||||||
schema_class = BuiltInModuleSchema
|
|
||||||
|
|
||||||
def __init__(self, session: AsyncSession):
|
|
||||||
self.repository = BuiltInModuleRepository(session)
|
|
||||||
@ -1,5 +1,4 @@
|
|||||||
from lexorank import lexorank
|
import lexorank
|
||||||
|
|
||||||
from models import DealGroup, Deal
|
from models import DealGroup, Deal
|
||||||
from repositories import DealGroupRepository, DealRepository, DealTagRepository
|
from repositories import DealGroupRepository, DealRepository, DealTagRepository
|
||||||
from schemas.deal_group import *
|
from schemas.deal_group import *
|
||||||
|
|||||||
93
services/module.py
Normal file
93
services/module.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
from models import Module, Attribute
|
||||||
|
from repositories import ModuleRepository, AttributeRepository
|
||||||
|
from schemas.module import *
|
||||||
|
from services.mixins import *
|
||||||
|
from utils.exceptions import ForbiddenException
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleService(
|
||||||
|
ServiceGetAllMixin[Module, ModuleSchema],
|
||||||
|
ServiceCreateMixin[Module, CreateModuleRequest, ModuleSchema],
|
||||||
|
ServiceUpdateMixin[Module, UpdateModuleCommonInfoRequest],
|
||||||
|
ServiceDeleteMixin[Module],
|
||||||
|
):
|
||||||
|
schema_class = ModuleSchema
|
||||||
|
entity_updated_msg = "Модуль успешно обновлен"
|
||||||
|
entity_deleted_msg = "Модуль успешно удален"
|
||||||
|
|
||||||
|
def __init__(self, session: AsyncSession):
|
||||||
|
self.repository = ModuleRepository(session)
|
||||||
|
|
||||||
|
async def get_with_attributes(self) -> GetAllWithAttributesResponse:
|
||||||
|
result = await self.repository.get_with_attributes_as_tuples()
|
||||||
|
modules = self._build_modules_with_attributes(result)
|
||||||
|
return GetAllWithAttributesResponse(items=list(modules.values()))
|
||||||
|
|
||||||
|
async def get_by_id_with_attributes(self, pk: int) -> GetByIdWithAttributesResponse:
|
||||||
|
result = await self.repository.get_with_attributes_as_tuple_by_id(pk)
|
||||||
|
modules = self._build_modules_with_attributes(result)
|
||||||
|
module = next(iter(modules.values()), None)
|
||||||
|
if module is None:
|
||||||
|
raise ObjectNotFoundException(f"Модуль с ID {pk} не найден")
|
||||||
|
return GetByIdWithAttributesResponse(entity=module)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_modules_with_attributes(
|
||||||
|
result: list[tuple],
|
||||||
|
) -> dict[int, ModuleWithAttributesSchema]:
|
||||||
|
module_attrs_dict: dict[int, ModuleWithAttributesSchema] = {}
|
||||||
|
|
||||||
|
for module, attribute, attribute_label in result:
|
||||||
|
new_attr = None
|
||||||
|
|
||||||
|
if attribute:
|
||||||
|
original_label = attribute.label
|
||||||
|
label = attribute_label.label if attribute_label else original_label
|
||||||
|
attr_values = {
|
||||||
|
**attribute.__dict__,
|
||||||
|
"label": label,
|
||||||
|
"original_label": original_label,
|
||||||
|
}
|
||||||
|
new_attr = ModuleAttributeSchema(**attr_values)
|
||||||
|
|
||||||
|
module_schema = module_attrs_dict.get(module.id)
|
||||||
|
if not module_schema:
|
||||||
|
module_data = module.__dict__
|
||||||
|
del module_data["attributes"]
|
||||||
|
module_schema = ModuleWithAttributesSchema(
|
||||||
|
**module_data,
|
||||||
|
attributes=[new_attr] if new_attr else [],
|
||||||
|
)
|
||||||
|
module_attrs_dict[module.id] = module_schema
|
||||||
|
elif new_attr:
|
||||||
|
module_schema.attributes.append(new_attr)
|
||||||
|
|
||||||
|
return module_attrs_dict
|
||||||
|
|
||||||
|
async def is_soft_delete(self, module: Module) -> bool:
|
||||||
|
if module.is_built_in:
|
||||||
|
raise ForbiddenException("Нельзя менять встроенный модуль")
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def add_attribute(self, module_id: int, attribute_id: int) -> AddAttributeResponse:
|
||||||
|
module, attribute = await self._get_module_and_attr_from_request(module_id, attribute_id)
|
||||||
|
await self.repository.add_attribute_to_module(module, attribute)
|
||||||
|
return AddAttributeResponse(message="Аттрибут успешно добавлен к модулю")
|
||||||
|
|
||||||
|
async def delete_attribute(
|
||||||
|
self, module_id: int, attribute_id: int
|
||||||
|
) -> DeleteAttributeResponse:
|
||||||
|
module, attribute = await self._get_module_and_attr_from_request(module_id, attribute_id)
|
||||||
|
await self.repository.delete_attribute_from_module(module, attribute)
|
||||||
|
return DeleteAttributeResponse(message="Аттрибут успешно удален из модуля")
|
||||||
|
|
||||||
|
async def _get_module_and_attr_from_request(
|
||||||
|
self, module_id: int, attribute_id: int,
|
||||||
|
) -> tuple[Module, Attribute]:
|
||||||
|
module = await self.repository.get_by_id(module_id)
|
||||||
|
if module.is_built_in:
|
||||||
|
raise ForbiddenException("Нельзя менять встроенный модуль")
|
||||||
|
|
||||||
|
attr_repo = AttributeRepository(self.repository.session)
|
||||||
|
attribute = await attr_repo.get_by_id(attribute_id)
|
||||||
|
return module, attribute
|
||||||
1
task_management/__init__.py
Normal file
1
task_management/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .taskiq_broker import broker as broker
|
||||||
15
task_management/taskiq_broker.py
Normal file
15
task_management/taskiq_broker.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from taskiq_aio_pika import AioPikaBroker
|
||||||
|
from taskiq_postgresql import PostgresqlResultBackend
|
||||||
|
|
||||||
|
from backend.config import RABBITMQ_URL
|
||||||
|
from backend.session import DATABASE_URL
|
||||||
|
|
||||||
|
result_backend = PostgresqlResultBackend(
|
||||||
|
dsn=DATABASE_URL,
|
||||||
|
)
|
||||||
|
|
||||||
|
broker = AioPikaBroker(
|
||||||
|
RABBITMQ_URL,
|
||||||
|
).with_result_backend(
|
||||||
|
result_backend,
|
||||||
|
)
|
||||||
@ -1,3 +1,11 @@
|
|||||||
class ObjectNotFoundException(Exception):
|
class BaseResponseException(Exception):
|
||||||
def __init__(self, name: str):
|
def __init__(self, name: str):
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectNotFoundException(BaseResponseException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ForbiddenException(BaseResponseException):
|
||||||
|
pass
|
||||||
|
|||||||
173
uv.lock
generated
173
uv.lock
generated
@ -2,6 +2,19 @@ version = 1
|
|||||||
revision = 3
|
revision = 3
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aio-pika"
|
||||||
|
version = "9.5.7"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "aiormq" },
|
||||||
|
{ name = "yarl" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/42/ad/0ddde89d7a018f4304aac687e5b65c07d308644f51da3c4ae411184bb237/aio_pika-9.5.7.tar.gz", hash = "sha256:0569b59d3c7b36ca76abcb213cdc3677e2a4710a3c371dd27359039f9724f4ee", size = 47298, upload-time = "2025-08-05T18:21:18.397Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/be/9b08e7c4d1b3b9a1184e63965d13c811366444cb42c6e809910ab17e916c/aio_pika-9.5.7-py3-none-any.whl", hash = "sha256:684316a0e92157754bb2d6927c5568fd997518b123add342e97405aa9066772b", size = 54297, upload-time = "2025-08-05T18:21:16.99Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aioboto3"
|
name = "aioboto3"
|
||||||
version = "15.4.0"
|
version = "15.4.0"
|
||||||
@ -118,6 +131,19 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/85/13/58b70a580de00893223d61de8fea167877a3aed97d4a5e1405c9159ef925/aioitertools-0.12.0-py3-none-any.whl", hash = "sha256:fc1f5fac3d737354de8831cbba3eb04f79dd649d8f3afb4c5b114925e662a796", size = 24345, upload-time = "2024-09-02T03:34:59.454Z" },
|
{ url = "https://files.pythonhosted.org/packages/85/13/58b70a580de00893223d61de8fea167877a3aed97d4a5e1405c9159ef925/aioitertools-0.12.0-py3-none-any.whl", hash = "sha256:fc1f5fac3d737354de8831cbba3eb04f79dd649d8f3afb4c5b114925e662a796", size = 24345, upload-time = "2024-09-02T03:34:59.454Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aiormq"
|
||||||
|
version = "6.9.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pamqp" },
|
||||||
|
{ name = "yarl" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/8e/f6/01bc850db6d9b46ae825e3c373f610b0544e725a1159745a6de99ad0d9f1/aiormq-6.9.2.tar.gz", hash = "sha256:d051d46086079934d3a7157f4d8dcb856b77683c2a94aee9faa165efa6a785d3", size = 30554, upload-time = "2025-10-20T10:49:59.763Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/ec/763b13f148f3760c1562cedb593feaffbae177eeece61af5d0ace7b72a3e/aiormq-6.9.2-py3-none-any.whl", hash = "sha256:ab0f4e88e70f874b0ea344b3c41634d2484b5dc8b17cb6ae0ae7892a172ad003", size = 31829, upload-time = "2025-10-20T10:49:58.547Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiosignal"
|
name = "aiosignal"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
@ -362,17 +388,22 @@ dependencies = [
|
|||||||
{ name = "fastapi-endpoints" },
|
{ name = "fastapi-endpoints" },
|
||||||
{ name = "fpdf" },
|
{ name = "fpdf" },
|
||||||
{ name = "gunicorn" },
|
{ name = "gunicorn" },
|
||||||
{ name = "lexorank" },
|
{ name = "lexorank-py" },
|
||||||
{ name = "orjson" },
|
{ name = "orjson" },
|
||||||
{ name = "pathlib" },
|
{ name = "pathlib" },
|
||||||
{ name = "pdfrw" },
|
{ name = "pdfrw" },
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "pymupdf" },
|
{ name = "pymupdf" },
|
||||||
{ name = "python-dotenv" },
|
{ name = "python-dotenv" },
|
||||||
|
{ name = "python-multipart" },
|
||||||
{ name = "redis", extra = ["hiredis"] },
|
{ name = "redis", extra = ["hiredis"] },
|
||||||
{ name = "reportlab" },
|
{ name = "reportlab" },
|
||||||
{ name = "sqlalchemy", extra = ["asyncio"] },
|
{ name = "sqlalchemy", extra = ["asyncio"] },
|
||||||
{ name = "starlette" },
|
{ name = "starlette" },
|
||||||
|
{ name = "taskiq" },
|
||||||
|
{ name = "taskiq-aio-pika" },
|
||||||
|
{ name = "taskiq-fastapi" },
|
||||||
|
{ name = "taskiq-postgresql" },
|
||||||
{ name = "uvicorn", extra = ["standard"] },
|
{ name = "uvicorn", extra = ["standard"] },
|
||||||
{ name = "uvicorn-worker" },
|
{ name = "uvicorn-worker" },
|
||||||
]
|
]
|
||||||
@ -392,17 +423,22 @@ requires-dist = [
|
|||||||
{ name = "fastapi-endpoints", git = "https://github.com/vladNed/fastapi-endpoints.git?rev=main" },
|
{ name = "fastapi-endpoints", git = "https://github.com/vladNed/fastapi-endpoints.git?rev=main" },
|
||||||
{ name = "fpdf", specifier = ">=1.7.2" },
|
{ name = "fpdf", specifier = ">=1.7.2" },
|
||||||
{ name = "gunicorn", specifier = ">=23.0.0" },
|
{ name = "gunicorn", specifier = ">=23.0.0" },
|
||||||
{ name = "lexorank", specifier = ">=1.0.1" },
|
{ name = "lexorank-py", specifier = "==0.1.1" },
|
||||||
{ name = "orjson", specifier = ">=3.11.1" },
|
{ name = "orjson", specifier = ">=3.11.1" },
|
||||||
{ name = "pathlib", specifier = ">=1.0.1" },
|
{ name = "pathlib", specifier = ">=1.0.1" },
|
||||||
{ name = "pdfrw", specifier = ">=0.4" },
|
{ name = "pdfrw", specifier = ">=0.4" },
|
||||||
{ name = "pydantic", specifier = ">=2.11.7" },
|
{ name = "pydantic", specifier = ">=2.11.7" },
|
||||||
{ name = "pymupdf", specifier = ">=1.26.5" },
|
{ name = "pymupdf", specifier = ">=1.26.5" },
|
||||||
{ name = "python-dotenv", specifier = ">=1.1.1" },
|
{ name = "python-dotenv", specifier = ">=1.1.1" },
|
||||||
|
{ name = "python-multipart", specifier = ">=0.0.20" },
|
||||||
{ name = "redis", extras = ["hiredis"], specifier = ">=6.2.0" },
|
{ name = "redis", extras = ["hiredis"], specifier = ">=6.2.0" },
|
||||||
{ name = "reportlab", specifier = ">=4.4.4" },
|
{ name = "reportlab", specifier = ">=4.4.4" },
|
||||||
{ name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.41" },
|
{ name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.41" },
|
||||||
{ name = "starlette", specifier = ">=0.47.2" },
|
{ name = "starlette", specifier = ">=0.47.2" },
|
||||||
|
{ name = "taskiq", specifier = ">=0.11.19" },
|
||||||
|
{ name = "taskiq-aio-pika", specifier = ">=0.4.4" },
|
||||||
|
{ name = "taskiq-fastapi", specifier = ">=0.3.5" },
|
||||||
|
{ name = "taskiq-postgresql", specifier = ">=0.4.0" },
|
||||||
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.35.0" },
|
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.35.0" },
|
||||||
{ name = "uvicorn-worker", specifier = ">=0.3.0" },
|
{ name = "uvicorn-worker", specifier = ">=0.3.0" },
|
||||||
]
|
]
|
||||||
@ -671,6 +707,27 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "importlib-metadata"
|
||||||
|
version = "8.7.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "zipp" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "izulu"
|
||||||
|
version = "0.50.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d0/58/6d6335c78b7ade54d8a6c6dbaa589e5c21b3fd916341d5a16f774c72652a/izulu-0.50.0.tar.gz", hash = "sha256:cc8e252d5e8560c70b95380295008eeb0786f7b745a405a40d3556ab3252d5f5", size = 48558, upload-time = "2025-03-24T15:52:21.51Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/9f/bf9d33546bbb6e5e80ebafe46f90b7d8b4a77410b7b05160b0ca8978c15a/izulu-0.50.0-py3-none-any.whl", hash = "sha256:4e9ae2508844e7c5f62c468a8b9e2deba2f60325ef63f01e65b39fd9a6b3fab4", size = 18095, upload-time = "2025-03-24T15:52:19.667Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jinja2"
|
name = "jinja2"
|
||||||
version = "3.1.6"
|
version = "3.1.6"
|
||||||
@ -693,12 +750,12 @@ wheels = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lexorank"
|
name = "lexorank-py"
|
||||||
version = "1.0.1"
|
version = "0.1.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/11/58/87b3ced0716ea1997315b6f690edbe52d38f8145c12e56c35e1b7cfe306e/lexorank-1.0.1.tar.gz", hash = "sha256:e84869f626ddf4295cc848fd639a76f87bb8a17b2f649aa1d6449a4c53530fd7", size = 1758, upload-time = "2022-05-25T22:07:16.683Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/af/b9/a8213f5ed28cbb6f1f24354157745787e1d6776cbbc8106f11c49625d3bd/lexorank-py-0.1.1.tar.gz", hash = "sha256:2045d8cef314cea8f5bd7df036b4dffafab12a569842e60efed6c8f7c3bc31e8", size = 48670, upload-time = "2023-08-26T09:49:41.214Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/37/d7/b7bde2527103aeb4ddfd02e13b689a2c2ead51971276b4633e24fbbbdc68/lexorank-1.0.1-py3-none-any.whl", hash = "sha256:3a734155866e7c52b2e0e11f92226d002e79b7442271bf969b513aa5278b65c5", size = 1804, upload-time = "2022-05-25T22:07:15.03Z" },
|
{ url = "https://files.pythonhosted.org/packages/fa/ce/563e2935fb89ea66828201d2eba5ce737e76866d1d0067791374007cb380/lexorank_py-0.1.1-py3-none-any.whl", hash = "sha256:b0798b15e7d47d0f94591f872e5d6cf5f2d2ea0186226673141cd1e35f28dfe4", size = 11301, upload-time = "2023-08-26T09:49:40.07Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -850,6 +907,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pamqp"
|
||||||
|
version = "3.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/fb/62/35bbd3d3021e008606cd0a9532db7850c65741bbf69ac8a3a0d8cfeb7934/pamqp-3.3.0.tar.gz", hash = "sha256:40b8795bd4efcf2b0f8821c1de83d12ca16d5760f4507836267fd7a02b06763b", size = 30993, upload-time = "2024-01-12T20:37:25.085Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/8d/c1e93296e109a320e508e38118cf7d1fc2a4d1c2ec64de78565b3c445eb5/pamqp-3.3.0-py2.py3-none-any.whl", hash = "sha256:c901a684794157ae39b52cbf700db8c9aae7a470f13528b9d7b4e5f7202f8eb0", size = 33848, upload-time = "2024-01-12T20:37:21.359Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pathlib"
|
name = "pathlib"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@ -1002,6 +1068,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" },
|
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pycron"
|
||||||
|
version = "3.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/8e/5d/340be12ae4a69c33102dfb6ddc1dc6e53e69b2d504fa26b5d34a472c3057/pycron-3.2.0.tar.gz", hash = "sha256:e125a28aca0295769541a40633f70b602579df48c9cb357c36c28d2628ba2b13", size = 4248, upload-time = "2025-06-05T13:24:12.636Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/76/caf316909f4545e7158e0e1defd8956a1da49f4af04f5d16b18c358dfeac/pycron-3.2.0-py3-none-any.whl", hash = "sha256:6d2349746270bd642b71b9f7187cf13f4d9ee2412b4710396a507b5fe4f60dac", size = 4904, upload-time = "2025-06-05T13:24:11.477Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic"
|
name = "pydantic"
|
||||||
version = "2.11.7"
|
version = "2.11.7"
|
||||||
@ -1104,6 +1179,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" },
|
{ url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytz"
|
||||||
|
version = "2025.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyyaml"
|
name = "pyyaml"
|
||||||
version = "6.0.2"
|
version = "6.0.2"
|
||||||
@ -1322,6 +1406,74 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/f7/1f/b876b1f83aef204198a42dc101613fefccb32258e5428b5f9259677864b4/starlette-0.47.2-py3-none-any.whl", hash = "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b", size = 72984, upload-time = "2025-07-20T17:31:56.738Z" },
|
{ url = "https://files.pythonhosted.org/packages/f7/1f/b876b1f83aef204198a42dc101613fefccb32258e5428b5f9259677864b4/starlette-0.47.2-py3-none-any.whl", hash = "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b", size = 72984, upload-time = "2025-07-20T17:31:56.738Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "taskiq"
|
||||||
|
version = "0.11.19"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "aiohttp" },
|
||||||
|
{ name = "anyio" },
|
||||||
|
{ name = "importlib-metadata" },
|
||||||
|
{ name = "izulu" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pycron" },
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "pytz" },
|
||||||
|
{ name = "taskiq-dependencies" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ed/f8/e26c3a6d169e8ecee228d67d12003d25192d663b27f8007e41575e7a710c/taskiq-0.11.19.tar.gz", hash = "sha256:b6f03e398f0c6e7eb784f22b3c63a2efeb9e113bdce792dbd9ccb81b9edb17c4", size = 55910, upload-time = "2025-10-23T15:01:10.071Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/3a/55fe61591ba4d282cbfc12e66c6d8f5646b9ce81bbfbd958bca55eebbe5b/taskiq-0.11.19-py3-none-any.whl", hash = "sha256:630b39531284f802860fa28ec54707c7e68e57b2011bcb5f8ce9c71508a273f7", size = 81848, upload-time = "2025-10-23T15:01:08.907Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "taskiq-aio-pika"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "aio-pika" },
|
||||||
|
{ name = "taskiq" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e9/b0/e12a219535ad55034156ab66c3aa07a3673a64d2a6fda83eeabaa6115732/taskiq_aio_pika-0.4.4.tar.gz", hash = "sha256:357f37a6b8828bc61fc4091dcd80b54c6b820eb6774cc72d2f9e372c6a8c0037", size = 7180, upload-time = "2025-10-23T20:16:18.411Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/cb/808172ab28959a6ce9a0638398ed0a59b2ad61d09e87c092b3508f04ad3a/taskiq_aio_pika-0.4.4-py3-none-any.whl", hash = "sha256:8f3405daab41d46f9c5273bdbbb977dd43fd44e82d1b87619078794ae37bcc45", size = 7251, upload-time = "2025-10-23T20:16:17.689Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "taskiq-dependencies"
|
||||||
|
version = "1.5.7"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/47/90/47a627696e53bfdcacabc3e8c05b73bf1424685bcb5f17209cb8b12da1bf/taskiq_dependencies-1.5.7.tar.gz", hash = "sha256:0d3b240872ef152b719153b9526d866d2be978aeeaea6600e878414babc2dcb4", size = 14875, upload-time = "2025-02-26T22:07:39.876Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/6d/4a012f2de002c2e93273f5e7d3e3feea02f7fdbb7b75ca2ca1dd10703091/taskiq_dependencies-1.5.7-py3-none-any.whl", hash = "sha256:6fcee5d159bdb035ef915d4d848826169b6f06fe57cc2297a39b62ea3e76036f", size = 13801, upload-time = "2025-02-26T22:07:38.622Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "taskiq-fastapi"
|
||||||
|
version = "0.3.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "fastapi" },
|
||||||
|
{ name = "taskiq" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/cd/29/d4f08a3730d23870f73404d855e5145d13d0bcf1e5376008522804afa51b/taskiq_fastapi-0.3.5.tar.gz", hash = "sha256:0df3df3e7e0a355b9f21a4f81429ad95d0d33af305e75039654ac13ae26c538f", size = 5102, upload-time = "2025-04-20T22:45:21.924Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/06/6ba382f300aebc5d739cd01d708572329d4360ea0c02b7734583a2894b16/taskiq_fastapi-0.3.5-py3-none-any.whl", hash = "sha256:35553cb05cfb8e34d50179bad747d79a42d782f5b9c529128ad64263af8c246f", size = 5116, upload-time = "2025-04-20T22:45:21.049Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "taskiq-postgresql"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "taskiq" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e1/1a/dc903813bc238fcba16624e22f959e681f96bd210583a98e77c1d1c7607c/taskiq_postgresql-0.4.0.tar.gz", hash = "sha256:3e8cda663ec2893adfcf2d1f30447b2c03cc46067622e1f9e1c9a45f4e22731f", size = 125271, upload-time = "2025-10-01T17:30:28.653Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/c7/9a5bec60989cd1d7d97c624fd272a36efe087a0e54ae1f17117c2b56655f/taskiq_postgresql-0.4.0-py3-none-any.whl", hash = "sha256:0e64adc6faf5ffa6b67a59722e56181fc6e79a1bac6d5584fbaca6916bb1ec7b", size = 25844, upload-time = "2025-10-01T17:30:27.348Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typer"
|
name = "typer"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
@ -1578,3 +1730,12 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/94/c3/b2e9f38bc3e11191981d57ea08cab2166e74ea770024a646617c9cddd9f6/yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f", size = 93003, upload-time = "2025-06-10T00:45:27.752Z" },
|
{ url = "https://files.pythonhosted.org/packages/94/c3/b2e9f38bc3e11191981d57ea08cab2166e74ea770024a646617c9cddd9f6/yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f", size = 93003, upload-time = "2025-06-10T00:45:27.752Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542, upload-time = "2025-06-10T00:46:07.521Z" },
|
{ url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542, upload-time = "2025-06-10T00:46:07.521Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zipp"
|
||||||
|
version = "3.23.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
|
||||||
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user