From 3583d178e97e5dc7807ba4763f245737c31260ea Mon Sep 17 00:00:00 2001 From: berkay Date: Wed, 2 Apr 2025 18:53:33 +0300 Subject: [PATCH] Schemas updated --- ApiServices/AuthService/Dockerfile | 28 + ApiServices/AuthService/README.md | 0 ApiServices/AuthService/app.py | 16 + ApiServices/AuthService/config.py | 64 ++ ApiServices/AuthService/create_app.py | 41 ++ ApiServices/AuthService/create_route.py | 14 + ApiServices/AuthService/endpoints/__init__.py | 0 ApiServices/AuthService/endpoints/routes.py | 20 + .../endpoints/test_template/route.py | 40 ++ ApiServices/AuthService/events/__init__.py | 0 .../exception_handlers/__init__.py | 0 .../AuthService/middlewares/__init__.py | 0 .../middlewares/token_middleware.py | 17 + ApiServices/AuthService/open_api_creator.py | 118 ++++ ApiServices/AuthService/schemas/__init__.py | 1 + .../AuthService/validations/__init__.py | 0 ApiServices/TemplateService/app.py | 9 +- ApiServices/TemplateService/config.py | 2 +- .../TemplateService/endpoints/routes.py | 5 +- .../endpoints/test_template/route.py | 40 ++ .../middlewares/token_middleware.py | 6 +- .../TemplateService/open_api_creator.py | 35 +- Controllers/Redis/implementations.py | 24 +- Modules/Token/config.py | 15 + Modules/Token/password_module.py | 39 ++ Schemas/__init__.py | 198 ++++++ Schemas/account/account.py | 2 - Schemas/address/address.py | 571 ++++++++++++++++++ Schemas/building/build.py | 1 + Schemas/company/company.py | 239 ++++++++ Schemas/company/department.py | 73 +++ Schemas/company/employee.py | 75 +++ Schemas/event/event.py | 430 +++++++++++++ Schemas/identity/identity.py | 434 +++++++++++++ Schemas/others/enums.py | 106 ++++ Schemas/rules/rules.py | 35 ++ api_env.env | 1 + docker-compose.yml | 1 + 38 files changed, 2672 insertions(+), 28 deletions(-) create mode 100644 ApiServices/AuthService/Dockerfile create mode 100644 ApiServices/AuthService/README.md create mode 100644 ApiServices/AuthService/app.py create mode 100644 ApiServices/AuthService/config.py create mode 100644 ApiServices/AuthService/create_app.py create mode 100644 ApiServices/AuthService/create_route.py create mode 100644 ApiServices/AuthService/endpoints/__init__.py create mode 100644 ApiServices/AuthService/endpoints/routes.py create mode 100644 ApiServices/AuthService/endpoints/test_template/route.py create mode 100644 ApiServices/AuthService/events/__init__.py create mode 100644 ApiServices/AuthService/exception_handlers/__init__.py create mode 100644 ApiServices/AuthService/middlewares/__init__.py create mode 100644 ApiServices/AuthService/middlewares/token_middleware.py create mode 100644 ApiServices/AuthService/open_api_creator.py create mode 100644 ApiServices/AuthService/schemas/__init__.py create mode 100644 ApiServices/AuthService/validations/__init__.py create mode 100644 ApiServices/TemplateService/endpoints/test_template/route.py create mode 100644 Modules/Token/config.py create mode 100644 Modules/Token/password_module.py create mode 100644 Schemas/address/address.py create mode 100644 Schemas/company/company.py create mode 100644 Schemas/company/department.py create mode 100644 Schemas/company/employee.py create mode 100644 Schemas/event/event.py create mode 100644 Schemas/identity/identity.py create mode 100644 Schemas/others/enums.py create mode 100644 Schemas/rules/rules.py diff --git a/ApiServices/AuthService/Dockerfile b/ApiServices/AuthService/Dockerfile new file mode 100644 index 0000000..9524e9f --- /dev/null +++ b/ApiServices/AuthService/Dockerfile @@ -0,0 +1,28 @@ +FROM python:3.12-slim + +WORKDIR / + +# Install system dependencies and Poetry +RUN apt-get update && apt-get install -y --no-install-recommends gcc \ + && rm -rf /var/lib/apt/lists/* && pip install --no-cache-dir poetry + +# Copy Poetry configuration +COPY /pyproject.toml ./pyproject.toml + +# Configure Poetry and install dependencies with optimizations +RUN poetry config virtualenvs.create false \ + && poetry install --no-interaction --no-ansi --no-root --only main \ + && pip cache purge && rm -rf ~/.cache/pypoetry + +# Copy application code +COPY /ApiServices/AuthService /ApiServices/AuthService +COPY /Controllers /Controllers +COPY /Schemas/building /Schemas/building +COPY /Schemas/company /Schemas/company +COPY /Schemas/identity /Schemas/identity + +# Set Python path to include app directory +ENV PYTHONPATH=/ PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1 + +# Run the application using the configured uvicorn server +CMD ["poetry", "run", "python", "ApiServices/TemplateService/app.py"] diff --git a/ApiServices/AuthService/README.md b/ApiServices/AuthService/README.md new file mode 100644 index 0000000..e69de29 diff --git a/ApiServices/AuthService/app.py b/ApiServices/AuthService/app.py new file mode 100644 index 0000000..34b8712 --- /dev/null +++ b/ApiServices/AuthService/app.py @@ -0,0 +1,16 @@ +import uvicorn + +from config import api_config + +from ApiServices.TemplateService.create_app import create_app +# from prometheus_fastapi_instrumentator import Instrumentator + + +app = create_app() # Create FastAPI application +# Instrumentator().instrument(app=app).expose(app=app) # Setup Prometheus metrics + + +if __name__ == "__main__": + # Run the application with Uvicorn Server + uvicorn_config = uvicorn.Config(**api_config.app_as_dict) + uvicorn.Server(uvicorn_config).run() diff --git a/ApiServices/AuthService/config.py b/ApiServices/AuthService/config.py new file mode 100644 index 0000000..73741bf --- /dev/null +++ b/ApiServices/AuthService/config.py @@ -0,0 +1,64 @@ +from pydantic_settings import BaseSettings, SettingsConfigDict +from fastapi.responses import JSONResponse + + +class Configs(BaseSettings): + """ + ApiTemplate configuration settings. + """ + + PATH: str = "" + HOST: str = "", + PORT: int = 0, + LOG_LEVEL: str = "info", + RELOAD: int = 0 + ACCESS_TOKEN_TAG: str = "" + + ACCESS_EMAIL_EXT: str = "" + TITLE: str = "" + ALGORITHM: str = "" + ACCESS_TOKEN_LENGTH: int = 90 + REFRESHER_TOKEN_LENGTH: int = 144 + EMAIL_HOST: str = "" + DATETIME_FORMAT: str = "" + FORGOT_LINK: str = "" + ALLOW_ORIGINS: list = ["http://localhost:3000"] + VERSION: str = "0.1.001" + DESCRIPTION: str = "" + + @property + def app_as_dict(self) -> dict: + """ + Convert the settings to a dictionary. + """ + return { + "app": self.PATH, + "host": self.HOST, + "port": int(self.PORT), + "log_level": self.LOG_LEVEL, + "reload": bool(self.RELOAD) + } + + @property + def api_info(self): + """ + Returns a dictionary with application information. + """ + return { + "title": self.TITLE, + "description": self.DESCRIPTION, + "default_response_class": JSONResponse, + "version": self.VERSION, + } + + @classmethod + def forgot_link(cls, forgot_key): + """ + Generate a forgot password link. + """ + return cls.FORGOT_LINK + forgot_key + + model_config = SettingsConfigDict(env_prefix="API_") + + +api_config = Configs() diff --git a/ApiServices/AuthService/create_app.py b/ApiServices/AuthService/create_app.py new file mode 100644 index 0000000..4a1b3e6 --- /dev/null +++ b/ApiServices/AuthService/create_app.py @@ -0,0 +1,41 @@ +from fastapi import FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import RedirectResponse +from fastapi.staticfiles import StaticFiles + +from ApiServices.TemplateService.create_route import RouteRegisterController +from ApiServices.TemplateService.endpoints.routes import get_routes +from ApiServices.TemplateService.open_api_creator import create_openapi_schema +from ApiServices.TemplateService.middlewares.token_middleware import token_middleware +from ApiServices.TemplateService.config import template_api_config + + +def create_app(): + + application = FastAPI(**template_api_config.api_info) + # application.mount( + # "/application/static", + # StaticFiles(directory="application/static"), + # name="static", + # ) + application.add_middleware( + CORSMiddleware, + allow_origins=template_api_config.ALLOW_ORIGINS, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + @application.middleware("http") + async def add_token_middleware(request: Request, call_next): + return await token_middleware(request, call_next) + + @application.get("/", description="Redirect Route", include_in_schema=False) + async def redirect_to_docs(): + return RedirectResponse(url="/docs") + + route_register = RouteRegisterController(app=application, router_list=get_routes()) + application = route_register.register_routes() + + application.openapi = lambda _=application: create_openapi_schema(_) + return application diff --git a/ApiServices/AuthService/create_route.py b/ApiServices/AuthService/create_route.py new file mode 100644 index 0000000..bc8545c --- /dev/null +++ b/ApiServices/AuthService/create_route.py @@ -0,0 +1,14 @@ +from typing import List +from fastapi import APIRouter, FastAPI + + +class RouteRegisterController: + + def __init__(self, app: FastAPI, router_list: List[APIRouter]): + self.router_list = router_list + self.app = app + + def register_routes(self): + for router in self.router_list: + self.app.include_router(router) + return self.app diff --git a/ApiServices/AuthService/endpoints/__init__.py b/ApiServices/AuthService/endpoints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ApiServices/AuthService/endpoints/routes.py b/ApiServices/AuthService/endpoints/routes.py new file mode 100644 index 0000000..91c4ead --- /dev/null +++ b/ApiServices/AuthService/endpoints/routes.py @@ -0,0 +1,20 @@ +from fastapi import APIRouter +from .test_template.route import test_template_route + + +def get_routes() -> list[APIRouter]: + return [test_template_route] + + +def get_safe_endpoint_urls() -> list[tuple[str, str]]: + return [ + ("/", "GET"), + ("/docs", "GET"), + ("/redoc", "GET"), + ("/openapi.json", "GET"), + ("/auth/register", "POST"), + ("/auth/login", "POST"), + ("/metrics", "GET"), + ("/test/template", "GET"), + ("/test/template", "POST"), + ] \ No newline at end of file diff --git a/ApiServices/AuthService/endpoints/test_template/route.py b/ApiServices/AuthService/endpoints/test_template/route.py new file mode 100644 index 0000000..1977b47 --- /dev/null +++ b/ApiServices/AuthService/endpoints/test_template/route.py @@ -0,0 +1,40 @@ +from fastapi import APIRouter, Request, Response + +test_template_route = APIRouter(prefix="/test", tags=["Test"]) + + +@test_template_route.get(path="/template", description="Test Template Route") +def test_template(request: Request, response: Response): + """ + Test Template Route + """ + headers = dict(request.headers) + response.headers["X-Header"] = "Test Header GET" + return { + "completed": True, + "message": "Test Template Route", + "info": { + "host": headers.get("host", "Not Found"), + "user_agent": headers.get("user-agent", "Not Found"), + }, + } + + +@test_template_route.post( + path="/template", + description="Test Template Route with Post Method", +) +def test_template_post(request: Request, response: Response): + """ + Test Template Route with Post Method + """ + headers = dict(request.headers) + response.headers["X-Header"] = "Test Header POST" + return { + "completed": True, + "message": "Test Template Route with Post Method", + "info": { + "host": headers.get("host", "Not Found"), + "user_agent": headers.get("user-agent", "Not Found"), + }, + } diff --git a/ApiServices/AuthService/events/__init__.py b/ApiServices/AuthService/events/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ApiServices/AuthService/exception_handlers/__init__.py b/ApiServices/AuthService/exception_handlers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ApiServices/AuthService/middlewares/__init__.py b/ApiServices/AuthService/middlewares/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ApiServices/AuthService/middlewares/token_middleware.py b/ApiServices/AuthService/middlewares/token_middleware.py new file mode 100644 index 0000000..7298717 --- /dev/null +++ b/ApiServices/AuthService/middlewares/token_middleware.py @@ -0,0 +1,17 @@ +from fastapi import Request, Response +from ApiServices.TemplateService.endpoints.routes import get_safe_endpoint_urls + + +async def token_middleware(request: Request, call_next): + + base_url = "/".join(request.url.path.split("/")[:3]) + safe_endpoints = [_[0] for _ in get_safe_endpoint_urls()] + if base_url in safe_endpoints: + return await call_next(request) + + token = request.headers.get("Authorization") + if not token: + return Response(content="Missing token", status_code=400) + + response = await call_next(request) + return response diff --git a/ApiServices/AuthService/open_api_creator.py b/ApiServices/AuthService/open_api_creator.py new file mode 100644 index 0000000..50adc67 --- /dev/null +++ b/ApiServices/AuthService/open_api_creator.py @@ -0,0 +1,118 @@ +from typing import Any, Dict +from fastapi import FastAPI +from fastapi.routing import APIRoute +from fastapi.openapi.utils import get_openapi + +from ApiServices.TemplateService.config import template_api_config +from ApiServices.TemplateService.endpoints.routes import get_safe_endpoint_urls + + +class OpenAPISchemaCreator: + """ + OpenAPI schema creator and customizer for FastAPI applications. + """ + + def __init__(self, app: FastAPI): + """ + Initialize the OpenAPI schema creator. + + Args: + app: FastAPI application instance + """ + self.app = app + self.safe_endpoint_list: list[tuple[str, str]] = get_safe_endpoint_urls() + self.routers_list = self.app.routes + + @staticmethod + def create_security_schemes() -> Dict[str, Any]: + """ + Create security scheme definitions. + + Returns: + Dict[str, Any]: Security scheme configurations + """ + + return { + "BearerAuth": { + "type": "apiKey", + "in": "header", + "name": template_api_config.ACCESS_TOKEN_TAG, + "description": "Enter: **'Bearer <JWT>'**, where JWT is the access token", + } + } + + def configure_route_security( + self, path: str, method: str, schema: Dict[str, Any] + ) -> None: + """ + Configure security requirements for a specific route. + + Args: + path: Route path + method: HTTP method + schema: OpenAPI schema to modify + """ + if not schema.get("paths", {}).get(path, {}).get(method): + return + + # Check if endpoint is in safe list + endpoint_path = f"{path}:{method}" + list_of_safe_endpoints = [ + f"{e[0]}:{str(e[1]).lower()}" for e in self.safe_endpoint_list + ] + if endpoint_path not in list_of_safe_endpoints: + if "security" not in schema["paths"][path][method]: + schema["paths"][path][method]["security"] = [] + schema["paths"][path][method]["security"].append({"BearerAuth": []}) + + def create_schema(self) -> Dict[str, Any]: + """ + Create the complete OpenAPI schema. + + Returns: + Dict[str, Any]: Complete OpenAPI schema + """ + openapi_schema = get_openapi( + title=template_api_config.TITLE, + description=template_api_config.DESCRIPTION, + version=template_api_config.VERSION, + routes=self.app.routes, + ) + + # Add security schemes + if "components" not in openapi_schema: + openapi_schema["components"] = {} + + openapi_schema["components"][ + "securitySchemes" + ] = self.create_security_schemes() + + # Configure route security and responses + for route in self.app.routes: + if isinstance(route, APIRoute) and route.include_in_schema: + path = str(route.path) + methods = [method.lower() for method in route.methods] + for method in methods: + self.configure_route_security(path, method, openapi_schema) + + # Add custom documentation extensions + openapi_schema["x-documentation"] = { + "postman_collection": "/docs/postman", + "swagger_ui": "/docs", + "redoc": "/redoc", + } + return openapi_schema + + +def create_openapi_schema(app: FastAPI) -> Dict[str, Any]: + """ + Create OpenAPI schema for a FastAPI application. + + Args: + app: FastAPI application instance + + Returns: + Dict[str, Any]: Complete OpenAPI schema + """ + creator = OpenAPISchemaCreator(app) + return creator.create_schema() \ No newline at end of file diff --git a/ApiServices/AuthService/schemas/__init__.py b/ApiServices/AuthService/schemas/__init__.py new file mode 100644 index 0000000..8c83d71 --- /dev/null +++ b/ApiServices/AuthService/schemas/__init__.py @@ -0,0 +1 @@ +from Schemas.building.build import BuildParts, BuildLivingSpace diff --git a/ApiServices/AuthService/validations/__init__.py b/ApiServices/AuthService/validations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ApiServices/TemplateService/app.py b/ApiServices/TemplateService/app.py index b4dfd9c..34b8712 100644 --- a/ApiServices/TemplateService/app.py +++ b/ApiServices/TemplateService/app.py @@ -1,15 +1,16 @@ import uvicorn -from ApiServices.TemplateService.config import template_api_config +from config import api_config + from ApiServices.TemplateService.create_app import create_app # from prometheus_fastapi_instrumentator import Instrumentator -app = create_app() # Create FastAPI application -# Instrumentator().instrument(app=app).expose(app=app) # Setup Prometheus metrics +app = create_app() # Create FastAPI application +# Instrumentator().instrument(app=app).expose(app=app) # Setup Prometheus metrics if __name__ == "__main__": # Run the application with Uvicorn Server - uvicorn_config = uvicorn.Config(**template_api_config.app_as_dict) + uvicorn_config = uvicorn.Config(**api_config.app_as_dict) uvicorn.Server(uvicorn_config).run() diff --git a/ApiServices/TemplateService/config.py b/ApiServices/TemplateService/config.py index bf4b542..73741bf 100644 --- a/ApiServices/TemplateService/config.py +++ b/ApiServices/TemplateService/config.py @@ -61,4 +61,4 @@ class Configs(BaseSettings): model_config = SettingsConfigDict(env_prefix="API_") -template_api_config = Configs() +api_config = Configs() diff --git a/ApiServices/TemplateService/endpoints/routes.py b/ApiServices/TemplateService/endpoints/routes.py index 46a2bab..91c4ead 100644 --- a/ApiServices/TemplateService/endpoints/routes.py +++ b/ApiServices/TemplateService/endpoints/routes.py @@ -1,8 +1,9 @@ from fastapi import APIRouter +from .test_template.route import test_template_route def get_routes() -> list[APIRouter]: - return [] + return [test_template_route] def get_safe_endpoint_urls() -> list[tuple[str, str]]: @@ -14,4 +15,6 @@ def get_safe_endpoint_urls() -> list[tuple[str, str]]: ("/auth/register", "POST"), ("/auth/login", "POST"), ("/metrics", "GET"), + ("/test/template", "GET"), + ("/test/template", "POST"), ] \ No newline at end of file diff --git a/ApiServices/TemplateService/endpoints/test_template/route.py b/ApiServices/TemplateService/endpoints/test_template/route.py new file mode 100644 index 0000000..1977b47 --- /dev/null +++ b/ApiServices/TemplateService/endpoints/test_template/route.py @@ -0,0 +1,40 @@ +from fastapi import APIRouter, Request, Response + +test_template_route = APIRouter(prefix="/test", tags=["Test"]) + + +@test_template_route.get(path="/template", description="Test Template Route") +def test_template(request: Request, response: Response): + """ + Test Template Route + """ + headers = dict(request.headers) + response.headers["X-Header"] = "Test Header GET" + return { + "completed": True, + "message": "Test Template Route", + "info": { + "host": headers.get("host", "Not Found"), + "user_agent": headers.get("user-agent", "Not Found"), + }, + } + + +@test_template_route.post( + path="/template", + description="Test Template Route with Post Method", +) +def test_template_post(request: Request, response: Response): + """ + Test Template Route with Post Method + """ + headers = dict(request.headers) + response.headers["X-Header"] = "Test Header POST" + return { + "completed": True, + "message": "Test Template Route with Post Method", + "info": { + "host": headers.get("host", "Not Found"), + "user_agent": headers.get("user-agent", "Not Found"), + }, + } diff --git a/ApiServices/TemplateService/middlewares/token_middleware.py b/ApiServices/TemplateService/middlewares/token_middleware.py index da06ba0..7298717 100644 --- a/ApiServices/TemplateService/middlewares/token_middleware.py +++ b/ApiServices/TemplateService/middlewares/token_middleware.py @@ -1,12 +1,8 @@ from fastapi import Request, Response - - -def get_safe_endpoint_urls() -> list: - return [] +from ApiServices.TemplateService.endpoints.routes import get_safe_endpoint_urls async def token_middleware(request: Request, call_next): - # from application.routes.routes import get_safe_endpoint_urls base_url = "/".join(request.url.path.split("/")[:3]) safe_endpoints = [_[0] for _ in get_safe_endpoint_urls()] diff --git a/ApiServices/TemplateService/open_api_creator.py b/ApiServices/TemplateService/open_api_creator.py index 45b4bed..50adc67 100644 --- a/ApiServices/TemplateService/open_api_creator.py +++ b/ApiServices/TemplateService/open_api_creator.py @@ -2,7 +2,9 @@ from typing import Any, Dict from fastapi import FastAPI from fastapi.routing import APIRoute from fastapi.openapi.utils import get_openapi + from ApiServices.TemplateService.config import template_api_config +from ApiServices.TemplateService.endpoints.routes import get_safe_endpoint_urls class OpenAPISchemaCreator: @@ -18,8 +20,11 @@ class OpenAPISchemaCreator: app: FastAPI application instance """ self.app = app + self.safe_endpoint_list: list[tuple[str, str]] = get_safe_endpoint_urls() + self.routers_list = self.app.routes - def create_security_schemes(self) -> Dict[str, Any]: + @staticmethod + def create_security_schemes() -> Dict[str, Any]: """ Create security scheme definitions. @@ -36,6 +41,30 @@ class OpenAPISchemaCreator: } } + def configure_route_security( + self, path: str, method: str, schema: Dict[str, Any] + ) -> None: + """ + Configure security requirements for a specific route. + + Args: + path: Route path + method: HTTP method + schema: OpenAPI schema to modify + """ + if not schema.get("paths", {}).get(path, {}).get(method): + return + + # Check if endpoint is in safe list + endpoint_path = f"{path}:{method}" + list_of_safe_endpoints = [ + f"{e[0]}:{str(e[1]).lower()}" for e in self.safe_endpoint_list + ] + if endpoint_path not in list_of_safe_endpoints: + if "security" not in schema["paths"][path][method]: + schema["paths"][path][method]["security"] = [] + schema["paths"][path][method]["security"].append({"BearerAuth": []}) + def create_schema(self) -> Dict[str, Any]: """ Create the complete OpenAPI schema. @@ -46,7 +75,7 @@ class OpenAPISchemaCreator: openapi_schema = get_openapi( title=template_api_config.TITLE, description=template_api_config.DESCRIPTION, - version=template_api_config, + version=template_api_config.VERSION, routes=self.app.routes, ) @@ -56,7 +85,7 @@ class OpenAPISchemaCreator: openapi_schema["components"][ "securitySchemes" - ] = self._create_security_schemes() + ] = self.create_security_schemes() # Configure route security and responses for route in self.app.routes: diff --git a/Controllers/Redis/implementations.py b/Controllers/Redis/implementations.py index 14bf92d..557f7b2 100644 --- a/Controllers/Redis/implementations.py +++ b/Controllers/Redis/implementations.py @@ -29,19 +29,19 @@ def example_get_json_iterator() -> None: """Example of using the JSON iterator for large datasets.""" keys = ["user", "profile", "*"] for row in RedisActions.get_json_iterator(list_keys=keys): - print("Iterating over JSON row:", row.as_dict if isinstance(row.as_dict, dict) else row.as_dict()) + print("Iterating over JSON row:", row.as_dict if isinstance(row.as_dict, dict) else row.as_dict) -# def example_delete_key() -> None: -# """Example of deleting a specific key.""" -# key = "user:profile:123" -# result = RedisActions.delete_key(key) -# print("Delete specific key:", result) -# -# def example_delete() -> None: -# """Example of deleting multiple keys matching a pattern.""" -# keys = ["user", "profile", "*"] -# result = RedisActions.delete(list_keys=keys) -# print("Delete multiple keys:", result) +def example_delete_key() -> None: + """Example of deleting a specific key.""" + key = "user:profile:123" + result = RedisActions.delete_key(key) + print("Delete specific key:", result) + +def example_delete() -> None: + """Example of deleting multiple keys matching a pattern.""" + keys = ["user", "profile", "*"] + result = RedisActions.delete(list_keys=keys) + print("Delete multiple keys:", result) def example_refresh_ttl() -> None: """Example of refreshing TTL for a key.""" diff --git a/Modules/Token/config.py b/Modules/Token/config.py new file mode 100644 index 0000000..8af8929 --- /dev/null +++ b/Modules/Token/config.py @@ -0,0 +1,15 @@ +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Configs(BaseSettings): + """ + ApiTemplate configuration settings. + """ + + ACCESS_TOKEN_LENGTH: int = 90 + REFRESHER_TOKEN_LENGTH: int = 144 + + model_config = SettingsConfigDict(env_prefix="API_") + + +token_config = Configs() diff --git a/Modules/Token/password_module.py b/Modules/Token/password_module.py new file mode 100644 index 0000000..517d4cb --- /dev/null +++ b/Modules/Token/password_module.py @@ -0,0 +1,39 @@ +import hashlib +import uuid +import secrets +import random + +from config import token_config + + +class PasswordModule: + + @staticmethod + def generate_random_uu_id(str_std: bool = True): + return str(uuid.uuid4()) if str_std else uuid.uuid4() + + @staticmethod + def generate_token(length=32) -> str: + letters = "abcdefghijklmnopqrstuvwxyz" + merged_letters = [letter for letter in letters] + [letter.upper() for letter in letters] + token_generated = secrets.token_urlsafe(length) + for i in str(token_generated): + if i not in merged_letters: + token_generated = token_generated.replace(i, random.choice(merged_letters), 1) + return token_generated + + @classmethod + def generate_access_token(cls) -> str: + return cls.generate_token(int(token_config.ACCESS_TOKEN_LENGTH)) + + @classmethod + def generate_refresher_token(cls) -> str: + return cls.generate_token(int(token_config.REFRESHER_TOKEN_LENGTH)) + + @staticmethod + def create_hashed_password(domain: str, id_: str, password: str) -> str: + return hashlib.sha256(f"{domain}:{id_}:{password}".encode("utf-8")).hexdigest() + + @classmethod + def check_password(cls, domain, id_, password, password_hashed) -> bool: + return cls.create_hashed_password(domain, id_, password) == password_hashed diff --git a/Schemas/__init__.py b/Schemas/__init__.py index e69de29..19995e2 100644 --- a/Schemas/__init__.py +++ b/Schemas/__init__.py @@ -0,0 +1,198 @@ +from Schemas.account.account import ( + AccountBooks, + AccountCodes, + AccountCodeParser, + AccountMaster, + AccountDetail, + AccountRecordExchanges, + AccountRecords, +) +from Schemas.account.iban import ( + BuildIbans, + BuildIbanDescription, +) +from Schemas.address.address import ( + RelationshipEmployee2PostCode, + AddressPostcode, + Addresses, + AddressGeographicLocations, + AddressCountry, + AddressState, + AddressCity, + AddressDistrict, + AddressLocality, + AddressNeighborhood, + AddressStreet, +) +from Schemas.building.build import ( + BuildTypes, + Part2Employee, + RelationshipEmployee2Build, + Build, + BuildParts, + BuildLivingSpace, + BuildManagement, + BuildArea, + BuildSites, + BuildCompaniesProviding, + BuildPersonProviding, +) +from Schemas.building.decision_book import ( + BuildDecisionBook, + BuildDecisionBookInvitations, + BuildDecisionBookPerson, + BuildDecisionBookPersonOccupants, + BuildDecisionBookItems, + BuildDecisionBookItemsUnapproved, + BuildDecisionBookPayments, + BuildDecisionBookLegal, + BuildDecisionBookProjects, + BuildDecisionBookProjectPerson, + BuildDecisionBookProjectItems, +) +from Schemas.building.budget import ( + DecisionBookBudgetBooks, + DecisionBookBudgetCodes, + DecisionBookBudgetMaster, + DecisionBookBudgets, +) +from Schemas.company.company import ( + Companies, + RelationshipDutyCompany, +) +from Schemas.company.employee import ( + Employees, + EmployeesSalaries, + EmployeeHistory, + Staff, +) +from Schemas.company.department import ( + Duty, + Duties, + Departments, +) +from Schemas.event.event import ( + Modules, + Services, + Service2Events, + Events, + Event2Occupant, + Event2Employee, + Event2OccupantExtra, + Event2EmployeeExtra, +) +from Schemas.identity.identity import ( + UsersTokens, + OccupantTypes, + People, + Users, + RelationshipDutyPeople, + Contracts, +) +from Schemas.address.address import ( + Addresses, + AddressCity, + AddressStreet, + AddressLocality, + AddressDistrict, + AddressNeighborhood, + AddressState, + AddressCountry, + AddressPostcode, + AddressGeographicLocations, + RelationshipEmployee2PostCode, +) +from Schemas.others.enums import ( + ApiEnumDropdown, +) +from Schemas.rules.rules import ( + EndpointRestriction, +) + +__all__ = [ + "AccountBooks", + "AccountCodes", + "AccountCodeParser", + "AccountMaster", + "AccountDetail", + "AccountRecordExchanges", + "AccountRecords", + "BuildIbans", + "BuildIbanDescription", + "RelationshipEmployee2PostCode", + "AddressPostcode", + "Addresses", + "AddressGeographicLocations", + "AddressCountry", + "AddressState", + "AddressCity", + "AddressDistrict", + "AddressLocality", + "AddressNeighborhood", + "AddressStreet", + "BuildTypes", + "Part2Employee", + "RelationshipEmployee2Build", + "Build", + "BuildParts", + "BuildLivingSpace", + "BuildManagement", + "BuildArea", + "BuildSites", + "BuildCompaniesProviding", + "BuildPersonProviding", + "BuildDecisionBook", + "BuildDecisionBookInvitations", + "BuildDecisionBookPerson", + "BuildDecisionBookPersonOccupants", + "BuildDecisionBookItems", + "BuildDecisionBookItemsUnapproved", + "BuildDecisionBookPayments", + "BuildDecisionBookLegal", + "BuildDecisionBookProjects", + "BuildDecisionBookProjectPerson", + "BuildDecisionBookPersonOccupants", + "BuildDecisionBookProjectItems", + "DecisionBookBudgetBooks", + "DecisionBookBudgetCodes", + "DecisionBookBudgetMaster", + "DecisionBookBudgets", + "Companies", + "RelationshipDutyCompany", + "Employees", + "EmployeesSalaries", + "EmployeeHistory", + "Staff", + "Duty", + "Duties", + "Departments", + "Modules", + "Services", + "Service2Events", + "Events", + "Event2Occupant", + "Event2Employee", + "Event2OccupantExtra", + "Event2EmployeeExtra", + "Addresses", + "AddressCity", + "AddressStreet", + "AddressLocality", + "AddressDistrict", + "AddressNeighborhood", + "AddressState", + "AddressCountry", + "AddressPostcode", + "AddressGeographicLocations", + "UsersTokens", + "OccupantTypes", + "People", + "Users", + "RelationshipDutyPeople", + "RelationshipEmployee2PostCode", + "Contracts", + "ApiEnumDropdown", + "EndpointRestriction", + "RelationshipEmployee2Build", + # ------------------------------------------------ +] diff --git a/Schemas/account/account.py b/Schemas/account/account.py index 7c9d9de..858d40e 100644 --- a/Schemas/account/account.py +++ b/Schemas/account/account.py @@ -574,5 +574,3 @@ class AccountRecords(CrudCollection): # ) # print("is all dues_type", payment_dict["dues_type"], paid_value) - - diff --git a/Schemas/address/address.py b/Schemas/address/address.py new file mode 100644 index 0000000..1640298 --- /dev/null +++ b/Schemas/address/address.py @@ -0,0 +1,571 @@ +from sqlalchemy import ( + String, + ForeignKey, + Index, + Numeric, + Boolean, + BigInteger, + Integer, + Text, or_, +) +from sqlalchemy.orm import mapped_column, Mapped +from Controllers.Postgres.mixin import CrudCollection + + +class RelationshipEmployee2PostCode(CrudCollection): + """ + Build2EmployeeRelationship class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "relationship_employee2postcode" + __exclude__fields__ = [] + __include__fields__ = [] + + company_id: Mapped[int] = mapped_column( + ForeignKey("companies.id"), nullable=True + ) # 1, 2, 3 + employee_id: Mapped[int] = mapped_column(ForeignKey("employees.id"), nullable=False) + member_id: Mapped[int] = mapped_column( + ForeignKey("address_postcode.id"), nullable=False + ) + + relationship_type: Mapped[str] = mapped_column( + String, nullable=True, server_default="Employee" + ) # Commercial + show_only: Mapped[bool] = mapped_column(Boolean, server_default="0") + + __table_args__ = ({"comment": "Build2Employee Relationship Information"},) + + +class AddressPostcode(CrudCollection): + """ + Postcode class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "address_postcode" + __exclude__fields__ = [] + __access_by__ = [] + __many__table__ = RelationshipEmployee2PostCode + + street_id: Mapped[int] = mapped_column(ForeignKey("address_street.id")) + street_uu_id: Mapped[str] = mapped_column( + String, server_default="", comment="Street UUID" + ) + postcode: Mapped[str] = mapped_column( + String(32), nullable=False, comment="Postcode" + ) + + __table_args__ = ({"comment": "Postcode Information"},) + + +class Addresses(CrudCollection): + """ + Address class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "addresses" + __exclude__fields__ = [] + + build_number: Mapped[str] = mapped_column( + String(24), nullable=False, comment="Build Number" + ) + door_number: Mapped[str] = mapped_column( + String(24), nullable=True, comment="Door Number" + ) + floor_number: Mapped[str] = mapped_column( + String(24), nullable=True, comment="Floor Number" + ) + + comment_address: Mapped[str] = mapped_column( + String, nullable=False, comment="Address" + ) + letter_address: Mapped[str] = mapped_column( + String, nullable=False, comment="Address" + ) + short_letter_address: Mapped[str] = mapped_column( + String, nullable=False, comment="Address" + ) + + latitude: Mapped[float] = mapped_column(Numeric(20, 12), server_default="0") + longitude: Mapped[float] = mapped_column(Numeric(20, 12), server_default="0") + + street_id: Mapped[int] = mapped_column( + ForeignKey("address_street.id"), nullable=False + ) + street_uu_id: Mapped[str] = mapped_column( + String, server_default="", comment="Street UUID" + ) + + __table_args__ = ( + # Index("_address_ndx_00", country_code, b_state, city, district), + {"comment": "Address Information"}, + ) + + @classmethod + def list_via_employee(cls, token_dict, filter_expr=None): + with cls.new_session() as db_session: + post_code_list = RelationshipEmployee2PostCode.filter_all( + RelationshipEmployee2PostCode.employee_id + == token_dict.selected_company.employee_id, + db=db_session + ).data + post_code_id_list = [post_code.member_id for post_code in post_code_list] + if not post_code_id_list: + raise ValueError( + "User has no post code registered. User can not list addresses." + ) + # raise HTTPException( + # status_code=404, + # detail="User has no post code registered. User can not list addresses.", + # ) + cls.pre_query = cls.filter_all(cls.post_code_id.in_(post_code_id_list), db=db_session).query + filter_cls = cls.filter_all(*filter_expr or [], db=db_session) + cls.pre_query = None + return filter_cls.data + + # buildings: Mapped["Build"] = relationship( + # "Build", back_populates="addresses", foreign_keys="Build.address_id" + # ) + # site: Mapped["BuildSites"] = relationship( + # "BuildSites", back_populates="addresses", foreign_keys="BuildSites.address_id" + # ) + # official_companies: Mapped["Companies"] = relationship( + # "Company", + # back_populates="official_address", + # foreign_keys="Company.official_address_id", + # ) + + # @classmethod + # def create_action(cls, request, create_address: InsertAddress): + # from services.redis.auth_actions.token import parse_token_object_to_dict + # + # token_dict = parse_token_object_to_dict(request=request) + # data_dict = create_address.model_dump() + # post_code = AddressPostcode.find_one(uu_id=create_address.post_code_uu_id) + # if not post_code: + # raise HTTPException( + # status_code=404, + # detail="Post code not found.", + # ) + # if Employee2AddressRelationship.post_code_id.find_one( + # employee_id=token_dict.selected_company.employee_id, + # post_code_id=post_code.id, + # ): + # data_dict["post_code_id"] = post_code.id + # del data_dict["post_code_uu_id"] + # return cls.find_or_create(**create_address.model_dump()) + # raise HTTPException( + # status_code=401, + # detail=f"User is not qualified to create address at this post code {post_code.postcode}", + # ) + + # __table_args__ = ( + # Index("_address_ndx_00", country_code, b_state, city, district), + # {"comment": "Address Information"}, + # ) + + +class AddressGeographicLocations(CrudCollection): + """ + Country class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "address_geographic_locations" + __exclude__fields__ = [] + + geo_table: Mapped[str] = mapped_column( + String, nullable=False, comment="Address Table Name" + ) + geo_id: Mapped[int] = mapped_column( + Integer, nullable=False, comment="Address Table ID" + ) + geo_name: Mapped[str] = mapped_column( + String, nullable=False, comment="Geographic Location Name" + ) + geo_latitude: Mapped[float] = mapped_column( + Numeric(20, 6), server_default="0", comment="Geographic Location Name" + ) + geo_longitude: Mapped[float] = mapped_column( + Numeric(20, 6), server_default="0", comment="Geographic Location Latitude" + ) + geo_altitude: Mapped[float] = mapped_column( + Numeric(20, 6), server_default="0", comment="Geographic Location Longitude" + ) + geo_description: Mapped[str] = mapped_column( + Text, nullable=False, comment="Geographic Location Description" + ) + geo_area_size: Mapped[float] = mapped_column( + Numeric(20, 2), + nullable=True, + server_default="0", + comment="Geographic Location Area Size", + ) + geo_population: Mapped[int] = mapped_column( + BigInteger, nullable=True, comment="Geographic Location Population" + ) + # geo_geom_point = mapped_column(Geometry('POINT', srid=4326), nullable=True, comment="Geographic Location Points") + # geo_geom_polygon = mapped_column(Geometry('POLYGON', srid=4326), nullable=True, + # comment="Geographic Location Vector geographic information (polygon)") + # geo_centroid = mapped_column( GEOMETRY(POINT, 4326), nullable=True, + # comment="Geographic Location center of gravity of the region(points)") + + __table_args__ = ( + Index("_address_geographic_locations_ndx_00", geo_table, geo_id), + Index("_address_geographic_locations_ndx_01", geo_latitude, geo_longitude), + {"comment": "Geographic Location Information"}, + ) + + +class AddressCountry(CrudCollection): + """ + Country class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "address_country" + __exclude__fields__ = [] + + country_code: Mapped[str] = mapped_column( + String(16), nullable=False, comment="Country Code" + ) + country_name: Mapped[str] = mapped_column( + String, nullable=False, comment="Country Name" + ) + money_code: Mapped[str] = mapped_column( + String(12), nullable=True, comment="Money Code" + ) + language: Mapped[str] = mapped_column( + String, nullable=True, comment="Language Code" + ) + address_geographic_id: Mapped[int] = mapped_column( + BigInteger, nullable=True, comment="Address Geographic Id" + ) + + __table_args__ = ( + Index("_address_country_ndx_00", money_code), + Index("_address_country_ndx_01", country_code, unique=True), + {"comment": "Country Information"}, + ) + + +class AddressState(CrudCollection): + """ + State class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "address_state" + __exclude__fields__ = [] + + state_code: Mapped[str] = mapped_column( + String(16), nullable=False, comment="State Code" + ) + state_name: Mapped[str] = mapped_column( + String, nullable=False, comment="State Name" + ) + licence_plate: Mapped[str] = mapped_column( + String(24), nullable=True, comment="Sign Code" + ) + phone_code: Mapped[str] = mapped_column( + String(36), nullable=True, comment="Phone Code" + ) + gov_code: Mapped[str] = mapped_column( + String(128), nullable=True, comment="Government Code" + ) + address_geographic_id: Mapped[int] = mapped_column( + BigInteger, nullable=True, comment="Address Geographic Id" + ) + + country_id: Mapped[int] = mapped_column(ForeignKey("address_country.id")) + country_uu_id: Mapped[str] = mapped_column( + String, server_default="", comment="Country UUID" + ) + + __table_args__ = ( + Index( + "_address_state_ndx_01", + country_id, + state_code, + unique=True, + ), + {"comment": "State Information"}, + ) + + +class AddressCity(CrudCollection): + """ + City class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "address_city" + __exclude__fields__ = [] + + city_code: Mapped[str] = mapped_column( + String(24), nullable=False, comment="City Code" + ) + city_name: Mapped[str] = mapped_column(String, nullable=False, comment="City Name") + licence_plate: Mapped[str] = mapped_column( + String(24), nullable=True, comment="Sign Code" + ) + phone_code: Mapped[str] = mapped_column( + String(36), nullable=True, comment="Phone Code" + ) + gov_code: Mapped[str] = mapped_column( + String(128), nullable=True, comment="Government Code" + ) + address_geographic_id: Mapped[int] = mapped_column( + BigInteger, nullable=True, comment="Address Geographic Id" + ) + + state_id: Mapped[int] = mapped_column(ForeignKey("address_state.id")) + state_uu_id: Mapped[str] = mapped_column( + String, server_default="", comment="State UUID" + ) + + __table_args__ = ( + Index( + "_address_city_ndx_01", + state_id, + city_code, + unique=True, + ), + {"comment": "City Information"}, + ) + + +class AddressDistrict(CrudCollection): + """ + District class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "address_district" + __exclude__fields__ = [] + + district_code: Mapped[str] = mapped_column( + String(16), nullable=False, comment="District Code" + ) + district_name: Mapped[str] = mapped_column( + String, nullable=False, comment="District Name" + ) + phone_code: Mapped[str] = mapped_column( + String(36), nullable=True, comment="Phone Code" + ) + gov_code: Mapped[str] = mapped_column( + String(128), nullable=True, comment="Government Code" + ) + address_geographic_id: Mapped[int] = mapped_column( + BigInteger, nullable=True, comment="Address Geographic Id" + ) + + city_id: Mapped[int] = mapped_column( + ForeignKey("address_city.id"), nullable=False, comment="City ID" + ) + city_uu_id: Mapped[str] = mapped_column( + String, server_default="", comment="City UUID" + ) + + __table_args__ = ( + Index( + "_address_district_ndx_01", + city_id, + district_code, + unique=True, + ), + {"comment": "District Information"}, + ) + + +class AddressLocality(CrudCollection): + """ + Locality class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "address_locality" + __exclude__fields__ = [] + + locality_code: Mapped[str] = mapped_column( + String(16), nullable=False, comment="Locality Code" + ) + locality_name: Mapped[str] = mapped_column( + String, nullable=False, comment="Locality Name" + ) + type_code: Mapped[str] = mapped_column(String, nullable=True, comment="Type Name") + type_description: Mapped[str] = mapped_column( + String, nullable=True, comment="Type Name" + ) + gov_code: Mapped[str] = mapped_column( + String(128), nullable=True, comment="Government Code" + ) + address_show: Mapped[bool] = mapped_column(Boolean, server_default="1") + address_geographic_id: Mapped[int] = mapped_column( + BigInteger, nullable=True, comment="Address Geographic Id" + ) + + district_id: Mapped[int] = mapped_column( + ForeignKey("address_district.id"), nullable=False, comment="District ID" + ) + district_uu_id: Mapped[str] = mapped_column( + String, server_default="", comment="District UUID" + ) + + __table_args__ = ( + Index( + "_address_locality_ndx_01", + district_id, + locality_code, + unique=True, + ), + {"comment": "Locality Information"}, + ) + + +class AddressNeighborhood(CrudCollection): + """ + Neighborhood class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "address_neighborhood" + __exclude__fields__ = [] + + neighborhood_code: Mapped[str] = mapped_column( + String(16), nullable=False, comment="Neighborhood Code" + ) + neighborhood_name: Mapped[str] = mapped_column( + String, nullable=False, comment="Neighborhood Name" + ) + type_code: Mapped[str] = mapped_column(String, nullable=True, comment="Type Name") + type_description: Mapped[str] = mapped_column( + String, nullable=True, comment="Type Name" + ) + gov_code: Mapped[str] = mapped_column( + String(128), nullable=True, comment="Government Code" + ) + address_show: Mapped[bool] = mapped_column(Boolean, server_default="1") + address_geographic_id: Mapped[int] = mapped_column( + BigInteger, nullable=True, comment="Address Geographic Id" + ) + + district_id: Mapped[int] = mapped_column( + ForeignKey("address_district.id"), nullable=True, comment="District ID" + ) + district_uu_id: Mapped[str] = mapped_column( + String, server_default="", comment="District UUID" + ) + locality_id: Mapped[int] = mapped_column( + ForeignKey("address_locality.id"), nullable=True, comment="Locality ID" + ) + locality_uu_id: Mapped[str] = mapped_column( + String, server_default="", comment="Locality UUID" + ) + + __table_args__ = ( + Index( + "_address_neighborhood_ndx_01", + locality_id, + neighborhood_code, + unique=True, + ), + {"comment": "Neighborhood Information"}, + ) + + +class AddressStreet(CrudCollection): + """ + Street class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "address_street" + __exclude__fields__ = [] + + street_code: Mapped[str] = mapped_column( + String(16), nullable=False, comment="Street Code" + ) + street_name: Mapped[str] = mapped_column( + String, nullable=False, comment="Street Name" + ) + type_code: Mapped[str] = mapped_column(String, nullable=True, comment="Type Name") + type_description: Mapped[str] = mapped_column( + String, nullable=True, comment="Type Name" + ) + gov_code: Mapped[str] = mapped_column( + String(128), nullable=True, comment="Government Code" + ) + + address_geographic_id: Mapped[int] = mapped_column( + BigInteger, nullable=True, comment="Address Geographic Id" + ) + neighborhood_id: Mapped[int] = mapped_column( + ForeignKey("address_neighborhood.id"), nullable=False, comment="Neighborhood ID" + ) + neighborhood_uu_id: Mapped[str] = mapped_column( + String, server_default="", comment="Neighborhood UUID" + ) + + __table_args__ = ( + Index("_address_street_ndx_01", neighborhood_id, street_code, unique=True), + {"comment": "Street Information"}, + ) + + @classmethod + def search_address_text(cls, search_text, token_dict=None): + field_dict = { + "AddressStreet.uu_id": cls.uu_id, + "AddressCountry.uu_id": AddressCountry.uu_id, + "AddressState.uu_id": AddressState.uu_id, + "AddressCity.uu_id": AddressCity.uu_id, + "AddressDistrict.uu_id": AddressDistrict.uu_id, + "AddressLocality.uu_id": AddressLocality.uu_id, + "AddressNeighborhood.uu_id": AddressNeighborhood.uu_id, + "AddressCountry.country_name": AddressCountry.country_name, + "AddressState.state_name": AddressState.state_name, + "AddressCity.city_name": AddressCity.city_name, + "AddressDistrict.district_name": AddressDistrict.district_name, + "AddressLocality.locality_name": AddressLocality.locality_name, + "AddressNeighborhood.neighborhood_name": AddressNeighborhood.neighborhood_name, + "AddressStreet.street_name": cls.street_name, + } + joined_data = ( + cls.session.query(*list(field_dict.values())) + .select_from(cls) + .join(AddressNeighborhood, AddressNeighborhood.id == cls.neighborhood_id) + .join( + AddressLocality, AddressLocality.id == AddressNeighborhood.locality_id + ) + .join(AddressDistrict, AddressDistrict.id == AddressLocality.district_id) + .join(AddressCity, AddressCity.id == AddressDistrict.city_id) + .join(AddressState, AddressState.id == AddressCity.state_id) + .join(AddressCountry, AddressCountry.id == AddressState.country_id) + .filter( + or_( + AddressNeighborhood.neighborhood_name.ilike( + f"%{str(search_text).upper()}%" + ), + AddressLocality.locality_name.ilike( + f"%{str(search_text).upper()}%" + ), + AddressDistrict.district_name.ilike( + f"%{str(search_text).upper()}%" + ), + # AddressCity.city_name.ilike(f"%{str(search_text).upper()}%"), + # AddressState.state_name.ilike(f"%{str(search_text).upper()}%"), + # AddressCountry.country_name.ilike(f"%{str(search_text).upper()}%"), + cls.street_name.ilike(f"%{str(search_text).upper()}%"), + ), + ) + ) + # select([mytable.c.id]).where( + # func.to_tsvector('english', mytable.c.title) \ + # .match('somestring', postgresql_regconfig='english') + # ) + joined_statement = joined_data + joined_data = joined_data.first() + if not joined_data: + raise ValueError( + "No address found with the given search text.", + ) + # raise HTTPException( + # status_code=404, + # detail="No address found with the given search text.", + # ) + return dict( + query=joined_statement, + schema=list(field_dict.keys()), + ) diff --git a/Schemas/building/build.py b/Schemas/building/build.py index ddb0466..2b15fa2 100644 --- a/Schemas/building/build.py +++ b/Schemas/building/build.py @@ -625,3 +625,4 @@ class BuildPersonProviding(CrudCollection): ), {"comment": "People providing services for building"}, ) + diff --git a/Schemas/company/company.py b/Schemas/company/company.py new file mode 100644 index 0000000..cf7102e --- /dev/null +++ b/Schemas/company/company.py @@ -0,0 +1,239 @@ +from typing import Any + +from sqlalchemy import ( + String, + Integer, + Boolean, + ForeignKey, + Index, + Identity, + TIMESTAMP, + func, +) +from sqlalchemy.orm import mapped_column, relationship, Mapped + +from Controllers.Postgres.mixin import CrudCollection +from Schemas import Duties, Addresses + + +class RelationshipDutyCompany(CrudCollection): + """ + CompanyRelationship class based on declarative_base and CrudCollection via session + Company -> Sub Company -> Sub-Sub Company + + if owner_id == parent_id: can manipulate data of any record + else: Read-Only + duty_id = if relationship_type == base An organization / not operational / no responsible person + + relationship = company_id filter -> Action filter(company_id) relationship_type = Organization + relationship = company_id filter -> Action filter(company_id) relationship_type = Commercial + """ + + __tablename__ = "relationship_duty_company" + __exclude__fields__ = [] + + owner_id: Mapped[int] = mapped_column( + ForeignKey("companies.id"), nullable=False + ) # 1 + duties_id: Mapped[int] = mapped_column( + ForeignKey("duties.id"), nullable=False + ) # duty -> (n)employee Evyos LTD + + member_id: Mapped[int] = mapped_column( + ForeignKey("companies.id"), nullable=False + ) # 2, 3, 4 + parent_id: Mapped[int] = mapped_column( + ForeignKey("companies.id"), nullable=True + ) # None + + relationship_type: Mapped[str] = mapped_column( + String, nullable=True, server_default="Commercial" + ) # Commercial, Organization # Bulk + child_count: Mapped[int] = mapped_column(Integer) # 0 + show_only: Mapped[bool] = mapped_column(Boolean, server_default="0") + + # related_company: Mapped[List["Companies"]] = relationship( + # "Companies", + # back_populates="related_companies", + # foreign_keys=[related_company_id], + # ) + + @classmethod + def match_company_to_company_commercial(cls, data: Any, token): + with cls.new_session() as db_session: + token_duties_id, token_company_id = token.get("duty_id"), token.get( + "company_id" + ) + list_match_company_id = [] + send_duties = Duties.filter_one( + Duties.uu_id == data.duty_uu_id, + db=db_session + ) + send_user_duties = Duties.filter_one( + Duties.duties_id == send_duties.id, + Duties.company_id == token_duties_id, + db=db_session + ) + if not send_user_duties: + raise Exception( + "Send Duty is not found in company. Please check duty uuid and try again." + ) + + for company_uu_id in list(data.match_company_uu_id): + company = Companies.filter_one( + Companies.uu_id == company_uu_id, + db=db_session + ) + bulk_company = RelationshipDutyCompany.filter_one( + RelationshipDutyCompany.owner_id == token_company_id, + RelationshipDutyCompany.relationship_type == "Bulk", + RelationshipDutyCompany.member_id == company.id, + db=db_session + ) + if not bulk_company: + raise Exception( + f"Bulk Company is not found in company. " + f"Please check company uuid {bulk_company.uu_id} and try again." + ) + list_match_company_id.append(bulk_company) + + for match_company_id in list_match_company_id: + RelationshipDutyCompany.find_or_create( + owner_id=token_company_id, + duties_id=send_user_duties.id, + member_id=match_company_id.id, + parent_id=match_company_id.parent_id, + relationship_type="Commercial", + show_only=False, + db=db_session + ) + + @classmethod + def match_company_to_company_organization(cls, data: Any, token): + with cls.new_session() as db_session: + token_duties_id, token_company_id = token.get("duty_id"), token.get( + "company_id" + ) + list_match_company_id = [] + send_duties = Duties.filter_one( + Duties.uu_id == data.duty_uu_id, + db=db_session + ) + send_user_duties = Duties.filter_one( + Duties.duties_id == send_duties.id, + Duties.company_id == token_duties_id, + db=db_session + ) + if not send_user_duties: + raise Exception( + "Send Duty is not found in company. Please check duty uuid and try again." + ) + + for company_uu_id in list(data.match_company_uu_id): + company = Companies.filter_one( + Companies.uu_id == company_uu_id, + db=db_session + ) + bulk_company = RelationshipDutyCompany.filter_one( + RelationshipDutyCompany.owner_id == token_company_id, + RelationshipDutyCompany.relationship_type == "Bulk", + RelationshipDutyCompany.member_id == company.id, + db=db_session + ) + if not bulk_company: + raise Exception( + f"Bulk Company is not found in company. " + f"Please check company uuid {bulk_company.uu_id} and try again." + ) + list_match_company_id.append(bulk_company) + + for match_company_id in list_match_company_id: + Duties.init_a_company_default_duties( + company_id=match_company_id.id, + company_uu_id=str(match_company_id.uu_id), + db=db_session + ) + RelationshipDutyCompany.find_or_create( + owner_id=token_company_id, + duties_id=send_user_duties.id, + member_id=match_company_id.id, + parent_id=match_company_id.parent_id, + relationship_type="Organization", + show_only=False, + db=db_session + ) + + __table_args__ = ( + Index( + "_company_relationship_ndx_01", + duties_id, + owner_id, + member_id, + relationship_type, + unique=True, + ), + {"comment": "Company Relationship Information"}, + ) + + +class Companies(CrudCollection): + """ + Company class based on declarative_base and CrudCollection via session + formal_name = Government register name by offical + public_name = Public registered name by User + nick_name = Search by nickname, commercial_type = Tüzel veya birey + """ + + __tablename__ = "companies" + + __exclude__fields__ = ["is_blacklist", "is_commercial"] + __access_by__ = [] + __many__table__ = RelationshipDutyCompany + + formal_name: Mapped[str] = mapped_column( + String, nullable=False, comment="Formal Name" + ) + company_type: Mapped[str] = mapped_column( + String, nullable=False, comment="Company Type" + ) + commercial_type: Mapped[str] = mapped_column( + String, nullable=False, comment="Commercial Type" + ) + tax_no: Mapped[str] = mapped_column( + String, index=True, unique=True, nullable=False, comment="Tax No" + ) + + public_name: Mapped[str] = mapped_column(String, comment="Public Name of a company") + company_tag: Mapped[str] = mapped_column(String, comment="Company Tag") + default_lang_type: Mapped[str] = mapped_column(String, server_default="TR") + default_money_type: Mapped[str] = mapped_column(String, server_default="TL") + is_commercial: Mapped[bool] = mapped_column(Boolean, server_default="False") + is_blacklist: Mapped[bool] = mapped_column(Boolean, server_default="False") + parent_id = mapped_column(Integer, nullable=True) + workplace_no: Mapped[str] = mapped_column(String, nullable=True) + + official_address_id: Mapped[int] = mapped_column( + ForeignKey("addresses.id"), nullable=True + ) + official_address_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Official Address UUID" + ) + top_responsible_company_id: Mapped[int] = mapped_column( + ForeignKey("companies.id"), nullable=True + ) + top_responsible_company_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Top Responsible Company UUID" + ) + + # buildings: Mapped[List["Build"]] = relationship( + # "Build", + # back_populates="companies", + # foreign_keys="Build.company_id", + # ) + + __table_args__ = ( + Index("_company_ndx_01", tax_no, unique=True), + Index("_company_ndx_02", formal_name, public_name), + {"comment": "Company Information"}, + ) + diff --git a/Schemas/company/department.py b/Schemas/company/department.py new file mode 100644 index 0000000..73a13a0 --- /dev/null +++ b/Schemas/company/department.py @@ -0,0 +1,73 @@ +from sqlalchemy import String, Integer, ForeignKey, Index, Boolean, Identity +from sqlalchemy.orm import mapped_column, Mapped + +from Controllers.Postgres.mixin import CrudCollection + + +class Departments(CrudCollection): + + __tablename__ = "departments" + __exclude__fields__ = [] + + parent_department_id = mapped_column(Integer, server_default="0") + department_code = mapped_column( + String(16), nullable=False, index=True, comment="Department Code" + ) + department_name: Mapped[str] = mapped_column( + String(128), nullable=False, comment="Department Name" + ) + department_description: Mapped[str] = mapped_column(String, server_default="") + + company_id: Mapped[int] = mapped_column(ForeignKey("companies.id"), nullable=False) + company_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Company UUID" + ) + + __table_args__ = {"comment": "Departments Information"} + + +class Duty(CrudCollection): + + __tablename__ = "duty" + __exclude__fields__ = [] + + duty_name: Mapped[str] = mapped_column( + String, unique=True, nullable=False, comment="Duty Name" + ) + duty_code: Mapped[str] = mapped_column(String, nullable=False, comment="Duty Code") + duty_description: Mapped[str] = mapped_column(String, comment="Duty Description") + + __table_args__ = ({"comment": "Duty Information"},) + + +class Duties(CrudCollection): + + __tablename__ = "duties" + __exclude__fields__ = [] + + users_default_duty = mapped_column( + ForeignKey("duty.id"), nullable=True, comment="Default Duty for Users" + ) + company_id: Mapped[int] = mapped_column(Integer) + company_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Company UUID" + ) + duties_id: Mapped[int] = mapped_column(ForeignKey("duty.id"), nullable=False) + duties_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Duty UUID" + ) + department_id = mapped_column( + ForeignKey("departments.id"), nullable=False, comment="Department ID" + ) + department_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Department UUID" + ) + # priority_id: Mapped[int] = mapped_column(ForeignKey("priority.id"), nullable=True) + management_duty = mapped_column( + Boolean, server_default="0" + ) # is this a prime Company Duty ??? + + __table_args__ = ( + Index("duty_ndx_00", company_id, duties_id, department_id, unique=True), + {"comment": "Duty & Company & Department Information"}, + ) diff --git a/Schemas/company/employee.py b/Schemas/company/employee.py new file mode 100644 index 0000000..fae2315 --- /dev/null +++ b/Schemas/company/employee.py @@ -0,0 +1,75 @@ +from sqlalchemy import ( + String, + ForeignKey, + Index, + Numeric, +) +from sqlalchemy.orm import mapped_column, Mapped +from Controllers.Postgres.mixin import CrudCollection + + +class Staff(CrudCollection): + + __tablename__ = "staff" + __exclude__fields__ = [] + + staff_description: Mapped[str] = mapped_column(String, server_default="", comment="Staff Description") + staff_name: Mapped[str] = mapped_column(String, nullable=False, comment="Staff Name") + staff_code: Mapped[str] = mapped_column(String, nullable=False, comment="Staff Code") + + duties_id: Mapped[int] = mapped_column(ForeignKey("duties.id"), nullable=False) + duties_uu_id: Mapped[str] = mapped_column(String, nullable=False, comment="Duty UUID") + + __table_args__ = ({"comment": "Staff Information"},) + + +class Employees(CrudCollection): + + __tablename__ = "employees" + __exclude__fields__ = [] + + staff_id: Mapped[int] = mapped_column(ForeignKey("staff.id")) + staff_uu_id: Mapped[str] = mapped_column(String, nullable=False, comment="Staff UUID") + people_id: Mapped[int] = mapped_column(ForeignKey("people.id"), nullable=True) + people_uu_id: Mapped[str] = mapped_column(String, nullable=True, comment="People UUID") + + __table_args__ = ( + Index("employees_ndx_00", people_id, staff_id, unique=True), + {"comment": "Employee Person Information"}, + ) + + +class EmployeeHistory(CrudCollection): + + __tablename__ = "employee_history" + __exclude__fields__ = [] + + staff_id: Mapped[int] = mapped_column(ForeignKey("staff.id"), nullable=False, comment="Staff ID") + staff_uu_id: Mapped[str] = mapped_column(String, nullable=False, comment="Staff UUID") + people_id: Mapped[int] = mapped_column(ForeignKey("people.id"), nullable=False, comment="People ID") + people_uu_id: Mapped[str] = mapped_column(String, nullable=False, comment="People UUID") + + __table_args__ = ( + Index("_employee_history_ndx_00", people_id, staff_id), + {"comment": "Employee History Information"}, + ) + + +class EmployeesSalaries(CrudCollection): + + __tablename__ = "employee_salaries" + __exclude__fields__ = [] + + gross_salary: Mapped[float] = mapped_column( + Numeric(20, 6), nullable=False, comment="Gross Salary" + ) + net_salary: Mapped[float] = mapped_column( + Numeric(20, 6), nullable=False, comment="Net Salary" + ) + people_id: Mapped[int] = mapped_column(ForeignKey("people.id"), nullable=False) + people_uu_id: Mapped[str] = mapped_column(String, nullable=False, comment="People UUID") + + __table_args__ = ( + Index("_employee_salaries_ndx_00", people_id, "expiry_starts"), + {"comment": "Employee Salaries Information"}, + ) diff --git a/Schemas/event/event.py b/Schemas/event/event.py new file mode 100644 index 0000000..643370b --- /dev/null +++ b/Schemas/event/event.py @@ -0,0 +1,430 @@ +from sqlalchemy import ( + String, + ForeignKey, + Numeric, + SmallInteger, + Boolean, + Integer, + Index, +) +from sqlalchemy.orm import mapped_column, Mapped +from Controllers.Postgres.mixin import CrudCollection +from Schemas import OccupantTypes + + +class Events(CrudCollection): + """ + Events class based on declarative_base and BaseMixin via session + If Events2Occupants and Events2Employees are not found for user request, response 401 Unauthorized + """ + + __tablename__ = "events" + __exclude__fields__ = [] + + event_type: Mapped[str] = mapped_column( + String, nullable=False, comment="Event Type" + ) + function_code: Mapped[str] = mapped_column( + String, nullable=False, comment="function code" + ) + function_class: Mapped[str] = mapped_column( + String, nullable=False, comment="class name" + ) + + # name: Mapped[str] = mapped_column(String, nullable=True) # form or page title + description: Mapped[str] = mapped_column( + String, server_default="" + ) # form or page description + property_description: Mapped[str] = mapped_column(String, server_default="") + + marketing_layer = mapped_column(SmallInteger, server_default="3") + cost: Mapped[float] = mapped_column(Numeric(20, 2), server_default="0.00") + unit_price: Mapped[float] = mapped_column(Numeric(20, 2), server_default="0.00") + + endpoint_id: Mapped[int] = mapped_column( + ForeignKey("endpoint_restriction.id"), nullable=True + ) + endpoint_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Endpoint UUID" + ) + + __table_args__ = ({"comment": "Events Information"},) + + +class Modules(CrudCollection): + """ + Modules class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "modules" + __exclude__fields__ = [] + + module_name: Mapped[str] = mapped_column( + String, nullable=False, comment="Module Name" + ) + module_description: Mapped[str] = mapped_column(String, server_default="") + module_code: Mapped[str] = mapped_column( + String, nullable=False, comment="Module Code" + ) + module_layer = mapped_column(Integer, nullable=False, comment="Module Layer") + is_default_module = mapped_column(Boolean, server_default="0") + + def retrieve_services(self): + services = Services.filter_all(Services.module_id == self.id).data + if not services: + self.raise_http_exception( + status_code="HTTP_404_NOT_FOUND", + error_case="RECORD_NOT_FOUND", + message=f"No services found for this module : {str(self.uu_id)}", + data={ + "module_uu_id": str(self.uu_id), + }, + ) + return services + + __table_args__ = ({"comment": "Modules Information"},) + + +class Services(CrudCollection): + """ + Services class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "services" + __exclude__fields__ = [] + + module_id: Mapped[int] = mapped_column(ForeignKey("modules.id"), nullable=False) + module_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Module UUID" + ) + service_name: Mapped[str] = mapped_column( + String, nullable=False, comment="Service Name" + ) + service_description: Mapped[str] = mapped_column(String, server_default="") + service_code: Mapped[str] = mapped_column( + String, nullable=True, comment="Service Code" + ) + related_responsibility: Mapped[str] = mapped_column(String, server_default="") + + @classmethod + def retrieve_service_via_occupant_code(cls, occupant_code): + with cls.new_session() as db_session: + occupant_type = OccupantTypes.filter_by_one( + system=True, + occupant_code=occupant_code, + db=db_session + ).data + if not occupant_type: + cls.raise_http_exception( + status_code="HTTP_404_NOT_FOUND", + error_case="RECORD_NOT_FOUND", + message=f"No occupant type found for this code : {occupant_code}", + data={ + "occupant_code": occupant_code, + }, + ) + return cls.filter_one( + cls.related_responsibility == occupant_type.occupant_code, + db=db_session + ).data + + __table_args__ = ({"comment": "Services Information"},) + + +class Service2Events(CrudCollection): + """ + Service2Actions class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "services2events" + __exclude__fields__ = [] + + service_id: Mapped[int] = mapped_column(ForeignKey("services.id"), nullable=False) + service_uu_id = mapped_column(String, nullable=False, comment="Service UUID") + event_id: Mapped[int] = mapped_column(ForeignKey("events.id"), nullable=False) + event_uu_id = mapped_column(String, nullable=False, comment="Event UUID") + + __table_args__ = ({"comment": "Service2Events Information"},) + + +class Event2OccupantExtra(CrudCollection): + + __tablename__ = "event2occupant_extra" + __exclude__fields__ = [] + + build_living_space_id: Mapped[int] = mapped_column( + ForeignKey("build_living_space.id"), nullable=False + ) + build_living_space_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Build Living Space UUID" + ) + event_id: Mapped[int] = mapped_column(ForeignKey("events.id"), nullable=False) + event_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Event UUID" + ) + + __table_args__ = ( + Index( + "event2occupant_extra_bind_event_to_occupant", + build_living_space_id, + event_id, + unique=True, + ), + {"comment": "Occupant2Event Information"}, + ) + + +class Event2EmployeeExtra(CrudCollection): + """ + Employee2Event class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "event2employee_extra" + __exclude__fields__ = [] + + employee_id: Mapped[int] = mapped_column(ForeignKey("employees.id"), nullable=False) + employee_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Employee UUID" + ) + + event_id: Mapped[int] = mapped_column(ForeignKey("events.id"), nullable=False) + event_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Event UUID" + ) + + __table_args__ = ( + Index( + "event2employee_extra_employee_to_event", + employee_id, + event_id, + unique=True, + ), + {"comment": "Employee to Event Information"}, + ) + + +class Event2Employee(CrudCollection): + """ + Employee2Event class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "event2employee" + __exclude__fields__ = [] + + employee_id: Mapped[int] = mapped_column(ForeignKey("employees.id"), nullable=False) + employee_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Employee UUID" + ) + event_service_id: Mapped[int] = mapped_column( + ForeignKey("services.id"), nullable=False + ) + event_service_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Event Cluster UUID" + ) + + __table_args__ = ( + Index( + "event2employee_employee_to_event", + employee_id, + event_service_id, + unique=True, + ), + {"comment": "Employee to Event Information"}, + ) + + @classmethod + def get_event_codes(cls, employee_id: int) -> list: + db = cls.new_session() + employee_events = cls.filter_all( + cls.employee_id == employee_id, + db=db, + ).data + active_event_ids = Service2Events.filter_all_system( + Service2Events.service_id.in_( + [event.event_service_id for event in employee_events] + ), + db=db, + ).data + active_events = Events.filter_all( + Events.id.in_([event.event_id for event in active_event_ids]), + db=db, + ).data + if extra_events := Event2EmployeeExtra.filter_all( + Event2EmployeeExtra.employee_id == employee_id, + db=db, + ).data: + events_extra = Events.filter_all( + Events.id.in_([event.event_id for event in extra_events]), + db=db, + ).data + active_events.extend(events_extra) + return [event.function_code for event in active_events] + + # @classmethod + # def get_event_endpoints(cls, employee_id: int) -> list: + # from Schemas import EndpointRestriction + # + # db = cls.new_session() + # employee_events = cls.filter_all( + # cls.employee_id == employee_id, + # db=db, + # ).data + # active_event_ids = Service2Events.filter_all( + # Service2Events.service_id.in_( + # [event.event_service_id for event in employee_events] + # ), + # db=db, + # system=True, + # ).data + # active_events = Events.filter_all( + # Events.id.in_([event.event_id for event in active_event_ids]), + # db=db, + # ).data + # if extra_events := Event2EmployeeExtra.filter_all( + # Event2EmployeeExtra.employee_id == employee_id, + # db=db, + # ).data: + # events_extra = Events.filter_all( + # Events.id.in_([event.event_id for event in extra_events]), + # db=db, + # ).data + # active_events.extend(events_extra) + # endpoint_restrictions = EndpointRestriction.filter_all( + # EndpointRestriction.id.in_([event.endpoint_id for event in active_events]), + # db=db, + # ).data + # return [event.endpoint_name for event in endpoint_restrictions] + # + + +class Event2Occupant(CrudCollection): + """ + Occupant2Event class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "event2occupant" + __exclude__fields__ = [] + + build_living_space_id: Mapped[str] = mapped_column( + ForeignKey("build_living_space.id"), nullable=False + ) + build_living_space_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Build Living Space UUID" + ) + event_service_id: Mapped[int] = mapped_column( + ForeignKey("services.id"), nullable=False + ) + event_service_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Event Cluster UUID" + ) + # event_id: Mapped[int] = mapped_column(ForeignKey("events.id"), nullable=False) + # event_uu_id = mapped_column(String, nullable=False, comment="Event UUID") + + __table_args__ = ( + Index( + "event2occupant_bind_event_to_occupant", + build_living_space_id, + event_service_id, + unique=True, + ), + {"comment": "Occupant2Event Information"}, + ) + + @classmethod + def get_event_codes(cls, build_living_space_id) -> list: + db = cls.new_session() + occupant_events = cls.filter_all( + cls.build_living_space_id == build_living_space_id, + db=db, + ).data + active_event_ids = Service2Events.filter_all_system( + Service2Events.service_id.in_( + [event.event_service_id for event in occupant_events] + ), + db=db, + ).data + active_events = Events.filter_all( + Events.id.in_([event.event_id for event in active_event_ids]), + db=db, + ).data + if extra_events := Event2OccupantExtra.filter_all( + Event2OccupantExtra.build_living_space_id == build_living_space_id, + db=db, + ).data: + events_extra = Events.filter_all( + Events.id.in_([event.event_id for event in extra_events]), + db=db, + ).data + active_events.extend(events_extra) + return [event.function_code for event in active_events] + + # @classmethod + # def get_event_endpoints(cls, build_living_space_id) -> list: + # from Schemas import EndpointRestriction + # + # db = cls.new_session() + # occupant_events = cls.filter_all( + # cls.build_living_space_id == build_living_space_id, + # db=db, + # ).data + # active_event_ids = Service2Events.filter_all( + # Service2Events.service_id.in_( + # [event.event_service_id for event in occupant_events] + # ), + # db=db, + # system=True, + # ).data + # active_events = Events.filter_all( + # Events.id.in_([event.event_id for event in active_event_ids]), + # db=db, + # ).data + # if extra_events := Event2OccupantExtra.filter_all( + # Event2OccupantExtra.build_living_space_id == build_living_space_id, + # db=db, + # ).data: + # events_extra = Events.filter_all( + # Events.id.in_([event.event_id for event in extra_events]), + # db=db, + # ).data + # active_events.extend(events_extra) + # endpoint_restrictions = EndpointRestriction.filter_all( + # EndpointRestriction.id.in_([event.endpoint_id for event in active_events]), + # db=db, + # ).data + # return [event.endpoint_name for event in endpoint_restrictions] + + +class ModulePrice(CrudCollection): + """ + ModulePrice class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "module_price" + __exclude__fields__ = [] + + campaign_code: Mapped[str] = mapped_column( + String, nullable=False, comment="Campaign Code" + ) + module_id: Mapped[int] = mapped_column(ForeignKey("modules.id"), nullable=False) + module_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Module UUID" + ) + service_id: Mapped[int] = mapped_column(ForeignKey("services.id"), nullable=False) + service_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Service UUID" + ) + event_id: Mapped[int] = mapped_column(ForeignKey("events.id"), nullable=False) + event_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Event UUID" + ) + is_counted_percentage: Mapped[float] = mapped_column( + Numeric(6, 2), server_default="0.00" + ) # %22 + discounted_price: Mapped[float] = mapped_column( + Numeric(20, 2), server_default="0.00" + ) # Normal: 78.00 TL + calculated_price: Mapped[float] = mapped_column( + Numeric(20, 2), server_default="0.00" + ) # sana düz 75.00 TL yapar + + __table_args__ = ({"comment": "ModulePrice Information"},) diff --git a/Schemas/identity/identity.py b/Schemas/identity/identity.py new file mode 100644 index 0000000..a32bad0 --- /dev/null +++ b/Schemas/identity/identity.py @@ -0,0 +1,434 @@ +import arrow + +from sqlalchemy import ( + String, + Integer, + Boolean, + ForeignKey, + Index, + TIMESTAMP, + Text, + func, + BigInteger, + Numeric, + or_, +) +from sqlalchemy.orm import mapped_column, relationship, Mapped +from Controllers.Postgres.mixin import CrudCollection + + +class UsersTokens(CrudCollection): + + __tablename__ = "users_tokens" + __exclude__fields__ = [] + + user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False) + + token_type: Mapped[str] = mapped_column(String(16), server_default="RememberMe") + token: Mapped[str] = mapped_column(String, server_default="") + domain: Mapped[str] = mapped_column(String, server_default="") + expires_at: Mapped[TIMESTAMP] = mapped_column( + TIMESTAMP(timezone=True), + default=str(arrow.now().shift(date=arrow.now(), days=3)), + ) + + # users = relationship("Users", back_populates="tokens", foreign_keys=[user_id]) + + +class Users(CrudCollection): + """ + Application User frame to connect to API with assigned token-based HTTP connection + """ + + __tablename__ = "users" + __exclude__fields__ = [ + "hash_password", + "password_token", + "expiry_begins", + "related_company", + ] + + user_tag: Mapped[str] = mapped_column( + String(64), server_default="", comment="Unique tag for the user", index=True + ) + email: Mapped[str] = mapped_column( + String(128), server_default="", comment="Email address of the user", index=True + ) + phone_number: Mapped[str] = mapped_column( + String, server_default="", comment="Phone number of the user", index=True + ) + via: Mapped[str] = mapped_column( + String, + server_default="111", + comment="Email 1/ Phone 2/ User Tag 3 All 111 Only 100", + ) + + avatar: Mapped[str] = mapped_column( + String, server_default="", comment="Avatar URL for the user" + ) + hash_password: Mapped[str] = mapped_column( + String(256), server_default="", comment="Hashed password for security" + ) + password_token: Mapped[str] = mapped_column( + String(256), server_default="", comment="Token for password reset" + ) + remember_me: Mapped[bool] = mapped_column( + Boolean, server_default="0", comment="Flag to remember user login" + ) + + password_expires_day: Mapped[int] = mapped_column( + Integer, + server_default=str(30), + comment="Password expires in days", + ) + password_expiry_begins: Mapped[TIMESTAMP] = mapped_column( + TIMESTAMP(timezone=True), + server_default=func.now(), + comment="Timestamp when password expiry begins", + ) + related_company: Mapped[str] = mapped_column(String, comment="Related Company UUID") + + person_id: Mapped[int] = mapped_column( + ForeignKey("people.id"), nullable=False, comment="Foreign key to person table" + ) + person_uu_id: Mapped[str] = mapped_column( + String, server_default="", comment="Person UUID", index=True + ) + local_timezone = mapped_column( + String, server_default="GMT+3", comment="Local timezone of user" + ) + person = relationship("People", back_populates="user", foreign_keys=[person_id]) + # + # @property + # def is_occupant(self): + # return not str(self.email).split("@")[1] == Auth.ACCESS_EMAIL_EXT + # + # @property + # def is_employee(self): + # return str(self.email).split("@")[1] == Auth.ACCESS_EMAIL_EXT + # + # @property + # def user_type(self): + # return "Occupant" if self.is_occupant else "Employee" + # + # @classmethod + # def credentials(cls): + # db_session = cls.new_session() + # person_object: People = People.filter_by_one( + # db=db_session, system=True, id=cls.person_id + # ).data + # if person_object: + # return { + # "person_id": person_object.id, + # "person_uu_id": str(person_object.uu_id), + # } + # return { + # "person_id": None, + # "person_uu_id": None, + # } + # + # @property + # def password_expiry_ends(self): + # """Calculates the expiry end date based on expiry begins and expires day""" + # return self.password_expiry_begins + timedelta( + # days=int( + # "".join( + # [ + # _ + # for _ in str(self.password_expires_day).split(",")[0] + # if _.isdigit() + # ] + # ) + # ) + # ) + # + # @classmethod + # def create_action(cls, create_user: InsertUsers, token_dict): + # db_session = cls.new_session() + # found_person = People.filter_one( + # People.uu_id == create_user.people_uu_id, + # db=db_session, + # ).data + # + # if not found_person: + # raise HTTPException(status_code=400, detail="Person not found.") + # if ( + # not any(i in str(create_user.email) for i in ["@", "."]) + # and not len(str(create_user.phone_number)) >= 10 + # ): + # raise HTTPException( + # status_code=400, + # detail="Please enter at least one valid email or phone number.", + # ) + # if not create_user.avatar: + # create_user.avatar = ApiStatic.PLACEHOLDER + # create_dict = create_user.model_dump() + # del create_dict["people_uu_id"] + # create_dict["person_id"] = found_person.id + # create_dict["person_uu_id"] = str(found_person.uu_id) + # create_dict["related_company"] = token_dict.selected_company.company_uu_id + # created_user = cls.find_or_create(**create_dict) + # created_user.reset_password_token(found_user=created_user) + # return created_user + # + # def get_employee_and_duty_details(self): + # from ApiLayers.Schemas import Employees, Duties + # + # db_session = self.new_session() + # found_person = People.filter_one( + # People.id == self.person_id, + # db=db_session, + # ) + # found_employees = Employees.filter_by_active( + # people_id=found_person.id, is_confirmed=True, db=db_session + # ) + # found_duties = Duties.filter_all( + # Duties.is_confirmed == True, + # Duties.id.in_( + # list(found_employee.duty_id for found_employee in found_employees.data) + # ), + # db=db_session, + # ) + # if not found_employees.count: + # raise HTTPException( + # status_code=401, + # detail={ + # "message": "Person has no confirmed duty. No employee match please register " + # "your super admin", + # "completed": False, + # }, + # ) + # return { + # "duty_list": [ + # { + # "duty_id": duty.id, + # "duty_uu_id": duty.uu_id.__str__(), + # "duty_code": duty.duty_code, + # "duty_name": duty.duty_name, + # "duty_description": duty.duty_description, + # } + # for duty in found_duties.data + # ], + # } + # + # def get_main_domain_and_other_domains(self, get_main_domain: bool = True): + # from ApiLayers.Schemas import MongoQueryIdentity + # + # query_engine = MongoQueryIdentity(company_uuid=self.related_company) + # domain_via_user = query_engine.get_domain_via_user(user_uu_id=str(self.uu_id)) + # if not domain_via_user: + # raise HTTPException( + # status_code=401, + # detail="Domain not found. Please contact the admin.", + # ) + # domain_via_user = domain_via_user[0] + # if get_main_domain: + # return domain_via_user.get("main_domain", None) + # return domain_via_user.get("other_domains_list", None) + + +class RelationshipDutyPeople(CrudCollection): + + __tablename__ = "relationship_duty_people" + __exclude__fields__ = [] + + company_id: Mapped[int] = mapped_column( + ForeignKey("companies.id"), nullable=False + ) # 1, 2, 3 + duties_id: Mapped[int] = mapped_column( + ForeignKey("duties.id"), nullable=False + ) # duty -> (n)person Evyos LTD + member_id: Mapped[int] = mapped_column( + ForeignKey("people.id"), nullable=False + ) # 2, 3, 4 + + relationship_type: Mapped[str] = mapped_column( + String, nullable=True, server_default="Employee" + ) # Commercial + show_only: Mapped[bool] = mapped_column(Boolean, server_default="0") + + # related_company: Mapped[List["Company"]] = relationship( + # "Company", + # back_populates="related_companies", + # foreign_keys=[related_company_id], + # ) + + __table_args__ = ( + Index( + "person_relationship_ndx_01", + company_id, + duties_id, + member_id, + relationship_type, + unique=True, + ), + {"comment": "Person Relationship Information"}, + ) + + +class People(CrudCollection): + """ + People that are related to users in the application + """ + + __tablename__ = "people" + __exclude__fields__ = [] + __many__table__ = RelationshipDutyPeople + __encrypt_list__ = [ + "father_name", + "mother_name", + "country_code", + "national_identity_id", + "birth_place", + "birth_date", + "tax_no", + ] + + firstname: Mapped[str] = mapped_column( + String, nullable=False, comment="First name of the person" + ) + surname: Mapped[str] = mapped_column( + String(24), nullable=False, comment="Surname of the person" + ) + middle_name: Mapped[str] = mapped_column( + String, server_default="", comment="Middle name of the person" + ) + sex_code: Mapped[str] = mapped_column( + String(1), nullable=False, comment="Sex code of the person (e.g., M/F)" + ) + person_ref: Mapped[str] = mapped_column( + String, server_default="", comment="Reference ID for the person" + ) + person_tag: Mapped[str] = mapped_column( + String, server_default="", comment="Unique tag for the person" + ) + + # ENCRYPT DATA + father_name: Mapped[str] = mapped_column( + String, server_default="", comment="Father's name of the person" + ) + mother_name: Mapped[str] = mapped_column( + String, server_default="", comment="Mother's name of the person" + ) + country_code: Mapped[str] = mapped_column( + String(4), server_default="TR", comment="Country code of the person" + ) + national_identity_id: Mapped[str] = mapped_column( + String, server_default="", comment="National identity ID of the person" + ) + birth_place: Mapped[str] = mapped_column( + String, server_default="", comment="Birth place of the person" + ) + birth_date: Mapped[TIMESTAMP] = mapped_column( + TIMESTAMP(timezone=True), + server_default="1900-01-01", + comment="Birth date of the person", + ) + tax_no: Mapped[str] = mapped_column( + String, server_default="", comment="Tax number of the person" + ) + # Receive at Create person + # language = mapped_column( + # String, comment="Language code of the person" + # ) + # currency = mapped_column( + # String, comment="Currency code of the person" + # ) + + # ENCRYPT DATA + user = relationship( + "Users", back_populates="person", foreign_keys="Users.person_id" + ) + + __table_args__ = ( + Index( + "person_ndx_001", + national_identity_id, + unique=True, + ), + {"comment": "Person Information"}, + ) + + @property + def full_name(self): + if self.middle_name: + return f"{self.firstname} {self.middle_name} {self.surname}" + return f"{self.firstname} {self.surname}" + + +class OccupantTypes(CrudCollection): + """ + Occupant Types class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "occupant_types" + __exclude__fields__ = [] + + occupant_type: Mapped[str] = mapped_column( + String, nullable=False, comment="Occupant Type" + ) + occupant_description: Mapped[str] = mapped_column(String, server_default="") + occupant_code: Mapped[str] = mapped_column(String, server_default="") + occupant_category: Mapped[str] = mapped_column(String, server_default="") + occupant_category_type: Mapped[str] = mapped_column(String, server_default="") + occupant_is_unique: Mapped[bool] = mapped_column(Boolean, server_default="0") + + __table_args__ = ({"comment": "Occupant Types Information"},) + + +class Contracts(CrudCollection): + """ + Contract class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "contracts" + __exclude__fields__ = [] + + contract_type: Mapped[str] = mapped_column( + String(5), + nullable=False, + comment="The code for personnel is P and the code for companies is C.", + ) + contract_title: Mapped[str] = mapped_column(String(255)) + contract_details: Mapped[str] = mapped_column(Text) + contract_terms: Mapped[str] = mapped_column(Text) + + contract_code: Mapped[str] = mapped_column( + String(100), + nullable=False, + comment="contract_code is the unique code given by the system.", + ) + contract_date: Mapped[TIMESTAMP] = mapped_column( + TIMESTAMP(timezone=True), + server_default="2099-12-31 23:59:59", + comment="contract date is the date the contract is made. " + "expire start is the start date of the contract, expire en is the end date of the contract.", + ) + + company_id: Mapped[int] = mapped_column( + Integer, ForeignKey("companies.id"), nullable=True + ) + company_uu_id: Mapped[str] = mapped_column( + String, server_default="", comment="Company UUID" + ) + + person_id: Mapped[int] = mapped_column( + Integer, ForeignKey("people.id"), nullable=True + ) + person_uu_id: Mapped[str] = mapped_column( + String, server_default="", comment="Person UUID" + ) + + @classmethod + def retrieve_contact_no(cls): + # todo When create record contract_code == below string + related_date, counter = Contracts.client_arrow.now(), 1 + return ( + f"{related_date.date().year}{str(cls.contract_type)}{str(counter).zfill(6)}" + ) + + __table_args__ = ( + Index("_contract_ndx_01", contract_code, unique=True), + {"comment": "Contract Information"}, + ) + diff --git a/Schemas/others/enums.py b/Schemas/others/enums.py new file mode 100644 index 0000000..c956635 --- /dev/null +++ b/Schemas/others/enums.py @@ -0,0 +1,106 @@ +from sqlalchemy import ( + UUID, + String, + text, +) +from sqlalchemy.orm import ( + Mapped, + mapped_column, +) +from Controllers.Postgres.mixin import CrudCollection + + +class ApiEnumDropdown(CrudCollection): + __tablename__ = "api_enum_dropdown" + __exclude__fields__ = ["enum_class"] + __language_model__ = None + + id: Mapped[int] = mapped_column(primary_key=True) + uu_id: Mapped[str] = mapped_column( + UUID, server_default=text("gen_random_uuid()"), index=True, unique=True + ) + enum_class: Mapped[str] = mapped_column( + String, nullable=False, comment="Enum Constant Name" + ) + key: Mapped[str] = mapped_column(String, nullable=False, comment="Enum Key") + value: Mapped[str] = mapped_column(String, nullable=False, comment="Enum Value") + description: Mapped[str] = mapped_column(String, nullable=True) + + __table_args__ = ({"comment": "Enum objets that are linked to tables"},) + + @classmethod + def get_by_uuid(cls, uuid: str): + with cls.new_session() as db_session: + return cls.filter_by_one(uu_id=str(uuid), db=db_session).data + + @classmethod + def get_debit_search(cls, search_debit: str = None, search_uu_id: str = None): + with cls.new_session() as db_session: + if search_uu_id: + if search := cls.filter_one_system( + cls.enum_class.in_(["DebitTypes"]), + cls.uu_id == search_uu_id, + db=db_session + ).data: + return search + elif search_debit: + if search := cls.filter_one( + cls.enum_class.in_(["DebitTypes"]), cls.key == search_debit, db=db_session + ).data: + return search + return cls.filter_all_system(cls.enum_class.in_(["DebitTypes"]), db=db_session).data + + @classmethod + def get_due_types(cls): + with cls.new_session() as db_session: + if due_list := cls.filter_all_system( + cls.enum_class == "BuildDuesTypes", + cls.key.in_(["BDT-A", "BDT-D"]), + db=db_session + ).data: + return [due.uu_id.__str__() for due in due_list] + # raise HTTPException( + # status_code=404, + # detail="No dues types found", + # ) + + @classmethod + def due_type_search(cls, search_management: str = None, search_uu_id: str = None): + with cls.new_session() as db_session: + if search_uu_id: + if search := cls.filter_one_system( + cls.enum_class.in_(["BuildDuesTypes"]), + cls.uu_id == search_uu_id, + db=db_session + ).data: + return search + elif search_management: + if search := cls.filter_one_system( + cls.enum_class.in_(["BuildDuesTypes"]), + cls.key == search_management, + db=db_session + ).data: + return search + return cls.filter_all_system(cls.enum_class.in_(["BuildDuesTypes"]), db=db_session).data + + def get_enum_dict(self): + return { + "uu_id": str(self.uu_id), + "enum_class": self.enum_class, + "key": self.key, + "value": self.value, + "description": self.description, + } + + @classmethod + def uuid_of_enum(cls, enum_class: str, key: str): + with cls.new_session() as db_session: + return str( + getattr( + cls.filter_one_system( + cls.enum_class == enum_class, cls.key == key, db=db_session + ).data, + "uu_id", + None, + ) + ) diff --git a/Schemas/rules/rules.py b/Schemas/rules/rules.py new file mode 100644 index 0000000..3ff41a2 --- /dev/null +++ b/Schemas/rules/rules.py @@ -0,0 +1,35 @@ +from sqlalchemy import ( + UUID, + String, + text, +) +from sqlalchemy.orm import ( + Mapped, + mapped_column, +) +from Controllers.Postgres.mixin import CrudCollection + + +class EndpointRestriction(CrudCollection): + """ + Initialize Endpoint Restriction with default values + """ + + __tablename__ = "endpoint_restriction" + __exclude__fields__ = [] + + endpoint_function: Mapped[str] = mapped_column( + String, server_default="", comment="Function name of the API endpoint" + ) + endpoint_name: Mapped[str] = mapped_column( + String, server_default="", comment="Name of the API endpoint" + ) + endpoint_method: Mapped[str] = mapped_column( + String, server_default="", comment="HTTP method used by the endpoint" + ) + endpoint_desc: Mapped[str] = mapped_column( + String, server_default="", comment="Description of the endpoint" + ) + endpoint_code: Mapped[str] = mapped_column( + String, server_default="", unique=True, comment="Unique code for the endpoint" + ) diff --git a/api_env.env b/api_env.env index ecf714c..ad2410b 100644 --- a/api_env.env +++ b/api_env.env @@ -28,3 +28,4 @@ API_REFRESHER_TOKEN_LENGTH=144 API_EMAIL_HOST=10.10.2.36 API_DATETIME_FORMAT=YYYY-MM-DD HH:mm:ss Z API_FORGOT_LINK=https://www.evyos.com.tr/password/create?tokenUrl= +API_VERSION=0.1.001 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 3714bfc..2b4366d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,5 @@ services: + mongo_service: container_name: mongo_service image: "bitnami/mongodb:latest"