updated services api

This commit is contained in:
2025-05-30 23:43:46 +03:00
parent e5829f0525
commit f8184246d9
31 changed files with 2963 additions and 9 deletions

View File

@@ -0,0 +1,2 @@
result_with_keys_dict {'Employees.id': 3, 'Employees.uu_id': UUID('2b757a5c-01bb-4213-9cf1-402480e73edc'), 'People.id': 2, 'People.uu_id': UUID('d945d320-2a4e-48db-a18c-6fd024beb517'), 'Users.id': 3, 'Users.uu_id': UUID('bdfa84d9-0e05-418c-9406-d6d1d41ae2a1'), 'Companies.id': 1, 'Companies.uu_id': UUID('da1de172-2f89-42d2-87f3-656b36a79d5b'), 'Departments.id': 3, 'Departments.uu_id': UUID('4edcec87-e072-408d-a780-3a62151b3971'), 'Duty.id': 9, 'Duty.uu_id': UUID('00d29292-c29e-4435-be41-9704ccf4b24d'), 'Addresses.id': None, 'Addresses.letter_address': None}

View File

@@ -0,0 +1,56 @@
from fastapi import Header, Request, Response
from pydantic import BaseModel
from config import api_config
class CommonHeaders(BaseModel):
language: str | None = None
domain: str | None = None
timezone: str | None = None
token: str | None = None
request: Request | None = None
response: Response | None = None
operation_id: str | None = None
model_config = {
"arbitrary_types_allowed": True
}
@classmethod
def as_dependency(
cls,
request: Request,
response: Response,
language: str = Header(None, alias="language"),
domain: str = Header(None, alias="domain"),
tz: str = Header(None, alias="timezone"),
):
token = request.headers.get(api_config.ACCESS_TOKEN_TAG, None)
# Extract operation_id from the route
operation_id = None
if hasattr(request.scope.get("route"), "operation_id"):
operation_id = request.scope.get("route").operation_id
return cls(
language=language,
domain=domain,
timezone=tz,
token=token,
request=request,
response=response,
operation_id=operation_id,
)
def get_headers_dict(self):
"""Convert the headers to a dictionary format used in the application"""
import uuid
return {
"language": self.language or "",
"domain": self.domain or "",
"eys-ext": f"{str(uuid.uuid4())}",
"tz": self.timezone or "GMT+3",
"token": self.token,
}

View File

@@ -0,0 +1,17 @@
class BaseModelCore(BaseModel):
"""
BaseModelCore
model_dump override for alias support Users.name -> Table[Users] Field(alias="name")
"""
__abstract__ = True
class Config:
validate_by_name = True
use_enum_values = True
def model_dump(self, *args, **kwargs):
data = super().model_dump(*args, **kwargs)
return {self.__class__.model_fields[field].alias: value for field, value in data.items()}

View File

@@ -0,0 +1,4 @@
from .pagination import PaginateOnly, ListOptions, PaginationConfig
from .result import Pagination, PaginationResult
from .base import PostgresResponseSingle, PostgresResponse, ResultQueryJoin, ResultQueryJoinSingle
from .api import EndpointResponse, CreateEndpointResponse

View File

@@ -0,0 +1,60 @@
from .result import PaginationResult
from .base import PostgresResponseSingle
from pydantic import BaseModel
from typing import Any, Type
class EndpointResponse(BaseModel):
"""Endpoint response model."""
completed: bool = True
message: str = "Success"
pagination_result: PaginationResult
@property
def response(self):
"""Convert response to dictionary format."""
result_data = getattr(self.pagination_result, "data", None)
if not result_data:
return {
"completed": False,
"message": "MSG0004-NODATA",
"data": None,
"pagination": None,
}
result_pagination = getattr(self.pagination_result, "pagination", None)
if not result_pagination:
raise ValueError("Invalid pagination result pagination.")
pagination_dict = getattr(result_pagination, "as_dict", None)
if not pagination_dict:
raise ValueError("Invalid pagination result as_dict.")
return {
"completed": self.completed,
"message": self.message,
"data": result_data,
"pagination": pagination_dict,
}
model_config = {
"arbitrary_types_allowed": True
}
class CreateEndpointResponse(BaseModel):
"""Create endpoint response model."""
completed: bool = True
message: str = "Success"
data: PostgresResponseSingle
@property
def response(self):
"""Convert response to dictionary format."""
return {
"completed": self.completed,
"message": self.message,
"data": self.data.data,
}
model_config = {
"arbitrary_types_allowed": True
}

View File

@@ -0,0 +1,193 @@
"""
Response handler for PostgreSQL query results.
This module provides a wrapper class for SQLAlchemy query results,
adding convenience methods for accessing data and managing query state.
"""
from typing import Any, Dict, Optional, TypeVar, Generic, Union
from pydantic import BaseModel
from sqlalchemy.orm import Query
T = TypeVar("T")
class PostgresResponse(Generic[T]):
"""
Wrapper for PostgreSQL/SQLAlchemy query results.
Properties:
count: Total count of results
query: Get query object
as_dict: Convert response to dictionary format
"""
def __init__(self, query: Query, base_model: Optional[BaseModel] = None):
self._query = query
self._count: Optional[int] = None
self._base_model: Optional[BaseModel] = base_model
self.single = False
@property
def query(self) -> Query:
"""Get query object."""
return self._query
@property
def data(self) -> Union[list[T], T]:
"""Get query object."""
return self._query.all()
@property
def count(self) -> int:
"""Get query object."""
return self._query.count()
@property
def to_dict(self, **kwargs) -> list[dict]:
"""Get query object."""
if self._base_model:
return [self._base_model(**item.to_dict()).model_dump(**kwargs) for item in self.data]
return [item.to_dict() for item in self.data]
@property
def as_dict(self) -> Dict[str, Any]:
"""Convert response to dictionary format."""
return {
"query": str(self.query),
"count": self.count,
"data": self.to_dict,
}
class PostgresResponseSingle(Generic[T]):
"""
Wrapper for PostgreSQL/SQLAlchemy query results.
Properties:
count: Total count of results
query: Get query object
as_dict: Convert response to dictionary format
data: Get query object
"""
def __init__(self, query: Query, base_model: Optional[BaseModel] = None):
self._query = query
self._count: Optional[int] = None
self._base_model: Optional[BaseModel] = base_model
self.single = True
@property
def query(self) -> Query:
"""Get query object."""
return self._query
@property
def to_dict(self, **kwargs) -> dict:
"""Get query object."""
if self._base_model:
return self._base_model(**self._query.first().to_dict()).model_dump(**kwargs)
return self._query.first().to_dict()
@property
def data(self) -> T:
"""Get query object."""
return self._query.first()
@property
def count(self) -> int:
"""Get query object."""
return self._query.count()
@property
def as_dict(self) -> Dict[str, Any]:
"""Convert response to dictionary format."""
return {"query": str(self.query),"data": self.to_dict, "count": self.count}
class ResultQueryJoin:
"""
ResultQueryJoin
params:
list_of_instrumented_attributes: list of instrumented attributes
query: query object
"""
def __init__(self, list_of_instrumented_attributes, query):
"""Initialize ResultQueryJoin"""
self.list_of_instrumented_attributes = list_of_instrumented_attributes
self._query = query
@property
def query(self):
"""Get query object."""
return self._query
@property
def to_dict(self):
"""Convert response to dictionary format."""
list_of_dictionaries, result = [], dict()
for user_orders_shipping_iter in self.query.all():
for index, instrumented_attribute_iter in enumerate(self.list_of_instrumented_attributes):
result[str(instrumented_attribute_iter)] = user_orders_shipping_iter[index]
list_of_dictionaries.append(result)
return list_of_dictionaries
@property
def count(self):
"""Get count of query."""
return self.query.count()
@property
def data(self):
"""Get query object."""
return self.query.all()
@property
def as_dict(self):
"""Convert response to dictionary format."""
return {"query": str(self.query), "data": self.data, "count": self.count}
class ResultQueryJoinSingle:
"""
ResultQueryJoinSingle
params:
list_of_instrumented_attributes: list of instrumented attributes
query: query object
"""
def __init__(self, list_of_instrumented_attributes, query):
"""Initialize ResultQueryJoinSingle"""
self.list_of_instrumented_attributes = list_of_instrumented_attributes
self._query = query
@property
def query(self):
"""Get query object."""
return self._query
@property
def to_dict(self):
"""Convert response to dictionary format."""
data, result = self.query.first(), dict()
for index, instrumented_attribute_iter in enumerate(self.list_of_instrumented_attributes):
result[str(instrumented_attribute_iter)] = data[index]
return result
@property
def count(self):
"""Get count of query."""
return self.query.count()
@property
def data(self):
"""Get query object."""
return self._query.first()
@property
def as_dict(self):
"""Convert response to dictionary format."""
return {"query": str(self.query), "data": self.data, "count": self.count}

View File

@@ -0,0 +1,19 @@
class UserPydantic(BaseModel):
username: str = Field(..., alias='user.username')
account_balance: float = Field(..., alias='user.account_balance')
preferred_category_id: Optional[int] = Field(None, alias='user.preferred_category_id')
last_ordered_product_id: Optional[int] = Field(None, alias='user.last_ordered_product_id')
supplier_rating_id: Optional[int] = Field(None, alias='user.supplier_rating_id')
other_rating_id: Optional[int] = Field(None, alias='product.supplier_rating_id')
id: int = Field(..., alias='user.id')
class Config:
validate_by_name = True
use_enum_values = True
def model_dump(self, *args, **kwargs):
data = super().model_dump(*args, **kwargs)
return {self.__class__.model_fields[field].alias: value for field, value in data.items()}

View File

@@ -0,0 +1,70 @@
from typing import Any, Dict, Optional, Union, TypeVar, Type
from sqlalchemy import desc, asc
from pydantic import BaseModel
from .base import PostgresResponse
# Type variable for class methods returning self
T = TypeVar("T", bound="BaseModel")
class PaginateConfig:
"""
Configuration for pagination settings.
Attributes:
DEFAULT_SIZE: Default number of items per page (10)
MIN_SIZE: Minimum allowed page size (10)
MAX_SIZE: Maximum allowed page size (40)
"""
DEFAULT_SIZE = 10
MIN_SIZE = 5
MAX_SIZE = 100
class ListOptions(BaseModel):
"""
Query for list option abilities
"""
page: Optional[int] = 1
size: Optional[int] = 10
orderField: Optional[Union[tuple[str], list[str]]] = ["uu_id"]
orderType: Optional[Union[tuple[str], list[str]]] = ["asc"]
# include_joins: Optional[list] = None
class PaginateOnly(ListOptions):
"""
Query for list option abilities
"""
query: Optional[dict] = None
class PaginationConfig(BaseModel):
"""
Configuration for pagination settings.
Attributes:
page: Current page number (default: 1)
size: Items per page (default: 10)
orderField: Field to order by (default: "created_at")
orderType: Order direction (default: "desc")
"""
page: int = 1
size: int = 10
orderField: Optional[Union[tuple[str], list[str]]] = ["created_at"]
orderType: Optional[Union[tuple[str], list[str]]] = ["desc"]
def __init__(self, **data):
super().__init__(**data)
if self.orderField is None:
self.orderField = ["created_at"]
if self.orderType is None:
self.orderType = ["desc"]
default_paginate_config = PaginateConfig()

View File

@@ -0,0 +1,180 @@
from typing import Optional, Union, Type, Any, Dict, TypeVar
from pydantic import BaseModel
from sqlalchemy.orm import Query
from sqlalchemy import asc, desc
from .pagination import default_paginate_config
from .base import PostgresResponse
from .pagination import PaginationConfig
T = TypeVar("T")
class Pagination:
"""
Handles pagination logic for query results.
Manages page size, current page, ordering, and calculates total pages
and items based on the data source.
Attributes:
DEFAULT_SIZE: Default number of items per page (10)
MIN_SIZE: Minimum allowed page size (10)
MAX_SIZE: Maximum allowed page size (40)
"""
DEFAULT_SIZE = default_paginate_config.DEFAULT_SIZE
MIN_SIZE = default_paginate_config.MIN_SIZE
MAX_SIZE = default_paginate_config.MAX_SIZE
def __init__(self, data: PostgresResponse):
self.query = data
self.size: int = self.DEFAULT_SIZE
self.page: int = 1
self.orderField: Optional[Union[tuple[str], list[str]]] = ["uu_id"]
self.orderType: Optional[Union[tuple[str], list[str]]] = ["asc"]
self.page_count: int = 1
self.total_count: int = 0
self.all_count: int = 0
self.total_pages: int = 1
self._update_page_counts()
def change(self, **kwargs) -> None:
"""Update pagination settings from config."""
config = PaginationConfig(**kwargs)
self.size = (
config.size
if self.MIN_SIZE <= config.size <= self.MAX_SIZE
else self.DEFAULT_SIZE
)
self.page = config.page
self.orderField = config.orderField
self.orderType = config.orderType
self._update_page_counts()
def feed(self, data: PostgresResponse) -> None:
"""Calculate pagination based on data source."""
self.query = data
self._update_page_counts()
def _update_page_counts(self) -> None:
"""Update page counts and validate current page."""
if self.query:
self.total_count = self.query.count()
self.all_count = self.query.count()
self.size = (
self.size
if self.MIN_SIZE <= self.size <= self.MAX_SIZE
else self.DEFAULT_SIZE
)
self.total_pages = max(1, (self.total_count + self.size - 1) // self.size)
self.page = max(1, min(self.page, self.total_pages))
self.page_count = (
self.total_count % self.size
if self.page == self.total_pages and self.total_count % self.size
else self.size
)
def refresh(self) -> None:
"""Reset pagination state to defaults."""
self._update_page_counts()
def reset(self) -> None:
"""Reset pagination state to defaults."""
self.size = self.DEFAULT_SIZE
self.page = 1
self.orderField = "uu_id"
self.orderType = "asc"
@property
def next_available(self) -> bool:
if self.page < self.total_pages:
return True
return False
@property
def back_available(self) -> bool:
if self.page > 1:
return True
return False
@property
def as_dict(self) -> Dict[str, Any]:
"""Convert pagination state to dictionary format."""
self.refresh()
return {
"size": self.size,
"page": self.page,
"allCount": self.all_count,
"totalCount": self.total_count,
"totalPages": self.total_pages,
"pageCount": self.page_count,
"orderField": self.orderField,
"orderType": self.orderType,
"next": self.next_available,
"back": self.back_available,
}
class PaginationResult:
"""
Result of a paginated query.
Contains the query result and pagination state.
data: PostgresResponse of query results
pagination: Pagination state
Attributes:
_query: Original query object
pagination: Pagination state
"""
def __init__(
self,
data: PostgresResponse,
pagination: Pagination,
is_list: bool = True,
response_model: Type[T] = None,
):
self._query = data
self.pagination = pagination
self.response_type = is_list
self.limit = self.pagination.size
self.offset = self.pagination.size * (self.pagination.page - 1)
self.order_by = self.pagination.orderField
self.response_model = response_model
def dynamic_order_by(self):
"""
Dynamically order a query by multiple fields.
Returns:
Ordered query object.
"""
if not len(self.order_by) == len(self.pagination.orderType):
raise ValueError(
"Order by fields and order types must have the same length."
)
order_criteria = zip(self.order_by, self.pagination.orderType)
for field, direction in order_criteria:
if hasattr(self._query.column_descriptions[0]["entity"], field):
if direction.lower().startswith("d"):
self._query = self._query.order_by(
desc(
getattr(self._query.column_descriptions[0]["entity"], field)
)
)
else:
self._query = self._query.order_by(
asc(
getattr(self._query.column_descriptions[0]["entity"], field)
)
)
return self._query
@property
def data(self) -> Union[list | dict]:
"""Get query object."""
query_paginated = self.dynamic_order_by().limit(self.limit).offset(self.offset)
queried_data = (query_paginated.all() if self.response_type else query_paginated.first())
data = ([result.get_dict() for result in queried_data] if self.response_type else queried_data.get_dict())
return [self.response_model(**item).model_dump() for item in data] if self.response_model else data

View File

@@ -0,0 +1,123 @@
from enum import Enum
from pydantic import BaseModel
from typing import Optional, Union
class UserType(Enum):
employee = 1
occupant = 2
class Credentials(BaseModel):
person_id: int
person_name: str
class ApplicationToken(BaseModel):
# Application Token Object -> is the main object for the user
user_type: int = UserType.occupant.value
credential_token: str = ""
user_uu_id: str
user_id: int
person_id: int
person_uu_id: str
request: Optional[dict] = None # Request Info of Client
expires_at: Optional[float] = None # Expiry timestamp
class OccupantToken(BaseModel):
# Selection of the occupant type for a build part is made by the user
living_space_id: int # Internal use
living_space_uu_id: str # Outer use
occupant_type_id: int
occupant_type_uu_id: str
occupant_type: str
build_id: int
build_uuid: str
build_part_id: int
build_part_uuid: str
responsible_company_id: Optional[int] = None
responsible_company_uuid: Optional[str] = None
responsible_employee_id: Optional[int] = None
responsible_employee_uuid: Optional[str] = None
# ID list of reachable event codes as "endpoint_code": ["UUID", "UUID"]
reachable_event_codes: Optional[dict[str, str]] = None
# ID list of reachable applications as "page_url": ["UUID", "UUID"]
reachable_app_codes: Optional[dict[str, str]] = None
class CompanyToken(BaseModel):
# Selection of the company for an employee is made by the user
company_id: int
company_uu_id: str
department_id: int # ID list of departments
department_uu_id: str # ID list of departments
duty_id: int
duty_uu_id: str
staff_id: int
staff_uu_id: str
employee_id: int
employee_uu_id: str
bulk_duties_id: int
# ID list of reachable event codes as "endpoint_code": ["UUID", "UUID"]
reachable_event_codes: Optional[dict[str, str]] = None
# ID list of reachable applications as "page_url": ["UUID", "UUID"]
reachable_app_codes: Optional[dict[str, str]] = None
class OccupantTokenObject(ApplicationToken):
# Occupant Token Object -> Requires selection of the occupant type for a specific build part
available_occupants: dict = None
selected_occupant: Optional[OccupantToken] = None # Selected Occupant Type
@property
def is_employee(self) -> bool:
return False
@property
def is_occupant(self) -> bool:
return True
class EmployeeTokenObject(ApplicationToken):
# Full hierarchy Employee[staff_id] -> Staff -> Duty -> Department -> Company
companies_id_list: list[int] # List of company objects
companies_uu_id_list: list[str] # List of company objects
duty_id_list: list[int] # List of duty objects
duty_uu_id_list: list[str] # List of duty objects
selected_company: Optional[CompanyToken] = None # Selected Company Object
@property
def is_employee(self) -> bool:
return True
@property
def is_occupant(self) -> bool:
return False
TokenDictType = Union[EmployeeTokenObject, OccupantTokenObject]