From 1d4f00e8b2ab363efdf9a6d91c988655d65ecb0b Mon Sep 17 00:00:00 2001 From: Berkay Date: Mon, 12 May 2025 17:42:33 +0300 Subject: [PATCH] initializer service deployed and tested --- api_services/.python-version | 1 + .../README.md | 0 api_services/api_builds/a.txt | 5 + .../api_builds/auth-service/Dockerfile | 28 + .../auth-service/endpoints/__init__.py | 3 + .../auth-service/endpoints/index.py | 9 + .../auth-service/endpoints/routes.py | 15 + .../auth-service/events/__init__.py | 3 + .../api_builds/auth-service/events/index.py | 10 + .../api_builds/initial-service/Dockerfile | 25 + .../api_builds/initial-service/alembic.ini | 119 +++ .../api_builds/initial-service/alembic/README | 1 + .../api_builds/initial-service/alembic/env.py | 89 ++ .../initial-service/alembic/script.py.mako | 28 + .../api_builds/initial-service/app.py | 43 + .../initial-service/init_address.py | 144 +++ .../initial-service/init_alembic.py | 23 + .../initial-service/init_app_defaults.py | 660 +++++++++++++ .../api_builds/initial-service/init_enums.py | 266 ++++++ .../initial-service/init_occ_defaults.py | 299 ++++++ .../initial-service/init_occupant_types.py | 225 +++++ .../initial-service/init_services.py | 78 ++ .../initial-service/modules/Token/config.py | 15 + .../modules/Token/password_module.py | 44 + api_services/api_controllers/README.md | 143 +++ api_services/api_controllers/email/config.py | 31 + .../api_controllers/email/implementations.py | 29 + .../api_controllers/email/send_email.py | 90 ++ api_services/api_controllers/mongo/README.md | 219 +++++ api_services/api_controllers/mongo/config.py | 31 + .../api_controllers/mongo/database.py | 373 ++++++++ .../api_controllers/mongo/implementations.py | 519 ++++++++++ .../api_controllers/mongo/local_test.py | 93 ++ .../api_controllers/postgres/config.py | 31 + .../api_controllers/postgres/engine.py | 63 ++ .../api_controllers/postgres/mixin.py | 141 +++ .../api_controllers/redis/Broadcast/README.md | 67 ++ .../redis/Broadcast/actions.py | 248 +++++ .../redis/Broadcast/implementations.py | 205 ++++ api_services/api_controllers/redis/README.md | 85 ++ api_services/api_controllers/redis/base.py | 328 +++++++ api_services/api_controllers/redis/config.py | 25 + .../api_controllers/redis/connection.py | 215 +++++ .../api_controllers/redis/database.py | 355 +++++++ .../api_controllers/redis/implementations.py | 280 ++++++ .../api_controllers/redis/response.py | 200 ++++ api_services/api_initializer/app.py | 16 + api_services/api_initializer/config.py | 64 ++ api_services/api_initializer/create_app.py | 57 ++ api_services/api_initializer/create_route.py | 58 ++ .../api_initializer/event_clusters.py | 134 +++ .../api_initializer/open_api_creator.py | 116 +++ api_services/api_initializer/wh.py | 8 + .../api_middlewares/__init__.py | 0 .../api_middlewares/token_middleware.py | 24 + .../api_validations/a.txt | 0 .../api_validations/defaults/validations.py | 56 ++ api_services/api_validations/pydantic_core.py | 17 + api_services/api_validations/response/api.py | 53 ++ api_services/api_validations/response/base.py | 193 ++++ .../api_validations/response/example.py | 19 + .../api_validations/response/pagination.py | 70 ++ .../api_validations/response/result.py | 183 ++++ api_services/pyproject.toml | 28 + api_services/schemas/__init__.py | 209 +++++ api_services/schemas/account/account.py | 575 ++++++++++++ api_services/schemas/account/iban.py | 83 ++ api_services/schemas/address/address.py | 575 ++++++++++++ api_services/schemas/base_imports.py | 24 + api_services/schemas/building/budget.py | 159 ++++ api_services/schemas/building/build.py | 602 ++++++++++++ .../schemas/building/decision_book.py | 888 ++++++++++++++++++ api_services/schemas/company/company.py | 235 +++++ api_services/schemas/company/department.py | 82 ++ api_services/schemas/company/employee.py | 102 ++ api_services/schemas/event/event.py | 605 ++++++++++++ api_services/schemas/identity/identity.py | 488 ++++++++++ api_services/schemas/others/enums.py | 110 +++ api_services/schemas/rules/rules.py | 37 + {bank-services => bank_services}/README.md | 0 .../bank-email-service/README.md | 0 .../bank-parser-service/README.md | 0 .../bank-routine-email-service/README.md | 0 .../bank-sender-service/README.md | 0 .../bank-service-depends/README.md | 0 .../bank-writer-service/README.md | 0 .../z-bank-templates/template_accounts.html | 0 docker-compose.yml | 47 + pyproject.toml | 27 + .../client-frontend/README.md | 0 .../management-frontend/README.md | 0 .../web-controllers/README.md | 0 z-templates/a-service/a.txt | 0 z-templates/a-service/endpoints/index.py | 9 + z-templates/a-service/endpoints/routes.py | 15 + z-templates/a-service/events/index.py | 9 + 96 files changed, 11881 insertions(+) create mode 100644 api_services/.python-version rename {api-services/api-controllers => api_services}/README.md (100%) create mode 100644 api_services/api_builds/a.txt create mode 100644 api_services/api_builds/auth-service/Dockerfile create mode 100644 api_services/api_builds/auth-service/endpoints/__init__.py create mode 100644 api_services/api_builds/auth-service/endpoints/index.py create mode 100644 api_services/api_builds/auth-service/endpoints/routes.py create mode 100644 api_services/api_builds/auth-service/events/__init__.py create mode 100644 api_services/api_builds/auth-service/events/index.py create mode 100644 api_services/api_builds/initial-service/Dockerfile create mode 100644 api_services/api_builds/initial-service/alembic.ini create mode 100644 api_services/api_builds/initial-service/alembic/README create mode 100644 api_services/api_builds/initial-service/alembic/env.py create mode 100644 api_services/api_builds/initial-service/alembic/script.py.mako create mode 100644 api_services/api_builds/initial-service/app.py create mode 100644 api_services/api_builds/initial-service/init_address.py create mode 100644 api_services/api_builds/initial-service/init_alembic.py create mode 100644 api_services/api_builds/initial-service/init_app_defaults.py create mode 100644 api_services/api_builds/initial-service/init_enums.py create mode 100644 api_services/api_builds/initial-service/init_occ_defaults.py create mode 100644 api_services/api_builds/initial-service/init_occupant_types.py create mode 100644 api_services/api_builds/initial-service/init_services.py create mode 100644 api_services/api_builds/initial-service/modules/Token/config.py create mode 100644 api_services/api_builds/initial-service/modules/Token/password_module.py create mode 100644 api_services/api_controllers/README.md create mode 100644 api_services/api_controllers/email/config.py create mode 100644 api_services/api_controllers/email/implementations.py create mode 100644 api_services/api_controllers/email/send_email.py create mode 100644 api_services/api_controllers/mongo/README.md create mode 100644 api_services/api_controllers/mongo/config.py create mode 100644 api_services/api_controllers/mongo/database.py create mode 100644 api_services/api_controllers/mongo/implementations.py create mode 100644 api_services/api_controllers/mongo/local_test.py create mode 100644 api_services/api_controllers/postgres/config.py create mode 100644 api_services/api_controllers/postgres/engine.py create mode 100644 api_services/api_controllers/postgres/mixin.py create mode 100644 api_services/api_controllers/redis/Broadcast/README.md create mode 100644 api_services/api_controllers/redis/Broadcast/actions.py create mode 100644 api_services/api_controllers/redis/Broadcast/implementations.py create mode 100644 api_services/api_controllers/redis/README.md create mode 100644 api_services/api_controllers/redis/base.py create mode 100644 api_services/api_controllers/redis/config.py create mode 100644 api_services/api_controllers/redis/connection.py create mode 100644 api_services/api_controllers/redis/database.py create mode 100644 api_services/api_controllers/redis/implementations.py create mode 100644 api_services/api_controllers/redis/response.py create mode 100644 api_services/api_initializer/app.py create mode 100644 api_services/api_initializer/config.py create mode 100644 api_services/api_initializer/create_app.py create mode 100644 api_services/api_initializer/create_route.py create mode 100644 api_services/api_initializer/event_clusters.py create mode 100644 api_services/api_initializer/open_api_creator.py create mode 100644 api_services/api_initializer/wh.py rename api-services/api-initializer/README.md => api_services/api_middlewares/__init__.py (100%) create mode 100644 api_services/api_middlewares/token_middleware.py rename api-services/api-providers/README.md => api_services/api_validations/a.txt (100%) create mode 100644 api_services/api_validations/defaults/validations.py create mode 100644 api_services/api_validations/pydantic_core.py create mode 100644 api_services/api_validations/response/api.py create mode 100644 api_services/api_validations/response/base.py create mode 100644 api_services/api_validations/response/example.py create mode 100644 api_services/api_validations/response/pagination.py create mode 100644 api_services/api_validations/response/result.py create mode 100644 api_services/pyproject.toml create mode 100644 api_services/schemas/__init__.py create mode 100644 api_services/schemas/account/account.py create mode 100644 api_services/schemas/account/iban.py create mode 100644 api_services/schemas/address/address.py create mode 100644 api_services/schemas/base_imports.py create mode 100644 api_services/schemas/building/budget.py create mode 100644 api_services/schemas/building/build.py create mode 100644 api_services/schemas/building/decision_book.py create mode 100644 api_services/schemas/company/company.py create mode 100644 api_services/schemas/company/department.py create mode 100644 api_services/schemas/company/employee.py create mode 100644 api_services/schemas/event/event.py create mode 100644 api_services/schemas/identity/identity.py create mode 100644 api_services/schemas/others/enums.py create mode 100644 api_services/schemas/rules/rules.py rename {bank-services => bank_services}/README.md (100%) rename {bank-services => bank_services}/bank-email-service/README.md (100%) rename {bank-services => bank_services}/bank-parser-service/README.md (100%) rename {bank-services => bank_services}/bank-routine-email-service/README.md (100%) rename {bank-services => bank_services}/bank-sender-service/README.md (100%) rename {bank-services => bank_services}/bank-service-depends/README.md (100%) rename {bank-services => bank_services}/bank-writer-service/README.md (100%) rename {bank-services => bank_services}/z-bank-templates/template_accounts.html (100%) create mode 100644 docker-compose.yml create mode 100644 pyproject.toml rename {web-services => web_services}/client-frontend/README.md (100%) rename {web-services => web_services}/management-frontend/README.md (100%) rename {web-services => web_services}/web-controllers/README.md (100%) create mode 100644 z-templates/a-service/a.txt create mode 100644 z-templates/a-service/endpoints/index.py create mode 100644 z-templates/a-service/endpoints/routes.py create mode 100644 z-templates/a-service/events/index.py diff --git a/api_services/.python-version b/api_services/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/api_services/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/api-services/api-controllers/README.md b/api_services/README.md similarity index 100% rename from api-services/api-controllers/README.md rename to api_services/README.md diff --git a/api_services/api_builds/a.txt b/api_services/api_builds/a.txt new file mode 100644 index 0000000..b0343c2 --- /dev/null +++ b/api_services/api_builds/a.txt @@ -0,0 +1,5 @@ +1. endpoint uuid must be imported from a common folder +2. authservice must be seperated from application checkers service even though it serves values from redis +3. Initial services are not functioning well enough +4. ApiResponse and SystemResponse must be seperated +5. \ No newline at end of file diff --git a/api_services/api_builds/auth-service/Dockerfile b/api_services/api_builds/auth-service/Dockerfile new file mode 100644 index 0000000..c89f53b --- /dev/null +++ b/api_services/api_builds/auth-service/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 /api_services/api_initializer /api_initializer +COPY /api_services/api_controllers /api_controllers +COPY /api_services/schemas /schemas + +COPY /api_services/api_middlewares /middlewares +COPY /api_services/api_builds/auth-service/endpoints /api_initializer/endpoints +COPY /api_services/api_builds/auth-service/events /api_initializer/events +COPY /api_services/api_builds/auth-service/validations /api_initializer/validations + +# 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", "/api_initializer/app.py"] diff --git a/api_services/api_builds/auth-service/endpoints/__init__.py b/api_services/api_builds/auth-service/endpoints/__init__.py new file mode 100644 index 0000000..9015d41 --- /dev/null +++ b/api_services/api_builds/auth-service/endpoints/__init__.py @@ -0,0 +1,3 @@ + + +__all__ = [] \ No newline at end of file diff --git a/api_services/api_builds/auth-service/endpoints/index.py b/api_services/api_builds/auth-service/endpoints/index.py new file mode 100644 index 0000000..3775b02 --- /dev/null +++ b/api_services/api_builds/auth-service/endpoints/index.py @@ -0,0 +1,9 @@ + +endpoints_index: dict = { + "Name": "d538deb4-38f4-4913-a1af-bbef14cf6873", + "Slot1": "c0f5ccb1-1e56-4653-af13-ec0bf5e6aa51", + "Slot2": "034a7eb7-0186-4f48-bb8c-165c429ad5c1", + "Slot3": "ec1f3ec3-3f28-4eaf-b89a-c463632c0b90", + "Slot4": "2cf99f10-72f0-4c2b-98be-3082d67b950d", + "Slot5": "15c24c6c-651b-4c5d-9c2b-5c6c6c6c6c6c", +} diff --git a/api_services/api_builds/auth-service/endpoints/routes.py b/api_services/api_builds/auth-service/endpoints/routes.py new file mode 100644 index 0000000..b82ac19 --- /dev/null +++ b/api_services/api_builds/auth-service/endpoints/routes.py @@ -0,0 +1,15 @@ +from fastapi import APIRouter + + +def get_routes() -> list[APIRouter]: + return [] + + +def get_safe_endpoint_urls() -> list[tuple[str, str]]: + return [ + ("/", "GET"), + ("/docs", "GET"), + ("/redoc", "GET"), + ("/openapi.json", "GET"), + ("/metrics", "GET"), + ] diff --git a/api_services/api_builds/auth-service/events/__init__.py b/api_services/api_builds/auth-service/events/__init__.py new file mode 100644 index 0000000..9015d41 --- /dev/null +++ b/api_services/api_builds/auth-service/events/__init__.py @@ -0,0 +1,3 @@ + + +__all__ = [] \ No newline at end of file diff --git a/api_services/api_builds/auth-service/events/index.py b/api_services/api_builds/auth-service/events/index.py new file mode 100644 index 0000000..14b0856 --- /dev/null +++ b/api_services/api_builds/auth-service/events/index.py @@ -0,0 +1,10 @@ + + +events_index: dict = { + "Slot1": "", + "Slot2": "", + "Slot3": "", + "Slot4": "", + "Slot5": "", +} + diff --git a/api_services/api_builds/initial-service/Dockerfile b/api_services/api_builds/initial-service/Dockerfile new file mode 100644 index 0000000..1628d3b --- /dev/null +++ b/api_services/api_builds/initial-service/Dockerfile @@ -0,0 +1,25 @@ +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 /api_services/api_controllers /api_controllers +COPY /api_services/schemas /schemas + +COPY /api_services/api_builds/initial-service /initial-service +COPY /api_services/api_builds/initial-service / + +# 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", "initial-service/app.py"] diff --git a/api_services/api_builds/initial-service/alembic.ini b/api_services/api_builds/initial-service/alembic.ini new file mode 100644 index 0000000..38dbce0 --- /dev/null +++ b/api_services/api_builds/initial-service/alembic.ini @@ -0,0 +1,119 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +# Use forward slashes (/) also on windows to provide an os agnostic path +script_location = alembic + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library. +# Any required deps can installed by adding `alembic[tz]` to the pip requirements +# string value is passed to ZoneInfo() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +# version_path_separator = newline +# +# Use os.pathsep. Default configuration used for new projects. +version_path_separator = os + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = postgresql+psycopg2://postgres:password@10.10.2.14:5432/postgres + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the exec runner, execute a binary +# hooks = ruff +# ruff.type = exec +# ruff.executable = %(here)s/.venv/bin/ruff +# ruff.options = check --fix REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARNING +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARNING +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/api_services/api_builds/initial-service/alembic/README b/api_services/api_builds/initial-service/alembic/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/api_services/api_builds/initial-service/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/api_services/api_builds/initial-service/alembic/env.py b/api_services/api_builds/initial-service/alembic/env.py new file mode 100644 index 0000000..c007425 --- /dev/null +++ b/api_services/api_builds/initial-service/alembic/env.py @@ -0,0 +1,89 @@ +import os +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool +from sqlalchemy import create_engine + +from alembic import context +from schemas import * +from api_controllers.postgres.engine import Base + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Override sqlalchemy.url with environment variables if they exist +db_host = os.getenv("POSTGRES_HOST", None) +db_port = os.getenv("POSTGRES_PORT", None) +db_user = os.getenv("POSTGRES_USER", None) +db_password = os.getenv("POSTGRES_PASSWORD", None) +db_name = os.getenv("POSTGRES_DB", None) + +# Build the connection URL from environment variables +db_url = f"postgresql+psycopg2://{db_user}:{db_password}@{db_host}:{db_port}/{db_name}" + +# Override the sqlalchemy.url in the alembic.ini file +config.set_main_option("sqlalchemy.url", db_url) + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +target_metadata = Base.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure(connection=connection, target_metadata=target_metadata) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/api_services/api_builds/initial-service/alembic/script.py.mako b/api_services/api_builds/initial-service/alembic/script.py.mako new file mode 100644 index 0000000..480b130 --- /dev/null +++ b/api_services/api_builds/initial-service/alembic/script.py.mako @@ -0,0 +1,28 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + """Upgrade schema.""" + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + """Downgrade schema.""" + ${downgrades if downgrades else "pass"} diff --git a/api_services/api_builds/initial-service/app.py b/api_services/api_builds/initial-service/app.py new file mode 100644 index 0000000..e0c0ffa --- /dev/null +++ b/api_services/api_builds/initial-service/app.py @@ -0,0 +1,43 @@ +import os + +from api_controllers.postgres.engine import get_db +from init_app_defaults import create_application_defaults +from init_enums import init_api_enums_build_types +from init_alembic import generate_alembic +from init_occupant_types import create_occupant_types_defaults +from init_services import create_modules_and_services_and_actions +from init_address import create_one_address +from init_occ_defaults import create_occupant_defaults + +set_alembic = bool(int(os.getenv("SET_ALEMBIC"), 0)) + +if __name__ == "__main__": + print(f"Set alembic: {set_alembic}") + + with get_db() as db_session: + if set_alembic: + generate_alembic(session=db_session) + try: + create_one_address(db_session=db_session) + except Exception as e: + print(f"Error creating address: {e}") + try: + init_api_enums_build_types(db_session=db_session) + except Exception as e: + print(f"Error creating enums: {e}") + try: + create_application_defaults(db_session=db_session) + except Exception as e: + print(f"Error creating application defaults: {e}") + try: + create_occupant_types_defaults(db_session=db_session) + except Exception as e: + print(f"Error creating occupant types defaults: {e}") + try: + create_modules_and_services_and_actions(db_session=db_session) + except Exception as e: + print(f"Error creating modules and services and actions: {e}") + try: + create_occupant_defaults(db_session=db_session) + except Exception as e: + print(f"Error creating occupant defaults: {e}") diff --git a/api_services/api_builds/initial-service/init_address.py b/api_services/api_builds/initial-service/init_address.py new file mode 100644 index 0000000..caeca40 --- /dev/null +++ b/api_services/api_builds/initial-service/init_address.py @@ -0,0 +1,144 @@ +from schemas import ( + Addresses, + AddressCity, + AddressStreet, + AddressLocality, + AddressDistrict, + AddressNeighborhood, + AddressState, + AddressCountry, +) + + +def create_one_address(db_session): + + address_list = [] + AddressCountry.set_session(db_session) + country = AddressCountry.query.filter_by(country_name="TÜRKİYE", country_code="TR").first() + if not country: + country = AddressCountry.create( + country_name="TÜRKİYE", country_code="TR", is_confirmed=True + ) + country.save() + address_list.append(country) + else: + print(f"Country already exists {country.to_dict()}") + + AddressState.set_session(db_session) + state = AddressState.query.filter_by(state_name="TÜRKİYE", state_code="TR").first() + if not state: + state = AddressState.create( + state_name="TÜRKİYE", + state_code="TR", + phone_code="90", + country_id=country.id, + country_uu_id=str(country.uu_id), + is_confirmed=True, + ) + state.save() + address_list.append(state) + else: + print(f"State already exists {state.to_dict()}") + + AddressCity.set_session(db_session) + city = AddressCity.query.filter_by(city_name="ANKARA", city_code="6").first() + if not city: + city = AddressCity.create( + city_name="ANKARA", + city_code="6", + licence_plate="06", + state_id=state.id, + state_uu_id=str(state.uu_id), + is_confirmed=True, + ) + city.save() + address_list.append(city) + else: + print(f"City already exists {city.to_dict()}") + + AddressDistrict.set_session(db_session) + district = AddressDistrict.query.filter_by(district_name="ÇANKAYA", district_code="1231").first() + if not district: + district = AddressDistrict.create( + district_name="ÇANKAYA", + district_code="1231", + city_id=city.id, + city_uu_id=str(city.uu_id), + is_confirmed=True, + ) + district.save() + address_list.append(district) + else: + print(f"District already exists {district.to_dict()}") + + AddressLocality.set_session(db_session) + locality = AddressLocality.query.filter_by(locality_name="MERKEZ", locality_code="2431").first() + if not locality: + locality = AddressLocality.create( + locality_name="MERKEZ", + locality_code="2431", + type_code="3", + type_description=None, + district_id=district.id, + district_uu_id=str(district.uu_id), + is_confirmed=True, + ) + locality.save() + address_list.append(locality) + else: + print(f"Locality already exists {locality.to_dict()}") + + AddressNeighborhood.set_session(db_session) + neighborhood = AddressNeighborhood.query.filter_by(neighborhood_name="AYRANCI MAHALLESİ", neighborhood_code="1522").first() + if not neighborhood: + neighborhood = AddressNeighborhood.create( + neighborhood_name="AYRANCI MAHALLESİ", + neighborhood_code="1522", + type_code="1", + type_description="MAHALLESİ", + locality_id=locality.id, + locality_uu_id=str(locality.uu_id), + is_confirmed=True, + ) + neighborhood.save() + address_list.append(neighborhood) + else: + print(f"Neighborhood already exists {neighborhood.to_dict()}") + + AddressStreet.set_session(db_session) + street = AddressStreet.query.filter_by(street_name="REŞAT NURİ CADDESİ", street_code="52270").first() + if not street: + street = AddressStreet.create( + street_name="REŞAT NURİ CADDESİ", + type_description="CADDESİ", + type_code="3", + street_code="52270", + neighborhood_id=neighborhood.id, + neighborhood_uu_id=str(neighborhood.uu_id), + is_confirmed=True, + ) + street.save() + address_list.append(street) + else: + print(f"Street already exists {street.to_dict()}") + + Addresses.set_session(db_session) + address = Addresses.query.filter_by(street_id=street.id, street_uu_id=str(street.uu_id)).first() + if not address: + address = Addresses.create( + street_id=street.id, + street_uu_id=str(street.uu_id), + build_number="Ex1", + door_number="1", + floor_number="1", + comment_address="Example Address", + letter_address="Example Address", + short_letter_address="Example Address", + latitude=0, + longitude=0, + is_confirmed=True, + ) + address.save() + address_list.append(address) + else: + print(f"Address already exists {address.to_dict()}") diff --git a/api_services/api_builds/initial-service/init_alembic.py b/api_services/api_builds/initial-service/init_alembic.py new file mode 100644 index 0000000..26d1802 --- /dev/null +++ b/api_services/api_builds/initial-service/init_alembic.py @@ -0,0 +1,23 @@ +import os +from sqlalchemy import text + +def generate_alembic(session): + + try: + result = session.execute( + text( + "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = " + "'alembic_version') AS table_existence;" + ) + ) + if result.first()[0]: + session.execute(text("delete from alembic_version;")) + session.commit() + except Exception as e: + print(e) + finally: + run_command = "python -m alembic stamp head;" + run_command += ( + "python -m alembic revision --autogenerate;python -m alembic upgrade head;" + ) + os.system(run_command) diff --git a/api_services/api_builds/initial-service/init_app_defaults.py b/api_services/api_builds/initial-service/init_app_defaults.py new file mode 100644 index 0000000..3e3765d --- /dev/null +++ b/api_services/api_builds/initial-service/init_app_defaults.py @@ -0,0 +1,660 @@ +import arrow + +from modules.Token.password_module import PasswordModule +from api_controllers.mongo.database import mongo_handler +from schemas import ( + Companies, + Departments, + Duty, + Duties, + Employees, + People, + Users, + Staff, + RelationshipDutyCompany, +) + +def create_application_defaults(db_session): + created_list, created_by, confirmed_by = [], "System", "System" + active_row = dict(is_confirmed=True, active=True, deleted=False, is_notification_send=True) + Companies.set_session(db_session) + Departments.set_session(db_session) + Duties.set_session(db_session) + Duty.set_session(db_session) + Staff.set_session(db_session) + People.set_session(db_session) + Users.set_session(db_session) + Employees.set_session(db_session) + RelationshipDutyCompany.set_session(db_session) + + company_management = Companies.query.filter_by(company_tag="Evyos",).first() + if not company_management: + company_management = Companies.find_or_create( + **{ + "formal_name": "Evyos LTD", + "public_name": "Evyos Verimlilik Sistemleri", + "company_type": "LTD", + "commercial_type": "Commercial", + "tax_no": "123132123132", + "company_tag": "Evyos", + "default_lang_type": "TR", + "default_money_type": "TL", + "is_commercial": True, + "is_confirmed": True, + } + ) + created_list.append(company_management) + else: + print(f"Company Management Found {company_management.to_dict()}") + company_id, company_uu_id = company_management.id, str(company_management.uu_id) + + execution = Departments.query.filter_by(department_code="EO001", company_id=company_id).first() + if not execution: + execution = Departments.create( + department_name="Execution Office", + department_code="EO001", + company_id=company_id, + company_uu_id=str(company_uu_id), + **active_row, + ) + created_list.append(execution) + else: + print(f"Execution Found {execution.to_dict()}") + + gen_man = Departments.query.filter_by(department_code="GM001", company_id=company_id).first() + if not gen_man: + gen_man = Departments.create( + department_name="General Manager Example", + department_code="GM001", + company_id=company_id, + company_uu_id=str(company_uu_id), + **active_row, + ) + created_list.append(gen_man) + else: + print(f"General Manager Found {gen_man.to_dict()}") + + it_dept = Departments.query.filter_by(department_code="ITD001", company_id=company_id).first() + if not it_dept: + it_dept = Departments.create( + department_name="IT Department", + department_code="ITD001", + company_id=company_id, + company_uu_id=str(company_uu_id), + **active_row, + ) + created_list.append(it_dept) + else: + print(f"IT Department Found {it_dept.to_dict()}") + + gen_duty = Duty.query.filter_by(duty_code="GM0001").first() + if not gen_duty: + gen_duty = Duty.create( + duty_name="General Manager", + duty_code="GM0001", + duty_description="General Manager", + **active_row, + ) + created_list.append(gen_duty) + else: + print(f"General Manager Found {gen_duty.to_dict()}") + + bm_duty = Duty.query.filter_by(duty_code="BM0001").first() + if not bm_duty: + bm_duty = Duty.create( + duty_name="Business Manager", + duty_code="BM0001", + duty_description="Business Manager", + **active_row, + ) + created_list.append(bm_duty) + else: + print(f"Business Manager Found {bm_duty.to_dict()}") + it_duty = Duty.query.filter_by(duty_code="IT0001").first() + if not it_duty: + it_duty = Duty.create( + duty_name="IT Manager", + duty_code="IT0001", + duty_description="IT Manager", + **active_row, + ) + created_list.append(it_duty) + else: + print(f"IT Manager Found {it_duty.to_dict()}") + bulk_duty = Duty.query.filter_by(duty_code="BULK").first() + if not bulk_duty: + bulk_duty = Duty.create( + duty_name="BULK", + duty_code="BULK", + duty_description="BULK RECORDS OF THE COMPANY", + **active_row, + ) + created_list.append(bulk_duty) + else: + print(f"Bulk Duty Found {bulk_duty.to_dict()}") + occu_duty = Duty.query.filter_by(duty_code="OCCUPANT").first() + if not occu_duty: + occu_duty = Duty.create( + duty_name="OCCUPANT", + duty_code="OCCUPANT", + duty_description="OCCUPANT RECORDS OF THE COMPANY", + **active_row, + ) + created_list.append(occu_duty) + else: + print(f"Occupant Duty Found {occu_duty.to_dict()}") + + duties_gen_man = Duties.query.filter_by(company_id=company_id, duties_id=gen_duty.id, department_id=gen_man.id).first() + if not duties_gen_man: + duties_gen_man = Duties.create( + company_id=company_id, + company_uu_id=str(company_uu_id), + duties_id=gen_duty.id, + duties_uu_id=str(gen_duty.uu_id), + department_id=gen_man.id, + department_uu_id=str(gen_man.uu_id), + **active_row, + ) + created_list.append(duties_gen_man) + else: + print(f"Duties General Manager Found {duties_gen_man.to_dict()}") + + duties_created_bm = Duties.query.filter_by(company_id=company_id, duties_id=bm_duty.id, department_id=execution.id).first() + if not duties_created_bm: + duties_created_bm = Duties.create( + company_id=company_id, + company_uu_id=str(company_uu_id), + duties_id=bm_duty.id, + duties_uu_id=str(bm_duty.uu_id), + department_id=execution.id, + department_uu_id=str(execution.uu_id), + **active_row, + ) + created_list.append(duties_created_bm) + else: + print(f"Duties Business Manager Found {duties_created_bm.to_dict()}") + duties_created_it = Duties.query.filter_by(company_id=company_id, duties_id=bulk_duty.id, department_id=execution.id).first() + if not duties_created_it: + duties_created_it = Duties.create( + company_id=company_id, + company_uu_id=str(company_uu_id), + duties_id=bulk_duty.id, + duties_uu_id=str(bulk_duty.uu_id), + department_id=execution.id, + department_uu_id=str(execution.uu_id), + **active_row, + ) + created_list.append(duties_created_it) + else: + print(f"Duties Bulk Found {duties_created_it.to_dict()}") + duties_created_occupant = Duties.query.filter_by(company_id=company_id, duties_id=occu_duty.id, department_id=execution.id).first() + if not duties_created_occupant: + duties_created_occupant = Duties.create( + company_id=company_id, + company_uu_id=str(company_uu_id), + duties_id=occu_duty.id, + duties_uu_id=str(occu_duty.uu_id), + department_id=execution.id, + department_uu_id=str(execution.uu_id), + **active_row, + ) + created_list.append(duties_created_occupant) + else: + print(f"Duties Occupant Found {duties_created_occupant.to_dict()}") + bulk_duty = Duty.query.filter_by(duty_code="BULK").first() + if not bulk_duty: + bulk_duty = Duty.create( + duty_name="BULK", + duty_code="BULK", + duty_description="BULK RECORDS OF THE COMPANY", + **active_row, + ) + created_list.append(bulk_duty) + else: + print(f"Bulk Duty Found {bulk_duty.to_dict()}") + it_dept = Departments.query.filter_by(department_code="ITD001", company_id=company_id).first() + if not it_dept: + it_dept = Departments.create( + department_name="IT Department", + department_code="ITD001", + company_id=company_id, + company_uu_id=str(company_uu_id), + **active_row, + ) + created_list.append(it_dept) + else: + print(f"IT Department Found {it_dept.to_dict()}") + created_duty = Duty.query.filter_by(duty_code="DM").first() + if not created_duty: + created_duty = Duty.create( + duty_name="Database Manager", + duty_code="DM", + duty_description="Database Manager", + created_by=created_by, + confirmed_by=confirmed_by, + is_confirmed=True, + active=True, + deleted=False, + is_notification_send=True, + ) + created_list.append(created_duty) + created_duty = Duty.query.filter_by(duty_code="NM").first() + if not created_duty: + created_duty = Duty.create( + duty_name="Network Manager", + duty_code="NM", + duty_description="Network Manager", + created_by=created_by, + confirmed_by=confirmed_by, + is_confirmed=True, + active=True, + deleted=False, + is_notification_send=True, + ) + created_list.append(created_duty) + application_manager_duty = Duty.query.filter_by(duty_code="AM").first() + if not application_manager_duty: + application_manager_duty = Duty.create( + duty_name="Application Manager", + duty_code="AM", + duty_description="Application Manager", + created_by=created_by, + confirmed_by=confirmed_by, + is_confirmed=True, + active=True, + deleted=False, + is_notification_send=True, + ) + created_list.append(application_manager_duty) + application_super_user_duty = Duty.query.filter_by(duty_code="SUE").first() + if not application_super_user_duty: + application_super_user_duty = Duty.create( + duty_name="Super User", + duty_code="SUE", + duty_description="Super User", + created_by=created_by, + confirmed_by=confirmed_by, + **active_row, + ) + created_list.append(application_super_user_duty) + + application_manager_duties = Duties.query.filter_by( + department_id=it_dept.id, + duties_id=application_manager_duty.id, + company_id=company_id, + ).first() + if not application_manager_duties: + application_manager_duties = Duties.create( + department_id=it_dept.id, + department_uu_id=str(it_dept.uu_id), + duties_id=application_manager_duty.id, + duties_uu_id=str(application_manager_duty.uu_id), + company_id=company_id, + company_uu_id=str(company_uu_id), + **active_row, + ) + created_list.append(application_manager_duties) + else: + print(f"Application Manager Duties Found {application_manager_duties.to_dict()}") + + + super_user_duties = Duties.query.filter_by( + department_id=it_dept.id, + duties_id=application_super_user_duty.id, + company_id=company_id, + ).first() + if not super_user_duties: + super_user_duties = Duties.create( + department_id=it_dept.id, + department_uu_id=str(it_dept.uu_id), + duties_id=application_super_user_duty.id, + duties_uu_id=str(application_manager_duty.uu_id), + company_id=company_id, + company_uu_id=str(company_uu_id), + **active_row, + ) + created_list.append(super_user_duties) + else: + print(f"Super User Duties Found {super_user_duties.to_dict()}") + relation_super_user_duties = RelationshipDutyCompany.query.filter_by( + duties_id=super_user_duties.id, + owner_id=company_id, + member_id=company_id, + ).first() + if not relation_super_user_duties: + relation_super_user_duties = RelationshipDutyCompany.create( + duties_id=super_user_duties.id, + owner_id=company_id, + member_id=company_id, + parent_id=None, + child_count=0, + **active_row, + ) + created_list.append(super_user_duties) + + relation_application_manager_duties = RelationshipDutyCompany.query.filter_by( + duties_id=application_manager_duties.id, + owner_id=company_id, + member_id=company_id, + ).first() + if not relation_application_manager_duties: + relation_application_manager_duties = RelationshipDutyCompany.create( + duties_id=application_manager_duties.id, + owner_id=company_id, + member_id=company_id, + parent_id=None, + child_count=0, + **active_row, + ) + created_list.append(relation_application_manager_duties) + + app_manager = People.query.filter_by( + person_tag="BAM-System", + ).first() + if not app_manager: + app_manager = People.create( + **{ + "person_tag": "BAM-System", + "firstname": "Berkay Application Manager", + "surname": "Karatay", + "sex_code": "M", + "middle_name": "", + "father_name": "Father", + "mother_name": "Mother", + "country_code": "TR", + "national_identity_id": "12312312312", + "birth_place": "Ankara", + "birth_date": "01.07.1990", + "tax_no": "1231231231", + **active_row, + }, + ) + created_list.append(app_manager) + else: + print(f"Application Manager Found {app_manager.to_dict()}") + + sup_manager = People.query.filter_by(person_tag="BSU-System").first() + if not sup_manager: + sup_manager = People.create( + **{ + "person_tag": "BSU-System", + "firstname": "Berkay Super User", + "surname": "Karatay", + "sex_code": "M", + "middle_name": "", + "father_name": "Father", + "mother_name": "Mother", + "country_code": "TR", + "national_identity_id": "12312312313", + "birth_place": "Ankara", + "birth_date": "01.07.1990", + "tax_no": "1231231232", + **active_row, + }, + ) + created_list.append(sup_manager) + else: + print(f"Super User Found {sup_manager.to_dict()}") + + gen_manager_people = People.query.filter_by(person_tag="BM-System").first() + if not gen_manager_people: + gen_manager_people = People.create( + **{ + "person_tag": "BM-System", + "firstname": "Example General Manager", + "surname": "Example", + "sex_code": "M", + "middle_name": "", + "father_name": "Father", + "mother_name": "Mother", + "country_code": "TR", + "national_identity_id": "12312312314", + "birth_place": "Ankara", + "birth_date": "01.07.1990", + "tax_no": "1231231233", + **active_row, + }, + ) + created_list.append(gen_manager_people) + else: + print(f"General Manager Found {gen_manager_people.to_dict()}") + + application_manager_staff = Staff.query.filter_by(staff_code="AME", duties_id=application_manager_duties.id).first() + if not application_manager_staff: + application_manager_staff = Staff.create( + **{ + "staff_code": "AME", + "staff_name": "Application Manager Employee", + "staff_description": "Application Manager Employee", + "duties_id": application_manager_duties.id, + "duties_uu_id": str(application_manager_duty.uu_id), + **active_row, + }, + ) + created_list.append(application_manager_staff) + else: + print(f"Application Manager Found {application_manager_staff.to_dict()}") + + super_user_staff = Staff.query.filter_by(staff_code="SUE", duties_id=super_user_duties.id).first() + if not super_user_staff: + super_user_staff = Staff.create( + **{ + "staff_code": "SUE", + "staff_name": "Super User Employee", + "staff_description": "Super User Employee", + "duties_id": super_user_duties.id, + "duties_uu_id": str(super_user_duties.uu_id), + **active_row, + }, + ) + created_list.append(super_user_staff) + else: + print(f"Super User Found {super_user_staff.to_dict()}") + + gen_manager_staff = Staff.query.filter_by(staff_code="GME", duties_id=duties_gen_man.id).first() + if not gen_manager_staff: + gen_manager_staff = Staff.create( + **{ + "staff_code": "GME", + "staff_name": "General Manager Employee", + "staff_description": "General Manager Employee", + "duties_id": duties_gen_man.id, + "duties_uu_id": str(duties_gen_man.uu_id), + **active_row, + }, + ) + created_list.append(gen_manager_staff) + else: + print(f"General Manager Found {gen_manager_staff.to_dict()}") + + application_manager_staff = Staff.query.filter_by(staff_code="AME", duties_id=application_manager_duty.id).first() + if not application_manager_staff: + application_manager_staff = Staff.create( + **{ + "staff_code": "AME", + "staff_name": "Application Manager Employee", + "staff_description": "Application Manager Employee", + "duties_id": application_manager_duty.id, + "duties_uu_id": str(application_manager_duty.uu_id), + **active_row, + }, + ) + created_list.append(application_manager_staff) + gen_man_staff = Staff.query.filter_by(staff_code="GME", duties_id=duties_gen_man.id).first() + if not gen_man_staff: + gen_man_staff = Staff.create( + **{ + "staff_code": "GME", + "staff_name": "General Manager Employee", + "staff_description": "General Manager Employee", + "duties_id": duties_gen_man.id, + "duties_uu_id": str(gen_duty.uu_id), + **active_row, + }, + ) + created_list.append(gen_man_staff) + + gen_man_employee = Employees.query.filter_by(staff_id=gen_man_staff.id, people_id=gen_manager_people.id).first() + if not gen_man_employee: + gen_man_employee = Employees.create( + staff_id=gen_man_staff.id, + staff_uu_id=str(gen_man_staff.uu_id), + people_id=gen_manager_people.id, + people_uu_id=str(gen_manager_people.uu_id), + **active_row, + ) + created_list.append(gen_man_employee) + + app_manager_employee = Employees.query.filter_by(staff_id=application_manager_staff.id, people_id=app_manager.id).first() + if not app_manager_employee: + app_manager_employee = Employees.create( + staff_id=application_manager_staff.id, + staff_uu_id=str(application_manager_staff.uu_id), + people_id=app_manager.id, + people_uu_id=str(app_manager.uu_id), + **active_row, + ) + created_list.append(app_manager_employee) + + super_user_employee = Employees.query.filter_by(staff_id=super_user_staff.id, people_id=sup_manager.id).first() + if not super_user_employee: + super_user_employee = Employees.create( + staff_id=super_user_staff.id, + staff_uu_id=str(super_user_staff.uu_id), + people_id=sup_manager.id, + people_uu_id=str(sup_manager.uu_id), + **active_row, + ) + created_list.append(super_user_employee) + + gen_manager_user = Users.query.filter_by(person_id=gen_manager_people.id, user_tag=gen_manager_people.person_tag).first() + if not gen_manager_user: + gen_manager_user = Users.create( + person_id=gen_manager_people.id, + person_uu_id=str(gen_manager_people.uu_id), + user_tag=gen_manager_people.person_tag, + email="example.general@evyos.com.tr", + phone_number="+901111111111", + avatar="https://s.tmimgcdn.com/scr/800x500/276800/building-home-nature-logo-vector-template-3_276851-original.jpg", + related_company=str(company_management.uu_id), + **active_row, + ) + created_list.append(gen_manager_user) + gen_manager_user.password_expiry_begins = str(arrow.now()) + gen_manager_user.password_token = PasswordModule.generate_refresher_token() + main_domain, collection_name = ( + "evyos.com.tr", + f"{str(company_management.uu_id)}*Domain", + ) + with mongo_handler.collection(collection_name) as mongo_engine: + existing_record = mongo_engine.find_one( + {"user_uu_id": str(gen_manager_user.uu_id)} + ) + if not existing_record: + mongo_engine.insert_one( + document={ + "user_uu_id": str(gen_manager_user.uu_id), + "other_domains_list": [main_domain], + "main_domain": main_domain, + "modified_at": arrow.now().timestamp(), + } + ) + else: + mongo_engine.update_one( + {"user_uu_id": str(gen_manager_user.uu_id)}, + { + "$set": { + "other_domains_list": [main_domain], + "main_domain": main_domain, + "modified_at": arrow.now().timestamp(), + } + }, + ) + + app_manager_user = Users.query.filter_by(person_id=app_manager.id, user_tag=app_manager.person_tag).first() + if not app_manager_user: + app_manager_user = Users.create( + person_id=app_manager.id, + user_tag=app_manager.person_tag, + email="karatay.berkay.man@evyos.com.tr", + phone_number="+901111111111", + avatar="https://s.tmimgcdn.com/scr/800x500/276800/building-home-nature-logo-vector-template-3_276851-original.jpg", + related_company=str(company_management.uu_id), + **active_row, + ) + created_list.append(app_manager_user) + app_manager_user.password_expiry_begins = str(arrow.now()) + app_manager_user.password_token = PasswordModule.generate_refresher_token() + + with mongo_handler.collection(collection_name) as mongo_engine: + existing_record = mongo_engine.find_one( + {"user_uu_id": str(app_manager_user.uu_id)} + ) + if not existing_record: + mongo_engine.insert_one( + document={ + "user_uu_id": str(app_manager_user.uu_id), + "other_domains_list": [main_domain], + "main_domain": main_domain, + "modified_at": arrow.now().timestamp(), + } + ) + else: + mongo_engine.update_one( + {"user_uu_id": str(app_manager_user.uu_id)}, + { + "$set": { + "other_domains_list": [main_domain], + "main_domain": main_domain, + "modified_at": arrow.now().timestamp(), + } + }, + ) + + sup_manager_user = Users.query.filter_by(person_id=sup_manager.id, user_tag=sup_manager.person_tag).first() + if not sup_manager_user: + sup_manager_user = Users.create( + person_id=sup_manager.id, + user_tag=sup_manager.person_tag, + email="karatay.berkay.sup@evyos.com.tr", + phone_number="+901111111112", + avatar="https://s.tmimgcdn.com/scr/800x500/276800/building-home-nature-logo-vector-template-3_276851-original.jpg", + created_by=created_by, + confirmed_by=confirmed_by, + related_company=str(company_management.uu_id), + **active_row, + ) + created_list.append(sup_manager_user) + + sup_manager_user.password_expiry_begins = str(arrow.now()) + sup_manager_user.password_token = PasswordModule.generate_refresher_token() + with mongo_handler.collection(collection_name) as mongo_engine: + existing_record = mongo_engine.find_one( + {"user_uu_id": str(sup_manager_employee.uu_id)} + ) + if not existing_record: + print("insert sup existing record", existing_record) + mongo_engine.insert_one( + document={ + "user_uu_id": str(sup_manager_employee.uu_id), + "other_domains_list": [main_domain, "management.com.tr"], + "main_domain": main_domain, + "modified_at": arrow.now().timestamp(), + } + ) + else: + print("update sup existing record", existing_record) + # Optionally update the existing record if needed + mongo_engine.update_one( + {"user_uu_id": str(sup_manager_employee.uu_id)}, + { + "$set": { + "other_domains_list": [main_domain, "management.com.tr"], + "main_domain": main_domain, + "modified_at": arrow.now().timestamp(), + } + }, + ) + + db_session.commit() + print("All Defaults Create is now completed") diff --git a/api_services/api_builds/initial-service/init_enums.py b/api_services/api_builds/initial-service/init_enums.py new file mode 100644 index 0000000..ab7ecfb --- /dev/null +++ b/api_services/api_builds/initial-service/init_enums.py @@ -0,0 +1,266 @@ +from pydantic import BaseModel +from schemas import BuildTypes, ApiEnumDropdown + + +class InsertBuildTypes(BaseModel): + function_code: str + type_code: str + lang: str + type_name: str + + +def init_api_enums_build_types(db_session): + + BuildTypes.set_session(db_session) + ApiEnumDropdown.set_session(db_session) + + insert_types = [ + { + "function_code": "EVYOS", + "type_code": "APT_KZN", + "type_name": "Apartman Kazan Dairesi", + "lang": "TR", + }, + { + "function_code": "EVYOS", + "type_code": "APT_GRJ", + "type_name": "Apartman Garaj", + "lang": "TR", + }, + { + "function_code": "EVYOS", + "type_code": "APT_DP", + "type_name": "Apartman Depo", + "lang": "TR", + }, + { + "function_code": "EVYOS", + "type_code": "DAIRE", + "type_name": "Apartman Dairesi", + "lang": "TR", + }, + { + "function_code": "EVYOS", + "type_code": "APT", + "type_name": "Apartman Binası", + "lang": "TR", + }, + { + "function_code": "EVYOS", + "type_code": "APT_YNT", + "type_name": "Apartman Yönetimi", + "lang": "TR", + }, + { + "function_code": "EVYOS", + "type_code": "APT_PRK", + "type_name": "Apartman Açık Park Alanı", + "lang": "TR", + }, + { + "function_code": "EVYOS", + "type_code": "APT_YSL", + "type_name": "Apartman Yeşil Alan", + "lang": "TR", + }, + { + "function_code": "EVYOS", + "type_code": "APT_YOL", + "type_name": "Apartman Ara Yol", + "lang": "TR", + }, + ] + + for insert_type in insert_types: + build_types = InsertBuildTypes( + function_code="EVYOS", + lang=insert_type["lang"], + type_code=str(insert_type["type_code"]).upper(), + type_name=insert_type["type_name"], + ) + created_build_type = BuildTypes.query.filter_by( + function_code=build_types.function_code, + type_code=build_types.type_code, + ).first() + if not created_build_type: + created_build_type = BuildTypes.find_or_create( + **build_types.model_dump(), is_confirmed=True, db=db_session + ) + created_build_type.save() + + insert_enums = [ + {"enum_class": "BuildDuesTypes", "type_code": "BDT-D", "type_name": "Debit"}, + { + "enum_class": "BuildDuesTypes", + "type_code": "BDT-A", + "type_name": "Add Debit", + }, + { + "enum_class": "BuildDuesTypes", + "type_code": "BDT-R", + "type_name": "Renovation", + }, + { + "enum_class": "BuildDuesTypes", + "type_code": "BDT-L", + "type_name": "Lawyer expence", + }, + { + "enum_class": "BuildDuesTypes", + "type_code": "BDT-S", + "type_name": "Service fee", + }, + { + "enum_class": "BuildDuesTypes", + "type_code": "BDT-I", + "type_name": "Information", + }, + { + "enum_class": "AccountingReceiptTypes", + "type_code": "ART-A", + "type_name": "Kasa Tahsil Fişi", + }, + { + "enum_class": "AccountingReceiptTypes", + "type_code": "ART-E", + "type_name": "Kasa Tediye Fişi", + }, + { + "enum_class": "AccountingReceiptTypes", + "type_code": "ART-M", + "type_name": "Mahsup Fişi", + }, + { + "enum_class": "AccountingReceiptTypes", + "type_code": "ART-O", + "type_name": "Açılış Fişi", + }, + { + "enum_class": "AccountingReceiptTypes", + "type_code": "ART-C", + "type_name": "Kapanış Fişi", + }, + {"enum_class": "IbanBudgetType", "type_code": "IBT-I", "type_name": "Iban"}, + {"enum_class": "IbanBudgetType", "type_code": "IBT-B", "type_name": "Budget"}, + { + "enum_class": "IbanBudgetType", + "type_code": "IBT-TR", + "type_name": "Transaction records", + }, + {"enum_class": "ProjectTypes", "type_code": "R", "type_name": "Tadilat"}, + { + "enum_class": "ProjectTypes", + "type_code": "PT-C", + "type_name": "Mahkeme süreçleri", + }, + { + "enum_class": "ProjectTypes", + "type_code": "PT-Z", + "type_name": "Sıfır Bakiye", + }, + { + "enum_class": "EdmBudgetType", + "type_code": "PT-B", + "type_name": "Banka records", + }, + { + "enum_class": "EdmBudgetType", + "type_code": "PT-S", + "type_name": "Sistem kaydı", + }, + { + "enum_class": "EdmBudgetType", + "type_code": "EBT-C", + "type_name": "Build, Flat or Site records", + }, + {"enum_class": "ExpireType", "type_code": "1", "type_name": "daily"}, + {"enum_class": "ExpireType", "type_code": "7", "type_name": "weekly"}, + {"enum_class": "ExpireType", "type_code": "30", "type_name": "monthly"}, + {"enum_class": "ExpireType", "type_code": "90", "type_name": "quarter"}, + {"enum_class": "ExpireType", "type_code": "180", "type_name": "six_month"}, + {"enum_class": "ExpireType", "type_code": "365", "type_name": "yearly"}, + {"enum_class": "PhoneType", "type_code": "M", "type_name": "cep tel"}, + {"enum_class": "PhoneType", "type_code": "L", "type_name": "sabit telefon"}, + {"enum_class": "PhoneType", "type_code": "F", "type_name": "fax"}, + {"enum_class": "PhoneType", "type_code": "C", "type_name": "santral"}, + { + "enum_class": "PhoneType", + "type_code": "G", + "type_name": "ülke genelindeki hatlar 444", + }, + {"enum_class": "PerComType", "type_code": "1", "type_name": "Person"}, + {"enum_class": "PerComType", "type_code": "2", "type_name": "Company"}, + {"enum_class": "Directions", "type_code": "NN", "type_name": "North"}, + {"enum_class": "Directions", "type_code": "EE", "type_name": "East"}, + {"enum_class": "Directions", "type_code": "SS", "type_name": "South"}, + {"enum_class": "Directions", "type_code": "WW", "type_name": "West"}, + {"enum_class": "Directions", "type_code": "NE", "type_name": "North East"}, + {"enum_class": "Directions", "type_code": "NW", "type_name": "North West"}, + {"enum_class": "Directions", "type_code": "SE", "type_name": "South East"}, + {"enum_class": "Directions", "type_code": "SW", "type_name": "South West"}, + { + "enum_class": "MeetingTypes", + "type_code": "MT-RBM", + "type_name": "Regular Building Meeting", + }, + { + "enum_class": "MeetingTypes", + "type_code": "MT-DBM", + "type_name": "Disaster Building Meeting", + }, + { + "enum_class": "MeetingTypes", + "type_code": "MT-EBM", + "type_name": "Emergency Building Meeting", + }, + { + "enum_class": "DebitTypes", + "type_code": "DT-D", + "type_name": "Debit Sender", + }, + { + "enum_class": "DebitTypes", + "type_code": "DT-R", + "type_name": "Credit Receiver", + }, + { + "enum_class": "DebitTypes", + "type_code": "DT-Z", + "type_name": "Zero Balance", + }, + { + "enum_class": "TimePeriod", + "type_code": "TP-W", + "type_name": "Weekly", + }, + { + "enum_class": "TimePeriod", + "type_code": "TP-M", + "type_name": "Monthly", + }, + { + "enum_class": "TimePeriod", + "type_code": "TP-Q", + "type_name": "Quarterly", + }, + { + "enum_class": "TimePeriod", + "type_code": "TP-Y", + "type_name": "Yearly", + }, + ] + + for insert_enum in insert_enums: + created_api_enum = ApiEnumDropdown.query.filter_by( + enum_class=insert_enum["enum_class"], + key=str(insert_enum["type_code"]).upper(), + ).first() + if not created_api_enum: + created_api_enum = ApiEnumDropdown.create( + enum_class=insert_enum["enum_class"], + value=insert_enum["type_name"], + key=str(insert_enum["type_code"]).upper(), + description=insert_enum["type_name"], + is_confirmed=True, + ) + created_api_enum.save() diff --git a/api_services/api_builds/initial-service/init_occ_defaults.py b/api_services/api_builds/initial-service/init_occ_defaults.py new file mode 100644 index 0000000..ca3387c --- /dev/null +++ b/api_services/api_builds/initial-service/init_occ_defaults.py @@ -0,0 +1,299 @@ +import arrow +from modules.Token.password_module import PasswordModule +from api_controllers.mongo.database import mongo_handler +from schemas import ( + Addresses, + BuildLivingSpace, + Users, + People, + Build, + BuildParts, + BuildTypes, + ApiEnumDropdown, + Companies, + OccupantTypes, +) + +def create_occupant_defaults(db_session): + + created_list = [] + Addresses.set_session(db_session) + BuildLivingSpace.set_session(db_session) + Users.set_session(db_session) + People.set_session(db_session) + Build.set_session(db_session) + BuildParts.set_session(db_session) + BuildTypes.set_session(db_session) + ApiEnumDropdown.set_session(db_session) + Companies.set_session(db_session) + OccupantTypes.set_session(db_session) + + company_management = Companies.query.filter_by(formal_name = "Evyos LTD",).first() + if not company_management: + raise Exception("Company not found") + + company_id, company_uu_id = company_management.id, str(company_management.uu_id) + active_row = dict(is_confirmed=True, active=True, deleted=False, is_notification_send=True) + build_type = BuildTypes.query.filter_by(type_code = "APT").first() + address = Addresses.query.filter_by(letter_address = "Example Address").first() + + created_build = Build.query.filter_by(build_name = "Build Example").first() + if not created_build: + created_build = Build.create( + build_name="Build Example", + build_code="B001", + build_no="B001", + build_date="01.07.1980", + address_id=address.id, + address_uu_id=str(address.uu_id), + build_types_id=build_type.id, + build_types_uu_id=str(build_type.uu_id), + **active_row + ) + created_list.append(created_build) + + build_type_created = BuildTypes.query.filter_by(type_code = "APT").first() + build_type_flat = BuildTypes.query.filter_by(type_code = "DAIRE").first() + enum_dropdown = ApiEnumDropdown.query.filter_by(key = "NE", enum_class = "Directions").first() + + occupant_type_prs = OccupantTypes.query.filter_by(occupant_code = "MT-PRS").first() + occupant_type_owner = OccupantTypes.query.filter_by(occupant_code = "FL-OWN").first() + occupant_type_tenant = OccupantTypes.query.filter_by(occupant_code = "FL-TEN").first() + created_managment_room = BuildParts.query.filter_by(part_code = "MR001").first() + + if not created_managment_room: + created_managment_room = BuildParts.create( + address_gov_code="123123123123", + build_id=created_build.id, + build_uu_id=str(created_build.uu_id), + part_code="MR001", + part_net_size=100, + part_no=0, + part_level=0, + part_type_id=build_type_created.id, + part_type_uu_id=str(build_type_created.uu_id), + part_direction_id=enum_dropdown.id, + part_direction_uu_id=str(enum_dropdown.uu_id), + human_livable=True, + due_part_key="Example", + **active_row, + ) + created_list.append(created_managment_room) + + created_flat = BuildParts.query.filter_by(part_code = "MF001").first() + if not created_flat: + created_flat = BuildParts.create( + address_gov_code="123123123124", + build_id=created_build.id, + build_uu_id=str(created_build.uu_id), + part_code="MF001", + part_net_size=100, + part_no=1, + part_level=1, + part_type_id=build_type_flat.id, + part_type_uu_id=str(build_type_flat.uu_id), + part_direction_id=enum_dropdown.id, + part_direction_uu_id=str(enum_dropdown.uu_id), + human_livable=True, + due_part_key="Example", + **active_row, + ) + created_list.append(created_flat) + + build_manager_people = People.query.filter_by(person_tag = "Build Manager Example").first() + if not build_manager_people: + build_manager_people = People.create( + **{ + "person_tag": "Build Manager Example", + "firstname": "Example Build Manager", + "surname": "Example", + "sex_code": "M", + "middle_name": "", + "father_name": "Father", + "mother_name": "Mother", + "country_code": "TR", + "national_identity_id": "12312312315", + "birth_place": "Ankara", + "birth_date": "01.07.1990", + "tax_no": "1231231234", + } + ) + created_list.append(build_manager_people) + + owner_people = People.query.filter_by(person_tag = "Owner Example").first() + if not owner_people: + owner_people = People.create( + **{ + "person_tag": "Owner Example", + "firstname": "Example Owner", + "surname": "Example", + "sex_code": "M", + "middle_name": "", + "father_name": "Father", + "mother_name": "Mother", + "country_code": "TR", + "national_identity_id": "12312312316", + "birth_place": "Ankara", + "birth_date": "01.07.1990", + "tax_no": "1231231234", + } + ) + created_list.append(owner_people) + + tenant_people = People.query.filter_by(person_tag = "Tenant Example").first() + if not tenant_people: + tenant_people = People.create( + **{ + "person_tag": "Tenant Example", + "firstname": "Example Tenant", + "surname": "Example", + "sex_code": "M", + "middle_name": "", + "father_name": "Father", + "mother_name": "Mother", + "country_code": "TR", + "national_identity_id": "12312312317", + "birth_place": "Ankara", + "birth_date": "01.07.1990", + "tax_no": "1231231234", + } + ) + created_list.append(tenant_people) + + main_domain, collection_name = "evyos.com.tr", f"{str(company_management.uu_id)}*Domain" + user_build_manager = Users.query.filter_by(user_tag = "Build Manager Example").first() + if not user_build_manager: + user_build_manager = Users.create( + person_id=build_manager_people.id, + person_uu_id=str(build_manager_people.uu_id), + user_tag=build_manager_people.person_tag, + email="example.build.manager@gmail.com", + phone_number="+901111111111", + avatar="https://s.tmimgcdn.com/scr/800x500/276800/building-home-nature-logo-vector-template-3_276851-original.jpg", + related_company=str(company_management.uu_id), + **active_row, + ) + created_list.append(user_build_manager) + user_build_manager.password_expiry_begins = str(arrow.now()) + user_build_manager.password_token = PasswordModule.generate_refresher_token() + + user_owner = Users.query.filter_by(user_tag = "Owner Example").first() + if not user_owner: + user_owner = Users.create( + person_id=owner_people.id, + person_uu_id=str(owner_people.uu_id), + user_tag=owner_people.person_tag, + email="example.owner@gmail.com", + phone_number="+901111111111", + avatar="https://s.tmimgcdn.com/scr/800x500/276800/building-home-nature-logo-vector-template-3_276851-original.jpg", + related_company=str(company_management.uu_id), + **active_row, + ) + created_list.append(user_owner) + user_owner.password_expiry_begins = str(arrow.now()) + user_owner.password_token = PasswordModule.generate_refresher_token() + + user_tenant = Users.query.filter_by(user_tag = "Tenant Example").first() + if not user_tenant: + user_tenant = Users.create( + person_id=tenant_people.id, + person_uu_id=str(tenant_people.uu_id), + user_tag=tenant_people.person_tag, + email="example.tenant@gmail.com", + phone_number="+901111111111", + avatar="https://s.tmimgcdn.com/scr/800x500/276800/building-home-nature-logo-vector-template-3_276851-original.jpg", + related_company=str(company_management.uu_id), + **active_row, + ) + created_list.append(user_tenant) + user_tenant.password_expiry_begins = str(arrow.now()) + user_tenant.password_token = PasswordModule.generate_refresher_token() + + with mongo_handler.collection(collection_name) as mongo_engine: + existing_record = mongo_engine.find_one({"user_uu_id": str(user_build_manager.uu_id)}) + if not existing_record: + mongo_engine.insert_one( + document={"user_uu_id": str(user_build_manager.uu_id), "other_domains_list": [main_domain], "main_domain": main_domain, "modified_at": arrow.now().timestamp()} + ) + else: + mongo_engine.update_one( + {"user_uu_id": str(user_build_manager.uu_id)}, + {"$set": {"other_domains_list": [main_domain], "main_domain": main_domain, "modified_at": arrow.now().timestamp()}} + ) + + with mongo_handler.collection(collection_name) as mongo_engine: + existing_record = mongo_engine.find_one({"user_uu_id": str(user_owner.uu_id)}) + if not existing_record: + mongo_engine.insert_one( + document={"user_uu_id": str(user_owner.uu_id), "other_domains_list": [main_domain], "main_domain": main_domain, "modified_at": arrow.now().timestamp()} + ) + else: + mongo_engine.update_one( + {"user_uu_id": str(user_owner.uu_id)}, + {"$set": {"other_domains_list": [main_domain], "main_domain": main_domain, "modified_at": arrow.now().timestamp()}} + ) + + with mongo_handler.collection(collection_name) as mongo_engine: + existing_record = mongo_engine.find_one({"user_uu_id": str(user_tenant.uu_id)}) + if not existing_record: + mongo_engine.insert_one( + document={"user_uu_id": str(user_tenant.uu_id), "other_domains_list": [main_domain], "main_domain": main_domain, "modified_at": arrow.now().timestamp()} + ) + else: + mongo_engine.update_one( + {"user_uu_id": str(user_tenant.uu_id)}, + {"$set": {"other_domains_list": [main_domain], "main_domain": main_domain, "modified_at": arrow.now().timestamp()}}) + + created_build_living_space_prs = BuildLivingSpace.query.filter_by( + build_id=created_build.id, build_parts_id=created_managment_room.id, person_id=build_manager_people.id + ).first() + if not created_build_living_space_prs: + created_build_living_space_prs = BuildLivingSpace.create( + build_id=created_build.id, + build_uu_id=str(created_build.uu_id), + build_parts_id=created_managment_room.id, + build_parts_uu_id=str(created_managment_room.uu_id), + person_id=build_manager_people.id, + person_uu_id=str(build_manager_people.uu_id), + occupant_type_id=occupant_type_prs.id, + occupant_type_uu_id=str(occupant_type_prs.uu_id), + **active_row, + ) + created_list.append(created_build_living_space_prs) + + created_build_living_space_owner = BuildLivingSpace.query.filter_by( + build_id=created_build.id, build_parts_id=created_flat.id, person_id=owner_people.id + ).first() + if not created_build_living_space_owner: + created_build_living_space_owner = BuildLivingSpace.create( + build_id=created_build.id, + build_uu_id=str(created_build.uu_id), + build_parts_id=created_flat.id, + build_parts_uu_id=str(created_flat.uu_id), + person_id=owner_people.id, + person_uu_id=str(owner_people.uu_id), + occupant_type_id=occupant_type_owner.id, + occupant_type_uu_id=str(occupant_type_owner.uu_id), + **active_row, + ) + created_list.append(created_build_living_space_owner) + + created_build_living_space_tenant = BuildLivingSpace.query.filter_by( + build_id=created_build.id, build_parts_id=created_flat.id, person_id=tenant_people.id + ).first() + if not created_build_living_space_tenant: + created_build_living_space_tenant = BuildLivingSpace.create( + build_id=created_build.id, + build_uu_id=str(created_build.uu_id), + build_parts_id=created_flat.id, + build_parts_uu_id=str(created_flat.uu_id), + person_id=tenant_people.id, + person_uu_id=str(tenant_people.uu_id), + occupant_type_id=occupant_type_tenant.id, + occupant_type_uu_id=str(occupant_type_tenant.uu_id), + **active_row, + ) + created_list.append(created_build_living_space_tenant) + + db_session.commit() + print("Occupant Defaults Create is now completed") diff --git a/api_services/api_builds/initial-service/init_occupant_types.py b/api_services/api_builds/initial-service/init_occupant_types.py new file mode 100644 index 0000000..20e0206 --- /dev/null +++ b/api_services/api_builds/initial-service/init_occupant_types.py @@ -0,0 +1,225 @@ +from schemas import OccupantTypes + + +def create_occupant_types_defaults(db_session): + + """ + occupant_category = mapped_column(String, server_default="") + occupant_category_type = mapped_column(String, server_default="") + occupant_is_unique = mapped_column(Boolean, server_default="0") + """ + + OccupantTypes.set_session(db_session) + list_occupant_types = [ + { + "occupant_type": "Toplantı Başkanı", + "occupant_description": "Toplantı Başkanı", + "occupant_code": "MT-PRS", + "occupant_category": "Toplantı", + "occupant_category_type": "MT", + "occupant_is_unique": True, + }, + { + "occupant_type": "Toplantı Katip", + "occupant_description": "Toplantıda tutanak tutan kişi", + "occupant_code": "MT-WRT", + "occupant_category": "Toplantı", + "occupant_category_type": "MT", + "occupant_is_unique": True, + }, + { + "occupant_type": "Toplantı Katılımcısı", + "occupant_description": "Toplantıda sadece katılan kişi", + "occupant_code": "MT-ATT", + "occupant_category": "Toplantı", + "occupant_category_type": "MT", + "occupant_is_unique": False, + }, + { + "occupant_type": "Toplantı Danışman", + "occupant_description": "Toplantıda danışmanlık yapan kişi", + "occupant_code": "MT-ADV", + "occupant_category": "Toplantı", + "occupant_category_type": "MT", + "occupant_is_unique": False, + }, + { + "occupant_type": "Toplantı Seçilmiş Başkanı", + "occupant_description": "Toplantı Seçilmiş Başkanı", + "occupant_code": "MT-VPR", + "occupant_category": "Toplantı", + "occupant_category_type": "MT", + "occupant_is_unique": True, + }, + { + "occupant_type": "Daire Sahibi", + "occupant_description": "Daire Sahibi", + "occupant_code": "FL-OWN", + "occupant_category": "Daire", + "occupant_category_type": "FL", + "occupant_is_unique": True, + }, + { + "occupant_type": "Daire Kiracısı", + "occupant_description": "Daire Kiracısı", + "occupant_code": "FL-TEN", + "occupant_category": "Daire", + "occupant_category_type": "FL", + "occupant_is_unique": True, + }, + { + "occupant_type": "Daire Sakini", + "occupant_description": "Daire Sakini", + "occupant_code": "FL-RES", + "occupant_category": "Daire", + "occupant_category_type": "FL", + "occupant_is_unique": False, + }, + { + "occupant_type": "Daire Sakini Vekili", + "occupant_description": "Daire Sakini Vekili", + "occupant_code": "FL-REP", + "occupant_category": "Daire", + "occupant_category_type": "FL", + "occupant_is_unique": False, + }, + { + "occupant_type": "Bina Avukatı", + "occupant_description": "Bina Avukatı", + "occupant_code": "BU-ATT", + "occupant_category": "Bina", + "occupant_category_type": "BU", + "occupant_is_unique": False, + }, + { + "occupant_type": "Bina Avukatı Yardımcısı", + "occupant_description": "Bina Avukatı Yardımcısı", + "occupant_code": "BU-ATA", + "occupant_category": "Bina", + "occupant_category_type": "BU", + "occupant_is_unique": False, + }, + { + "occupant_type": "Bina Denetmen Yardımcısı", + "occupant_description": "Bina Denetmen Yardımcısı", + "occupant_code": "BU-SPA", + "occupant_category": "Bina", + "occupant_category_type": "BU", + "occupant_is_unique": False, + }, + { + "occupant_type": "Bina Denetmeni", + "occupant_description": "Bina Denetmeni", + "occupant_code": "BU-SPV", + "occupant_category": "Bina", + "occupant_category_type": "BU", + "occupant_is_unique": False, + }, + { + "occupant_type": "Bina Yönetici Yardımcısı", + "occupant_description": "Bina Yönetici Yardımcısı", + "occupant_code": "BU-MNA", + "occupant_category": "Bina", + "occupant_category_type": "BU", + "occupant_is_unique": False, + }, + { + "occupant_type": "Bina Yöneticisi", + "occupant_description": "Bina Yöneticisi", + "occupant_code": "BU-MNG", + "occupant_category": "Bina", + "occupant_category_type": "BU", + "occupant_is_unique": True, + }, + { + "occupant_type": "Bina Muhasabecisi", + "occupant_description": "Bina Muhasabecisi", + "occupant_code": "BU-ACC", + "occupant_category": "Bina", + "occupant_category_type": "BU", + "occupant_is_unique": False, + }, + { + "occupant_type": "Proje Lideri", + "occupant_description": "Proje Lideri", + "occupant_code": "PRJ-LDR", + "occupant_category": "Proje", + "occupant_category_type": "PRJ", + "occupant_is_unique": False, + }, + { + "occupant_type": "Proje Sorumlusu", + "occupant_description": "Proje Sorumlusu", + "occupant_code": "PRJ-RES", + "occupant_category": "Proje", + "occupant_category_type": "PRJ", + "occupant_is_unique": False, + }, + { + "occupant_type": "Proje Ekibi", + "occupant_description": "Proje Ekibi", + "occupant_code": "PRJ-EMP", + "occupant_category": "Proje", + "occupant_category_type": "PRJ", + "occupant_is_unique": False, + }, + { + "occupant_type": "Proje Finans Sorumlusu", + "occupant_description": "Proje Finans Sorumlusu", + "occupant_code": "PRJ-FIN", + "occupant_category": "Proje", + "occupant_category_type": "PRJ", + "occupant_is_unique": False, + }, + { + "occupant_type": "Proje Teknik Sorumlusu", + "occupant_description": "Proje Teknik Sorumlusu", + "occupant_code": "PRJ-TEC", + "occupant_category": "Proje", + "occupant_category_type": "PRJ", + "occupant_is_unique": False, + }, + { + "occupant_type": "Daire Mülkiyet Vekili", + "occupant_description": "Daire Mülkiyet Vekili", + "occupant_code": "FL-DEP", # deputy + "occupant_category": "Daire", + "occupant_category_type": "FL", + "occupant_is_unique": False, + }, + { + "occupant_type": "Bina Teknik Sorumlusu", + "occupant_description": "Bina Teknik Sorumlusu", + "occupant_code": "BU-TEC", + "occupant_category": "Bina", + "occupant_category_type": "BU", + "occupant_is_unique": False, + }, + { + "occupant_type": "Bina Teknik Elemanı", + "occupant_description": "Bina Teknik Elemanı", + "occupant_code": "BU-EMP", + "occupant_category": "Bina", + "occupant_category_type": "BU", + "occupant_is_unique": False, + }, + { + "occupant_type": "Bina Teknik Freelancer", + "occupant_description": "Bina Teknik Freelancer", + "occupant_code": "BU-FLC", + "occupant_category": "Bina", + "occupant_category_type": "BU", + "occupant_is_unique": False, + }, + ] + for list_occupant_type in list_occupant_types: + try: + created_type = OccupantTypes.query.filter_by( + occupant_code=list_occupant_type["occupant_code"], + occupant_category=list_occupant_type["occupant_category_type"], + ).first() + if not created_type: + created_type = OccupantTypes.create(**list_occupant_type, is_confirmed=True) + created_type.save() + except Exception as e: + print(f"Error: {e}") diff --git a/api_services/api_builds/initial-service/init_services.py b/api_services/api_builds/initial-service/init_services.py new file mode 100644 index 0000000..510028b --- /dev/null +++ b/api_services/api_builds/initial-service/init_services.py @@ -0,0 +1,78 @@ +from schemas import ( + Duty, + OccupantTypes, + Modules, + Services, +) + +def create_modules_and_services_and_actions(db_session): + + Duty.set_session(db_session) + Services.set_session(db_session) + Modules.set_session(db_session) + OccupantTypes.set_session(db_session) + + erp_module = Modules.query.filter_by(module_code = "EVYOS-ERP").first() + if not erp_module: + erp_module = Modules.create( + **{ + "module_name": "EVYOS ERP", "module_description": "EVYOS Enterprise Resource Planning", "module_code": "EVYOS-ERP", + "module_layer": 1, "is_default_module": False, "is_confirmed": True, + } + ) + erp_module.save() + + build_module = Modules.query.filter_by(module_code = "BLD-MNG").first() + if not build_module: + build_module = Modules.create( + **{ + "module_name": "Bina Yönetim Modülü", "module_description": "Building Management Module", "module_code": "BLD-MNG", + "module_layer": 1, "is_default_module": False, "is_confirmed": True, + } + ) + build_module.save() + + user_module = Modules.query.filter_by(module_code = "USR-PUB").first() + if not user_module: + user_module = Modules.create( + **{ + "module_name": "Kullancı Modülü", "module_description": "Kullanıcı Genel Modülü", "module_code": "USR-PUB", "module_layer": 1, + "is_default_module": True, "is_confirmed": True + } + ) + user_module.save() + + erp_module_module_dict = dict(module_id=erp_module.id, module_uu_id=str(erp_module.uu_id)) + build_module_module_dict = dict(module_id=build_module.id, module_uu_id=str(build_module.uu_id)) + duty_objects = Duty.query.filter(Duty.module_id == erp_module.id).all() + if not duty_objects: + raise Exception("Duty objects not found") + + for duty_object in duty_objects: + created_service = Services.query.filter(Services.service_code == f"SRE-{duty_object.duty_code}").first() + if not created_service: + created_service = Services.create( + **erp_module_module_dict, + service_name=duty_object.duty_name, + service_description=duty_object.duty_description, + service_code=f"SRE-{duty_object.duty_code}", + related_responsibility=duty_object.duty_code, + is_confirmed=True, + ) + created_service.save() + + occupant_types = OccupantTypes.query.filter(OccupantTypes.module_id == build_module.id).all() + if not occupant_types: + raise Exception("Occupant types not found") + for occupant_type in occupant_types: + created_service = Services.query.filter(Services.service_code == f"SRO-{occupant_type.occupant_code}").first() + if not created_service: + created_service = Services.create( + **build_module_module_dict, + service_name=occupant_type.occupant_type, + service_description=occupant_type.occupant_description, + service_code=f"SRO-{occupant_type.occupant_code}", + related_responsibility=occupant_type.occupant_code, + is_confirmed=True, + ) + created_service.save() diff --git a/api_services/api_builds/initial-service/modules/Token/config.py b/api_services/api_builds/initial-service/modules/Token/config.py new file mode 100644 index 0000000..8af8929 --- /dev/null +++ b/api_services/api_builds/initial-service/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/api_services/api_builds/initial-service/modules/Token/password_module.py b/api_services/api_builds/initial-service/modules/Token/password_module.py new file mode 100644 index 0000000..c1860f6 --- /dev/null +++ b/api_services/api_builds/initial-service/modules/Token/password_module.py @@ -0,0 +1,44 @@ +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 + raise ValueError("EYS_0004") + + @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/api_services/api_controllers/README.md b/api_services/api_controllers/README.md new file mode 100644 index 0000000..57e2b7a --- /dev/null +++ b/api_services/api_controllers/README.md @@ -0,0 +1,143 @@ +# Database Handlers + +This directory contains database handlers for MongoDB and PostgreSQL used in the backend automate services. + +## Overview + +The database handlers provide a consistent interface for interacting with different database systems. They implement: + +- Connection pooling +- Retry mechanisms +- Error handling +- Thread safety +- Context managers for resource management + +## MongoDB Handler + +The MongoDB handler is implemented as a singleton pattern to ensure efficient connection management across the application. It provides: + +- Connection pooling via PyMongo's built-in connection pool +- Automatic retry capabilities for MongoDB operations +- Context manager for MongoDB collections to ensure connections are properly closed +- Thread safety for concurrent operations + +### MongoDB Performance + +The MongoDB handler has been tested with a concurrent load test: + +``` +Concurrent Operation Test Results: +Total threads: 100 +Passed: 100 +Failed: 0 +Execution time: 0.73 seconds +Operations per second: 137.61 +``` + +## PostgreSQL Handler + +The PostgreSQL handler leverages SQLAlchemy for ORM capabilities and connection management. It provides: + +- Connection pooling via SQLAlchemy's connection pool +- ORM models with CRUD operations +- Filter methods for querying data +- Transaction management + +### PostgreSQL Performance + +The PostgreSQL handler has been tested with a concurrent load test: + +``` +Concurrent Operation Test Results: +Total threads: 100 +Passed: 100 +Failed: 0 +Execution time: 0.30 seconds +Operations per second: 332.11 +``` + +## Usage Examples + +### MongoDB Example + +```python +from Controllers.Mongo.database import mongo_handler + +# Using the context manager for automatic connection management +with mongo_handler.collection("users") as users_collection: + # Perform operations + users_collection.insert_one({"name": "John", "email": "john@example.com"}) + user = users_collection.find_one({"email": "john@example.com"}) +``` + +### PostgreSQL Example + +```python +from Controllers.Postgres.schema import EndpointRestriction + +# Using the session context manager +with EndpointRestriction.new_session() as db_session: + # Create a new record + new_endpoint = EndpointRestriction( + endpoint_code="TEST_API", + endpoint_name="Test API", + endpoint_method="GET", + endpoint_function="test_function", + endpoint_desc="Test description", + is_confirmed=True + ) + new_endpoint.save(db=db_session) + + # Query records + result = EndpointRestriction.filter_one( + EndpointRestriction.endpoint_code == "TEST_API", + db=db_session + ).data +``` + +## Configuration + +Both handlers are configured via environment variables: + +### MongoDB Configuration + +- `MONGO_ENGINE`: Database engine (mongodb) +- `MONGO_HOST`: Database host +- `MONGO_PORT`: Database port +- `MONGO_USER`: Database username +- `MONGO_PASSWORD`: Database password +- `MONGO_DB`: Database name +- `MONGO_AUTH_DB`: Authentication database + +### PostgreSQL Configuration + +- `POSTGRES_ENGINE`: Database engine (postgresql+psycopg2) +- `POSTGRES_HOST`: Database host +- `POSTGRES_PORT`: Database port +- `POSTGRES_USER`: Database username +- `POSTGRES_PASSWORD`: Database password +- `POSTGRES_DB`: Database name +- `POSTGRES_POOL_SIZE`: Connection pool size +- `POSTGRES_POOL_PRE_PING`: Whether to ping the database before using a connection + +## Testing + +Both handlers include comprehensive test suites that verify: + +- Basic CRUD operations +- Complex queries +- Nested documents (MongoDB) +- Array operations (MongoDB) +- Aggregation (MongoDB) +- Index operations +- Concurrent operations + +To run the tests: + +```bash +# MongoDB tests +python -m Controllers.Mongo.implementations + +# PostgreSQL tests +python -m Controllers.Postgres.implementations +``` diff --git a/api_services/api_controllers/email/config.py b/api_services/api_controllers/email/config.py new file mode 100644 index 0000000..9ebfccd --- /dev/null +++ b/api_services/api_controllers/email/config.py @@ -0,0 +1,31 @@ +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Configs(BaseSettings): + """ + Email configuration settings. + """ + + HOST: str = "" + USERNAME: str = "" + PASSWORD: str = "" + PORT: int = 0 + SEND: bool = True + + @property + def is_send(self): + return bool(self.SEND) + + def as_dict(self): + return dict( + host=self.HOST, + port=self.PORT, + username=self.USERNAME, + password=self.PASSWORD, + ) + + model_config = SettingsConfigDict(env_prefix="EMAIL_") + + +# singleton instance of the POSTGRESQL configuration settings +email_configs = Configs() diff --git a/api_services/api_controllers/email/implementations.py b/api_services/api_controllers/email/implementations.py new file mode 100644 index 0000000..3bfcbbe --- /dev/null +++ b/api_services/api_controllers/email/implementations.py @@ -0,0 +1,29 @@ +from send_email import EmailService, EmailSendModel + + +# Create email parameters +email_params = EmailSendModel( + subject="Test Email", + html="

Hello world!

", + receivers=["recipient@example.com"], + text="Hello world!", +) + +another_email_params = EmailSendModel( + subject="Test Email2", + html="

Hello world!2

", + receivers=["recipient@example.com"], + text="Hello world!2", +) + + +# The context manager handles connection errors +with EmailService.new_session() as email_session: + # Send email - any exceptions here will propagate up + EmailService.send_email(email_session, email_params) + + # Or send directly through the session + email_session.send(email_params) + + # Send more emails in the same session if needed + EmailService.send_email(email_session, another_email_params) diff --git a/api_services/api_controllers/email/send_email.py b/api_services/api_controllers/email/send_email.py new file mode 100644 index 0000000..b928c63 --- /dev/null +++ b/api_services/api_controllers/email/send_email.py @@ -0,0 +1,90 @@ +from redmail import EmailSender +from typing import List, Optional, Dict +from pydantic import BaseModel +from contextlib import contextmanager +from .config import email_configs + + +class EmailSendModel(BaseModel): + subject: str + html: str = "" + receivers: List[str] + text: Optional[str] = "" + cc: Optional[List[str]] = None + bcc: Optional[List[str]] = None + headers: Optional[Dict] = None + attachments: Optional[Dict] = None + + +class EmailSession: + + def __init__(self, email_sender): + self.email_sender = email_sender + + def send(self, params: EmailSendModel) -> bool: + """Send email using this session.""" + if not email_configs.is_send: + print("Email sending is disabled", params) + return False + receivers = [email_configs.USERNAME] + + # Ensure connection is established before sending + try: + # Check if connection exists, if not establish it + if not hasattr(self.email_sender, '_connected') or not self.email_sender._connected: + self.email_sender.connect() + + self.email_sender.send( + subject=params.subject, + receivers=receivers, + text=params.text + f" : Gonderilen [{str(receivers)}]", + html=params.html, + cc=params.cc, + bcc=params.bcc, + headers=params.headers or {}, + attachments=params.attachments or {}, + ) + return True + except Exception as e: + print(f"Error sending email: {e}") + raise + + +class EmailService: + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super(EmailService, cls).__new__(cls) + return cls._instance + + @classmethod + @contextmanager + def new_session(cls): + """Create and yield a new email session with active connection.""" + email_sender = EmailSender(**email_configs.as_dict()) + session = EmailSession(email_sender) + connection_established = False + try: + # Establish connection and set flag + email_sender.connect() + # Set a flag to track connection state + email_sender._connected = True + connection_established = True + yield session + except Exception as e: + print(f"Error with email connection: {e}") + raise + finally: + # Only close if connection was successfully established + if connection_established: + try: + email_sender.close() + email_sender._connected = False + except Exception as e: + print(f"Error closing email connection: {e}") + + @classmethod + def send_email(cls, session: EmailSession, params: EmailSendModel) -> bool: + """Send email using the provided session.""" + return session.send(params) diff --git a/api_services/api_controllers/mongo/README.md b/api_services/api_controllers/mongo/README.md new file mode 100644 index 0000000..3c1acc0 --- /dev/null +++ b/api_services/api_controllers/mongo/README.md @@ -0,0 +1,219 @@ +# MongoDB Handler + +A singleton MongoDB handler with context manager support for MongoDB collections and automatic retry capabilities. + +## Features + +- **Singleton Pattern**: Ensures only one instance of the MongoDB handler exists +- **Context Manager**: Automatically manages connection lifecycle +- **Retry Capability**: Automatically retries MongoDB operations on failure +- **Connection Pooling**: Configurable connection pooling +- **Graceful Degradation**: Handles connection failures without crashing + +## Usage + +```python +from Controllers.Mongo.database import mongo_handler + +# Use the context manager to access a collection +with mongo_handler.collection("users") as users_collection: + # Perform operations on the collection + users_collection.insert_one({"username": "john", "email": "john@example.com"}) + user = users_collection.find_one({"username": "john"}) + # Connection is automatically closed when exiting the context +``` + +## Configuration + +MongoDB connection settings are configured via environment variables with the `MONGO_` prefix: + +- `MONGO_ENGINE`: Database engine (e.g., "mongodb") +- `MONGO_USER`: MongoDB username +- `MONGO_PASSWORD`: MongoDB password +- `MONGO_HOST`: MongoDB host +- `MONGO_PORT`: MongoDB port +- `MONGO_DB`: Database name +- `MONGO_AUTH_DB`: Authentication database + +## Monitoring Connection Closure + +To verify that MongoDB sessions are properly closed, you can implement one of the following approaches: + +### 1. Add Logging to the `__exit__` Method + +```python +def __exit__(self, exc_type, exc_val, exc_tb): + """ + Exit context, closing the connection. + """ + if self.client: + print(f"Closing MongoDB connection for collection: {self.collection_name}") + # Or use a proper logger + # logger.info(f"Closing MongoDB connection for collection: {self.collection_name}") + self.client.close() + self.client = None + self.collection = None + print(f"MongoDB connection closed successfully") +``` + +### 2. Add Connection Tracking + +```python +class MongoDBHandler: + # Add these to your class + _open_connections = 0 + + def get_connection_stats(self): + """Return statistics about open connections""" + return {"open_connections": self._open_connections} +``` + +Then modify the `CollectionContext` class: + +```python +def __enter__(self): + try: + # Create a new client connection + self.client = MongoClient(self.db_handler.uri, **self.db_handler.client_options) + # Increment connection counter + self.db_handler._open_connections += 1 + # Rest of your code... + +def __exit__(self, exc_type, exc_val, exc_tb): + if self.client: + # Decrement connection counter + self.db_handler._open_connections -= 1 + self.client.close() + self.client = None + self.collection = None +``` + +### 3. Use MongoDB's Built-in Monitoring + +```python +from pymongo import monitoring + +class ConnectionCommandListener(monitoring.CommandListener): + def started(self, event): + print(f"Command {event.command_name} started on server {event.connection_id}") + + def succeeded(self, event): + print(f"Command {event.command_name} succeeded in {event.duration_micros} microseconds") + + def failed(self, event): + print(f"Command {event.command_name} failed in {event.duration_micros} microseconds") + +# Register the listener +monitoring.register(ConnectionCommandListener()) +``` + +### 4. Add a Test Function + +```python +def test_connection_closure(): + """Test that MongoDB connections are properly closed.""" + print("\nTesting connection closure...") + + # Record initial connection count (if you implemented the counter) + initial_count = mongo_handler.get_connection_stats()["open_connections"] + + # Use multiple nested contexts + for i in range(5): + with mongo_handler.collection("test_collection") as collection: + # Do some simple operation + collection.find_one({}) + + # Check final connection count + final_count = mongo_handler.get_connection_stats()["open_connections"] + + if final_count == initial_count: + print("Test passed: All connections were properly closed") + return True + else: + print(f"Test failed: {final_count - initial_count} connections remain open") + return False +``` + +### 5. Use MongoDB Server Logs + +You can also check the MongoDB server logs to see connection events: + +```bash +# Run this on your MongoDB server +tail -f /var/log/mongodb/mongod.log | grep "connection" +``` + +## Best Practices + +1. Always use the context manager pattern to ensure connections are properly closed +2. Keep operations within the context manager as concise as possible +3. Handle exceptions within the context to prevent unexpected behavior +4. Avoid nesting multiple context managers unnecessarily +5. Use the retry decorator for operations that might fail due to transient issues + +## LXC Container Configuration + +### Authentication Issues + +If you encounter authentication errors when connecting to the MongoDB container at 10.10.2.13:27017, you may need to update the container configuration: + +1. **Check MongoDB Authentication**: Ensure the MongoDB container is configured with the correct authentication mechanism + +2. **Verify Network Configuration**: Make sure the container network allows connections from your application + +3. **Update MongoDB Configuration**: + - Edit the MongoDB configuration file in the container + - Ensure `bindIp` is set correctly (e.g., `0.0.0.0` to allow connections from any IP) + - Check that authentication is enabled with the correct mechanism + +4. **User Permissions**: + - Verify that the application user (`appuser`) exists in the MongoDB instance + - Ensure the user has the correct roles and permissions for the database + +### Example MongoDB Container Configuration + +```yaml +# Example docker-compose.yml configuration +services: + mongodb: + image: mongo:latest + container_name: mongodb + environment: + - MONGO_INITDB_ROOT_USERNAME=admin + - MONGO_INITDB_ROOT_PASSWORD=password + volumes: + - ./init-mongo.js:/docker-entrypoint-initdb.d/init-mongo.js:ro + ports: + - "27017:27017" + command: mongod --auth +``` + +```javascript +// Example init-mongo.js +db.createUser({ + user: 'appuser', + pwd: 'apppassword', + roles: [ + { role: 'readWrite', db: 'appdb' } + ] +}); +``` + +## Troubleshooting + +### Common Issues + +1. **Authentication Failed**: + - Verify username and password in environment variables + - Check that the user exists in the specified authentication database + - Ensure the user has appropriate permissions + +2. **Connection Refused**: + - Verify the MongoDB host and port are correct + - Check network connectivity between application and MongoDB container + - Ensure MongoDB is running and accepting connections + +3. **Resource Leaks**: + - Use the context manager pattern to ensure connections are properly closed + - Monitor connection pool size and active connections + - Implement proper error handling to close connections in case of exceptions diff --git a/api_services/api_controllers/mongo/config.py b/api_services/api_controllers/mongo/config.py new file mode 100644 index 0000000..bbeceac --- /dev/null +++ b/api_services/api_controllers/mongo/config.py @@ -0,0 +1,31 @@ +import os +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Configs(BaseSettings): + """ + MongoDB configuration settings. + """ + + # MongoDB connection settings + ENGINE: str = "mongodb" + USERNAME: str = "appuser" # Application user + PASSWORD: str = "apppassword" # Application password + HOST: str = "10.10.2.13" + PORT: int = 27017 + DB: str = "appdb" # The application database + AUTH_DB: str = "appdb" # Authentication is done against admin database + + @property + def url(self): + """Generate the database URL. + mongodb://{MONGO_USERNAME}:{MONGO_PASSWORD}@{MONGO_HOST}:{MONGO_PORT}/{DB}?authSource={MONGO_AUTH_DB} + """ + # Include the database name in the URI + return f"{self.ENGINE}://{self.USERNAME}:{self.PASSWORD}@{self.HOST}:{self.PORT}/{self.DB}?authSource={self.DB}" + + model_config = SettingsConfigDict(env_prefix="_MONGO_") + + +# Create a singleton instance of the MongoDB configuration settings +mongo_configs = Configs() diff --git a/api_services/api_controllers/mongo/database.py b/api_services/api_controllers/mongo/database.py new file mode 100644 index 0000000..4c0192a --- /dev/null +++ b/api_services/api_controllers/mongo/database.py @@ -0,0 +1,373 @@ +import time +import functools + +from pymongo import MongoClient +from pymongo.errors import PyMongoError +from .config import mongo_configs + + +def retry_operation(max_attempts=3, delay=1.0, backoff=2.0, exceptions=(PyMongoError,)): + """ + Decorator for retrying MongoDB operations with exponential backoff. + + Args: + max_attempts: Maximum number of retry attempts + delay: Initial delay between retries in seconds + backoff: Multiplier for delay after each retry + exceptions: Tuple of exceptions to catch and retry + """ + + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + mtries, mdelay = max_attempts, delay + while mtries > 1: + try: + return func(*args, **kwargs) + except exceptions as e: + time.sleep(mdelay) + mtries -= 1 + mdelay *= backoff + return func(*args, **kwargs) + + return wrapper + + return decorator + + +class MongoDBHandler: + """ + A MongoDB handler that provides context manager access to specific collections + with automatic retry capability. Implements singleton pattern. + """ + + _instance = None + _debug_mode = False # Set to True to enable debug mode + + def __new__(cls, *args, **kwargs): + """ + Implement singleton pattern for the handler. + """ + if cls._instance is None: + cls._instance = super(MongoDBHandler, cls).__new__(cls) + cls._instance._initialized = False + return cls._instance + + def __init__(self, debug_mode=False, mock_mode=False): + """Initialize the MongoDB handler. + + Args: + debug_mode: If True, use a simplified connection for debugging + mock_mode: If True, use mock collections instead of real MongoDB connections + """ + if not hasattr(self, "_initialized") or not self._initialized: + self._debug_mode = debug_mode + self._mock_mode = mock_mode + + if mock_mode: + # In mock mode, we don't need a real connection string + self.uri = "mongodb://mock:27017/mockdb" + print("MOCK MODE: Using simulated MongoDB connections") + elif debug_mode: + # Use a direct connection without authentication for testing + self.uri = f"mongodb://{mongo_configs.HOST}:{mongo_configs.PORT}/{mongo_configs.DB}" + print(f"DEBUG MODE: Using direct connection: {self.uri}") + else: + # Use the configured connection string with authentication + self.uri = mongo_configs.url + print(f"Connecting to MongoDB: {self.uri}") + + # Define MongoDB client options with increased timeouts for better reliability + self.client_options = { + "maxPoolSize": 5, + "minPoolSize": 1, + "maxIdleTimeMS": 60000, + "waitQueueTimeoutMS": 5000, + "serverSelectionTimeoutMS": 10000, + "connectTimeoutMS": 30000, + "socketTimeoutMS": 45000, + "retryWrites": True, + "retryReads": True, + } + self._initialized = True + + def collection(self, collection_name: str): + """ + Get a context manager for a specific collection. + + Args: + collection_name: Name of the collection to access + + Returns: + A context manager for the specified collection + """ + return CollectionContext(self, collection_name) + + +class CollectionContext: + """ + Context manager for MongoDB collections with automatic retry capability. + """ + + def __init__(self, db_handler: MongoDBHandler, collection_name: str): + """ + Initialize collection context. + + Args: + db_handler: Reference to the MongoDB handler + collection_name: Name of the collection to access + """ + self.db_handler = db_handler + self.collection_name = collection_name + self.client = None + self.collection = None + + def __enter__(self): + """ + Enter context, establishing a new connection. + + Returns: + The MongoDB collection object with retry capabilities + """ + # If we're in mock mode, return a mock collection immediately + if self.db_handler._mock_mode: + return self._create_mock_collection() + + try: + # Create a new client connection + self.client = MongoClient( + self.db_handler.uri, **self.db_handler.client_options + ) + + if self.db_handler._debug_mode: + # In debug mode, we explicitly use the configured DB + db_name = mongo_configs.DB + print(f"DEBUG MODE: Using database '{db_name}'") + else: + # In normal mode, extract database name from the URI + try: + db_name = self.client.get_database().name + except Exception: + db_name = mongo_configs.DB + print(f"Using fallback database '{db_name}'") + + self.collection = self.client[db_name][self.collection_name] + + # Enhance collection methods with retry capabilities + self._add_retry_capabilities() + + return self.collection + except pymongo.errors.OperationFailure as e: + if "Authentication failed" in str(e): + print(f"MongoDB authentication error: {e}") + print("Attempting to reconnect with direct connection...") + + try: + # Try a direct connection without authentication for testing + direct_uri = f"mongodb://{mongo_configs.HOST}:{mongo_configs.PORT}/{mongo_configs.DB}" + print(f"Trying direct connection: {direct_uri}") + self.client = MongoClient( + direct_uri, **self.db_handler.client_options + ) + self.collection = self.client[mongo_configs.DB][ + self.collection_name + ] + self._add_retry_capabilities() + return self.collection + except Exception as inner_e: + print(f"Direct connection also failed: {inner_e}") + # Fall through to mock collection creation + else: + print(f"MongoDB operation error: {e}") + if self.client: + self.client.close() + self.client = None + except Exception as e: + print(f"MongoDB connection error: {e}") + if self.client: + self.client.close() + self.client = None + + return self._create_mock_collection() + + def _create_mock_collection(self): + """ + Create a mock collection for testing or graceful degradation. + This prevents the application from crashing when MongoDB is unavailable. + + Returns: + A mock MongoDB collection with simulated behaviors + """ + from unittest.mock import MagicMock + + if self.db_handler._mock_mode: + print(f"MOCK MODE: Using mock collection '{self.collection_name}'") + else: + print( + f"Using mock MongoDB collection '{self.collection_name}' for graceful degradation" + ) + + # Create in-memory storage for this mock collection + if not hasattr(self.db_handler, "_mock_storage"): + self.db_handler._mock_storage = {} + + if self.collection_name not in self.db_handler._mock_storage: + self.db_handler._mock_storage[self.collection_name] = [] + + mock_collection = MagicMock() + mock_data = self.db_handler._mock_storage[self.collection_name] + + # Define behavior for find operations + def mock_find(query=None, *args, **kwargs): + # Simple implementation that returns all documents + return mock_data + + def mock_find_one(query=None, *args, **kwargs): + # Simple implementation that returns the first matching document + if not mock_data: + return None + return mock_data[0] + + def mock_insert_one(document, *args, **kwargs): + # Add _id if not present + if "_id" not in document: + document["_id"] = f"mock_id_{len(mock_data)}" + mock_data.append(document) + result = MagicMock() + result.inserted_id = document["_id"] + return result + + def mock_insert_many(documents, *args, **kwargs): + inserted_ids = [] + for doc in documents: + result = mock_insert_one(doc) + inserted_ids.append(result.inserted_id) + result = MagicMock() + result.inserted_ids = inserted_ids + return result + + def mock_update_one(query, update, *args, **kwargs): + result = MagicMock() + result.modified_count = 1 + return result + + def mock_update_many(query, update, *args, **kwargs): + result = MagicMock() + result.modified_count = len(mock_data) + return result + + def mock_delete_one(query, *args, **kwargs): + result = MagicMock() + result.deleted_count = 1 + if mock_data: + mock_data.pop(0) # Just remove the first item for simplicity + return result + + def mock_delete_many(query, *args, **kwargs): + count = len(mock_data) + mock_data.clear() + result = MagicMock() + result.deleted_count = count + return result + + def mock_count_documents(query, *args, **kwargs): + return len(mock_data) + + def mock_aggregate(pipeline, *args, **kwargs): + return [] + + def mock_create_index(keys, **kwargs): + return f"mock_index_{keys}" + + # Assign the mock implementations + mock_collection.find.side_effect = mock_find + mock_collection.find_one.side_effect = mock_find_one + mock_collection.insert_one.side_effect = mock_insert_one + mock_collection.insert_many.side_effect = mock_insert_many + mock_collection.update_one.side_effect = mock_update_one + mock_collection.update_many.side_effect = mock_update_many + mock_collection.delete_one.side_effect = mock_delete_one + mock_collection.delete_many.side_effect = mock_delete_many + mock_collection.count_documents.side_effect = mock_count_documents + mock_collection.aggregate.side_effect = mock_aggregate + mock_collection.create_index.side_effect = mock_create_index + + # Add retry capabilities to the mock collection + self._add_retry_capabilities_to_mock(mock_collection) + + self.collection = mock_collection + return self.collection + + def _add_retry_capabilities(self): + """ + Add retry capabilities to all collection methods. + """ + # Store original methods for common operations + original_insert_one = self.collection.insert_one + original_insert_many = self.collection.insert_many + original_find_one = self.collection.find_one + original_find = self.collection.find + original_update_one = self.collection.update_one + original_update_many = self.collection.update_many + original_delete_one = self.collection.delete_one + original_delete_many = self.collection.delete_many + original_replace_one = self.collection.replace_one + original_count_documents = self.collection.count_documents + + # Add retry capabilities to methods + self.collection.insert_one = retry_operation()(original_insert_one) + self.collection.insert_many = retry_operation()(original_insert_many) + self.collection.find_one = retry_operation()(original_find_one) + self.collection.find = retry_operation()(original_find) + self.collection.update_one = retry_operation()(original_update_one) + self.collection.update_many = retry_operation()(original_update_many) + self.collection.delete_one = retry_operation()(original_delete_one) + self.collection.delete_many = retry_operation()(original_delete_many) + self.collection.replace_one = retry_operation()(original_replace_one) + self.collection.count_documents = retry_operation()(original_count_documents) + + def _add_retry_capabilities_to_mock(self, mock_collection): + """ + Add retry capabilities to mock collection methods. + This is a simplified version that just wraps the mock methods. + + Args: + mock_collection: The mock collection to enhance + """ + # List of common MongoDB collection methods to add retry capabilities to + methods = [ + "insert_one", + "insert_many", + "find_one", + "find", + "update_one", + "update_many", + "delete_one", + "delete_many", + "replace_one", + "count_documents", + "aggregate", + ] + + # Add retry decorator to each method + for method_name in methods: + if hasattr(mock_collection, method_name): + original_method = getattr(mock_collection, method_name) + setattr( + mock_collection, + method_name, + retry_operation(max_retries=1, retry_interval=0)(original_method), + ) + + def __exit__(self, exc_type, exc_val, exc_tb): + """ + Exit context, closing the connection. + """ + if self.client: + self.client.close() + self.client = None + self.collection = None + + +# Create a singleton instance of the MongoDB handler +mongo_handler = MongoDBHandler() diff --git a/api_services/api_controllers/mongo/implementations.py b/api_services/api_controllers/mongo/implementations.py new file mode 100644 index 0000000..d9ebdf0 --- /dev/null +++ b/api_services/api_controllers/mongo/implementations.py @@ -0,0 +1,519 @@ +# Initialize the MongoDB handler with your configuration +from datetime import datetime +from .database import MongoDBHandler, mongo_handler + + +def cleanup_test_data(): + """Clean up any test data before running tests.""" + try: + with mongo_handler.collection("test_collection") as collection: + collection.delete_many({}) + print("Successfully cleaned up test data") + except Exception as e: + print(f"Warning: Could not clean up test data: {e}") + print("Continuing with tests using mock data...") + + +def test_basic_crud_operations(): + """Test basic CRUD operations on users collection.""" + print("\nTesting basic CRUD operations...") + try: + with mongo_handler.collection("users") as users_collection: + # First, clear any existing data + users_collection.delete_many({}) + print("Cleared existing data") + + # Insert multiple documents + insert_result = users_collection.insert_many( + [ + {"username": "john", "email": "john@example.com", "role": "user"}, + {"username": "jane", "email": "jane@example.com", "role": "admin"}, + {"username": "bob", "email": "bob@example.com", "role": "user"}, + ] + ) + print(f"Inserted {len(insert_result.inserted_ids)} documents") + + # Find with multiple conditions + admin_users = list(users_collection.find({"role": "admin"})) + print(f"Found {len(admin_users)} admin users") + if admin_users: + print(f"Admin user: {admin_users[0].get('username')}") + + # Update multiple documents + update_result = users_collection.update_many( + {"role": "user"}, {"$set": {"last_login": datetime.now().isoformat()}} + ) + print(f"Updated {update_result.modified_count} documents") + + # Delete documents + delete_result = users_collection.delete_many({"username": "bob"}) + print(f"Deleted {delete_result.deleted_count} documents") + + # Count remaining documents + remaining = users_collection.count_documents({}) + print(f"Remaining documents: {remaining}") + + # Check each condition separately + condition1 = len(admin_users) == 1 + condition2 = admin_users and admin_users[0].get("username") == "jane" + condition3 = update_result.modified_count == 2 + condition4 = delete_result.deleted_count == 1 + + print(f"Condition 1 (admin count): {condition1}") + print(f"Condition 2 (admin is jane): {condition2}") + print(f"Condition 3 (updated 2 users): {condition3}") + print(f"Condition 4 (deleted bob): {condition4}") + + success = condition1 and condition2 and condition3 and condition4 + print(f"Test {'passed' if success else 'failed'}") + return success + except Exception as e: + print(f"Test failed with exception: {e}") + return False + + +def test_nested_documents(): + """Test operations with nested documents in products collection.""" + print("\nTesting nested documents...") + try: + with mongo_handler.collection("products") as products_collection: + # Clear any existing data + products_collection.delete_many({}) + print("Cleared existing data") + + # Insert a product with nested data + insert_result = products_collection.insert_one( + { + "name": "Laptop", + "price": 999.99, + "specs": {"cpu": "Intel i7", "ram": "16GB", "storage": "512GB SSD"}, + "in_stock": True, + "tags": ["electronics", "computers", "laptops"], + } + ) + print(f"Inserted document with ID: {insert_result.inserted_id}") + + # Find with nested field query + laptop = products_collection.find_one({"specs.cpu": "Intel i7"}) + print(f"Found laptop: {laptop is not None}") + if laptop: + print(f"Laptop RAM: {laptop.get('specs', {}).get('ram')}") + + # Update nested field + update_result = products_collection.update_one( + {"name": "Laptop"}, {"$set": {"specs.ram": "32GB"}} + ) + print(f"Update modified count: {update_result.modified_count}") + + # Verify the update + updated_laptop = products_collection.find_one({"name": "Laptop"}) + print(f"Found updated laptop: {updated_laptop is not None}") + if updated_laptop: + print(f"Updated laptop specs: {updated_laptop.get('specs')}") + if "specs" in updated_laptop: + print(f"Updated RAM: {updated_laptop['specs'].get('ram')}") + + # Check each condition separately + condition1 = laptop is not None + condition2 = laptop and laptop.get("specs", {}).get("ram") == "16GB" + condition3 = update_result.modified_count == 1 + condition4 = ( + updated_laptop and updated_laptop.get("specs", {}).get("ram") == "32GB" + ) + + print(f"Condition 1 (laptop found): {condition1}") + print(f"Condition 2 (original RAM is 16GB): {condition2}") + print(f"Condition 3 (update modified 1 doc): {condition3}") + print(f"Condition 4 (updated RAM is 32GB): {condition4}") + + success = condition1 and condition2 and condition3 and condition4 + print(f"Test {'passed' if success else 'failed'}") + return success + except Exception as e: + print(f"Test failed with exception: {e}") + return False + + +def test_array_operations(): + """Test operations with arrays in orders collection.""" + print("\nTesting array operations...") + try: + with mongo_handler.collection("orders") as orders_collection: + # Clear any existing data + orders_collection.delete_many({}) + print("Cleared existing data") + + # Insert an order with array of items + insert_result = orders_collection.insert_one( + { + "order_id": "ORD001", + "customer": "john", + "items": [ + {"product": "Laptop", "quantity": 1}, + {"product": "Mouse", "quantity": 2}, + ], + "total": 1099.99, + "status": "pending", + } + ) + print(f"Inserted order with ID: {insert_result.inserted_id}") + + # Find orders containing specific items + laptop_orders = list(orders_collection.find({"items.product": "Laptop"})) + print(f"Found {len(laptop_orders)} orders with Laptop") + + # Update array elements + update_result = orders_collection.update_one( + {"order_id": "ORD001"}, + {"$push": {"items": {"product": "Keyboard", "quantity": 1}}}, + ) + print(f"Update modified count: {update_result.modified_count}") + + # Verify the update + updated_order = orders_collection.find_one({"order_id": "ORD001"}) + print(f"Found updated order: {updated_order is not None}") + + if updated_order: + print( + f"Number of items in order: {len(updated_order.get('items', []))}" + ) + items = updated_order.get("items", []) + if items: + last_item = items[-1] if items else None + print(f"Last item in order: {last_item}") + + # Check each condition separately + condition1 = len(laptop_orders) == 1 + condition2 = update_result.modified_count == 1 + condition3 = updated_order and len(updated_order.get("items", [])) == 3 + condition4 = ( + updated_order + and updated_order.get("items", []) + and updated_order["items"][-1].get("product") == "Keyboard" + ) + + print(f"Condition 1 (found 1 laptop order): {condition1}") + print(f"Condition 2 (update modified 1 doc): {condition2}") + print(f"Condition 3 (order has 3 items): {condition3}") + print(f"Condition 4 (last item is keyboard): {condition4}") + + success = condition1 and condition2 and condition3 and condition4 + print(f"Test {'passed' if success else 'failed'}") + return success + except Exception as e: + print(f"Test failed with exception: {e}") + return False + + +def test_aggregation(): + """Test aggregation operations on sales collection.""" + print("\nTesting aggregation operations...") + try: + with mongo_handler.collection("sales") as sales_collection: + # Clear any existing data + sales_collection.delete_many({}) + print("Cleared existing data") + + # Insert sample sales data + insert_result = sales_collection.insert_many( + [ + {"product": "Laptop", "amount": 999.99, "date": datetime.now()}, + {"product": "Mouse", "amount": 29.99, "date": datetime.now()}, + {"product": "Keyboard", "amount": 59.99, "date": datetime.now()}, + ] + ) + print(f"Inserted {len(insert_result.inserted_ids)} sales documents") + + # Calculate total sales by product - use a simpler aggregation pipeline + pipeline = [ + {"$match": {}}, # Match all documents + {"$group": {"_id": "$product", "total": {"$sum": "$amount"}}}, + ] + + # Execute the aggregation + sales_summary = list(sales_collection.aggregate(pipeline)) + print(f"Aggregation returned {len(sales_summary)} results") + + # Print the results for debugging + for item in sales_summary: + print(f"Product: {item.get('_id')}, Total: {item.get('total')}") + + # Check each condition separately + condition1 = len(sales_summary) == 3 + condition2 = any( + item.get("_id") == "Laptop" + and abs(item.get("total", 0) - 999.99) < 0.01 + for item in sales_summary + ) + condition3 = any( + item.get("_id") == "Mouse" and abs(item.get("total", 0) - 29.99) < 0.01 + for item in sales_summary + ) + condition4 = any( + item.get("_id") == "Keyboard" + and abs(item.get("total", 0) - 59.99) < 0.01 + for item in sales_summary + ) + + print(f"Condition 1 (3 summary items): {condition1}") + print(f"Condition 2 (laptop total correct): {condition2}") + print(f"Condition 3 (mouse total correct): {condition3}") + print(f"Condition 4 (keyboard total correct): {condition4}") + + success = condition1 and condition2 and condition3 and condition4 + print(f"Test {'passed' if success else 'failed'}") + return success + except Exception as e: + print(f"Test failed with exception: {e}") + return False + + +def test_index_operations(): + """Test index creation and unique constraints.""" + print("\nTesting index operations...") + try: + with mongo_handler.collection("test_collection") as collection: + # Create indexes + collection.create_index("email", unique=True) + collection.create_index([("username", 1), ("role", 1)]) + + # Insert initial document + collection.insert_one( + {"username": "test_user", "email": "test@example.com"} + ) + + # Try to insert duplicate email (should fail) + try: + collection.insert_one( + {"username": "test_user2", "email": "test@example.com"} + ) + success = False # Should not reach here + except Exception: + success = True + + print(f"Test {'passed' if success else 'failed'}") + return success + except Exception as e: + print(f"Test failed with exception: {e}") + return False + + +def test_complex_queries(): + """Test complex queries with multiple conditions.""" + print("\nTesting complex queries...") + try: + with mongo_handler.collection("products") as products_collection: + # Insert test data + products_collection.insert_many( + [ + { + "name": "Expensive Laptop", + "price": 999.99, + "tags": ["electronics", "computers"], + "in_stock": True, + }, + { + "name": "Cheap Mouse", + "price": 29.99, + "tags": ["electronics", "peripherals"], + "in_stock": True, + }, + ] + ) + + # Find products with price range and specific tags + expensive_electronics = list( + products_collection.find( + { + "price": {"$gt": 500}, + "tags": {"$in": ["electronics"]}, + "in_stock": True, + } + ) + ) + + # Update with multiple conditions - split into separate operations for better compatibility + # First set the discount + products_collection.update_many( + {"price": {"$lt": 100}, "in_stock": True}, {"$set": {"discount": 0.1}} + ) + + # Then update the price + update_result = products_collection.update_many( + {"price": {"$lt": 100}, "in_stock": True}, {"$inc": {"price": -10}} + ) + + # Verify the update + updated_product = products_collection.find_one({"name": "Cheap Mouse"}) + + # Print debug information + print(f"Found expensive electronics: {len(expensive_electronics)}") + if expensive_electronics: + print( + f"First expensive product: {expensive_electronics[0].get('name')}" + ) + print(f"Modified count: {update_result.modified_count}") + if updated_product: + print(f"Updated product price: {updated_product.get('price')}") + print(f"Updated product discount: {updated_product.get('discount')}") + + # More flexible verification with approximate float comparison + success = ( + len(expensive_electronics) >= 1 + and expensive_electronics[0].get("name") + in ["Expensive Laptop", "Laptop"] + and update_result.modified_count >= 1 + and updated_product is not None + and updated_product.get("discount", 0) + > 0 # Just check that discount exists and is positive + ) + print(f"Test {'passed' if success else 'failed'}") + return success + except Exception as e: + print(f"Test failed with exception: {e}") + return False + + +def run_concurrent_operation_test(num_threads=100): + """Run a simple operation in multiple threads to verify connection pooling.""" + import threading + import time + import uuid + from concurrent.futures import ThreadPoolExecutor + + print(f"\nStarting concurrent operation test with {num_threads} threads...") + + # Results tracking + results = {"passed": 0, "failed": 0, "errors": []} + results_lock = threading.Lock() + + def worker(thread_id): + # Create a unique collection name for this thread + collection_name = f"concurrent_test_{thread_id}" + + try: + # Generate unique data for this thread + unique_id = str(uuid.uuid4()) + + with mongo_handler.collection(collection_name) as collection: + # Insert a document + collection.insert_one( + { + "thread_id": thread_id, + "uuid": unique_id, + "timestamp": time.time(), + } + ) + + # Find the document + doc = collection.find_one({"thread_id": thread_id}) + + # Update the document + collection.update_one( + {"thread_id": thread_id}, {"$set": {"updated": True}} + ) + + # Verify update + updated_doc = collection.find_one({"thread_id": thread_id}) + + # Clean up + collection.delete_many({"thread_id": thread_id}) + + success = ( + doc is not None + and updated_doc is not None + and updated_doc.get("updated") is True + ) + + # Update results with thread safety + with results_lock: + if success: + results["passed"] += 1 + else: + results["failed"] += 1 + results["errors"].append(f"Thread {thread_id} operation failed") + except Exception as e: + with results_lock: + results["failed"] += 1 + results["errors"].append(f"Thread {thread_id} exception: {str(e)}") + + # Create and start threads using a thread pool + start_time = time.time() + with ThreadPoolExecutor(max_workers=num_threads) as executor: + futures = [executor.submit(worker, i) for i in range(num_threads)] + + # Calculate execution time + execution_time = time.time() - start_time + + # Print results + print(f"\nConcurrent Operation Test Results:") + print(f"Total threads: {num_threads}") + print(f"Passed: {results['passed']}") + print(f"Failed: {results['failed']}") + print(f"Execution time: {execution_time:.2f} seconds") + print(f"Operations per second: {num_threads / execution_time:.2f}") + + if results["failed"] > 0: + print("\nErrors:") + for error in results["errors"][ + :10 + ]: # Show only first 10 errors to avoid flooding output + print(f"- {error}") + if len(results["errors"]) > 10: + print(f"- ... and {len(results['errors']) - 10} more errors") + + return results["failed"] == 0 + + +def run_all_tests(): + """Run all MongoDB tests and report results.""" + print("Starting MongoDB tests...") + + # Clean up any existing test data before starting + cleanup_test_data() + + tests = [ + test_basic_crud_operations, + test_nested_documents, + test_array_operations, + test_aggregation, + test_index_operations, + test_complex_queries, + ] + + passed_list, not_passed_list = [], [] + passed, failed = 0, 0 + + for test in tests: + # Clean up test data before each test + cleanup_test_data() + try: + if test(): + passed += 1 + passed_list.append(f"Test {test.__name__} passed") + else: + failed += 1 + not_passed_list.append(f"Test {test.__name__} failed") + except Exception as e: + print(f"Test {test.__name__} failed with exception: {e}") + failed += 1 + not_passed_list.append(f"Test {test.__name__} failed") + + print(f"\nTest Results: {passed} passed, {failed} failed") + print("Passed Tests:") + print("\n".join(passed_list)) + print("Failed Tests:") + print("\n".join(not_passed_list)) + + return passed, failed + + +if __name__ == "__main__": + mongo_handler = MongoDBHandler() + + # Run standard tests first + passed, failed = run_all_tests() + + # If all tests pass, run the concurrent operation test + if failed == 0: + run_concurrent_operation_test(10000) diff --git a/api_services/api_controllers/mongo/local_test.py b/api_services/api_controllers/mongo/local_test.py new file mode 100644 index 0000000..abb0332 --- /dev/null +++ b/api_services/api_controllers/mongo/local_test.py @@ -0,0 +1,93 @@ +""" +Test script for MongoDB handler with a local MongoDB instance. +""" + +import os +from datetime import datetime +from .database import MongoDBHandler, CollectionContext + +# Create a custom handler class for local testing +class LocalMongoDBHandler(MongoDBHandler): + """A MongoDB handler for local testing without authentication.""" + + def __init__(self): + """Initialize with a direct MongoDB URI.""" + self._initialized = False + self.uri = "mongodb://localhost:27017/test" + self.client_options = { + "maxPoolSize": 5, + "minPoolSize": 2, + "maxIdleTimeMS": 30000, + "waitQueueTimeoutMS": 2000, + "serverSelectionTimeoutMS": 5000, + } + self._initialized = True + + +# Create a custom handler for local testing +def create_local_handler(): + """Create a MongoDB handler for local testing.""" + # Create a fresh instance with direct MongoDB URI + handler = LocalMongoDBHandler() + return handler + + +def test_connection_monitoring(): + """Test connection monitoring with the MongoDB handler.""" + print("\nTesting connection monitoring...") + + # Create a local handler + local_handler = create_local_handler() + + # Add connection tracking to the handler + local_handler._open_connections = 0 + + # Modify the CollectionContext class to track connections + original_enter = CollectionContext.__enter__ + original_exit = CollectionContext.__exit__ + + def tracked_enter(self): + result = original_enter(self) + self.db_handler._open_connections += 1 + print(f"Connection opened. Total open: {self.db_handler._open_connections}") + return result + + def tracked_exit(self, exc_type, exc_val, exc_tb): + self.db_handler._open_connections -= 1 + print(f"Connection closed. Total open: {self.db_handler._open_connections}") + return original_exit(self, exc_type, exc_val, exc_tb) + + # Apply the tracking methods + CollectionContext.__enter__ = tracked_enter + CollectionContext.__exit__ = tracked_exit + + try: + # Test with multiple operations + for i in range(3): + print(f"\nTest iteration {i+1}:") + try: + with local_handler.collection("test_collection") as collection: + # Try a simple operation + try: + collection.find_one({}) + print("Operation succeeded") + except Exception as e: + print(f"Operation failed: {e}") + except Exception as e: + print(f"Connection failed: {e}") + + # Final connection count + print(f"\nFinal open connections: {local_handler._open_connections}") + if local_handler._open_connections == 0: + print("✅ All connections were properly closed") + else: + print(f"❌ {local_handler._open_connections} connections remain open") + + finally: + # Restore original methods + CollectionContext.__enter__ = original_enter + CollectionContext.__exit__ = original_exit + + +if __name__ == "__main__": + test_connection_monitoring() diff --git a/api_services/api_controllers/postgres/config.py b/api_services/api_controllers/postgres/config.py new file mode 100644 index 0000000..53fbec6 --- /dev/null +++ b/api_services/api_controllers/postgres/config.py @@ -0,0 +1,31 @@ +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Configs(BaseSettings): + """ + Postgresql configuration settings. + """ + + DB: str = "" + USER: str = "" + PASSWORD: str = "" + HOST: str = "" + PORT: int = 0 + ENGINE: str = "postgresql+psycopg2" + POOL_PRE_PING: bool = True + POOL_SIZE: int = 20 + MAX_OVERFLOW: int = 10 + POOL_RECYCLE: int = 600 + POOL_TIMEOUT: int = 30 + ECHO: bool = True + + @property + def url(self): + """Generate the database URL.""" + return f"{self.ENGINE}://{self.USER}:{self.PASSWORD}@{self.HOST}:{self.PORT}/{self.DB}" + + model_config = SettingsConfigDict(env_prefix="POSTGRES_") + + +# singleton instance of the POSTGRESQL configuration settings +postgres_configs = Configs() diff --git a/api_services/api_controllers/postgres/engine.py b/api_services/api_controllers/postgres/engine.py new file mode 100644 index 0000000..3c294e8 --- /dev/null +++ b/api_services/api_controllers/postgres/engine.py @@ -0,0 +1,63 @@ +from contextlib import contextmanager +from functools import lru_cache +from typing import Generator +from api_controllers.postgres.config import postgres_configs + +from sqlalchemy import create_engine +from sqlalchemy.orm import declarative_base, sessionmaker, scoped_session, Session + + +# Configure the database engine with proper pooling +engine = create_engine( + postgres_configs.url, + pool_pre_ping=True, + pool_size=10, # Reduced from 20 to better match your CPU cores + max_overflow=5, # Reduced from 10 to prevent too many connections + pool_recycle=600, # Keep as is + pool_timeout=30, # Keep as is + echo=False, # Consider setting to False in production +) + + +Base = declarative_base() + + +# Create a cached session factory +@lru_cache() +def get_session_factory() -> scoped_session: + """Create a thread-safe session factory.""" + session_local = sessionmaker( + bind=engine, + autocommit=False, + autoflush=False, + expire_on_commit=True, # Prevent expired object issues + ) + return scoped_session(session_local) + + +# Get database session with proper connection management +@contextmanager +def get_db() -> Generator[Session, None, None]: + """Get database session with proper connection management. + + This context manager ensures: + - Proper connection pooling + - Session cleanup + - Connection return to pool + - Thread safety + + Yields: + Session: SQLAlchemy session object + """ + + session_factory = get_session_factory() + session = session_factory() + try: + yield session + session.commit() + except Exception: + session.rollback() + raise + finally: + session.close() + session_factory.remove() # Clean up the session from the registry diff --git a/api_services/api_controllers/postgres/mixin.py b/api_services/api_controllers/postgres/mixin.py new file mode 100644 index 0000000..9cce20a --- /dev/null +++ b/api_services/api_controllers/postgres/mixin.py @@ -0,0 +1,141 @@ +import arrow + +from sqlalchemy import Column, Integer, String, Float, ForeignKey, UUID, TIMESTAMP, Boolean, SmallInteger, Numeric, func, text +from sqlalchemy.orm import relationship +from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy_mixins.serialize import SerializeMixin +from sqlalchemy_mixins.repr import ReprMixin +from sqlalchemy_mixins.smartquery import SmartQueryMixin +from sqlalchemy_mixins.activerecord import ActiveRecordMixin +from api_controllers.postgres.engine import get_db, Base + + +class BasicMixin( + Base, + ActiveRecordMixin, + SerializeMixin, + ReprMixin, + SmartQueryMixin, +): + + __abstract__ = True + __repr__ = ReprMixin.__repr__ + + @classmethod + def new_session(cls): + """Get database session.""" + return get_db() + + +class CrudMixin(BasicMixin): + """ + Base mixin providing CRUD operations and common fields for PostgreSQL models. + + Features: + - Automatic timestamps (created_at, updated_at) + - Soft delete capability + - User tracking (created_by, updated_by) + - Data serialization + - Multi-language support + """ + + __abstract__ = True + + # Primary and reference fields + id: Mapped[int] = mapped_column(Integer, primary_key=True) + uu_id: Mapped[str] = mapped_column( + UUID, + server_default=text("gen_random_uuid()"), + index=True, + unique=True, + comment="Unique identifier UUID", + ) + + # Common timestamp fields for all models + expiry_starts: Mapped[TIMESTAMP] = mapped_column( + TIMESTAMP(timezone=True), + server_default=func.now(), + comment="Record validity start timestamp", + ) + expiry_ends: Mapped[TIMESTAMP] = mapped_column( + TIMESTAMP(timezone=True), + default=str(arrow.get("2099-12-31")), + server_default=func.now(), + comment="Record validity end timestamp", + ) + + # Timestamps + created_at: Mapped[TIMESTAMP] = mapped_column( + TIMESTAMP(timezone=True), + server_default=func.now(), + nullable=False, + index=True, + comment="Record creation timestamp", + ) + updated_at: Mapped[TIMESTAMP] = mapped_column( + TIMESTAMP(timezone=True), + server_default=func.now(), + onupdate=func.now(), + nullable=False, + index=True, + comment="Last update timestamp", + ) + + +class CrudCollection(CrudMixin): + """ + Full-featured model class with all common fields. + + Includes: + - UUID and reference ID + - Timestamps + - User tracking + - Confirmation status + - Soft delete + - Notification flags + """ + + __abstract__ = True + __repr__ = ReprMixin.__repr__ + + # Outer reference fields + ref_id: Mapped[str] = mapped_column( + String(100), nullable=True, index=True, comment="External reference ID" + ) + replication_id: Mapped[int] = mapped_column( + SmallInteger, server_default="0", comment="Replication identifier" + ) + + # Cryptographic and user tracking + cryp_uu_id: Mapped[str] = mapped_column( + String, nullable=True, index=True, comment="Cryptographic UUID" + ) + + # Token fields of modification + created_credentials_token: Mapped[str] = mapped_column( + String, nullable=True, comment="Created Credentials token" + ) + updated_credentials_token: Mapped[str] = mapped_column( + String, nullable=True, comment="Updated Credentials token" + ) + confirmed_credentials_token: Mapped[str] = mapped_column( + String, nullable=True, comment="Confirmed Credentials token" + ) + + # Status flags + is_confirmed: Mapped[bool] = mapped_column( + Boolean, server_default="0", comment="Record confirmation status" + ) + deleted: Mapped[bool] = mapped_column( + Boolean, server_default="0", comment="Soft delete flag" + ) + active: Mapped[bool] = mapped_column( + Boolean, server_default="1", comment="Record active status" + ) + is_notification_send: Mapped[bool] = mapped_column( + Boolean, server_default="0", comment="Notification sent flag" + ) + is_email_send: Mapped[bool] = mapped_column( + Boolean, server_default="0", comment="Email sent flag" + ) + diff --git a/api_services/api_controllers/redis/Broadcast/README.md b/api_services/api_controllers/redis/Broadcast/README.md new file mode 100644 index 0000000..3fdf588 --- /dev/null +++ b/api_services/api_controllers/redis/Broadcast/README.md @@ -0,0 +1,67 @@ +# Redis Pub/Sub Chain Implementation + +This module implements a chain of services communicating through Redis Pub/Sub channels. Each service in the chain subscribes to the previous service's channel and publishes to its own channel, creating a processing pipeline. + +## Architecture + +The implementation follows a simple chain pattern: + +``` +READER → PROCESSOR → WRITER +``` + +- **READER**: Generates mock data with a "red" stage and publishes to `chain:reader` +- **PROCESSOR**: Subscribes to `chain:reader`, processes messages with "red" stage, updates stage to "processed", and publishes to `chain:processor` +- **WRITER**: Subscribes to `chain:processor`, processes messages with "processed" stage, updates stage to "completed", and publishes to `chain:writer` + +## Message Flow + +Each message flows through the chain with a stage attribute that determines how it's processed: + +1. READER generates a message with `stage="red"` +2. PROCESSOR receives the message, checks if `stage="red"`, processes it, and sets `stage="processed"` +3. WRITER receives the message, checks if `stage="processed"`, processes it, and sets `stage="completed"` + +## Performance + +The implementation includes timing information to track how long messages take to flow through the entire chain. Sample output: + +``` +[READER] 1745176466.132082 | Published UUID: 74cf2312-25ec-4da8-bc0a-521b6ccd5206 +[PROCESSOR] 1745176466.132918 | Received UUID: 74cf2312-25ec-4da8-bc0a-521b6ccd5206 | Published UUID: 74cf2312-25ec-4da8-bc0a-521b6ccd5206 +[WRITER] 1745176466.133097 | Received UUID: 74cf2312-25ec-4da8-bc0a-521b6ccd5206 | Published UUID: 74cf2312-25ec-4da8-bc0a-521b6ccd5206 | Elapsed: 1.83ms +[READER] 1745176468.133018 | Published UUID: 2ffd217f-650f-4e10-bc16-317adcf7a59a +[PROCESSOR] 1745176468.133792 | Received UUID: 2ffd217f-650f-4e10-bc16-317adcf7a59a | Published UUID: 2ffd217f-650f-4e10-bc16-317adcf7a59a +[WRITER] 1745176468.134001 | Received UUID: 2ffd217f-650f-4e10-bc16-317adcf7a59a | Published UUID: 2ffd217f-650f-4e10-bc16-317adcf7a59a | Elapsed: 1.76ms +[READER] 1745176470.133841 | Published UUID: 87e1f3af-c6c2-4fa5-9a65-57e7327d3989 +[PROCESSOR] 1745176470.134623 | Received UUID: 87e1f3af-c6c2-4fa5-9a65-57e7327d3989 | Published UUID: 87e1f3af-c6c2-4fa5-9a65-57e7327d3989 +[WRITER] 1745176470.134861 | Received UUID: 87e1f3af-c6c2-4fa5-9a65-57e7327d3989 | Published UUID: 87e1f3af-c6c2-4fa5-9a65-57e7327d3989 | Elapsed: 1.68ms +``` + +The elapsed time shows the total time from when the READER publishes a message until the WRITER completes processing it. In the samples above, the end-to-end processing time ranges from 1.68ms to 1.83ms. + +## Usage + +To run the demonstration: + +```bash +python -m Controllers.Redis.Broadcast.implementations +``` + +This will start all three services in the chain and begin processing messages. Press Ctrl+C to stop the demonstration. + +## Implementation Details + +The implementation uses: + +1. A singleton Redis Pub/Sub handler with publisher and subscriber capabilities +2. Thread-based message processing +3. JSON serialization for message passing +4. Stage-based message processing to track progress through the chain +5. Timing information to measure performance + +Each service in the chain follows these steps: +1. Subscribe to the appropriate channel +2. Define a message handler function +3. Process incoming messages based on their stage +4. Publish processed messages to the next channel in the chain diff --git a/api_services/api_controllers/redis/Broadcast/actions.py b/api_services/api_controllers/redis/Broadcast/actions.py new file mode 100644 index 0000000..756e9f3 --- /dev/null +++ b/api_services/api_controllers/redis/Broadcast/actions.py @@ -0,0 +1,248 @@ +import json +from typing import Optional, Dict, Any, List, Callable, Union +from threading import Thread + +from Controllers.Redis.connection import redis_cli +from Controllers.Redis.response import RedisResponse + + +class RedisPublisher: + """Redis Publisher class for broadcasting messages to channels.""" + + def __init__(self, redis_client=redis_cli): + self.redis_client = redis_client + + def publish(self, channel: str, message: Union[Dict, List, str]) -> RedisResponse: + """Publish a message to a Redis channel. + + Args: + channel: The channel to publish to + message: The message to publish (will be JSON serialized if dict or list) + + Returns: + RedisResponse with status and message + """ + try: + # Convert dict/list to JSON string if needed + if isinstance(message, (dict, list)): + message = json.dumps(message) + + # Publish the message + recipient_count = self.redis_client.publish(channel, message) + + return RedisResponse( + status=True, + message=f"Message published successfully to {channel}.", + data={"recipients": recipient_count}, + ) + except Exception as e: + return RedisResponse( + status=False, + message=f"Failed to publish message to {channel}.", + error=str(e), + ) + + +class RedisSubscriber: + """Redis Subscriber class for listening to channels.""" + + def __init__(self, redis_client=redis_cli): + self.redis_client = redis_client + self.pubsub = self.redis_client.pubsub() + self.active_threads = {} + + def subscribe( + self, channel: str, callback: Callable[[Dict], None] + ) -> RedisResponse: + """Subscribe to a Redis channel with a callback function. + + Args: + channel: The channel to subscribe to + callback: Function to call when a message is received + + Returns: + RedisResponse with status and message + """ + try: + # Subscribe to the channel + self.pubsub.subscribe(**{channel: self._message_handler(callback)}) + + return RedisResponse( + status=True, message=f"Successfully subscribed to {channel}." + ) + except Exception as e: + return RedisResponse( + status=False, message=f"Failed to subscribe to {channel}.", error=str(e) + ) + + def psubscribe( + self, pattern: str, callback: Callable[[Dict], None] + ) -> RedisResponse: + """Subscribe to Redis channels matching a pattern. + + Args: + pattern: The pattern to subscribe to (e.g., 'user.*') + callback: Function to call when a message is received + + Returns: + RedisResponse with status and message + """ + try: + # Subscribe to the pattern + self.pubsub.psubscribe(**{pattern: self._message_handler(callback)}) + + return RedisResponse( + status=True, message=f"Successfully pattern-subscribed to {pattern}." + ) + except Exception as e: + return RedisResponse( + status=False, + message=f"Failed to pattern-subscribe to {pattern}.", + error=str(e), + ) + + def _message_handler(self, callback: Callable[[Dict], None]): + """Create a message handler function for the subscription.""" + + def handler(message): + # Skip subscription confirmation messages + if message["type"] in ("subscribe", "psubscribe"): + return + + # Parse JSON if the message is a JSON string + data = message["data"] + if isinstance(data, bytes): + data = data.decode("utf-8") + try: + data = json.loads(data) + except json.JSONDecodeError: + # Not JSON, keep as is + pass + + # Call the callback with the message data + callback( + { + "channel": ( + message.get("channel", b"").decode("utf-8") + if isinstance(message.get("channel", b""), bytes) + else message.get("channel", "") + ), + "pattern": ( + message.get("pattern", b"").decode("utf-8") + if isinstance(message.get("pattern", b""), bytes) + else message.get("pattern", "") + ), + "data": data, + } + ) + + return handler + + def start_listening(self, in_thread: bool = True) -> RedisResponse: + """Start listening for messages on subscribed channels. + + Args: + in_thread: If True, start listening in a separate thread + + Returns: + RedisResponse with status and message + """ + try: + if in_thread: + thread = Thread(target=self._listen_thread, daemon=True) + thread.start() + self.active_threads["listener"] = thread + return RedisResponse( + status=True, message="Listening thread started successfully." + ) + else: + # This will block the current thread + self._listen_thread() + return RedisResponse( + status=True, message="Listening started successfully (blocking)." + ) + except Exception as e: + return RedisResponse( + status=False, message="Failed to start listening.", error=str(e) + ) + + def _listen_thread(self): + """Thread function for listening to messages.""" + self.pubsub.run_in_thread(sleep_time=0.01) + + def stop_listening(self) -> RedisResponse: + """Stop listening for messages.""" + try: + self.pubsub.close() + return RedisResponse(status=True, message="Successfully stopped listening.") + except Exception as e: + return RedisResponse( + status=False, message="Failed to stop listening.", error=str(e) + ) + + def unsubscribe(self, channel: Optional[str] = None) -> RedisResponse: + """Unsubscribe from a channel or all channels. + + Args: + channel: The channel to unsubscribe from, or None for all channels + + Returns: + RedisResponse with status and message + """ + try: + if channel: + self.pubsub.unsubscribe(channel) + message = f"Successfully unsubscribed from {channel}." + else: + self.pubsub.unsubscribe() + message = "Successfully unsubscribed from all channels." + + return RedisResponse(status=True, message=message) + except Exception as e: + return RedisResponse( + status=False, + message=f"Failed to unsubscribe from {'channel' if channel else 'all channels'}.", + error=str(e), + ) + + def punsubscribe(self, pattern: Optional[str] = None) -> RedisResponse: + """Unsubscribe from a pattern or all patterns. + + Args: + pattern: The pattern to unsubscribe from, or None for all patterns + + Returns: + RedisResponse with status and message + """ + try: + if pattern: + self.pubsub.punsubscribe(pattern) + message = f"Successfully unsubscribed from pattern {pattern}." + else: + self.pubsub.punsubscribe() + message = "Successfully unsubscribed from all patterns." + + return RedisResponse(status=True, message=message) + except Exception as e: + return RedisResponse( + status=False, + message=f"Failed to unsubscribe from {'pattern' if pattern else 'all patterns'}.", + error=str(e), + ) + + +class RedisPubSub: + """Singleton class that provides both publisher and subscriber functionality.""" + + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super(RedisPubSub, cls).__new__(cls) + cls._instance.publisher = RedisPublisher() + cls._instance.subscriber = RedisSubscriber() + return cls._instance + + +# Create a singleton instance +redis_pubsub = RedisPubSub() diff --git a/api_services/api_controllers/redis/Broadcast/implementations.py b/api_services/api_controllers/redis/Broadcast/implementations.py new file mode 100644 index 0000000..34a336f --- /dev/null +++ b/api_services/api_controllers/redis/Broadcast/implementations.py @@ -0,0 +1,205 @@ +import json +import time +import uuid +from datetime import datetime +from threading import Thread + +from Controllers.Redis.Broadcast.actions import redis_pubsub + + +# Define the channels for our chain +CHANNEL_READER = "chain:reader" +CHANNEL_PROCESSOR = "chain:processor" +CHANNEL_WRITER = "chain:writer" + +# Flag to control the demo +running = True + + +def generate_mock_data(): + """Generate a mock message with UUID, timestamp, and sample data.""" + return { + "uuid": str(uuid.uuid4()), + "timestamp": datetime.now().isoformat(), + "stage": "red", # Initial stage is 'red' + "data": { + "value": f"Sample data {int(time.time())}", + "status": "new", + "counter": 0, + }, + } + + +def reader_function(): + """ + First function in the chain. + Generates mock data and publishes to the reader channel. + """ + print("[READER] Function started") + + while running: + # Generate mock data + message = generate_mock_data() + start_time = time.time() + message["start_time"] = start_time + + # Publish to reader channel + result = redis_pubsub.publisher.publish(CHANNEL_READER, message) + + if result.status: + print(f"[READER] {time.time():.6f} | Published UUID: {message['uuid']}") + else: + print(f"[READER] Publish error: {result.error}") + + # Wait before generating next message + time.sleep(2) + + +def processor_function(): + """ + Second function in the chain. + Subscribes to reader channel, processes messages, and publishes to processor channel. + """ + print("[PROCESSOR] Function started") + + def on_reader_message(message): + # The message structure from the subscriber has 'data' containing our actual message + # If data is a string, parse it as JSON + data = message["data"] + if isinstance(data, str): + try: + data = json.loads(data) + except json.JSONDecodeError as e: + print(f"[PROCESSOR] Error parsing message data: {e}") + return + + # Check if stage is 'red' before processing + if data.get("stage") == "red": + # Process the message + data["processor_timestamp"] = datetime.now().isoformat() + data["data"]["status"] = "processed" + data["data"]["counter"] += 1 + + # Update stage to 'processed' + data["stage"] = "processed" + + # Add some processing metadata + data["processing"] = { + "duration_ms": 150, # Mock processing time + "processor_id": "main-processor", + } + + # Publish to processor channel + result = redis_pubsub.publisher.publish(CHANNEL_PROCESSOR, data) + + if result.status: + print( + f"[PROCESSOR] {time.time():.6f} | Received UUID: {data['uuid']} | Published UUID: {data['uuid']}" + ) + else: + print(f"[PROCESSOR] Publish error: {result.error}") + else: + print(f"[PROCESSOR] Skipped message: {data['uuid']} (stage is not 'red')") + + # Subscribe to reader channel + result = redis_pubsub.subscriber.subscribe(CHANNEL_READER, on_reader_message) + + if result.status: + print(f"[PROCESSOR] Subscribed to channel: {CHANNEL_READER}") + else: + print(f"[PROCESSOR] Subscribe error: {result.error}") + + +def writer_function(): + """ + Third function in the chain. + Subscribes to processor channel and performs final processing. + """ + print("[WRITER] Function started") + + def on_processor_message(message): + # The message structure from the subscriber has 'data' containing our actual message + # If data is a string, parse it as JSON + data = message["data"] + if isinstance(data, str): + try: + data = json.loads(data) + except json.JSONDecodeError as e: + print(f"[WRITER] Error parsing message data: {e}") + return + + # Check if stage is 'processed' before processing + if data.get("stage") == "processed": + # Process the message + data["writer_timestamp"] = datetime.now().isoformat() + data["data"]["status"] = "completed" + data["data"]["counter"] += 1 + + # Update stage to 'completed' + data["stage"] = "completed" + + # Add some writer metadata + data["storage"] = {"location": "main-db", "partition": "events-2025-04"} + + # Calculate elapsed time if start_time is available + current_time = time.time() + elapsed_ms = "" + if "start_time" in data: + elapsed_ms = ( + f" | Elapsed: {(current_time - data['start_time']) * 1000:.2f}ms" + ) + + # Optionally publish to writer channel for any downstream listeners + result = redis_pubsub.publisher.publish(CHANNEL_WRITER, data) + + if result.status: + print( + f"[WRITER] {current_time:.6f} | Received UUID: {data['uuid']} | Published UUID: {data['uuid']}{elapsed_ms}" + ) + else: + print(f"[WRITER] Publish error: {result.error}") + else: + print( + f"[WRITER] Skipped message: {data['uuid']} (stage is not 'processed')" + ) + + # Subscribe to processor channel + result = redis_pubsub.subscriber.subscribe(CHANNEL_PROCESSOR, on_processor_message) + + if result.status: + print(f"[WRITER] Subscribed to channel: {CHANNEL_PROCESSOR}") + else: + print(f"[WRITER] Subscribe error: {result.error}") + + +def run_demo(): + """Run a demonstration of the simple chain of functions.""" + print("=== Starting Redis Pub/Sub Chain Demonstration ===") + print("Chain: READER → PROCESSOR → WRITER") + print(f"Channels: {CHANNEL_READER} → {CHANNEL_PROCESSOR} → {CHANNEL_WRITER}") + print("Format: [SERVICE] TIMESTAMP | Received/Published UUID | [Elapsed time]") + + # Start the Redis subscriber listening thread + redis_pubsub.subscriber.start_listening() + + # Start processor and writer functions (these subscribe to channels) + processor_function() + writer_function() + + # Create a thread for the reader function (this publishes messages) + reader_thread = Thread(target=reader_function, daemon=True) + reader_thread.start() + + # Keep the main thread alive + try: + while True: + time.sleep(0.1) + except KeyboardInterrupt: + print("\nStopping demonstration...") + global running + running = False + redis_pubsub.subscriber.stop_listening() + + +if __name__ == "__main__": + run_demo() diff --git a/api_services/api_controllers/redis/README.md b/api_services/api_controllers/redis/README.md new file mode 100644 index 0000000..80bda34 --- /dev/null +++ b/api_services/api_controllers/redis/README.md @@ -0,0 +1,85 @@ +# Redis Controller + +## Overview +This module provides a robust, thread-safe Redis connection handler with comprehensive concurrent operation testing. The Redis controller is designed for high-performance, resilient database connection management that can handle multiple simultaneous operations efficiently. + +## Features +- Singleton pattern for efficient connection management +- Connection pooling with configurable settings +- Automatic retry capabilities for Redis operations +- Thread-safe operations with proper error handling +- Comprehensive JSON data handling +- TTL management and expiry time resolution +- Efficient batch operations using Redis pipelines + +## Configuration +The Redis controller is configured with the following default settings: +- Host: 10.10.2.15 +- Port: 6379 +- DB: 0 +- Connection pool size: 50 connections +- Health check interval: 30 seconds +- Socket timeout: 5.0 seconds +- Retry on timeout: Enabled +- Socket keepalive: Enabled + +## Usage Examples +The controller provides several high-level methods for Redis operations: +- `set_json`: Store JSON data with optional expiry +- `get_json`: Retrieve JSON data with pattern matching +- `get_json_iterator`: Memory-efficient iterator for large datasets +- `delete`: Remove keys matching a pattern +- `refresh_ttl`: Update expiry time for existing keys +- `key_exists`: Check if a key exists without retrieving it +- `resolve_expires_at`: Get human-readable expiry time + +## Concurrent Performance Testing +The Redis controller has been thoroughly tested for concurrent operations with impressive results: + +### Test Configuration +- 10,000 concurrent threads +- Each thread performs a set, get, and delete operation +- Pipeline used for efficient batching +- Exponential backoff for connection errors +- Comprehensive error tracking and reporting + +### Test Results +``` +Concurrent Redis Test Results: +Total threads: 10000 +Passed: 10000 +Failed: 0 +Operations with retries: 0 +Total retry attempts: 0 +Success rate: 100.00% + +Performance Metrics: +Total execution time: 4.30 seconds +Operations per second: 2324.35 +Average operation time: 1.92 ms +Minimum operation time: 0.43 ms +Maximum operation time: 40.45 ms +95th percentile operation time: 4.14 ms +``` + +## Thread Safety +The Redis controller is designed to be thread-safe with the following mechanisms: +- Connection pooling to manage concurrent connections efficiently +- Thread-local storage for operation-specific data +- Atomic operations using Redis pipelines +- Proper error handling and retry logic for connection issues +- Exponential backoff for handling connection limits + +## Error Handling +The controller implements comprehensive error handling: +- Connection errors are automatically retried with exponential backoff +- Detailed error reporting with context-specific information +- Graceful degradation under high load +- Connection health monitoring and automatic reconnection + +## Best Practices +- Use pipelines for batching multiple operations +- Implement proper key naming conventions +- Set appropriate TTL values for cached data +- Monitor connection pool usage in production +- Use the JSON iterator for large datasets to minimize memory usage diff --git a/api_services/api_controllers/redis/base.py b/api_services/api_controllers/redis/base.py new file mode 100644 index 0000000..aa2a0b7 --- /dev/null +++ b/api_services/api_controllers/redis/base.py @@ -0,0 +1,328 @@ +""" +Redis key-value operations with structured data handling. + +This module provides a class for managing Redis key-value operations with support for: +- Structured data storage and retrieval +- Key pattern generation for searches +- JSON serialization/deserialization +- Type-safe value handling +""" + +import arrow +import json +from typing import Union, Dict, List, Optional, Any, TypeVar + +from Controllers.Redis.connection import redis_cli + + +T = TypeVar("T", Dict[str, Any], List[Any]) + + +class RedisKeyError(Exception): + """Exception raised for Redis key-related errors.""" + + pass + + +class RedisValueError(Exception): + """Exception raised for Redis value-related errors.""" + + pass + + +class RedisRow: + """ + Handles Redis key-value operations with structured data. + + This class provides methods for: + - Managing compound keys with delimiters + - Converting between bytes and string formats + - JSON serialization/deserialization of values + - Pattern generation for Redis key searches + + Attributes: + key: The Redis key in bytes or string format + value: The stored value (will be JSON serialized) + delimiter: Character used to separate compound key parts + expires_at: Optional expiration timestamp + """ + + key: Union[str, bytes] + value: Optional[str] = None + delimiter: str = ":" + expires_at: Optional[dict] = {"seconds": 60 * 60 * 30} + expires_at_string: Optional[str] + + def get_expiry_time(self) -> int | None: + """Calculate expiry time in seconds from kwargs.""" + time_multipliers = {"days": 86400, "hours": 3600, "minutes": 60, "seconds": 1} + if self.expires_at: + return sum( + int(self.expires_at.get(unit, 0)) * multiplier + for unit, multiplier in time_multipliers.items() + ) + return None + + def merge(self, set_values: List[Union[str, bytes]]) -> None: + """ + Merge list of values into a single delimited key. + + Args: + set_values: List of values to merge into key + + Example: + >>> RedisRow.merge(["users", "123", "profile"]) + >>> print(RedisRow.key) + b'users:123:profile' + """ + if not set_values: + raise RedisKeyError("Cannot merge empty list of values") + + merged = [] + for value in set_values: + if value is None: + continue + if isinstance(value, bytes): + value = value.decode() + merged.append(str(value)) + + self.key = self.delimiter.join(merged).encode() + + @classmethod + def regex(cls, list_keys: List[Union[Optional[str], Optional[bytes]]]) -> str: + """ + Generate Redis search pattern from list of keys. + + Args: + list_keys: List of key parts, can include None for wildcards + + Returns: + str: Redis key pattern with wildcards + + Example: + >>> RedisRow.regex([None, "users", "active"]) + '*:users:active' + """ + if not list_keys: + return "" + + # Filter and convert valid keys + valid_keys = [] + for key in list_keys: + if key is None or str(key) == "None": + continue + if isinstance(key, bytes): + key = key.decode() + valid_keys.append(str(key)) + + # Build pattern + pattern = cls.delimiter.join(valid_keys) + if not pattern: + return "" + + # Add wildcard if first key was None + if list_keys[0] is None: + pattern = f"*{cls.delimiter}{pattern}" + if "*" not in pattern and any([list_key is None for list_key in list_keys]): + pattern = f"{pattern}:*" + return pattern + + def parse(self) -> List[str]: + """ + Parse the key into its component parts. + + Returns: + List[str]: Key parts split by delimiter + + Example: + >>> RedisRow.key = b'users:123:profile' + >>> RedisRow.parse() + ['users', '123', 'profile'] + """ + if not self.key: + return [] + + key_str = self.key.decode() if isinstance(self.key, bytes) else self.key + return key_str.split(self.delimiter) + + def feed(self, value: Union[bytes, Dict, List, str]) -> None: + """ + Convert and store value in JSON format. + + Args: + value: Value to store (bytes, dict, or list) + + Raises: + RedisValueError: If value type is not supported + + Example: + >>> RedisRow.feed({"name": "John", "age": 30}) + >>> print(RedisRow.value) + '{"name": "John", "age": 30}' + """ + try: + if isinstance(value, (dict, list)): + self.value = json.dumps(value) + elif isinstance(value, bytes): + self.value = json.dumps(json.loads(value.decode())) + elif isinstance(value, str): + self.value = value + else: + raise RedisValueError(f"Unsupported value type: {type(value)}") + except json.JSONDecodeError as e: + raise RedisValueError(f"Invalid JSON format: {str(e)}") + + def modify(self, add_dict: Dict) -> None: + """ + Modify existing data by merging with new dictionary. + + Args: + add_dict: Dictionary to merge with existing data + + Example: + >>> RedisRow.feed({"name": "John"}) + >>> RedisRow.modify({"age": 30}) + >>> print(RedisRow.data) + {"name": "John", "age": 30} + """ + if not isinstance(add_dict, dict): + raise RedisValueError("modify() requires a dictionary argument") + current_data = self.row if self.row else {} + if not isinstance(current_data, dict): + raise RedisValueError("Cannot modify non-dictionary data") + current_data = { + **current_data, + **add_dict, + } + self.feed(current_data) + self.save() + + def save(self): + """ + Save the data to Redis with optional expiration. + + Raises: + RedisKeyError: If key is not set + RedisValueError: If value is not set + """ + + if not self.key: + raise RedisKeyError("Cannot save data without a key") + if not self.value: + raise RedisValueError("Cannot save empty data") + + if self.expires_at: + redis_cli.setex( + name=self.redis_key, time=self.get_expiry_time(), value=self.value + ) + self.expires_at_string = str( + arrow.now() + .shift(seconds=self.get_expiry_time()) + .format("YYYY-MM-DD HH:mm:ss") + ) + return self.value + redis_cli.set(name=self.redis_key, value=self.value) + self.expires_at = None + self.expires_at_string = None + return self.value + + def remove(self, key: str) -> None: + """ + Remove a key from the stored dictionary. + + Args: + key: Key to remove from stored dictionary + + Raises: + KeyError: If key doesn't exist + RedisValueError: If stored value is not a dictionary + """ + current_data = self.row + if not isinstance(current_data, dict): + raise RedisValueError("Cannot remove key from non-dictionary data") + + try: + current_data.pop(key) + self.feed(current_data) + self.save() + except KeyError: + raise KeyError(f"Key '{key}' not found in stored data") + + def delete(self) -> None: + """Delete the key from Redis.""" + try: + redis_cli.delete(self.redis_key) + except Exception as e: + raise RedisKeyError(f"Failed to delete key: {str(e)}") + + @property + def keys(self) -> str: + """ + Get key as string. + + Returns: + str: Key in string format + """ + return self.key.decode() if isinstance(self.key, bytes) else self.key + + def set_key(self, key: Union[str, bytes]) -> None: + """ + Set key ensuring bytes format. + + Args: + key: Key in string or bytes format + + Raises: + RedisKeyError: If key is empty or invalid + """ + if not key: + raise RedisKeyError("Cannot set empty key") + + # Convert to string for validation + key_str = key.decode() if isinstance(key, bytes) else str(key) + + # Validate key length (Redis has a 512MB limit for keys) + if len(key_str) > 512 * 1024 * 1024: + raise RedisKeyError("Key exceeds maximum length of 512MB") + + # Validate key format (basic check for invalid characters) + if any(c in key_str for c in ["\n", "\r", "\t", "\0"]): + raise RedisKeyError("Key contains invalid characters") + + self.key = key if isinstance(key, bytes) else str(key).encode() + + @property + def redis_key(self) -> bytes: + """ + Get key in bytes format for Redis operations. + + Returns: + bytes: Key in bytes format + """ + return self.key if isinstance(self.key, bytes) else str(self.key).encode() + + @property + def row(self) -> Union[Dict, List]: + """ + Get stored value as Python object. + + Returns: + Union[Dict, List]: Deserialized JSON data + """ + try: + return json.loads(self.value) + except json.JSONDecodeError as e: + raise RedisValueError(f"Invalid JSON format in stored value: {str(e)}") + + @property + def as_dict(self) -> Dict[str, Any]: + """ + Get row data as dictionary. + + Returns: + Dict[str, Any]: Dictionary with keys and value + """ + return { + "keys": self.keys, + "value": self.row, + } diff --git a/api_services/api_controllers/redis/config.py b/api_services/api_controllers/redis/config.py new file mode 100644 index 0000000..337bff3 --- /dev/null +++ b/api_services/api_controllers/redis/config.py @@ -0,0 +1,25 @@ +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Configs(BaseSettings): + """ + Redis configuration settings. + """ + + HOST: str = "10.10.2.15" + PASSWORD: str = "your_strong_password_here" + PORT: int = 6379 + DB: int = 0 + + def as_dict(self): + return dict( + host=self.HOST, + password=self.PASSWORD, + port=int(self.PORT), + db=self.DB, + ) + + model_config = SettingsConfigDict(env_prefix="REDIS_") + + +redis_configs = Configs() # singleton instance of the REDIS configuration settings diff --git a/api_services/api_controllers/redis/connection.py b/api_services/api_controllers/redis/connection.py new file mode 100644 index 0000000..7cb48f8 --- /dev/null +++ b/api_services/api_controllers/redis/connection.py @@ -0,0 +1,215 @@ +import time + +from typing import Dict, Any +from redis import Redis, ConnectionError, TimeoutError, ConnectionPool +from Controllers.Redis.config import redis_configs + + +class RedisConn: + """ + Redis connection manager with connection pooling, retry logic, + and health check capabilities. + """ + + CONNECTION_RETRIES = 3 # Number of connection retries before failing + RETRY_DELAY = 0.5 # Delay between retries in seconds + DEFAULT_TIMEOUT = 5.0 # Default connection timeout in seconds + + def __init__( + self, + max_retries: int = CONNECTION_RETRIES, + ): + """ + Initialize Redis connection with configuration. + + Args: + max_retries: Maximum number of connection attempts. + """ + self.max_retries = max_retries + self.config = redis_configs.as_dict() + self._redis = None + self._pool = None + + # Add default parameters if not provided + if "socket_timeout" not in self.config: + self.config["socket_timeout"] = self.DEFAULT_TIMEOUT + if "socket_connect_timeout" not in self.config: + self.config["socket_connect_timeout"] = self.DEFAULT_TIMEOUT + if "decode_responses" not in self.config: + self.config["decode_responses"] = True + + # Add connection pooling settings if not provided + if "max_connections" not in self.config: + self.config["max_connections"] = 50 # Increased for better concurrency + + # Add connection timeout settings + if "health_check_interval" not in self.config: + self.config["health_check_interval"] = 30 # Health check every 30 seconds + + # Add retry settings for operations + if "retry_on_timeout" not in self.config: + self.config["retry_on_timeout"] = True + + # Add connection pool settings for better performance + if "socket_keepalive" not in self.config: + self.config["socket_keepalive"] = True + + # Initialize the connection with retry logic + self._connect_with_retry() + + def __del__(self): + """Cleanup Redis connection and pool on object destruction.""" + self.close() + + def close(self) -> None: + """Close Redis connection and connection pool.""" + try: + if self._redis: + self._redis.close() + self._redis = None + if self._pool: + self._pool.disconnect() + self._pool = None + except Exception as e: + print(f"Error closing Redis connection: {str(e)}") + + def _connect_with_retry(self) -> None: + """ + Attempt to establish a Redis connection with retry logic. + + Raises: + Exception: If all connection attempts fail. + """ + for attempt in range(1, self.max_retries + 1): + try: + if self._pool is None: + self._pool = ConnectionPool(**self.config) + self._redis = Redis(connection_pool=self._pool) + if self.check_connection(): + return + except (ConnectionError, TimeoutError) as e: + if attempt < self.max_retries: + time.sleep(self.RETRY_DELAY) + else: + raise Exception( + f"Redis connection error after {self.max_retries} attempts: {str(e)}" + ) + except Exception as e: + raise + + def check_connection(self) -> bool: + """ + Check if the Redis connection is alive with a PING command. + + Returns: + bool: True if connection is healthy, False otherwise. + """ + try: + return self._redis.ping() + except Exception as e: + err = e + return False + + def set_connection(self, **kwargs) -> Redis: + """ + Recreate Redis connection with new parameters. + + Args: + host: Redis server hostname or IP + password: Redis authentication password + port: Redis server port + db: Redis database number + **kwargs: Additional Redis connection parameters + + Returns: + Redis: The new Redis connection object + """ + try: + # Update configuration + self.config = { + "host": redis_configs.HOST, + "password": redis_configs.PASSWORD, + "port": redis_configs.PORT, + "db": redis_configs.PORT, + "socket_timeout": kwargs.get("socket_timeout", self.DEFAULT_TIMEOUT), + "socket_connect_timeout": kwargs.get( + "socket_connect_timeout", self.DEFAULT_TIMEOUT + ), + "decode_responses": kwargs.get("decode_responses", True), + "max_connections": kwargs.get("max_connections", 50), + "health_check_interval": kwargs.get("health_check_interval", 30), + "retry_on_timeout": kwargs.get("retry_on_timeout", True), + "socket_keepalive": kwargs.get("socket_keepalive", True), + } + + # Add any additional parameters + for key, value in kwargs.items(): + if key not in self.config: + self.config[key] = value + + # Create new connection + self._redis = Redis(**self.config) + if not self.check_connection(): + raise ConnectionError( + "Failed to establish connection with new parameters" + ) + + return self._redis + except Exception as e: + raise + + def get_connection_info(self) -> Dict[str, Any]: + """ + Get current connection configuration details. + + Returns: + Dict: Current connection configuration + """ + # Create a copy without password for security + info = self.config.copy() + if "password" in info: + info["password"] = "********" if info["password"] else None + return info + + def get_stats(self) -> Dict[str, Any]: + """ + Get Redis server statistics. + + Returns: + Dict: Redis server info + """ + try: + return self._redis.info() + except Exception as e: + return {"error": str(e)} + + @property + def redis(self) -> Redis: + """ + Property to access the Redis client. + + Returns: + Redis: The Redis client instance + + Raises: + Exception: If Redis connection is not available + """ + if not self._redis: + raise Exception("Redis client is not initialized") + + # Check connection health and reconnect if necessary + if not self.check_connection(): + self._connect_with_retry() + + return self._redis + + +# Create singleton instance with error handling +try: + redis_conn = RedisConn() + redis_cli = redis_conn.redis +except Exception as t: + # Optionally set a dummy/mock Redis client for testing or fallback behavior + # redis_cli = MockRedis() # If you have a mock implementation + # Or raise the exception to fail fast + raise diff --git a/api_services/api_controllers/redis/database.py b/api_services/api_controllers/redis/database.py new file mode 100644 index 0000000..d4eafeb --- /dev/null +++ b/api_services/api_controllers/redis/database.py @@ -0,0 +1,355 @@ +import arrow + +from typing import Optional, List, Dict, Union, Iterator + +from Controllers.Redis.response import RedisResponse +from Controllers.Redis.connection import redis_cli +from Controllers.Redis.base import RedisRow + + +class MainConfig: + DATETIME_FORMAT: str = "YYYY-MM-DD HH:mm:ss" + + +class RedisActions: + """Class for handling Redis operations with JSON data.""" + + @classmethod + def get_expiry_time(cls, expiry_kwargs: Dict[str, int]) -> int: + """ + Calculate expiry time in seconds from kwargs. + + Args: + expiry_kwargs: Dictionary with time units as keys (days, hours, minutes, seconds) + and their respective values. + + Returns: + Total expiry time in seconds. + """ + time_multipliers = {"days": 86400, "hours": 3600, "minutes": 60, "seconds": 1} + return sum( + int(expiry_kwargs.get(unit, 0)) * multiplier + for unit, multiplier in time_multipliers.items() + ) + + @classmethod + def set_expiry_time(cls, expiry_seconds: int) -> Dict[str, int]: + """ + Convert total seconds back into a dictionary of time units. + + Args: + expiry_seconds: Total expiry time in seconds. + + Returns: + Dictionary with time units and their values. + """ + time_multipliers = {"days": 86400, "hours": 3600, "minutes": 60, "seconds": 1} + result = {} + remaining_seconds = expiry_seconds + + if expiry_seconds < 0: + return {} + + for unit, multiplier in time_multipliers.items(): + if remaining_seconds >= multiplier: + result[unit], remaining_seconds = divmod(remaining_seconds, multiplier) + return result + + @classmethod + def resolve_expires_at(cls, redis_row: RedisRow) -> str: + """ + Resolve expiry time for Redis key. + + Args: + redis_row: RedisRow object containing the redis_key. + + Returns: + Formatted expiry time string or message indicating no expiry. + """ + expiry_time = redis_cli.ttl(redis_row.redis_key) + if expiry_time == -1: + return "Key has no expiry time." + if expiry_time == -2: + return "Key does not exist." + return arrow.now().shift(seconds=expiry_time).format(MainConfig.DATETIME_FORMAT) + + @classmethod + def key_exists(cls, key: Union[str, bytes]) -> bool: + """ + Check if a key exists in Redis without retrieving its value. + + Args: + key: Redis key to check. + + Returns: + Boolean indicating if key exists. + """ + return bool(redis_cli.exists(key)) + + @classmethod + def refresh_ttl( + cls, key: Union[str, bytes], expires: Dict[str, int] + ) -> RedisResponse: + """ + Refresh TTL for an existing key. + + Args: + key: Redis key to refresh TTL. + expires: Dictionary with time units to set new expiry. + + Returns: + RedisResponse with operation result. + """ + try: + if not cls.key_exists(key): + return RedisResponse( + status=False, + message="Cannot refresh TTL: Key does not exist.", + ) + + expiry_time = cls.get_expiry_time(expiry_kwargs=expires) + redis_cli.expire(name=key, time=expiry_time) + + expires_at_string = ( + arrow.now() + .shift(seconds=expiry_time) + .format(MainConfig.DATETIME_FORMAT) + ) + + return RedisResponse( + status=True, + message="TTL refreshed successfully.", + data={"key": key, "expires_at": expires_at_string}, + ) + except Exception as e: + return RedisResponse( + status=False, + message="Failed to refresh TTL.", + error=str(e), + ) + + @classmethod + def delete_key(cls, key: Union[Optional[str], Optional[bytes]]) -> RedisResponse: + """ + Delete a specific key from Redis. + + Args: + key: Redis key to delete. + + Returns: + RedisResponse with operation result. + """ + try: + deleted_count = redis_cli.delete(key) + if deleted_count > 0: + return RedisResponse( + status=True, + message="Key deleted successfully.", + data={"deleted_count": deleted_count}, + ) + return RedisResponse( + status=False, + message="Key not found or already deleted.", + data={"deleted_count": 0}, + ) + except Exception as e: + return RedisResponse( + status=False, + message="Failed to delete key.", + error=str(e), + ) + + @classmethod + def delete( + cls, list_keys: List[Union[Optional[str], Optional[bytes]]] + ) -> RedisResponse: + """ + Delete multiple keys matching a pattern. + + Args: + list_keys: List of key components to form pattern for deletion. + + Returns: + RedisResponse with operation result. + """ + try: + regex = RedisRow().regex(list_keys=list_keys) + json_get = redis_cli.scan_iter(match=regex) + + deleted_keys, deleted_count = [], 0 + + # Use pipeline for batch deletion + with redis_cli.pipeline() as pipe: + for row in json_get: + pipe.delete(row) + deleted_keys.append(row) + results = pipe.execute() + deleted_count = sum(results) + + return RedisResponse( + status=True, + message="Keys deleted successfully.", + data={"deleted_count": deleted_count, "deleted_keys": deleted_keys}, + ) + except Exception as e: + return RedisResponse( + status=False, + message="Failed to delete keys.", + error=str(e), + ) + + @classmethod + def set_json( + cls, + list_keys: List[Union[str, bytes]], + value: Optional[Union[Dict, List]], + expires: Optional[Dict[str, int]] = None, + ) -> RedisResponse: + """ + Set JSON value in Redis with optional expiry. + + Args: + list_keys: List of key components to form Redis key. + value: JSON-serializable data to store. + expires: Optional dictionary with time units for expiry. + + Returns: + RedisResponse with operation result. + """ + redis_row = RedisRow() + redis_row.merge(set_values=list_keys) + redis_row.feed(value) + redis_row.expires_at_string = None + redis_row.expires_at = None + + try: + if expires: + redis_row.expires_at = expires + expiry_time = cls.get_expiry_time(expiry_kwargs=expires) + redis_cli.setex( + name=redis_row.redis_key, + time=expiry_time, + value=redis_row.value, + ) + redis_row.expires_at_string = str( + arrow.now() + .shift(seconds=expiry_time) + .format(MainConfig.DATETIME_FORMAT) + ) + else: + redis_cli.set(name=redis_row.redis_key, value=redis_row.value) + + return RedisResponse( + status=True, + message="Value set successfully.", + data=redis_row, + ) + except Exception as e: + return RedisResponse( + status=False, + message="Failed to set value.", + error=str(e), + ) + + @classmethod + def get_json( + cls, + list_keys: List[Union[Optional[str], Optional[bytes]]], + limit: Optional[int] = None, + ) -> RedisResponse: + """ + Get JSON values from Redis using pattern matching. + + Args: + list_keys: List of key components to form pattern for retrieval. + limit: Optional limit on number of results to return. + + Returns: + RedisResponse with operation result. + """ + try: + list_of_rows, count = [], 0 + regex = RedisRow.regex(list_keys=list_keys) + json_get = redis_cli.scan_iter(match=regex) + + for row in json_get: + if limit is not None and count >= limit: + break + + redis_row = RedisRow() + redis_row.set_key(key=row) + + # Use pipeline for batch retrieval + with redis_cli.pipeline() as pipe: + pipe.get(row) + pipe.ttl(row) + redis_value, redis_value_expire = pipe.execute() + redis_row.expires_at = cls.set_expiry_time( + expiry_seconds=int(redis_value_expire) + ) + redis_row.expires_at_string = cls.resolve_expires_at( + redis_row=redis_row + ) + redis_row.feed(redis_value) + list_of_rows.append(redis_row) + count += 1 + + if list_of_rows: + return RedisResponse( + status=True, + message="Values retrieved successfully.", + data=list_of_rows, + ) + return RedisResponse( + status=False, + message="No matching keys found.", + data=list_of_rows, + ) + except Exception as e: + return RedisResponse( + status=False, + message="Failed to retrieve values.", + error=str(e), + ) + + @classmethod + def get_json_iterator( + cls, list_keys: List[Union[Optional[str], Optional[bytes]]] + ) -> Iterator[RedisRow]: + """ + Get JSON values from Redis as an iterator for memory-efficient processing of large datasets. + + Args: + list_keys: List of key components to form pattern for retrieval. + + Returns: + Iterator yielding RedisRow objects. + + Raises: + RedisValueError: If there's an error processing a row + """ + regex = RedisRow.regex(list_keys=list_keys) + json_get = redis_cli.scan_iter(match=regex) + + for row in json_get: + try: + redis_row = RedisRow() + redis_row.set_key(key=row) + + # Use pipeline for batch retrieval + with redis_cli.pipeline() as pipe: + pipe.get(row) + pipe.ttl(row) + redis_value, redis_value_expire = pipe.execute() + redis_row.expires_at = cls.set_expiry_time( + expiry_seconds=int(redis_value_expire) + ) + redis_row.expires_at_string = cls.resolve_expires_at( + redis_row=redis_row + ) + redis_row.feed(redis_value) + yield redis_row + except Exception as e: + # Log the error and continue with next row + print(f"Error processing row {row}: {str(e)}") + continue diff --git a/api_services/api_controllers/redis/implementations.py b/api_services/api_controllers/redis/implementations.py new file mode 100644 index 0000000..05c30b7 --- /dev/null +++ b/api_services/api_controllers/redis/implementations.py @@ -0,0 +1,280 @@ +from Controllers.Redis.database import RedisActions +import threading +import time +import random +import uuid +import concurrent.futures + + +def example_set_json() -> None: + """Example of setting JSON data in Redis with and without expiry.""" + # Example 1: Set JSON without expiry + data = {"name": "John", "age": 30, "city": "New York"} + keys = ["user", "profile", "123"] + result = RedisActions.set_json(list_keys=keys, value=data) + print("Set JSON without expiry:", result.as_dict()) + + # Example 2: Set JSON with expiry + expiry = {"hours": 1, "minutes": 30} + result = RedisActions.set_json(list_keys=keys, value=data, expires=expiry) + print("Set JSON with expiry:", result.as_dict()) + + +def example_get_json() -> None: + """Example of retrieving JSON data from Redis.""" + # Example 1: Get all matching keys + keys = ["user", "profile", "*"] + result = RedisActions.get_json(list_keys=keys) + print("Get all matching JSON:", result.as_dict()) + + # Example 2: Get with limit + result = RedisActions.get_json(list_keys=keys, limit=5) + print("Get JSON with limit:", result.as_dict()) + + +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, + ) + + +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.""" + key = "user:profile:123" + new_expiry = {"hours": 2, "minutes": 0} + result = RedisActions.refresh_ttl(key=key, expires=new_expiry) + print("Refresh TTL:", result.as_dict()) + + +def example_key_exists() -> None: + """Example of checking if a key exists.""" + key = "user:profile:123" + exists = RedisActions.key_exists(key) + print(f"Key {key} exists:", exists) + + +def example_resolve_expires_at() -> None: + """Example of resolving expiry time for a key.""" + from Controllers.Redis.base import RedisRow + + redis_row = RedisRow() + redis_row.set_key("user:profile:123") + print(redis_row.keys) + expires_at = RedisActions.resolve_expires_at(redis_row) + print("Resolve expires at:", expires_at) + + +def run_all_examples() -> None: + """Run all example functions to demonstrate RedisActions functionality.""" + print("\n=== Redis Actions Examples ===\n") + + print("1. Setting JSON data:") + example_set_json() + + print("\n2. Getting JSON data:") + example_get_json() + + print("\n3. Using JSON iterator:") + example_get_json_iterator() + + # print("\n4. Deleting specific key:") + # example_delete_key() + # + # print("\n5. Deleting multiple keys:") + # example_delete() + + print("\n6. Refreshing TTL:") + example_refresh_ttl() + + print("\n7. Checking key existence:") + example_key_exists() + + print("\n8. Resolving expiry time:") + example_resolve_expires_at() + + +def run_concurrent_test(num_threads=100): + """Run a comprehensive concurrent test with multiple threads to verify Redis connection handling.""" + print( + f"\nStarting comprehensive Redis concurrent test with {num_threads} threads..." + ) + + # Results tracking with detailed metrics + results = { + "passed": 0, + "failed": 0, + "retried": 0, + "errors": [], + "operation_times": [], + "retry_count": 0, + "max_retries": 3, + "retry_delay": 0.1, + } + results_lock = threading.Lock() + + def worker(thread_id): + # Track operation timing + start_time = time.time() + retry_count = 0 + success = False + error_message = None + + while retry_count <= results["max_retries"] and not success: + try: + # Generate unique key for this thread + unique_id = str(uuid.uuid4())[:8] + full_key = f"test:concurrent:{thread_id}:{unique_id}" + + # Simple string operations instead of JSON + test_value = f"test-value-{thread_id}-{time.time()}" + + # Set data in Redis with pipeline for efficiency + from Controllers.Redis.database import redis_cli + + # Use pipeline to reduce network overhead + with redis_cli.pipeline() as pipe: + pipe.set(full_key, test_value) + pipe.get(full_key) + pipe.delete(full_key) + results_list = pipe.execute() + + # Check results + set_ok = results_list[0] + retrieved_value = results_list[1] + if isinstance(retrieved_value, bytes): + retrieved_value = retrieved_value.decode("utf-8") + + # Verify data + success = set_ok and retrieved_value == test_value + + if success: + break + else: + error_message = f"Data verification failed: set_ok={set_ok}, value_match={retrieved_value == test_value}" + retry_count += 1 + with results_lock: + results["retry_count"] += 1 + time.sleep( + results["retry_delay"] * (2**retry_count) + ) # Exponential backoff + + except Exception as e: + error_message = str(e) + retry_count += 1 + with results_lock: + results["retry_count"] += 1 + + # Check if it's a connection error and retry + if "Too many connections" in str(e) or "Connection" in str(e): + # Exponential backoff for connection issues + backoff_time = results["retry_delay"] * (2**retry_count) + time.sleep(backoff_time) + else: + # For other errors, use a smaller delay + time.sleep(results["retry_delay"]) + + # Record operation time + operation_time = time.time() - start_time + + # Update results + with results_lock: + if success: + results["passed"] += 1 + results["operation_times"].append(operation_time) + if retry_count > 0: + results["retried"] += 1 + else: + results["failed"] += 1 + if error_message: + results["errors"].append( + f"Thread {thread_id} failed after {retry_count} retries: {error_message}" + ) + else: + results["errors"].append( + f"Thread {thread_id} failed after {retry_count} retries with unknown error" + ) + + # Create and start threads using a thread pool + start_time = time.time() + with concurrent.futures.ThreadPoolExecutor(max_workers=num_threads) as executor: + futures = [executor.submit(worker, i) for i in range(num_threads)] + concurrent.futures.wait(futures) + + # Calculate execution time and performance metrics + execution_time = time.time() - start_time + ops_per_second = num_threads / execution_time if execution_time > 0 else 0 + + # Calculate additional metrics if we have successful operations + avg_op_time = 0 + min_op_time = 0 + max_op_time = 0 + p95_op_time = 0 + + if results["operation_times"]: + avg_op_time = sum(results["operation_times"]) / len(results["operation_times"]) + min_op_time = min(results["operation_times"]) + max_op_time = max(results["operation_times"]) + # Calculate 95th percentile + sorted_times = sorted(results["operation_times"]) + p95_index = int(len(sorted_times) * 0.95) + p95_op_time = ( + sorted_times[p95_index] + if p95_index < len(sorted_times) + else sorted_times[-1] + ) + + # Print detailed results + print("\nConcurrent Redis Test Results:") + print(f"Total threads: {num_threads}") + print(f"Passed: {results['passed']}") + print(f"Failed: {results['failed']}") + print(f"Operations with retries: {results['retried']}") + print(f"Total retry attempts: {results['retry_count']}") + print(f"Success rate: {(results['passed'] / num_threads) * 100:.2f}%") + + print("\nPerformance Metrics:") + print(f"Total execution time: {execution_time:.2f} seconds") + print(f"Operations per second: {ops_per_second:.2f}") + + if results["operation_times"]: + print(f"Average operation time: {avg_op_time * 1000:.2f} ms") + print(f"Minimum operation time: {min_op_time * 1000:.2f} ms") + print(f"Maximum operation time: {max_op_time * 1000:.2f} ms") + print(f"95th percentile operation time: {p95_op_time * 1000:.2f} ms") + + # Print errors (limited to 10 for readability) + if results["errors"]: + print("\nErrors:") + for i, error in enumerate(results["errors"][:10]): + print(f"- {error}") + if len(results["errors"]) > 10: + print(f"- ... and {len(results['errors']) - 10} more errors") + + # Return results for potential further analysis + return results + + +if __name__ == "__main__": + # Run basic examples + run_all_examples() + + # Run enhanced concurrent test + run_concurrent_test(10000) diff --git a/api_services/api_controllers/redis/response.py b/api_services/api_controllers/redis/response.py new file mode 100644 index 0000000..ea4aa7c --- /dev/null +++ b/api_services/api_controllers/redis/response.py @@ -0,0 +1,200 @@ +from typing import Union, Dict, Optional, Any +from Controllers.Redis.base import RedisRow + + +class RedisResponse: + """ + Base class for Redis response handling. + + Provides a standardized way to return and process Redis operation results, + with tools to convert between different data representations. + """ + + def __init__( + self, + status: bool, + message: str, + data: Any = None, + error: Optional[str] = None, + ): + """ + Initialize a Redis response. + + Args: + status: Operation success status + message: Human-readable message about the operation + data: Response data (can be None, RedisRow, list, or dict) + error: Optional error message if operation failed + """ + self.status = status + self.message = message + self.data = data + self.error = error + + # Determine the data type + if isinstance(data, dict): + self.data_type = "dict" + elif isinstance(data, list): + self.data_type = "list" + elif isinstance(data, RedisRow): + self.data_type = "row" + elif isinstance(data, (int, float, str, bool)): + self.data_type = "primitive" + else: + self.data_type = None + + def as_dict(self) -> Dict: + """ + Convert the response to a dictionary format suitable for serialization. + + Returns: + Dictionary representation of the response + """ + # Base response fields + main_dict = { + "status": self.status, + "message": self.message, + "count": self.count, + "dataType": self.data_type, + } + + # Add error if present + if self.error: + main_dict["error"] = self.error + + data = self.all + + # Process single RedisRow + if isinstance(data, RedisRow): + result = {**main_dict} + if hasattr(data, "keys") and hasattr(data, "row"): + if not isinstance(data.keys, str): + raise ValueError("RedisRow keys must be string type") + result[data.keys] = data.row + return result + + # Process list of RedisRows + elif isinstance(data, list): + result = {**main_dict} + + # Handle list of RedisRow objects + rows_dict = {} + for row in data: + if ( + isinstance(row, RedisRow) + and hasattr(row, "keys") + and hasattr(row, "row") + ): + if not isinstance(row.keys, str): + raise ValueError("RedisRow keys must be string type") + rows_dict[row.keys] = row.row + + if rows_dict: + result["data"] = rows_dict + elif data: # If it's just a regular list with items + result["data"] = data + + return result + + # Process dictionary + elif isinstance(data, dict): + return {**main_dict, "data": data} + + return main_dict + + @property + def all(self) -> Any: + """ + Get all data from the response. + + Returns: + All data or empty list if None + """ + return self.data if self.data is not None else [] + + @property + def count(self) -> int: + """ + Count the number of items in the response data. + + Returns: + Number of items (0 if no data) + """ + data = self.all + + if isinstance(data, list): + return len(data) + elif isinstance(data, (RedisRow, dict)): + return 1 + return 0 + + @property + def first(self) -> Union[Dict, None]: + """ + Get the first item from the response data. + + Returns: + First item as a dictionary or None if no data + """ + if not self.data: + return None + + if isinstance(self.data, list) and self.data: + item = self.data[0] + if isinstance(item, RedisRow) and hasattr(item, "row"): + return item.row + return item + elif isinstance(self.data, RedisRow) and hasattr(self.data, "row"): + return self.data.row + elif isinstance(self.data, dict): + return self.data + + return None + + def is_successful(self) -> bool: + """ + Check if the operation was successful. + + Returns: + Boolean indicating success status + """ + return self.status + + def to_api_response(self) -> Dict: + """ + Format the response for API consumption. + + Returns: + API-friendly response dictionary + """ + try: + response = { + "success": self.status, + "message": self.message, + } + + if self.error: + response["error"] = self.error + + if self.data is not None: + if self.data_type == "row" and hasattr(self.data, "to_dict"): + response["data"] = self.data.to_dict() + elif self.data_type == "list": + try: + if all(hasattr(item, "to_dict") for item in self.data): + response["data"] = [item.to_dict() for item in self.data] + else: + response["data"] = self.data + except Exception as e: + response["error"] = f"Error converting list items: {str(e)}" + else: + response["data"] = self.data + + response["count"] = self.count + return response + except Exception as e: + return { + "success": False, + "message": "Error formatting response", + "error": str(e), + } diff --git a/api_services/api_initializer/app.py b/api_services/api_initializer/app.py new file mode 100644 index 0000000..5935a1c --- /dev/null +++ b/api_services/api_initializer/app.py @@ -0,0 +1,16 @@ +import uvicorn + +from api_initializer.config import api_config +from api_initializer.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/api_services/api_initializer/config.py b/api_services/api_initializer/config.py new file mode 100644 index 0000000..a9c0ea3 --- /dev/null +++ b/api_services/api_initializer/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", "http://localhost:3001"] + 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/api_services/api_initializer/create_app.py b/api_services/api_initializer/create_app.py new file mode 100644 index 0000000..bba3eb6 --- /dev/null +++ b/api_services/api_initializer/create_app.py @@ -0,0 +1,57 @@ +from fastapi import FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import RedirectResponse +from event_clusters import RouterCluster, EventCluster +from config import api_config + +cluster_is_set = False + + +def create_events_if_any_cluster_set(): + import events + + global cluster_is_set + if not events.__all__ or cluster_is_set: + return + + router_cluster_stack: list[RouterCluster] = [getattr(events, e, None) for e in events.__all__] + for router_cluster in router_cluster_stack: + event_cluster_stack: list[EventCluster] = list(router_cluster.event_clusters.values()) + for event_cluster in event_cluster_stack: + try: + event_cluster.set_events_to_database() + except Exception as e: + print(f"Error creating event cluster: {e}") + + cluster_is_set = True + + +def create_app(): + from open_api_creator import create_openapi_schema + from middlewares.token_middleware import token_middleware + from create_route import RouteRegisterController + from endpoints.routes import get_routes + + application = FastAPI(**api_config.api_info) + application.add_middleware( + CORSMiddleware, + allow_origins=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() + + create_events_if_any_cluster_set() + application.openapi = lambda _=application: create_openapi_schema(_) + return application diff --git a/api_services/api_initializer/create_route.py b/api_services/api_initializer/create_route.py new file mode 100644 index 0000000..e4e4237 --- /dev/null +++ b/api_services/api_initializer/create_route.py @@ -0,0 +1,58 @@ +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 + + @staticmethod + def add_router_with_event_to_database(router: APIRouter): + from schemas import EndpointRestriction + + # Endpoint operation_id is static now if record exits update() record else create() + with EndpointRestriction.new_session() as db_session: + for route in router.routes: + route_path = str(getattr(route, "path")) + route_summary = str(getattr(route, "name")) + operation_id = getattr(route, "operation_id", None) + if not operation_id: + raise ValueError(f"Route {route_path} operation_id is not found") + + for route_method in [ + method.lower() for method in getattr(route, "methods") + ]: + methods = [method.lower() for method in getattr(route, "methods")] + print('methods count : ', len(methods)) + print(dict( + route_method=route_method, + operation_uu_id=operation_id, + route_path=route_path, + route_summary=route_summary, + )) + # add_or_update_dict = dict( + # endpoint_method=route_method, + # endpoint_name=route_path, + # endpoint_desc=route_summary.replace("_", " "), + # endpoint_function=route_summary, + # operation_uu_id=operation_id, + # is_confirmed=True, + # ) + # endpoint_restriction_found = EndpointRestriction.filter_one_system( + # EndpointRestriction.operation_uu_id == operation_id, db=db_session, + # ).data + # if endpoint_restriction_found: + # endpoint_restriction_found.update(**add_or_update_dict, db=db_session) + # endpoint_restriction_found.save(db=db_session) + # else: + # restriction = EndpointRestriction.find_or_create(**add_or_update_dict, db=db_session) + # if restriction.meta_data.created: + # restriction.save(db=db_session) + + def register_routes(self): + for router in self.router_list: + self.app.include_router(router) + self.add_router_with_event_to_database(router) + return self.app diff --git a/api_services/api_initializer/event_clusters.py b/api_services/api_initializer/event_clusters.py new file mode 100644 index 0000000..c1903e0 --- /dev/null +++ b/api_services/api_initializer/event_clusters.py @@ -0,0 +1,134 @@ +from typing import Optional, Type +from pydantic import BaseModel + + +class EventCluster: + """ + EventCluster + """ + def __repr__(self): + return f"EventCluster(name={self.name})" + + def __init__(self, endpoint_uu_id: str, name: str): + self.endpoint_uu_id = endpoint_uu_id + self.name = name + self.events: list["Event"] = [] + + def add_event(self, event: "Event"): + """ + Add an event to the cluster + """ + if event.key not in [e.key for e in self.events]: + self.events.append(event) + + def get_event(self, event_key: str): + """ + Get an event by its key + """ + + for event in self.events: + if event.key == event_key: + return event + return None + + def set_events_to_database(self): + from schemas import Events, EndpointRestriction + + with Events.new_session() as db_session: + # if to_save_endpoint := EndpointRestriction.filter_one( + # EndpointRestriction.operation_uu_id == self.endpoint_uu_id, + # db=db_session, + # ).data: + for event in self.events: + event_dict_to_save = dict( + function_code=event.key, + function_class=event.name, + description=event.description, + endpoint_code=self.endpoint_uu_id, + endpoint_id=to_save_endpoint.id, + endpoint_uu_id=str(to_save_endpoint.uu_id), + is_confirmed=True, + db=db_session, + ) + print('event_dict_to_save', event_dict_to_save) + # event_found = Events.filter_one( + # Events.function_code == event_dict_to_save["function_code"], + # db=db_session, + # ).data + # if event_found: + # event_found.update(**event_dict_to_save) + # event_found.save(db=db_session) + # else: + # event_to_save_database = Events.find_or_create( + # **event_dict_to_save, + # include_args=[ + # Events.function_code, + # Events.function_class, + # Events.endpoint_code, + # Events.endpoint_uu_id, + # ] + # ) + # if event_to_save_database.meta_data.created: + # print(f"UUID: {event_to_save_database.uu_id} event is saved to {to_save_endpoint.uu_id}") + # event_to_save_database.save(db=db_session) + + def match_event(self, event_key: str) -> "Event": + """ + Match an event by its key + """ + if event := self.get_event(event_key=event_key): + return event + raise ValueError("Event key not found") + + +class Event: + + def __init__( + self, + name: str, + key: str, + request_validator: Optional[Type[BaseModel]] = None, + response_validator: Optional[Type[BaseModel]] = None, + description: str = "", + ): + self.name = name + self.key = key + self.request_validator = request_validator + self.response_validator = response_validator + self.description = description + + def event_callable(self): + """ + Example callable method + """ + print(self.name) + return {} + + +class RouterCluster: + """ + RouterCluster + """ + + def __repr__(self): + return f"RouterCluster(name={self.name})" + + def __init__(self, name: str): + self.name = name + self.event_clusters: dict[str, EventCluster] = {} + + def set_event_cluster(self, event_cluster: EventCluster): + """ + Add an event cluster to the set + """ + print("Setting event cluster:", event_cluster.name) + if event_cluster.name not in self.event_clusters: + self.event_clusters[event_cluster.name] = event_cluster + + def get_event_cluster(self, event_cluster_name: str) -> EventCluster: + """ + Get an event cluster by its name + """ + if event_cluster_name not in self.event_clusters: + raise ValueError("Event cluster not found") + return self.event_clusters[event_cluster_name] diff --git a/api_services/api_initializer/open_api_creator.py b/api_services/api_initializer/open_api_creator.py new file mode 100644 index 0000000..201b430 --- /dev/null +++ b/api_services/api_initializer/open_api_creator.py @@ -0,0 +1,116 @@ +from typing import Any, Dict +from fastapi import FastAPI +from fastapi.routing import APIRoute +from fastapi.openapi.utils import get_openapi + +from endpoints.routes import get_safe_endpoint_urls +from config import api_config + + +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": 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=api_config.TITLE, + description=api_config.DESCRIPTION, + version=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() diff --git a/api_services/api_initializer/wh.py b/api_services/api_initializer/wh.py new file mode 100644 index 0000000..1e4f5a6 --- /dev/null +++ b/api_services/api_initializer/wh.py @@ -0,0 +1,8 @@ + + +if __name__ == "__main__": + import time + + while True: + + time.sleep(10) \ No newline at end of file diff --git a/api-services/api-initializer/README.md b/api_services/api_middlewares/__init__.py similarity index 100% rename from api-services/api-initializer/README.md rename to api_services/api_middlewares/__init__.py diff --git a/api_services/api_middlewares/token_middleware.py b/api_services/api_middlewares/token_middleware.py new file mode 100644 index 0000000..cfac8a9 --- /dev/null +++ b/api_services/api_middlewares/token_middleware.py @@ -0,0 +1,24 @@ +from fastapi import Request, status +from fastapi.responses import JSONResponse +from config import api_config + +from endpoints.routes import get_safe_endpoint_urls + + +async def token_middleware(request: Request, call_next): + base_url = request.url.path + safe_endpoints = [_[0] for _ in get_safe_endpoint_urls()] + if base_url in safe_endpoints: + return await call_next(request) + + token = request.headers.get(api_config.ACCESS_TOKEN_TAG, None) + if not token: + return JSONResponse( + content={ + "error": "EYS_0002", + }, + status_code=status.HTTP_401_UNAUTHORIZED, + ) + + response = await call_next(request) + return response diff --git a/api-services/api-providers/README.md b/api_services/api_validations/a.txt similarity index 100% rename from api-services/api-providers/README.md rename to api_services/api_validations/a.txt diff --git a/api_services/api_validations/defaults/validations.py b/api_services/api_validations/defaults/validations.py new file mode 100644 index 0000000..2090466 --- /dev/null +++ b/api_services/api_validations/defaults/validations.py @@ -0,0 +1,56 @@ +from fastapi import Header, Request, Response +from pydantic import BaseModel + +from api_services.api_initializer.config import api_config + + +class CommonHeaders(BaseModel): + language: str | None = None + domain: str | None = None + timezone: str | None = None + token: str | None = None + request: Request | None = None + response: Response | None = None + operation_id: str | None = None + + model_config = { + "arbitrary_types_allowed": True + } + + @classmethod + def as_dependency( + cls, + request: Request, + response: Response, + language: str = Header(None, alias="language"), + domain: str = Header(None, alias="domain"), + tz: str = Header(None, alias="timezone"), + ): + token = request.headers.get(api_config.ACCESS_TOKEN_TAG, None) + + # Extract operation_id from the route + operation_id = None + if hasattr(request.scope.get("route"), "operation_id"): + operation_id = request.scope.get("route").operation_id + + return cls( + language=language, + domain=domain, + timezone=tz, + token=token, + request=request, + response=response, + operation_id=operation_id, + ) + + def get_headers_dict(self): + """Convert the headers to a dictionary format used in the application""" + import uuid + + return { + "language": self.language or "", + "domain": self.domain or "", + "eys-ext": f"{str(uuid.uuid4())}", + "tz": self.timezone or "GMT+3", + "token": self.token, + } diff --git a/api_services/api_validations/pydantic_core.py b/api_services/api_validations/pydantic_core.py new file mode 100644 index 0000000..c06050d --- /dev/null +++ b/api_services/api_validations/pydantic_core.py @@ -0,0 +1,17 @@ + + +class BaseModelCore(BaseModel): + + """ + BaseModelCore + model_dump override for alias support Users.name -> Table[Users] Field(alias="name") + """ + __abstract__ = True + + class Config: + validate_by_name = True + use_enum_values = True + + def model_dump(self, *args, **kwargs): + data = super().model_dump(*args, **kwargs) + return {self.__class__.model_fields[field].alias: value for field, value in data.items()} diff --git a/api_services/api_validations/response/api.py b/api_services/api_validations/response/api.py new file mode 100644 index 0000000..5243115 --- /dev/null +++ b/api_services/api_validations/response/api.py @@ -0,0 +1,53 @@ +from .result import PaginationResult +from .base import PostgresResponseSingle +from pydantic import BaseModel +from typing import Any + +class EndpointResponse(BaseModel): + """Endpoint response model.""" + + completed: bool = True + message: str = "Success" + pagination_result: PaginationResult + + @property + def response(self): + """Convert response to dictionary format.""" + result_data = getattr(self.pagination_result, "data", None) + if not result_data: + return { + "completed": False, + "message": "MSG0004-NODATA", + "data": None, + "pagination": None, + } + result_pagination = getattr(self.pagination_result, "pagination", None) + if not result_pagination: + raise ValueError("Invalid pagination result pagination.") + pagination_dict = getattr(result_pagination, "as_dict", None) + if not pagination_dict: + raise ValueError("Invalid pagination result as_dict.") + return { + "completed": self.completed, + "message": self.message, + "data": result_data, + "pagination": pagination_dict, + } + + +class CreateEndpointResponse(BaseModel): + """Create endpoint response model.""" + + completed: bool = True + message: str = "Success" + data: PostgresResponseSingle + + @property + def response(self): + """Convert response to dictionary format.""" + return { + "completed": self.completed, + "message": self.message, + "data": self.data.data, + } + diff --git a/api_services/api_validations/response/base.py b/api_services/api_validations/response/base.py new file mode 100644 index 0000000..618691a --- /dev/null +++ b/api_services/api_validations/response/base.py @@ -0,0 +1,193 @@ +""" +Response handler for PostgreSQL query results. + +This module provides a wrapper class for SQLAlchemy query results, +adding convenience methods for accessing data and managing query state. +""" + +from typing import Any, Dict, Optional, TypeVar, Generic, Union + +from pydantic import BaseModel +from sqlalchemy.orm import Query + + +T = TypeVar("T") + + +class PostgresResponse(Generic[T]): + """ + Wrapper for PostgreSQL/SQLAlchemy query results. + + Properties: + count: Total count of results + query: Get query object + as_dict: Convert response to dictionary format + """ + + def __init__(self, query: Query, base_model: Optional[BaseModel] = None): + self._query = query + self._count: Optional[int] = None + self._base_model: Optional[BaseModel] = base_model + self.single = False + + @property + def query(self) -> Query: + """Get query object.""" + return self._query + + @property + def data(self) -> Union[list[T], T]: + """Get query object.""" + return self._query.all() + + @property + def count(self) -> int: + """Get query object.""" + return self._query.count() + + @property + def to_dict(self, **kwargs) -> list[dict]: + """Get query object.""" + if self._base_model: + return [self._base_model(**item.to_dict()).model_dump(**kwargs) for item in self.data] + return [item.to_dict() for item in self.data] + + @property + def as_dict(self) -> Dict[str, Any]: + """Convert response to dictionary format.""" + return { + "query": str(self.query), + "count": self.count, + "data": self.to_dict, + } + + +class PostgresResponseSingle(Generic[T]): + """ + Wrapper for PostgreSQL/SQLAlchemy query results. + + Properties: + count: Total count of results + query: Get query object + as_dict: Convert response to dictionary format + data: Get query object + """ + + def __init__(self, query: Query, base_model: Optional[BaseModel] = None): + self._query = query + self._count: Optional[int] = None + self._base_model: Optional[BaseModel] = base_model + self.single = True + + @property + def query(self) -> Query: + """Get query object.""" + return self._query + + @property + def to_dict(self, **kwargs) -> dict: + """Get query object.""" + if self._base_model: + return self._base_model(**self._query.first().to_dict()).model_dump(**kwargs) + return self._query.first().to_dict() + + @property + def data(self) -> T: + """Get query object.""" + return self._query.first() + + @property + def count(self) -> int: + """Get query object.""" + return self._query.count() + + @property + def as_dict(self) -> Dict[str, Any]: + """Convert response to dictionary format.""" + return {"query": str(self.query),"data": self.to_dict, "count": self.count} + + +class ResultQueryJoin: + """ + ResultQueryJoin + params: + list_of_instrumented_attributes: list of instrumented attributes + query: query object + """ + + def __init__(self, list_of_instrumented_attributes, query): + """Initialize ResultQueryJoin""" + self.list_of_instrumented_attributes = list_of_instrumented_attributes + self._query = query + + @property + def query(self): + """Get query object.""" + return self._query + + @property + def to_dict(self): + """Convert response to dictionary format.""" + list_of_dictionaries, result = [], dict() + for user_orders_shipping_iter in self.query.all(): + for index, instrumented_attribute_iter in enumerate(self.list_of_instrumented_attributes): + result[str(instrumented_attribute_iter)] = user_orders_shipping_iter[index] + list_of_dictionaries.append(result) + return list_of_dictionaries + + @property + def count(self): + """Get count of query.""" + return self.query.count() + + @property + def data(self): + """Get query object.""" + return self.query.all() + + @property + def as_dict(self): + """Convert response to dictionary format.""" + return {"query": str(self.query), "data": self.data, "count": self.count} + + +class ResultQueryJoinSingle: + """ + ResultQueryJoinSingle + params: + list_of_instrumented_attributes: list of instrumented attributes + query: query object + """ + + def __init__(self, list_of_instrumented_attributes, query): + """Initialize ResultQueryJoinSingle""" + self.list_of_instrumented_attributes = list_of_instrumented_attributes + self._query = query + + @property + def query(self): + """Get query object.""" + return self._query + + @property + def to_dict(self): + """Convert response to dictionary format.""" + data, result = self.query.first(), dict() + for index, instrumented_attribute_iter in enumerate(self.list_of_instrumented_attributes): + result[str(instrumented_attribute_iter)] = data[index] + return result + + @property + def count(self): + """Get count of query.""" + return self.query.count() + + @property + def data(self): + """Get query object.""" + return self._query.first() + + @property + def as_dict(self): + """Convert response to dictionary format.""" + return {"query": str(self.query), "data": self.data, "count": self.count} diff --git a/api_services/api_validations/response/example.py b/api_services/api_validations/response/example.py new file mode 100644 index 0000000..8a8f8b7 --- /dev/null +++ b/api_services/api_validations/response/example.py @@ -0,0 +1,19 @@ + + +class UserPydantic(BaseModel): + + username: str = Field(..., alias='user.username') + account_balance: float = Field(..., alias='user.account_balance') + preferred_category_id: Optional[int] = Field(None, alias='user.preferred_category_id') + last_ordered_product_id: Optional[int] = Field(None, alias='user.last_ordered_product_id') + supplier_rating_id: Optional[int] = Field(None, alias='user.supplier_rating_id') + other_rating_id: Optional[int] = Field(None, alias='product.supplier_rating_id') + id: int = Field(..., alias='user.id') + + class Config: + validate_by_name = True + use_enum_values = True + + def model_dump(self, *args, **kwargs): + data = super().model_dump(*args, **kwargs) + return {self.__class__.model_fields[field].alias: value for field, value in data.items()} diff --git a/api_services/api_validations/response/pagination.py b/api_services/api_validations/response/pagination.py new file mode 100644 index 0000000..738de40 --- /dev/null +++ b/api_services/api_validations/response/pagination.py @@ -0,0 +1,70 @@ +from typing import Any, Dict, Optional, Union, TypeVar, Type +from sqlalchemy import desc, asc +from pydantic import BaseModel + +from .base import PostgresResponse + +# Type variable for class methods returning self +T = TypeVar("T", bound="BaseModel") + + +class PaginateConfig: + """ + Configuration for pagination settings. + + Attributes: + DEFAULT_SIZE: Default number of items per page (10) + MIN_SIZE: Minimum allowed page size (10) + MAX_SIZE: Maximum allowed page size (40) + """ + + DEFAULT_SIZE = 10 + MIN_SIZE = 5 + MAX_SIZE = 100 + + +class ListOptions(BaseModel): + """ + Query for list option abilities + """ + + page: Optional[int] = 1 + size: Optional[int] = 10 + orderField: Optional[Union[tuple[str], list[str]]] = ["uu_id"] + orderType: Optional[Union[tuple[str], list[str]]] = ["asc"] + # include_joins: Optional[list] = None + + +class PaginateOnly(ListOptions): + """ + Query for list option abilities + """ + + query: Optional[dict] = None + + +class PaginationConfig(BaseModel): + """ + Configuration for pagination settings. + + Attributes: + page: Current page number (default: 1) + size: Items per page (default: 10) + orderField: Field to order by (default: "created_at") + orderType: Order direction (default: "desc") + """ + + page: int = 1 + size: int = 10 + orderField: Optional[Union[tuple[str], list[str]]] = ["created_at"] + orderType: Optional[Union[tuple[str], list[str]]] = ["desc"] + + def __init__(self, **data): + super().__init__(**data) + if self.orderField is None: + self.orderField = ["created_at"] + if self.orderType is None: + self.orderType = ["desc"] + + +default_paginate_config = PaginateConfig() \ No newline at end of file diff --git a/api_services/api_validations/response/result.py b/api_services/api_validations/response/result.py new file mode 100644 index 0000000..858bdf6 --- /dev/null +++ b/api_services/api_validations/response/result.py @@ -0,0 +1,183 @@ +from .pagination import default_paginate_config +from .base import PostgresResponse +from typing import Optional, Union +from sqlalchemy.orm import Query +from pydantic import BaseModel + +class Pagination: + """ + Handles pagination logic for query results. + + Manages page size, current page, ordering, and calculates total pages + and items based on the data source. + + Attributes: + DEFAULT_SIZE: Default number of items per page (10) + MIN_SIZE: Minimum allowed page size (10) + MAX_SIZE: Maximum allowed page size (40) + """ + + DEFAULT_SIZE = default_paginate_config.DEFAULT_SIZE + MIN_SIZE = default_paginate_config.MIN_SIZE + MAX_SIZE = default_paginate_config.MAX_SIZE + + def __init__(self, data: PostgresResponse): + self.data = data + self.size: int = self.DEFAULT_SIZE + self.page: int = 1 + self.orderField: Optional[Union[tuple[str], list[str]]] = ["uu_id"] + self.orderType: Optional[Union[tuple[str], list[str]]] = ["asc"] + self.page_count: int = 1 + self.total_count: int = 0 + self.all_count: int = 0 + self.total_pages: int = 1 + self._update_page_counts() + + def change(self, **kwargs) -> None: + """Update pagination settings from config.""" + config = PaginationConfig(**kwargs) + self.size = ( + config.size + if self.MIN_SIZE <= config.size <= self.MAX_SIZE + else self.DEFAULT_SIZE + ) + self.page = config.page + self.orderField = config.order_field + self.orderType = config.order_type + self._update_page_counts() + + def feed(self, data: PostgresResponse) -> None: + """Calculate pagination based on data source.""" + self.data = data + self._update_page_counts() + + def _update_page_counts(self) -> None: + """Update page counts and validate current page.""" + if self.data: + self.total_count = self.data.count + self.all_count = self.data.total_count + + self.size = ( + self.size + if self.MIN_SIZE <= self.size <= self.MAX_SIZE + else self.DEFAULT_SIZE + ) + self.total_pages = max(1, (self.total_count + self.size - 1) // self.size) + self.page = max(1, min(self.page, self.total_pages)) + self.page_count = ( + self.total_count % self.size + if self.page == self.total_pages and self.total_count % self.size + else self.size + ) + + def refresh(self) -> None: + """Reset pagination state to defaults.""" + self._update_page_counts() + + def reset(self) -> None: + """Reset pagination state to defaults.""" + self.size = self.DEFAULT_SIZE + self.page = 1 + self.orderField = "uu_id" + self.orderType = "asc" + + @property + def next_available(self) -> bool: + if self.page < self.total_pages: + return True + return False + + @property + def back_available(self) -> bool: + if self.page > 1: + return True + return False + + @property + def as_dict(self) -> Dict[str, Any]: + """Convert pagination state to dictionary format.""" + self.refresh() + return { + "size": self.size, + "page": self.page, + "allCount": self.all_count, + "totalCount": self.total_count, + "totalPages": self.total_pages, + "pageCount": self.page_count, + "orderField": self.orderField, + "orderType": self.orderType, + "next": self.next_available, + "back": self.back_available, + } + + +class PaginationResult: + """ + Result of a paginated query. + + Contains the query result and pagination state. + data: PostgresResponse of query results + pagination: Pagination state + + Attributes: + _query: Original query object + pagination: Pagination state + """ + + def __init__( + self, + data: PostgresResponse, + pagination: Pagination, + response_model: Type[T] = None, + ): + self._query = data.query + self.pagination = pagination + self.response_type = data.is_list + self.limit = self.pagination.size + self.offset = self.pagination.size * (self.pagination.page - 1) + self.order_by = self.pagination.orderField + self.response_model = response_model + + def dynamic_order_by(self): + """ + Dynamically order a query by multiple fields. + Returns: + Ordered query object. + """ + if not len(self.order_by) == len(self.pagination.orderType): + raise ValueError( + "Order by fields and order types must have the same length." + ) + order_criteria = zip(self.order_by, self.pagination.orderType) + for field, direction in order_criteria: + if hasattr(self._query.column_descriptions[0]["entity"], field): + if direction.lower().startswith("d"): + self._query = self._query.order_by( + desc( + getattr(self._query.column_descriptions[0]["entity"], field) + ) + ) + else: + self._query = self._query.order_by( + asc( + getattr(self._query.column_descriptions[0]["entity"], field) + ) + ) + return self._query + + @property + def data(self) -> Union[list | dict]: + """Get query object.""" + query_ordered = self.dynamic_order_by() + query_paginated = query_ordered.limit(self.limit).offset(self.offset) + queried_data = ( + query_paginated.all() if self.response_type else query_paginated.first() + ) + data = ( + [result.get_dict() for result in queried_data] + if self.response_type + else queried_data.get_dict() + ) + if self.response_model: + return [self.response_model(**item).model_dump() for item in data] + return data diff --git a/api_services/pyproject.toml b/api_services/pyproject.toml new file mode 100644 index 0000000..b68438e --- /dev/null +++ b/api_services/pyproject.toml @@ -0,0 +1,28 @@ +[project] +name = "api-services" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "alembic>=1.15.2", + "arrow>=1.3.0", + "cryptography>=44.0.2", + "faker>=37.1.0", + "fastapi>=0.115.12", + "pandas>=2.2.3", + "prometheus-fastapi-instrumentator>=7.1.0", + "psycopg2-binary>=2.9.10", + "pydantic-settings>=2.8.1", + "pymongo>=4.11.3", + "pytest>=8.3.5", + "redbox>=0.2.1", + "redis>=5.2.1", + "redmail>=0.6.0", + "requests>=2.32.3", + "sqlalchemy-mixins>=2.0.5", + "textdistance>=4.6.3", + "unidecode>=1.3.8", + "uvicorn>=0.34.0", +] + diff --git a/api_services/schemas/__init__.py b/api_services/schemas/__init__.py new file mode 100644 index 0000000..10125b6 --- /dev/null +++ b/api_services/schemas/__init__.py @@ -0,0 +1,209 @@ +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, + Service2Application, + Events, + Event2Occupant, + Event2Employee, + Event2OccupantExtra, + Event2EmployeeExtra, + Applications, + Application2Employee, + Application2Occupant, + Application2EmployeeExtra, + Application2OccupantExtra, +) +from schemas.identity.identity import ( + UsersTokens, + OccupantTypes, + People, + Users, + Credentials, + 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", + "Applications", + "Application2Employee", + "Application2Occupant", + "Addresses", + "AddressCity", + "AddressStreet", + "AddressLocality", + "AddressDistrict", + "AddressNeighborhood", + "AddressState", + "AddressCountry", + "AddressPostcode", + "AddressGeographicLocations", + "UsersTokens", + "OccupantTypes", + "People", + "Users", + "Credentials", + "RelationshipDutyPeople", + "RelationshipEmployee2PostCode", + "Contracts", + "ApiEnumDropdown", + "EndpointRestriction", + "RelationshipEmployee2Build", + # ------------------------------------------------ +] diff --git a/api_services/schemas/account/account.py b/api_services/schemas/account/account.py new file mode 100644 index 0000000..22d60f5 --- /dev/null +++ b/api_services/schemas/account/account.py @@ -0,0 +1,575 @@ +from schemas.base_imports import ( + CrudCollection, + String, + Integer, + Boolean, + ForeignKey, + Index, + TIMESTAMP, + Numeric, + SmallInteger, + mapped_column, + Mapped, +) + + +class AccountBooks(CrudCollection): + + __tablename__ = "account_books" + __exclude__fields__ = [] + + country: Mapped[str] = mapped_column(String, nullable=False) + branch_type: Mapped[int] = mapped_column(SmallInteger, server_default="0") + + company_id: Mapped[int] = mapped_column(ForeignKey("companies.id"), nullable=False) + company_uu_id: Mapped[str] = mapped_column(String, nullable=False) + branch_id: Mapped[int] = mapped_column(ForeignKey("companies.id")) + branch_uu_id: Mapped[str] = mapped_column(String, comment="Branch UU ID") + + __table_args__ = ( + Index("account_companies_book_ndx_00", company_id, "expiry_starts"), + {"comment": "Account Book Information"}, + ) + + +class AccountCodes(CrudCollection): + + __tablename__ = "account_codes" + __exclude__fields__ = [] + + account_code: Mapped[str] = mapped_column( + String(48), nullable=False, comment="Account Code" + ) + comment_line: Mapped[str] = mapped_column( + String(128), nullable=False, comment="Comment Line" + ) + + is_receive_or_debit: Mapped[bool] = mapped_column(Boolean) + product_id: Mapped[int] = mapped_column(Integer, server_default="0") + nvi_id: Mapped[str] = mapped_column(String(48), server_default="") + status_id: Mapped[int] = mapped_column(SmallInteger, server_default="0") + account_code_seperator: Mapped[str] = mapped_column(String(1), server_default=".") + + system_id: Mapped[int] = mapped_column(SmallInteger, server_default="0") + locked: Mapped[int] = mapped_column(SmallInteger, server_default="0") + + company_id: Mapped[int] = mapped_column(ForeignKey("companies.id")) + company_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Company UU ID" + ) + customer_id: Mapped[int] = mapped_column(ForeignKey("companies.id")) + customer_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Customer UU ID" + ) + person_id: Mapped[int] = mapped_column(ForeignKey("people.id")) + person_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Person UU ID" + ) + + __table_args__ = ({"comment": "Account Code Information"},) + + +class AccountCodeParser(CrudCollection): + + __tablename__ = "account_code_parser" + __exclude__fields__ = [] + + account_code_1: Mapped[str] = mapped_column(String, nullable=False, comment="Order") + account_code_2: Mapped[str] = mapped_column(String, nullable=False, comment="Order") + account_code_3: Mapped[str] = mapped_column(String, nullable=False, comment="Order") + account_code_4: Mapped[str] = mapped_column(String, server_default="") + account_code_5: Mapped[str] = mapped_column(String, server_default="") + account_code_6: Mapped[str] = mapped_column(String, server_default="") + + account_code_id: Mapped[int] = mapped_column( + ForeignKey("account_codes.id"), nullable=False + ) + account_code_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Account Code UU ID" + ) + + __table_args__ = ( + Index("_account_code_parser_ndx_00", account_code_id), + {"comment": "Account Code Parser Information"}, + ) + + @property + def get_account_code(self): + return f"{self.account_codes.account_code_seperator}".join( + [ + getattr(self, f"account_code_{i}") + for i in range(1, 7) + if getattr(self, f"account_code_{i}") + ] + ) + + +class AccountMaster(CrudCollection): + """ + AccountCodes class based on declarative_base and CrudCollection via session + """ + + __tablename__ = "account_master" + __exclude__fields__ = [] + + doc_date: Mapped[TIMESTAMP] = mapped_column( + TIMESTAMP(timezone=True), nullable=False, comment="Document Date" + ) + plug_type: Mapped[str] = mapped_column(String, nullable=False, comment="Plug Type") + plug_number: Mapped[int] = mapped_column( + Integer, nullable=False, comment="Plug Number" + ) + + special_code: Mapped[str] = mapped_column(String(12), server_default="") + authorization_code: Mapped[str] = mapped_column(String(12), server_default="") + + doc_code: Mapped[str] = mapped_column(String(12), server_default="") + doc_type: Mapped[int] = mapped_column(SmallInteger, server_default="0") + + comment_line1: Mapped[str] = mapped_column(String, server_default="") + comment_line2: Mapped[str] = mapped_column(String, server_default="") + comment_line3: Mapped[str] = mapped_column(String, server_default="") + comment_line4: Mapped[str] = mapped_column(String, server_default="") + comment_line5: Mapped[str] = mapped_column(String, server_default="") + comment_line6: Mapped[str] = mapped_column(String, server_default="") + project_code: Mapped[str] = mapped_column(String(12), server_default="") + module_no: Mapped[str] = mapped_column(String, server_default="") + journal_no: Mapped[int] = mapped_column(Integer, server_default="0") + + status_id: Mapped[int] = mapped_column(SmallInteger, server_default="0") + canceled: Mapped[bool] = mapped_column(Boolean, server_default="0") + print_count: Mapped[int] = mapped_column(SmallInteger, server_default="0") + total_active: Mapped[float] = mapped_column(Numeric(20, 6), server_default="0") + total_passive: Mapped[float] = mapped_column(Numeric(20, 6), server_default="0") + total_active_1: Mapped[float] = mapped_column(Numeric(20, 6), server_default="0") + total_passive_1: Mapped[float] = mapped_column(Numeric(20, 6), server_default="0") + total_active_2: Mapped[float] = mapped_column(Numeric(20, 6), server_default="0") + total_passive_2: Mapped[float] = mapped_column(Numeric(20, 6), server_default="0") + total_active_3: Mapped[float] = mapped_column(Numeric(20, 6), server_default="0") + total_passive_3: Mapped[float] = mapped_column(Numeric(20, 6), server_default="0") + total_active_4: Mapped[float] = mapped_column(Numeric(20, 6), server_default="0") + total_passive_4: Mapped[float] = mapped_column(Numeric(20, 6), server_default="0") + cross_ref: Mapped[int] = mapped_column(Integer, server_default="0") + data_center_id: Mapped[str] = mapped_column(String, server_default="") + data_center_rec_num: Mapped[int] = mapped_column(Integer, server_default="0") + + account_header_id: Mapped[int] = mapped_column( + ForeignKey("account_books.id"), nullable=False + ) + account_header_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Account Header UU ID" + ) + project_item_id: Mapped[int] = mapped_column( + ForeignKey("build_decision_book_projects.id") + ) + project_item_uu_id: Mapped[str] = mapped_column( + String, comment="Project Item UU ID" + ) + department_id: Mapped[int] = mapped_column(ForeignKey("departments.id")) + department_uu_id: Mapped[str] = mapped_column(String, comment="Department UU ID") + + __table_args__ = ( + Index("_account_master_ndx_00", doc_date, account_header_id), + {"comment": "Account Master Information"}, + ) + + +class AccountDetail(CrudCollection): + """ + AccountCodes class based on declarative_base and CrudCollection via session + """ + + __tablename__ = "account_detail" + __exclude__fields__ = [] + __enum_list__ = [("plug_type", "AccountingReceiptTypes", "M")] + + doc_date: Mapped[TIMESTAMP] = mapped_column( + TIMESTAMP(timezone=True), nullable=False, comment="Document Date" + ) + line_no: Mapped[int] = mapped_column( + SmallInteger, nullable=False, comment="Line Number" + ) + receive_debit: Mapped[str] = mapped_column( + String(1), nullable=False, comment="Receive Debit" + ) + debit: Mapped[float] = mapped_column( + Numeric(20, 6), nullable=False, comment="Debit" + ) + + department: Mapped[str] = mapped_column(String(24), server_default="") + special_code: Mapped[str] = mapped_column(String(12), server_default="") + account_ref: Mapped[int] = mapped_column(Integer, server_default="0") + account_fiche_ref: Mapped[int] = mapped_column(Integer, server_default="0") + center_ref: Mapped[int] = mapped_column(Integer, server_default="0") + general_code: Mapped[str] = mapped_column(String(32), server_default="") + credit: Mapped[float] = mapped_column(Numeric(20, 6), server_default="0") + currency_type: Mapped[str] = mapped_column(String(4), server_default="TL") + exchange_rate: Mapped[float] = mapped_column(Numeric(20, 6), server_default="0") + debit_cur: Mapped[float] = mapped_column(Numeric(20, 6), server_default="0") + credit_cur: Mapped[float] = mapped_column(Numeric(20, 6), server_default="0") + discount_cur: Mapped[float] = mapped_column(Numeric(20, 6), server_default="0") + amount: Mapped[float] = mapped_column(Numeric(20, 6), server_default="0") + cross_account_code: Mapped[float] = mapped_column(String(32), server_default="") + inf_index: Mapped[float] = mapped_column(Numeric(20, 6), server_default="0") + not_inflated: Mapped[int] = mapped_column(SmallInteger, server_default="0") + not_calculated: Mapped[int] = mapped_column(SmallInteger, server_default="0") + comment_line1: Mapped[str] = mapped_column(String(64), server_default="") + comment_line2: Mapped[str] = mapped_column(String(64), server_default="") + comment_line3: Mapped[str] = mapped_column(String(64), server_default="") + comment_line4: Mapped[str] = mapped_column(String(64), server_default="") + comment_line5: Mapped[str] = mapped_column(String(64), server_default="") + comment_line6: Mapped[str] = mapped_column(String(64), server_default="") + owner_acc_ref: Mapped[int] = mapped_column(Integer, server_default="0") + from_where: Mapped[int] = mapped_column(Integer, server_default="0") + orj_eid: Mapped[int] = mapped_column(Integer, server_default="0") + canceled: Mapped[int] = mapped_column(SmallInteger, server_default="0") + cross_ref: Mapped[int] = mapped_column(Integer, server_default="0") + data_center_id: Mapped[str] = mapped_column(String, server_default="") + data_center_rec_num: Mapped[int] = mapped_column(Integer, server_default="0") + status_id: Mapped[int] = mapped_column(SmallInteger, server_default="0") + + plug_type_id: Mapped[int] = mapped_column( + ForeignKey("api_enum_dropdown.id"), nullable=True + ) + plug_type_uu_id = mapped_column(String, nullable=False, comment="Plug Type UU ID") + account_header_id: Mapped[int] = mapped_column( + ForeignKey("account_books.id"), nullable=False + ) + account_header_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Account Header UU ID" + ) + account_code_id: Mapped[int] = mapped_column( + ForeignKey("account_codes.id"), nullable=False + ) + account_code_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Account Code UU ID" + ) + account_master_id: Mapped[int] = mapped_column( + ForeignKey("account_master.id"), nullable=False + ) + account_master_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Account Master UU ID" + ) + project_id: Mapped[int] = mapped_column( + ForeignKey("build_decision_book_projects.id") + ) + project_uu_id: Mapped[str] = mapped_column(String, comment="Project UU ID") + + __table_args__ = ( + Index( + "_account_detail_ndx_00", + account_master_id, + doc_date, + line_no, + account_header_id, + unique=True, + ), + {"comment": "Account Detail Information"}, + ) + + +class AccountRecordExchanges(CrudCollection): + + __tablename__ = "account_record_exchanges" + __exclude__fields__ = [] + + are_currency: Mapped[str] = mapped_column( + String(5), nullable=False, comment="Unit of Currency" + ) + are_exchange_rate: Mapped[float] = mapped_column( + Numeric(18, 6), nullable=False, server_default="1" + ) + usd_exchange_rate_value: Mapped[float] = mapped_column( + Numeric(18, 6), + nullable=True, + server_default="0", + comment="It will be written by multiplying the usd exchange rate with the current value result.", + ) + eur_exchange_rate_value: Mapped[float] = mapped_column( + Numeric(18, 6), + nullable=True, + server_default="0", + comment="It will be written by multiplying the eur exchange rate with the current value result.", + ) + gbp_exchange_rate_value: Mapped[float] = mapped_column( + Numeric(18, 6), + nullable=True, + server_default="0", + comment="It will be written by multiplying the gpd exchange rate with the current value result.", + ) + cny_exchange_rate_value: Mapped[float] = mapped_column( + Numeric(18, 6), + nullable=True, + server_default="0", + comment="It will be written by multiplying the cny exchange rate with the current value result.", + ) + + account_records_id: Mapped[int] = mapped_column(ForeignKey("account_records.id")) + account_records_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Account Record UU ID" + ) + + __table_args__ = ( + Index("_account_record_exchanges_ndx_00", account_records_id), + {"comment": "Account Record Exchanges Information"}, + ) + + +class AccountRecords(CrudCollection): + """ + build_decision_book_id = kaydın sorumlu olduğu karar defteri + send_company_id = kaydı gönderen firma, send_person_id = gönderen kişi + customer_id = sorumlu kullanıcı bilgisi, company_id = sorumlu firma + """ + + __tablename__ = "account_records" + __exclude__fields__ = [] + __enum_list__ = [ + ("receive_debit", "DebitTypes", "D"), + ("budget_type", "BudgetType", "B"), + ] + + iban: Mapped[str] = mapped_column( + String(64), nullable=False, comment="IBAN Number of Bank" + ) + bank_date: Mapped[TIMESTAMP] = mapped_column( + TIMESTAMP(timezone=True), nullable=False, comment="Bank Transaction Date" + ) + + currency_value: Mapped[float] = mapped_column( + Numeric(20, 6), nullable=False, comment="Currency Value" + ) + bank_balance: Mapped[float] = mapped_column( + Numeric(20, 6), nullable=False, comment="Bank Balance" + ) + currency: Mapped[str] = mapped_column( + String(5), nullable=False, comment="Unit of Currency" + ) + additional_balance: Mapped[float] = mapped_column( + Numeric(20, 6), nullable=False, comment="Additional Balance" + ) + channel_branch: Mapped[str] = mapped_column( + String(120), nullable=False, comment="Branch Bank" + ) + process_name: Mapped[str] = mapped_column( + String, nullable=False, comment="Bank Process Type Name" + ) + process_type: Mapped[str] = mapped_column( + String, nullable=False, comment="Bank Process Type" + ) + process_comment: Mapped[str] = mapped_column( + String, nullable=False, comment="Transaction Record Comment" + ) + process_garbage: Mapped[str] = mapped_column( + String, nullable=True, comment="Transaction Record Garbage" + ) + bank_reference_code: Mapped[str] = mapped_column( + String, nullable=False, comment="Bank Reference Code" + ) + + add_comment_note: Mapped[str] = mapped_column(String, server_default="") + is_receipt_mail_send: Mapped[bool] = mapped_column(Boolean, server_default="0") + found_from = mapped_column(String, server_default="") + similarity: Mapped[float] = mapped_column(Numeric(20, 6), server_default="0") + remainder_balance: Mapped[float] = mapped_column(Numeric(20, 6), server_default="0") + + bank_date_y: Mapped[int] = mapped_column(Integer) + bank_date_m: Mapped[int] = mapped_column(SmallInteger) + bank_date_w: Mapped[int] = mapped_column(SmallInteger) + bank_date_d: Mapped[int] = mapped_column(SmallInteger) + + approving_accounting_record: Mapped[bool] = mapped_column( + Boolean, server_default="0" + ) + accounting_receipt_date: Mapped[TIMESTAMP] = mapped_column( + TIMESTAMP(timezone=True), server_default="1900-01-01 00:00:00" + ) + accounting_receipt_number: Mapped[int] = mapped_column(Integer, server_default="0") + status_id: Mapped[int] = mapped_column(SmallInteger, server_default="0") + + approved_record: Mapped[bool] = mapped_column(Boolean, server_default="0") + import_file_name: Mapped[str] = mapped_column( + String, nullable=True, comment="XLS Key" + ) + + receive_debit: Mapped[int] = mapped_column( + ForeignKey("api_enum_dropdown.id"), nullable=True + ) + receive_debit_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Debit UU ID" + ) + budget_type: Mapped[int] = mapped_column( + ForeignKey("api_enum_dropdown.id"), nullable=True + ) + budget_type_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Budget Type UU ID" + ) + company_id: Mapped[int] = mapped_column(ForeignKey("companies.id"), nullable=True) + company_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Company UU ID" + ) + send_company_id: Mapped[int] = mapped_column( + ForeignKey("companies.id"), nullable=True + ) + send_company_uu_id = mapped_column( + String, nullable=True, comment="Send Company UU ID" + ) + + send_person_id: Mapped[int] = mapped_column(ForeignKey("people.id"), nullable=True) + send_person_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Send Person UU ID" + ) + approving_accounting_person: Mapped[int] = mapped_column( + ForeignKey("people.id"), nullable=True + ) + approving_accounting_person_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Approving Accounting Person UU ID" + ) + + living_space_id: Mapped[int] = mapped_column( + ForeignKey("build_living_space.id"), nullable=True + ) + living_space_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Living Space UU ID" + ) + customer_id: Mapped[int] = mapped_column(ForeignKey("people.id"), nullable=True) + customer_uu_id = mapped_column(String, nullable=True, comment="Customer UU ID") + + build_id: Mapped[int] = mapped_column(ForeignKey("build.id"), nullable=True) + build_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Build UU ID" + ) + build_parts_id: Mapped[int] = mapped_column( + ForeignKey("build_parts.id"), nullable=True + ) + build_parts_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Build Parts UU ID" + ) + build_decision_book_id: Mapped[int] = mapped_column( + ForeignKey("build_decision_book.id"), nullable=True + ) + build_decision_book_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Build Decision Book UU ID" + ) + + __table_args__ = ( + Index("_budget_records_ndx_00", is_receipt_mail_send, bank_date), + Index( + "_budget_records_ndx_01", + iban, + bank_date, + bank_reference_code, + bank_balance, + unique=True, + ), + Index("_budget_records_ndx_02", status_id, bank_date), + { + "comment": "Bank Records that are related to building and financial transactions" + }, + ) + + # def payment_budget_record_close(self): + # from database_sql_models import ( + # DecisionBookProjectPaymentsMaster, + # ApiEnumDropdown, + # BuildDecisionBook, + # BuildDecisionBookPaymentsMaster, + # ) + # + # budget_record = self + # if self.receive_debit == ApiEnumDropdown.uuid_of_enum( + # enum_class="DebitTypes", key="R" + # ): + # print( + # "This record is not debit. Debit:", + # self.receive_debit, + # "DebitTypes.R.name", + # # str(DebitTypes.R.name), + # ) + # return + # if abs(budget_record.currency_value + budget_record.remainder_balance) > 0: + # payment_dict = { + # "budget_records_id": self.id, + # "build_decision_book_id": budget_record.build_decision_book_id, + # "build_parts_id": budget_record.build_parts_id, + # "start_date": budget_record.bank_date, + # "paid_value": budget_record.currency_value + # - budget_record.remainder_balance, + # "is_all": False, + # } + # (paid_value, start_paid_value, balance) = ( + # float(budget_record.currency_value - budget_record.remainder_balance), + # float(budget_record.currency_value - budget_record.remainder_balance), + # float(budget_record.remainder_balance), + # ) + # print( + # "self.id", + # self.id, + # "paid_value", + # paid_value, + # "start_paid_value", + # start_paid_value, + # "balance", + # balance, + # self.receive_debit, + # ) + # + # if not BuildDecisionBook.find_one( + # id=payment_dict["build_decision_book_id"] + # ): + # return paid_value + # + # if budget_record.replication_id == 55: + # if paid_value > 0: + # payment_dict["dues_type"] = ApiEnumDropdown.uuid_of_enum( + # enum_class="BuildDuesTypes", key="L" + # ) + # paid_value = ( + # DecisionBookProjectPaymentsMaster.pay_law_and_ren_of_build_part( + # **payment_dict + # ) + # ) + # print("dues_type", payment_dict["dues_type"], paid_value) + # if paid_value > 0: + # payment_dict.pop("dues_type", None) + # paid_value = BuildDecisionBookPaymentsMaster.pay_dues_of_build_part( + # **payment_dict + # ) + # print("dues_type", None, paid_value) + # if paid_value > 0: + # payment_dict["dues_type"] = ApiEnumDropdown.uuid_of_enum( + # enum_class="BuildDuesTypes", key="R" + # ) + # paid_value = ( + # DecisionBookProjectPaymentsMaster.pay_law_and_ren_of_build_part( + # **payment_dict + # ) + # ) + # print("dues_type", payment_dict["dues_type"], paid_value) + # payment_dict["is_all"] = True + # if paid_value > 0: + # payment_dict["dues_type"] = ApiEnumDropdown.uuid_of_enum( + # enum_class="BuildDuesTypes", key="L" + # ) + # paid_value = ( + # DecisionBookProjectPaymentsMaster.pay_law_and_ren_of_build_part( + # **payment_dict + # ) + # ) + # print("is all dues_type", payment_dict["dues_type"], paid_value) + # if paid_value > 0: + # payment_dict.pop("dues_type", None) + # paid_value = BuildDecisionBookPaymentsMaster.pay_dues_of_build_part( + # **payment_dict + # ) + # print("is all dues_type", None, paid_value) + # if paid_value > 0: + # payment_dict["dues_type"] = ApiEnumDropdown.uuid_of_enum( + # enum_class="BuildDuesTypes", key="R" + # ) + # paid_value = ( + # DecisionBookProjectPaymentsMaster.pay_law_and_ren_of_build_part( + # **payment_dict + # ) + # ) + # print("is all dues_type", payment_dict["dues_type"], paid_value) diff --git a/api_services/schemas/account/iban.py b/api_services/schemas/account/iban.py new file mode 100644 index 0000000..124512a --- /dev/null +++ b/api_services/schemas/account/iban.py @@ -0,0 +1,83 @@ +from schemas.base_imports import ( + CrudCollection, + String, + Integer, + Boolean, + ForeignKey, + Index, + TIMESTAMP, + Numeric, + SmallInteger, + mapped_column, + Mapped, +) + + +class BuildIbans(CrudCollection): + """ + BuildParts class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "build_ibans" + __exclude__fields__ = [] + + iban: Mapped[str] = mapped_column( + String(40), server_default="", nullable=False, comment="IBAN number" + ) + start_date: Mapped[TIMESTAMP] = mapped_column( + TIMESTAMP(timezone=True), nullable=False, comment="Bank Transaction Start Date" + ) + stop_date: Mapped[TIMESTAMP] = mapped_column( + TIMESTAMP(timezone=True), server_default="2900-01-01 00:00:00" + ) + bank_code: Mapped[str] = mapped_column(String(24), server_default="TR0000000000000") + xcomment: Mapped[str] = mapped_column(String(64), server_default="????") + build_id: Mapped[int] = mapped_column( + ForeignKey("build.id"), nullable=True, comment="Building ID" + ) + build_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Building UUID", index=True + ) + + __table_args__ = ( + Index("_build_ibans_ndx_01", iban, start_date, unique=True), + {"comment": "IBANs related to money transactions due to building objects"}, + ) + + +class BuildIbanDescription(CrudCollection): + """ + SearchComments class based on declarative_base and CrudCollection via session + """ + + __tablename__ = "build_iban_description" + __exclude__fields__ = [] + + iban: Mapped[str] = mapped_column(String, nullable=False, comment="IBAN Number") + group_id: Mapped[int] = mapped_column( + SmallInteger, nullable=False, comment="Group ID" + ) + search_word: Mapped[str] = mapped_column( + String, nullable=False, comment="Search Word", index=True + ) + customer_id: Mapped[int] = mapped_column(ForeignKey("people.id"), nullable=True) + customer_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Customer UUID" + ) + company_id: Mapped[int] = mapped_column(ForeignKey("companies.id"), nullable=True) + company_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Company UUID" + ) + build_parts_id: Mapped[int] = mapped_column( + ForeignKey("build_parts.id"), nullable=True + ) + build_parts_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Build Parts UUID" + ) + + __table_args__ = ( + Index( + "_search_iban_description_ndx_00", iban, search_word, group_id, unique=True + ), + {"comment": "Search Iban Description Information"}, + ) diff --git a/api_services/schemas/address/address.py b/api_services/schemas/address/address.py new file mode 100644 index 0000000..6af5809 --- /dev/null +++ b/api_services/schemas/address/address.py @@ -0,0 +1,575 @@ +from schemas.base_imports import ( + CrudCollection, + String, + Integer, + Boolean, + ForeignKey, + Index, + TIMESTAMP, + Numeric, + SmallInteger, + mapped_column, + Mapped, + BigInteger, + Text, +) + +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/api_services/schemas/base_imports.py b/api_services/schemas/base_imports.py new file mode 100644 index 0000000..53b6d1d --- /dev/null +++ b/api_services/schemas/base_imports.py @@ -0,0 +1,24 @@ +from api_controllers.postgres.mixin import CrudCollection +from sqlalchemy.orm import mapped_column, Mapped, relationship +from sqlalchemy import ( + String, + Integer, + Boolean, + ForeignKey, + Index, + TIMESTAMP, + Numeric, + SmallInteger, + text, + or_, + and_, + func, + UUID, + JSON, + BigInteger, + Float, + Text, + LargeBinary, + PickleType, + DATETIME, +) diff --git a/api_services/schemas/building/budget.py b/api_services/schemas/building/budget.py new file mode 100644 index 0000000..7b082f7 --- /dev/null +++ b/api_services/schemas/building/budget.py @@ -0,0 +1,159 @@ +from schemas.base_imports import ( + CrudCollection, + String, + ForeignKey, + Index, + SmallInteger, + TIMESTAMP, + Text, + Numeric, + Integer, + mapped_column, + Mapped, + relationship, + func, +) + + +class DecisionBookBudgetBooks(CrudCollection): + + __tablename__ = "decision_book_budget_books" + __exclude__fields__ = [] + + country: Mapped[str] = mapped_column(String, nullable=False) + branch_type: Mapped[int] = mapped_column(SmallInteger, server_default="0") + + company_id: Mapped[int] = mapped_column(ForeignKey("companies.id"), nullable=False) + company_uu_id: Mapped[str] = mapped_column(String, nullable=False) + branch_id: Mapped[int] = mapped_column(ForeignKey("companies.id"), nullable=True) + branch_uu_id: Mapped[str] = mapped_column( + String, comment="Branch UU ID", nullable=True + ) + build_decision_book_id: Mapped[int] = mapped_column( + ForeignKey("build_decision_book.id"), nullable=False + ) + build_decision_book_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Build Decision Book UU ID" + ) + + __table_args__ = ( + Index( + "_decision_book_budget_companies_book_ndx_00", + company_id, + "created_at", + ), + {"comment": "budget Book Information"}, + ) + + +class DecisionBookBudgetCodes(CrudCollection): + + __tablename__ = "decision_book_budget_codes" + __exclude__fields__ = [] + + budget_code: Mapped[str] = mapped_column( + String(48), nullable=False, comment="budget Code" + ) + comment_line: Mapped[str] = mapped_column( + Text, nullable=False, comment="Comment Line" + ) + + build_decision_book_id: Mapped[int] = mapped_column( + ForeignKey("build_decision_book.id"), nullable=True + ) + build_decision_book_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Build Decision Book UU ID" + ) + + build_parts_id: Mapped[int] = mapped_column( + ForeignKey("build_parts.id"), nullable=True + ) + build_parts_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Build Parts UU ID" + ) + + company_id: Mapped[int] = mapped_column(ForeignKey("companies.id"), nullable=True) + company_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Company UU ID" + ) + + __table_args__ = ( + Index("_decision_book_budget_codes_ndx_00", budget_code, "created_at"), + Index("_decision_book_budget_codes_ndx_01", company_id, "created_at"), + {"comment": "budget Book Information"}, + ) + + +class DecisionBookBudgetMaster(CrudCollection): + + __tablename__ = "decision_book_budget_master" + __exclude__fields__ = [] + + budget_type: Mapped[str] = mapped_column( + String(50), nullable=False + ) # Bütçe tipi (örneğin: Operasyonel, Yatırım) + currency: Mapped[str] = mapped_column( + String(8), server_default="TRY" + ) # Bütçe para birimi + total_budget: Mapped[float] = mapped_column( + Numeric(10, 2), nullable=False + ) # Toplam bütçe + + tracking_period_id: Mapped[int] = mapped_column( + ForeignKey("api_enum_dropdown.id"), nullable=True + ) + tracking_period_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Part Direction UUID" + ) + budget_books_id: Mapped[int] = mapped_column( + Integer, ForeignKey("decision_book_budget_books.id"), nullable=False + ) + budget_books_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Budget Books UU ID" + ) + department_id: Mapped[int] = mapped_column( + Integer, ForeignKey("departments.id"), nullable=False + ) # Departman ile ilişki + department_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Department UU ID" + ) + + __table_args__ = ({"comment": "budget Book Information"},) + + +class DecisionBookBudgets(CrudCollection): + + __tablename__ = "decision_book_budgets" + __exclude__fields__ = [] + + process_date: Mapped[TIMESTAMP] = mapped_column( + TIMESTAMP(timezone=True), nullable=False + ) # Başlangıç tarihi + budget_codes_id: Mapped[int] = mapped_column( + Integer, ForeignKey("decision_book_budget_codes.id"), nullable=False + ) + total_budget: Mapped[float] = mapped_column( + Numeric(10, 2), nullable=False + ) # Toplam bütçe + used_budget: Mapped[float] = mapped_column( + Numeric(10, 2), nullable=False, default=0.0 + ) # Kullanılan bütçe + remaining_budget: Mapped[float] = mapped_column( + Numeric(10, 2), nullable=False, default=0.0 + ) # Kullanılan bütçe + + decision_book_budget_master_id: Mapped[int] = mapped_column( + Integer, ForeignKey("decision_book_budget_master.id"), nullable=False + ) + decision_book_budget_master_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Decision Book Budget Master UU ID" + ) + + __table_args__ = ( + Index( + "_decision_book_budgets_ndx_00", + decision_book_budget_master_uu_id, + process_date, + ), + {"comment": "budget Book Information"}, + ) diff --git a/api_services/schemas/building/build.py b/api_services/schemas/building/build.py new file mode 100644 index 0000000..85a9c6a --- /dev/null +++ b/api_services/schemas/building/build.py @@ -0,0 +1,602 @@ +import arrow +from datetime import timedelta +from typing import List, Union, Any +from schemas.base_imports import ( + CrudCollection, + mapped_column, + Mapped, + relationship, + String, + Integer, + Boolean, + ForeignKey, + Index, + TIMESTAMP, + Text, + Numeric, + or_, + and_, +) + + +class BuildTypes(CrudCollection): + """ + BuildTypes class based on declarative_base and BaseMixin via session + + """ + + __tablename__ = "build_types" + __exclude__fields__ = [] + __include__fields__ = [] + + function_code: Mapped[str] = mapped_column( + String(12), server_default="", nullable=False, comment="Function Code" + ) + type_code: Mapped[str] = mapped_column( + String(12), server_default="", nullable=False, comment="Structure Type Code" + ) + lang: Mapped[str] = mapped_column( + String(4), server_default="TR", nullable=False, comment="Language" + ) + type_name: Mapped[str] = mapped_column( + String(48), server_default="", nullable=False, comment="Type Name" + ) + + __table_args__ = ( + Index("_build_types_ndx_00", type_code, function_code, lang, unique=True), + {"comment": "Function group of building types with their language information"}, + ) + + +class Part2Employee(CrudCollection): + """ + Employee2Parts class based on declarative_base and BaseMixin via session + In between start and end date, a part can be assigned to only one employee + """ + + __tablename__ = "part2employee" + __exclude__fields__ = [] + __include__fields__ = [] + + build_id: Mapped[int] = mapped_column(Integer, comment="Building ID") + part_id: Mapped[int] = mapped_column( + ForeignKey("build_parts.id"), nullable=False, comment="Part ID" + ) + employee_id: Mapped[int] = mapped_column( + ForeignKey("employees.id"), nullable=False, comment="Employee ID" + ) + + __table_args__ = ( + Index("_part2employee_ndx_00", employee_id, part_id, unique=True), + {"comment": "Employee2Parts Information"}, + ) + + +class RelationshipEmployee2Build(CrudCollection): + """ + CompanyRelationship class based on declarative_base and CrudCollection via session + Company -> Sub Company -> Sub-Sub Company + + """ + + __tablename__ = "relationship_employee2build" + __exclude__fields__ = [] + + company_id: Mapped[int] = mapped_column( + ForeignKey("companies.id"), nullable=False + ) # 1, 2, 3 + employee_id: Mapped[int] = mapped_column( + ForeignKey("employees.id"), nullable=False + ) # employee -> (n)person Evyos LTD + member_id: Mapped[int] = mapped_column( + ForeignKey("build.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="False") + + __table_args__ = ( + Index( + "relationship_build_employee_ndx_00", + company_id, + employee_id, + member_id, + relationship_type, + unique=True, + ), + {"comment": "Build & Employee Relationship Information"}, + ) + + +class Build(CrudCollection): + """ + Builds class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "build" + __exclude__fields__ = [] + __include__fields__ = [] + __access_by__ = [] + # __many__table__ = RelationshipEmployee2Build + + gov_address_code: Mapped[str] = mapped_column(String, server_default="", unique=True) + build_name: Mapped[str] = mapped_column(String, nullable=False, comment="Building Name") + build_no: Mapped[str] = mapped_column(String(8), nullable=False, comment="Building Number") + + max_floor: Mapped[int] = mapped_column(Integer, server_default="1", nullable=False, comment="Max Floor") + underground_floor: Mapped[int] = mapped_column(Integer, server_default="0", nullable=False, comment="Underground Floor") + build_date: Mapped[TIMESTAMP] = mapped_column(TIMESTAMP(timezone=True), server_default="1900-01-01") + decision_period_date: Mapped[TIMESTAMP] = mapped_column( + TIMESTAMP(timezone=True), + server_default="1900-01-01", + comment="Building annual ordinary meeting period", + ) + tax_no: Mapped[str] = mapped_column(String(24), server_default="") + lift_count: Mapped[int] = mapped_column(Integer, server_default="0") + heating_system: Mapped[bool] = mapped_column(Boolean, server_default="True") + cooling_system: Mapped[bool] = mapped_column(Boolean, server_default="False") + hot_water_system: Mapped[bool] = mapped_column(Boolean, server_default="False") + block_service_man_count: Mapped[int] = mapped_column(Integer, server_default="0") + security_service_man_count: Mapped[int] = mapped_column(Integer, server_default="0") + garage_count: Mapped[int] = mapped_column(Integer, server_default="0", comment="Garage Count") + management_room_id: Mapped[int] = mapped_column(Integer, nullable=True, comment="Management Room ID") + + site_id: Mapped[int] = mapped_column(ForeignKey("build_sites.id"), nullable=True) + site_uu_id: Mapped[str] = mapped_column(String, comment="Site UUID", nullable=True) + address_id: Mapped[int] = mapped_column(ForeignKey("addresses.id"), nullable=False) + address_uu_id: Mapped[str] = mapped_column(String, comment="Address UUID", nullable=False) + build_types_id: Mapped[int] = mapped_column(ForeignKey("build_types.id"), nullable=False, comment="Building Type") + build_types_uu_id: Mapped[str] = mapped_column(String, comment="Building Type UUID") + + parts: Mapped[List["BuildParts"]] = relationship("BuildParts", back_populates="buildings", foreign_keys="BuildParts.build_id") + decision_books: Mapped[List["BuildDecisionBook"]] = relationship( + "BuildDecisionBook", + back_populates="buildings", + foreign_keys="BuildDecisionBook.build_id", + ) + + # build_ibans: Mapped["BuildIbans"] = relationship( + # "BuildIbans", back_populates="building", foreign_keys="BuildIbans.build_id" + # ) + # areas: Mapped["BuildArea"] = relationship( + # "BuildArea", back_populates="buildings", foreign_keys="BuildArea.build_id" + # ) + # response_companies: Mapped["Companies"] = relationship( + # "Companies", + # back_populates="response_buildings", + # foreign_keys=[response_company_id], + # ) + # addresses: Mapped[List["Address"]] = relationship( + # "Address", back_populates="buildings", foreign_keys=[address_id] + # ) + # peoples: Mapped["People"] = relationship( + # "People", back_populates="buildings", foreign_keys=[people_id] + # ) + # sites: Mapped["BuildSites"] = relationship( + # "BuildSites", back_populates="buildings", foreign_keys=[site_id] + # ) + + __table_args__ = ( + Index("_builds_ndx_00", gov_address_code), + Index("_builds_ndx_01", build_name, build_no), + { + "comment": "Build objects are building that are created for living and store purposes" + }, + ) + + @property + def management_room(self): + with self.new_session() as db_session: + if management_room := BuildParts.filter_by_one( + system=True, id=self.management_room_id, build_id=self.id, db=db_session + ).data: + return management_room + return None + + @property + def top_flat(self): + max_flat_no = 0 + for part in self.parts: + if part.part_no > self.max_floor: + max_flat_no = part.part_no + return max_flat_no + + @property + def bottom_flat(self): + min_flat_no = 0 + for part in self.parts: + if part.part_no < self.max_floor: + min_flat_no = part.part_no + return min_flat_no + + @property + def human_livable_parts(self) -> tuple: + parts = list(part for part in self.parts if part.human_livable) + return parts, len(parts) + + @property + def livable_part_count(self): + with self.new_session() as db_session: + livable_parts = BuildParts.filter_all( + BuildParts.build_id == self.id, + BuildParts.human_livable == True, + db=db_session, + ) + if not livable_parts.data: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="There is no livable part in this building.", + ) + return livable_parts.count + + @property + def part_type_count(self): + with self.new_session() as db_session: + building_types = None + for part in self.parts: + building_types = {} + build_type = BuildTypes.filter_by_one( + system=True, id=part.build_part_type_id, db=db_session + ).data + if build_type.type_code in building_types: + building_types[build_type.type_code]["list"].append(part.part_no) + else: + building_types[build_type.type_code] = {"list": [part.part_no]} + + # for key, val in building_types.items(): + # list_parts = val["list"] + # building_types[key] = { + # "list": list_parts, + # "min": min(list_parts), + # "max": max(list_parts), + # "count": len(list_parts), + # } + return building_types + + +class BuildParts(CrudCollection): + """ + BuildParts class based on declarative_base and BaseMixin via session + Attentions: Part_no is unique for each building and Every building must have a management section.!!! default no 0 + """ + + __tablename__ = "build_parts" + __exclude__fields__ = [] + __include__fields__ = [] + __enum_list__ = [("part_direction", "Directions", "NN")] + + # https://adres.nvi.gov.tr/VatandasIslemleri/AdresSorgu + address_gov_code: Mapped[str] = mapped_column( + String, nullable=False, comment="Goverment Door Code" + ) + # part_name: Mapped[str] = mapped_column(String(24), server_default="", nullable=False, comment="Part Name") + part_no: Mapped[int] = mapped_column( + Integer, server_default="0", nullable=False, comment="Part Number" + ) + part_level: Mapped[int] = mapped_column( + Integer, server_default="0", comment="Building Part Level" + ) + part_code: Mapped[str] = mapped_column( + String, server_default="", nullable=False, comment="Part Code" + ) + part_gross_size: Mapped[int] = mapped_column( + Integer, server_default="0", comment="Part Gross Size" + ) + part_net_size: Mapped[int] = mapped_column( + Integer, server_default="0", comment="Part Net Size" + ) + default_accessory: Mapped[str] = mapped_column( + Text, server_default="0", comment="Default Accessory" + ) + human_livable: Mapped[bool] = mapped_column( + Boolean, server_default="1", comment="Human Livable" + ) + due_part_key: Mapped[str] = mapped_column( + String, server_default="", nullable=False, comment="Constant Payment Group" + ) + + build_id: Mapped[int] = mapped_column( + ForeignKey("build.id"), nullable=False, comment="Building ID" + ) + build_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Building UUID" + ) + part_direction_id: Mapped[int] = mapped_column( + ForeignKey("api_enum_dropdown.id"), nullable=True + ) + part_direction_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Part Direction UUID" + ) + part_type_id: Mapped[int] = mapped_column( + ForeignKey("build_types.id"), nullable=False, comment="Building Part Type" + ) + part_type_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Building Part Type UUID" + ) + + buildings: Mapped["Build"] = relationship( + "Build", back_populates="parts", foreign_keys=[build_id] + ) + + __table_args__ = ( + Index("build_parts_ndx_1", build_id, part_no, unique=True), + {"comment": "Part objects that are belong to building objects"}, + ) + + def part_name(self, db): + if build_type := BuildTypes.filter_by_one( + system=True, id=self.part_type_id, db=db + ).data: + return f"{str(build_type.type_name).upper()} : {str(self.part_no).upper()}" + return f"Undefined:{str(build_type.type_name).upper()}" + + +class BuildLivingSpace(CrudCollection): + """ + LivingSpace class based on declarative_base and BaseMixin via session + Owner or live person = Occupant of the build part + + Query OR(owner_person_id == person_id, life_person_id == person_id) AND (now(date)) + """ + + __tablename__ = "build_living_space" + __exclude__fields__ = [] + __include__fields__ = [] + + fix_value: Mapped[float] = mapped_column( + Numeric(20, 6), + server_default="0", + comment="Fixed value is deducted from debit.", + ) + fix_percent: Mapped[float] = mapped_column( + Numeric(6, 2), + server_default="0", + comment="Fixed percent is deducted from debit.", + ) + + agreement_no: Mapped[str] = mapped_column( + String, server_default="", comment="Agreement No" + ) + marketing_process: Mapped[bool] = mapped_column(Boolean, server_default="False") + marketing_layer: Mapped[int] = mapped_column(Integer, server_default="0") + + build_parts_id: Mapped[int] = mapped_column( + ForeignKey("build_parts.id"), + nullable=False, + index=True, + comment="Build Part ID", + ) + build_parts_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Build Part UUID" + ) + person_id: Mapped[int] = mapped_column( + ForeignKey("people.id"), + nullable=False, + index=True, + comment="Responsible People ID", + ) + person_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Responsible People UUID" + ) + occupant_type_id: Mapped[int] = mapped_column( + ForeignKey("occupant_types.id"), + nullable=False, + comment="Occupant Type", + ) + occupant_type_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Occupant Type UUID" + ) + + __table_args__ = ( + {"comment": "Living Space inside building parts that are related to people"}, + ) + + @classmethod + def find_living_from_customer_id( + cls, customer_id, process_date, add_days: int = 32 + ): + with cls.new_session() as db_session: + formatted_date = arrow.get(str(process_date)) + living_spaces = cls.filter_all( + or_( + cls.owner_person_id == customer_id, + cls.life_person_id == customer_id, + ), + cls.start_date < formatted_date - timedelta(days=add_days), + cls.stop_date > formatted_date + timedelta(days=add_days), + db=db_session, + ) + return living_spaces.data, living_spaces.count + + +class BuildManagement(CrudCollection): + + __tablename__ = "build_management" + __exclude__fields__ = [] + + discounted_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 + + occupant_type: Mapped[int] = mapped_column( + ForeignKey("occupant_types.id"), + nullable=False, + comment="Occupant Type", + ) + occupant_type_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Occupant Type UUID" + ) + build_id: Mapped[int] = mapped_column( + ForeignKey("build.id"), nullable=False, comment="Building ID" + ) + build_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Building UUID" + ) + build_parts_id: Mapped[int] = mapped_column( + ForeignKey("build_parts.id"), + nullable=False, + index=True, + comment="Build Part ID", + ) + build_parts_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Build Part UUID" + ) + + __table_args__ = ( + Index( + "build_management_ndx_00", + build_parts_id, + occupant_type, + "expiry_starts", + unique=True, + ), + {"comment": "Management of the building parts that are related to people"}, + ) + + +class BuildArea(CrudCollection): + """ + Builds class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "build_area" + __exclude__fields__ = [] + + area_name: Mapped[str] = mapped_column(String, server_default="") + area_code: Mapped[str] = mapped_column(String, server_default="") + area_type: Mapped[str] = mapped_column(String, server_default="GREEN") + area_direction: Mapped[str] = mapped_column(String(2), server_default="NN") + area_gross_size: Mapped[float] = mapped_column(Numeric(20, 6), server_default="0") + area_net_size: Mapped[float] = mapped_column(Numeric(20, 6), server_default="0") + width = mapped_column(Integer, server_default="0") + size = mapped_column(Integer, server_default="0") + + build_id: Mapped[int] = mapped_column(ForeignKey("build.id")) + build_uu_id: Mapped[str] = mapped_column(String, comment="Building UUID") + part_type_id: Mapped[int] = mapped_column( + ForeignKey("build_types.id"), nullable=True, comment="Building Part Type" + ) + part_type_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Building Part Type UUID" + ) + + # buildings: Mapped["Build"] = relationship( + # "Build", back_populates="areas", foreign_keys=[build_id] + # ) + + _table_args_ = ( + Index("_edm_build_parts_area_ndx_00", build_id, area_code, unique=True), + ) + + +class BuildSites(CrudCollection): + """ + Builds class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "build_sites" + __exclude__fields__ = [] + __include__fields__ = [] + + site_name: Mapped[str] = mapped_column(String(24), nullable=False) + site_no: Mapped[str] = mapped_column(String(8), nullable=False) + + address_id: Mapped[int] = mapped_column(ForeignKey("addresses.id")) + address_uu_id: Mapped[str] = mapped_column(String, comment="Address UUID") + + # addresses: Mapped["Address"] = relationship( + # "Address", back_populates="site", foreign_keys=[address_id] + # ) + # buildings: Mapped["Build"] = relationship( + # "Build", back_populates="sites", foreign_keys="Build.site_id" + # ) + + __table_args__ = ( + Index("_sites_ndx_01", site_no, site_name), + {"comment": "Sites that groups building objets"}, + ) + + +class BuildCompaniesProviding(CrudCollection): + """ """ + + __tablename__ = "build_companies_providing" + __exclude__fields__ = [] + __include__fields__ = [] + + build_id = mapped_column( + ForeignKey("build.id"), nullable=False, comment="Building ID" + ) + build_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Providing UUID" + ) + company_id: Mapped[int] = mapped_column(ForeignKey("companies.id")) + company_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Providing UUID" + ) + provide_id: Mapped[int] = mapped_column( + ForeignKey("api_enum_dropdown.id"), nullable=True + ) + provide_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Providing UUID" + ) + contract_id: Mapped[int] = mapped_column( + Integer, ForeignKey("companies.id"), nullable=True + ) + + __table_args__ = ( + Index( + "_build_companies_providing_ndx_00", + build_id, + company_id, + provide_id, + unique=True, + ), + {"comment": "Companies providing services for building"}, + ) + + +class BuildPersonProviding(CrudCollection): + """ """ + + __tablename__ = "build_person_providing" + __exclude__fields__ = [] + __include__fields__ = [] + + build_id = mapped_column( + ForeignKey("build.id"), nullable=False, comment="Building ID" + ) + build_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Providing UUID" + ) + people_id: Mapped[int] = mapped_column(ForeignKey("people.id")) + people_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="People UUID" + ) + provide_id: Mapped[int] = mapped_column( + ForeignKey("api_enum_dropdown.id"), nullable=True + ) + provide_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Providing UUID" + ) + contract_id: Mapped[int] = mapped_column( + Integer, ForeignKey("companies.id"), nullable=True + ) + + __table_args__ = ( + Index( + "_build_person_providing_ndx_00", + build_id, + people_id, + provide_id, + unique=True, + ), + {"comment": "People providing services for building"}, + ) diff --git a/api_services/schemas/building/decision_book.py b/api_services/schemas/building/decision_book.py new file mode 100644 index 0000000..ed0f706 --- /dev/null +++ b/api_services/schemas/building/decision_book.py @@ -0,0 +1,888 @@ +import math +import arrow +from datetime import datetime, timedelta +from decimal import Decimal +from typing import List, Any +from fastapi import HTTPException, status + +from schemas.base_imports import ( + CrudCollection, + String, + ForeignKey, + Index, + SmallInteger, + Boolean, + TIMESTAMP, + Text, + Numeric, + Integer, + mapped_column, + Mapped, + relationship, +) + + +class BuildDecisionBook(CrudCollection): + """ + Builds class based on declarative_base and BaseMixin via session + The start dates of the decision log periods are determined from the 'decision_period_date' field in the decision log table within the building information. + decision_period_date = Her yıl yapılan karar toplantısı + 365 gün her yıl tekrar eden + decision_book_pdf_path: Karar defteri pdf dosyasının yolu + resp_company_fix_wage: Karar defterinin oluşmasını sağlayan dışardaki danışmanlık ücreti + is_out_sourced: Karar defterinin dışardan alınan hizmetle oluşturulup oluşturulmadığı + contact_agreement_path: Karar defterinin oluşmasını sağlayan dışardaki danışmanlık anlaşması dosyasının yolu + contact_agreement_date: Karar defterinin oluşmasını sağlayan dışardaki danışmanlık anlaşma tarihi + meeting_date: Karar defterinin oluşmasını sağlayan toplantı tarihi + decision_type: Karar defterinin tipi (Bina Yönetim Toplantısı (BYT), Yıllık Acil Toplantı (YAT) + """ + + __tablename__ = "build_decision_book" + __exclude__fields__ = [] + + decision_book_pdf_path: Mapped[str] = mapped_column( + String, server_default="", nullable=True + ) + resp_company_fix_wage: Mapped[float] = mapped_column( + Numeric(10, 2), server_default="0" + ) # + is_out_sourced: Mapped[bool] = mapped_column(Boolean, server_default="0") + meeting_date: Mapped[TIMESTAMP] = mapped_column( + TIMESTAMP(timezone=True), server_default="1900-01-01" + ) + decision_type: Mapped[str] = mapped_column(String(3), server_default="RBM") + meeting_is_completed: Mapped[bool] = mapped_column(Boolean, server_default="0") + meeting_completed_date: Mapped[TIMESTAMP] = mapped_column( + TIMESTAMP(timezone=True), nullable=True, comment="Meeting Completed Date" + ) + + build_id: Mapped[int] = mapped_column(ForeignKey("build.id"), nullable=False) + build_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Build UUID" + ) + resp_company_id: Mapped[int] = mapped_column(ForeignKey("companies.id")) + resp_company_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Company UUID" + ) + contact_id: Mapped[int] = mapped_column( + ForeignKey("contracts.id"), nullable=True, comment="Contract id" + ) + contact_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Contract UUID" + ) + + buildings: Mapped["Build"] = relationship( + "Build", + back_populates="decision_books", + foreign_keys=build_id, + ) + decision_book_items: Mapped[List["BuildDecisionBookItems"]] = relationship( + "BuildDecisionBookItems", + back_populates="decision_books", + foreign_keys="BuildDecisionBookItems.build_decision_book_id", + ) + + __table_args__ = ( + Index("build_decision_book_ndx_011", meeting_date, build_id), + Index("build_decision_book_ndx_011", build_id, "expiry_starts", "expiry_ends"), + { + "comment": "Decision Book objects that are related to decision taken at building meetings" + }, + ) + + @classmethod + def retrieve_active_rbm(cls): + from Schemas.building.build import Build + + with cls.new_session() as db_session: + related_build = Build.find_one(id=cls.build_id) + related_date = arrow.get(related_build.build_date) + date_processed = related_date.replace( + year=arrow.now().date().year, month=related_date.month, day=1 + ) + if arrow.now().date() <= date_processed: + book = cls.filter_one( + cls.expiry_ends <= date_processed, + cls.decision_type == "RBM", + cls.build_id == related_build.id, + db=db_session, + ).data + if not book: + cls.raise_http_exception( + status_code="HTTP_404_NOT_FOUND", + error_case="NOTFOUND", + message=f"Decision Book is not found for {related_build.build_name}-RBM", + data=dict( + build_id=str(related_build.uu_id), + build_name=related_build.build_name, + decision_type="RBM", + ), + ) + return book + return + + @property + def semester(self): + start_format = "".join( + [str(self.expiry_starts.year), "-", str(self.expiry_starts.month)] + ) + end_format = "".join( + [str(self.expiry_ends.year), "-", str(self.expiry_ends.month)] + ) + return "".join([start_format, " ", end_format]) + + def check_book_is_valid(self, bank_date: str): + if all( + [True if letter in str(bank_date) else False for letter in ["-", " ", ":"]] + ): + bank_date = datetime.strptime(str(bank_date), "%Y-%m-%d %H:%M:%S") + date_valid = ( + arrow.get(self.expiry_starts) + < arrow.get(bank_date) + < arrow.get(self.expiry_ends) + ) + return date_valid and self.active and not self.deleted + + # @classmethod + # def retrieve_valid_book(cls, bank_date, iban): + # from Schemas import ( + # BuildIbans, + # ) + # with cls.new_session() as db_session: + # if all( + # [True if letter in str(bank_date) else False for letter in ["-", " ", ":"]] + # ): + # bank_date = datetime.strptime(str(bank_date), "%Y-%m-%d %H:%M:%S") + # build_iban = BuildIbans.find_one(iban=iban) + # decision_book: cls = cls.filter_one( + # cls.build_id == build_iban.build_id, + # cls.expiry_starts < bank_date, + # cls.expiry_ends > bank_date, + # cls.active == True, + # cls.deleted == False, + # db=db_session + # ).data + # decision_book.check_book_is_valid(bank_date.__str__()) + # return decision_book + # return + + +class BuildDecisionBookInvitations(CrudCollection): + """ + Builds class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "build_decision_book_invitations" + __exclude__fields__ = [] + + build_id: Mapped[int] = mapped_column(Integer, nullable=False) + build_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Build UUID" + ) + decision_book_id: Mapped[int] = mapped_column( + ForeignKey("build_decision_book.id"), nullable=False + ) + decision_book_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Decision Book UUID" + ) + + invitation_type: Mapped[str] = mapped_column( + String, nullable=False, comment="Invite Type" + ) + invitation_attempt: Mapped[int] = mapped_column(SmallInteger, server_default="1") + living_part_count: Mapped[int] = mapped_column(SmallInteger, server_default="1") + living_part_percentage: Mapped[float] = mapped_column( + Numeric(10, 2), server_default="0.51" + ) + + message: Mapped[str] = mapped_column( + Text, nullable=True, comment="Invitation Message" + ) + planned_date: Mapped[TIMESTAMP] = mapped_column( + TIMESTAMP(timezone=True), nullable=False, comment="Planned Meeting Date" + ) + planned_date_expires: Mapped[TIMESTAMP] = mapped_column( + TIMESTAMP(timezone=True), nullable=False, comment="Planned Meeting Date Expires" + ) + + __table_args__ = ( + Index( + "_build_decision_book_invitations_ndx_01", + invitation_type, + planned_date, + invitation_attempt, + unique=True, + ), + {"comment": "People that are invited to building meetings."}, + ) + + @classmethod + def check_invites_are_ready_for_meeting(cls, selected_decision_book, token_dict): + with cls.new_session() as db_session: + first_book_invitation = BuildDecisionBookInvitations.filter_one( + BuildDecisionBookInvitations.build_id + == token_dict.selected_occupant.build_id, + BuildDecisionBookInvitations.decision_book_id + == selected_decision_book.id, + BuildDecisionBookInvitations.invitation_attempt == 1, + db=db_session, + ).data + if not first_book_invitation: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"First Meeting Invitation is not found for Decision Book UUID : {selected_decision_book.uu_id}", + ) + need_attend_count = int(first_book_invitation.living_part_count) * Decimal( + first_book_invitation.living_part_percentage + ) + valid_invite_count = ( + BuildDecisionBookPerson.filter_all_system( + BuildDecisionBookPerson.invite_id == first_book_invitation.id, + BuildDecisionBookPerson.build_decision_book_id + == selected_decision_book.id, + BuildDecisionBookPerson.is_attending == True, + db=db_session, + ) + .query.distinct(BuildDecisionBookPerson.person_id) + .count() + ) + + second_book_invitation = BuildDecisionBookInvitations.filter_one_system( + BuildDecisionBookInvitations.build_id + == token_dict.selected_occupant.build_id, + BuildDecisionBookInvitations.decision_book_id + == selected_decision_book.id, + BuildDecisionBookInvitations.invitation_attempt == 2, + db=db_session, + ).data + if ( + not valid_invite_count >= need_attend_count + and not second_book_invitation + ): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"In order meeting to be held, {math.ceil(need_attend_count)} people must attend " + f"to the meeting. Only {valid_invite_count} people are attending to the meeting.", + ) + return first_book_invitation or second_book_invitation + + +class BuildDecisionBookPerson(CrudCollection): + """ + Builds class based on declarative_base and BaseMixin via session + Karar Defteri toplantılarına katılan kişiler veya yetkililer + dues_percent_discount: Katılımcının aidat indirim oranı Aidatdan yüzde indirim alır + dues_fix_discount: Katılımcının aidat sabit miktarı Aidatdan sabit bir miktar indirim alır + dues_discount_approval_date: Bu kişinin indiriminin onayladığı tarih + management_typecode: Kişinin toplantı görevi + """ + + __tablename__ = "build_decision_book_person" + __exclude__fields__ = [] + __enum_list__ = [("management_typecode", "BuildManagementType", "bm")] + + dues_percent_discount: Mapped[int] = mapped_column(SmallInteger, server_default="0") + dues_fix_discount: Mapped[float] = mapped_column(Numeric(10, 2), server_default="0") + dues_discount_approval_date: Mapped[TIMESTAMP] = mapped_column( + TIMESTAMP(timezone=True), server_default="1900-01-01 00:00:00" + ) + send_date: Mapped[TIMESTAMP] = mapped_column( + TIMESTAMP(timezone=True), nullable=False, comment="Confirmation Date" + ) + is_attending: Mapped[bool] = mapped_column( + Boolean, server_default="0", comment="Occupant is Attending to invitation" + ) + confirmed_date: Mapped[TIMESTAMP] = mapped_column( + TIMESTAMP(timezone=True), nullable=True, comment="Confirmation Date" + ) + token: Mapped[str] = mapped_column( + String, server_default="", comment="Invitation Token" + ) + + vicarious_person_id: Mapped[int] = mapped_column( + ForeignKey("people.id"), nullable=True, comment="Vicarious Person ID" + ) + vicarious_person_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Vicarious Person UUID" + ) + + invite_id: Mapped[int] = mapped_column( + ForeignKey("build_decision_book_invitations.id"), nullable=False + ) + invite_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Invite UUID" + ) + + build_decision_book_id: Mapped[int] = mapped_column( + ForeignKey("build_decision_book.id"), nullable=False + ) + build_decision_book_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Decision Book UUID" + ) + 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=True, comment="Living Space UUID" + ) + person_id: Mapped[int] = mapped_column(ForeignKey("people.id"), nullable=False) + # person_uu_id: Mapped[str] = mapped_column(String, nullable=False, comment="Person UUID") + + __table_args__ = ( + Index( + "_build_decision_book_person_ndx_01", + build_decision_book_id, + invite_id, + build_living_space_id, + unique=True, + ), + {"comment": "People that are attended to building meetings."}, + ) + + def retrieve_all_occupant_types(self): + with self.new_session() as db_session: + all_decision_book_people = self.filter_all_system( + BuildDecisionBookPersonOccupants.invite_id == self.invite_id, + db=db_session, + ) + BuildDecisionBookPersonOccupants.pre_query = all_decision_book_people.query + return BuildDecisionBookPersonOccupants.filter_all_system( + db=db_session + ).data + + def get_occupant_types(self): + with self.new_session() as db_session: + if occupants := BuildDecisionBookPersonOccupants.filter_all( + BuildDecisionBookPersonOccupants.build_decision_book_person_id + == self.id, + db=db_session, + ).data: + return occupants + return + + def check_occupant_type(self, occupant_type): + with self.new_session() as db_session: + book_person_occupant_type = BuildDecisionBookPersonOccupants.filter_one( + BuildDecisionBookPersonOccupants.build_decision_book_person_id + == self.id, + BuildDecisionBookPersonOccupants.occupant_type_id == occupant_type.id, + BuildDecisionBookPersonOccupants.active == True, + BuildDecisionBookPersonOccupants.is_confirmed == True, + db=db_session, + ).data + if not book_person_occupant_type: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Occupant Type : {occupant_type.occupant_code} is not found in " + f"Decision Book Person UUID {self.uu_id}", + ) + + +class BuildDecisionBookPersonOccupants(CrudCollection): + """ + Builds class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "build_decision_book_person_occupants" + __exclude__fields__ = [] + + build_decision_book_person_id: Mapped[int] = mapped_column( + ForeignKey("build_decision_book_person.id"), nullable=False + ) + build_decision_book_person_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Decision Book Person UUID" + ) + invite_id: Mapped[int] = mapped_column( + ForeignKey("build_decision_book_invitations.id"), nullable=True + ) + invite_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Invite UUID" + ) + + occupant_type_id: Mapped[int] = mapped_column( + ForeignKey("occupant_types.id"), nullable=False + ) + occupant_type_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Occupant UUID" + ) + + __table_args__ = ( + Index( + "_build_decision_book_person_occupants_ndx_01", + build_decision_book_person_id, + occupant_type_id, + unique=True, + ), + {"comment": "Occupant Types of People that are attended to building meetings."}, + ) + + +class BuildDecisionBookItems(CrudCollection): + """ + Builds class based on declarative_base and BaseMixin via session + item_commentary = metine itiraz şerh maddesi için + item_order = maddelerin sıralanma numarası + item_objection = maddelerin itiraz şerhi Text şeklinde + """ + + __tablename__ = "build_decision_book_items" + __exclude__fields__ = [] + + item_order: Mapped[int] = mapped_column( + SmallInteger, nullable=False, comment="Order Number of Item" + ) + item_comment: Mapped[str] = mapped_column( + Text, nullable=False, comment="Comment Content" + ) + item_objection: Mapped[str] = mapped_column( + Text, nullable=True, comment="Objection Content" + ) + info_is_completed: Mapped[bool] = mapped_column( + Boolean, server_default="0", comment="Info process is Completed" + ) + is_payment_created: Mapped[bool] = mapped_column( + Boolean, server_default="0", comment="Are payment Records Created" + ) + + info_type_id: Mapped[int] = mapped_column( + ForeignKey("api_enum_dropdown.id"), nullable=True + ) + info_type_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Info Type UUID" + ) + + build_decision_book_id: Mapped[int] = mapped_column( + ForeignKey("build_decision_book.id"), nullable=False + ) + build_decision_book_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Decision Book UUID" + ) + item_short_comment: Mapped[str] = mapped_column( + String(24), + nullable=True, + comment="This field is reserved for use in grouping data or in the pivot heading.", + ) + + decision_books: Mapped["BuildDecisionBook"] = relationship( + "BuildDecisionBook", + back_populates="decision_book_items", + foreign_keys=[build_decision_book_id], + ) + decision_book_project: Mapped["BuildDecisionBookProjects"] = relationship( + "BuildDecisionBookProjects", + back_populates="build_decision_book_item", + foreign_keys="BuildDecisionBookProjects.build_decision_book_item_id", + ) + + __table_args__ = ( + Index("_build_decision_book_item_ndx_01", build_decision_book_id), + Index( + "_build_decision_book_item_ndx_02", + build_decision_book_id, + item_order, + unique=True, + ), + { + "comment": "Decision Book Items that are related to decision taken at building meetings" + }, + ) + + +class BuildDecisionBookItemsUnapproved(CrudCollection): + """ + Builds class based on declarative_base and BaseMixin via session unapproved personnel + """ + + __tablename__ = "build_decision_book_items_unapproved" + __exclude__fields__ = [] + + item_objection: Mapped[str] = mapped_column( + Text, nullable=False, comment="Objection Content" + ) + item_order: Mapped[int] = mapped_column( + SmallInteger, nullable=False, comment="Order Number" + ) + + decision_book_item_id: Mapped[int] = mapped_column( + ForeignKey("build_decision_book_items.id"), nullable=False + ) + decision_book_item_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Decision Book Item" + ) + person_id: Mapped[int] = mapped_column(ForeignKey("people.id"), nullable=False) + person_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Person UUID" + ) + build_decision_book_item: Mapped[int] = mapped_column( + ForeignKey("build_decision_book_items.id"), nullable=False + ) + build_decision_book_item_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Decision Book Item UUID" + ) + + __table_args__ = ( + Index("_build_decision_book_item_unapproved_ndx_01", build_decision_book_item), + { + "comment": "People that are unapproved partially or completely in decision book items" + }, + ) + + +class BuildDecisionBookPayments(CrudCollection): + """ + Builds class based on declarative_base and BaseMixin via session + period_time = to_char(NEW.process_date, 'YYYY-MM'); + """ + + __tablename__ = "build_decision_book_payments" + __exclude__fields__ = [] + __enum_list__ = [("receive_debit", "DebitTypes", "D")] + + payment_plan_time_periods: Mapped[str] = mapped_column( + String(10), nullable=False, comment="Payment Plan Time Periods" + ) + process_date: Mapped[TIMESTAMP] = mapped_column( + TIMESTAMP(timezone=True), nullable=False, comment="Payment Due Date" + ) + payment_amount: Mapped[float] = mapped_column( + Numeric(16, 2), nullable=False, comment="Payment Amount" + ) + currency: Mapped[str] = mapped_column(String(8), server_default="TRY") + + payment_types_id: Mapped[int] = mapped_column( + ForeignKey("api_enum_dropdown.id"), nullable=True + ) + payment_types_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Dues Type UUID" + ) + + period_time: Mapped[str] = mapped_column(String(12)) + process_date_y: Mapped[int] = mapped_column(SmallInteger) + process_date_m: Mapped[int] = mapped_column(SmallInteger) + + build_decision_book_item_id: Mapped[int] = mapped_column( + ForeignKey("build_decision_book_items.id"), + nullable=False, + comment="Build Decision Book Item ID", + ) + build_decision_book_item_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Decision Book Item UUID" + ) + # build_decision_book_id: Mapped[int] = mapped_column( + # ForeignKey("build_decision_book.id"), nullable=True + # ) + # build_decision_book_uu_id: Mapped[str] = mapped_column( + # String, nullable=True, comment="Decision Book UUID" + # ) + build_parts_id: Mapped[int] = mapped_column( + ForeignKey("build_parts.id"), nullable=False + ) + build_parts_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Build Part UUID" + ) + decision_book_project_id: Mapped[int] = mapped_column( + ForeignKey("build_decision_book_projects.id"), + nullable=True, + comment="Decision Book Project ID", + ) + decision_book_project_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Decision Book Project UUID" + ) + account_records_id: Mapped[int] = mapped_column( + ForeignKey("account_records.id"), nullable=True + ) + account_records_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Account Record UU ID" + ) + + # budget_records_id: Mapped[int] = mapped_column(ForeignKey("account_records.id"), nullable=True) + # budget_records_uu_id: Mapped[str] = mapped_column( + # String, nullable=True, comment="Budget UUID" + # ) + # accounting_id: Mapped[int] = mapped_column(ForeignKey("account_detail.id"), nullable=True) + # accounting_uu_id: Mapped[str] = mapped_column( + # String, nullable=True, comment="Accounting UUID" + # ) + # receive_debit_id: Mapped[int] = mapped_column(ForeignKey("api_enum_dropdown.id"), nullable=True) + # receive_debit_uu_id: Mapped[str] = mapped_column(String, nullable=True, comment="Debit UUID") + + # accounting: Mapped["AccountDetail"] = relationship( + # "AccountDetail", + # back_populates="decision_book_payment_detail", + # foreign_keys=[accounting_id], + # ) + # + # decision_book_master: Mapped["BuildDecisionBookPaymentsMaster"] = relationship( + # "BuildDecisionBookPaymentsMaster", + # back_populates="decision_book_payment_detail", + # foreign_keys=[build_decision_book_payments_master_id], + # ) + # budget_records: Mapped["CompanyBudgetRecords"] = relationship( + # "CompanyBudgetRecords", + # back_populates="decision_book_payment_detail", + # foreign_keys=[budget_records_id], + # ) + + __table_args__ = ( + Index( + "build_decision_book_payments_detail_ndx_00", + build_decision_book_item_id, + build_parts_id, + payment_plan_time_periods, + process_date, + payment_types_id, + account_records_id, + unique=True, + ), + Index("build_decision_book_payments_detail_ndx_01", account_records_id), + {"comment": "Payment Details of Decision Book Payments"}, + ) + + +class BuildDecisionBookLegal(CrudCollection): + """ + Builds class based on declarative_base and BaseMixin via session + lawsuits_type C:Court Mehkeme M: mediator arabulucu + """ + + __tablename__ = "build_decision_book_legal" + __exclude__fields__ = [] + + period_start_date: Mapped[TIMESTAMP] = mapped_column( + TIMESTAMP(timezone=True), nullable=False, comment="Start Date of Legal Period" + ) + lawsuits_decision_number: Mapped[str] = mapped_column( + String, nullable=False, comment="Lawsuits Decision Number" + ) + lawsuits_decision_date: Mapped[TIMESTAMP] = mapped_column( + TIMESTAMP(timezone=True), nullable=False, comment="Lawsuits Decision Date" + ) + + period_stop_date: Mapped[TIMESTAMP] = mapped_column( + TIMESTAMP(timezone=True), server_default="2099-12-31 23:59:59" + ) + decision_book_pdf_path: Mapped[str] = mapped_column( + String, server_default="", nullable=True + ) + resp_company_total_wage: Mapped[float] = mapped_column( + Numeric(10, 2), server_default="0", nullable=True + ) + contact_agreement_path: Mapped[str] = mapped_column( + String, server_default="", nullable=True + ) + contact_agreement_date: Mapped[TIMESTAMP] = mapped_column( + TIMESTAMP(timezone=True), server_default="1900-01-01 00:00:00", nullable=True + ) + meeting_date: Mapped[TIMESTAMP] = mapped_column( + TIMESTAMP(timezone=True), server_default="1900-01-01 00:00:00" + ) + lawsuits_type: Mapped[str] = mapped_column(String(1), server_default="C") + lawsuits_name: Mapped[str] = mapped_column(String(128)) + lawsuits_note: Mapped[str] = mapped_column(String(512)) + lawyer_cost: Mapped[float] = mapped_column(Numeric(20, 2)) + mediator_lawyer_cost: Mapped[float] = mapped_column(Numeric(20, 2)) + other_cost: Mapped[float] = mapped_column(Numeric(20, 2)) + legal_cost: Mapped[float] = mapped_column(Numeric(20, 2)) + approved_cost: Mapped[float] = mapped_column(Numeric(20, 2)) + total_price: Mapped[float] = mapped_column(Numeric(20, 2)) + + build_db_item_id: Mapped[int] = mapped_column( + ForeignKey("build_decision_book_items.id"), nullable=False + ) + build_db_item_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Decision Book Item UUID" + ) + resp_attorney_id: Mapped[int] = mapped_column( + ForeignKey("people.id"), nullable=False + ) + resp_attorney_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Attorney UUID" + ) + resp_attorney_company_id: Mapped[int] = mapped_column(ForeignKey("companies.id")) + resp_attorney_company_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Company UUID" + ) + mediator_lawyer_person_id: Mapped[int] = mapped_column(ForeignKey("people.id")) + mediator_lawyer_person_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Mediator Lawyer UUID" + ) + + __table_args__ = ( + Index("_build_decision_book_legal_ndx_00", meeting_date), + { + "comment": "Legal items related to decision book items recoreded at building meetings" + }, + ) + + +class BuildDecisionBookProjects(CrudCollection): + """ + Builds class based on declarative_base and BaseMixin via session + project_type = C:Court Mehkeme M: mediator arabulucu + """ + + __tablename__ = "build_decision_book_projects" + __exclude__fields__ = [] + + project_no: Mapped[str] = mapped_column( + String(12), nullable=True, comment="Project Number of Decision Book" + ) + project_name: Mapped[str] = mapped_column( + String, nullable=False, comment="Project Name" + ) + project_start_date: Mapped[TIMESTAMP] = mapped_column( + TIMESTAMP(timezone=True), nullable=False, comment="Project Start Date" + ) + project_stop_date: Mapped[TIMESTAMP] = mapped_column( + TIMESTAMP(timezone=True), server_default="2099-12-31 23:59:59" + ) + project_type: Mapped[str] = mapped_column(String, server_default="C") + project_note: Mapped[str] = mapped_column(Text) + + decision_book_pdf_path: Mapped[str] = mapped_column( + String, server_default="", nullable=True + ) + is_completed: Mapped[bool] = mapped_column( + Boolean, server_default="0", comment="Project is Completed" + ) + status_code: Mapped[int] = mapped_column(SmallInteger, nullable=True) + resp_company_fix_wage: Mapped[float] = mapped_column( + Numeric(10, 2), server_default="0" + ) + is_out_sourced: Mapped[bool] = mapped_column(Boolean, server_default="0") + + meeting_date: Mapped[TIMESTAMP] = mapped_column( + TIMESTAMP(timezone=True), server_default="1900-01-01 00:00:00", index=True + ) + currency: Mapped[str] = mapped_column(String(8), server_default="TRY") + bid_price: Mapped[float] = mapped_column(Numeric(16, 4), server_default="0") + approved_price: Mapped[float] = mapped_column(Numeric(16, 4), server_default="0") + final_price: Mapped[float] = mapped_column(Numeric(16, 4), server_default="0") + + contact_id: Mapped[int] = mapped_column( + ForeignKey("contracts.id"), nullable=True, comment="Contract id" + ) + contact_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Contract UUID" + ) + build_decision_book_id: Mapped[int] = mapped_column( + ForeignKey("build_decision_book.id"), nullable=False + ) + build_decision_book_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Decision Book UUID" + ) + build_decision_book_item_id: Mapped[int] = mapped_column( + ForeignKey("build_decision_book_items.id"), nullable=False + ) + build_decision_book_item_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Decision Book Item UUID" + ) + project_response_living_space_id: Mapped[int] = mapped_column( + ForeignKey("build_living_space.id"), + nullable=True, + comment="Project Response Person ID", + ) + project_response_living_space_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Project Response Person UUID" + ) + resp_company_id: Mapped[int] = mapped_column( + ForeignKey("companies.id"), nullable=True + ) + resp_company_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Company UUID" + ) + + build_decision_book_item: Mapped["BuildDecisionBookItems"] = relationship( + "BuildDecisionBookItems", + back_populates="decision_book_project", + foreign_keys=[build_decision_book_item_id], + ) + + __table_args__ = ( + Index( + "_build_decision_book_project_ndx_00", + project_no, + project_start_date, + unique=True, + ), + { + "comment": "Project related to decision taken at building meetings on book items" + }, + ) + + @property + def get_project_year(self): + return self.decision_book_items.decision_books.period_start_date.year + + @property + def get_project_no(self): + return f"{self.get_project_year}-{str(self.id)[-4:].zfill(4)}" + + +class BuildDecisionBookProjectPerson(CrudCollection): + """ + Builds class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "build_decision_book_project_person" + __exclude__fields__ = [] + # __enum_list__ = [("management_typecode", "ProjectTeamTypes", "PTT-EMP")] + + dues_percent_discount: Mapped[int] = mapped_column(SmallInteger, server_default="0") + job_fix_wage: Mapped[float] = mapped_column(Numeric(10, 2), server_default="0") + bid_price: Mapped[float] = mapped_column(Numeric(10, 2), server_default="0") + decision_price: Mapped[float] = mapped_column(Numeric(10, 2), server_default="0") + + build_decision_book_project_id: Mapped[int] = mapped_column( + ForeignKey("build_decision_book_projects.id"), nullable=False + ) + build_decision_book_project_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Decision Book Project UUID" + ) + living_space_id: Mapped[int] = mapped_column( + ForeignKey("build_living_space.id"), nullable=False + ) + living_space_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Living Space UUID" + ) + + __table_args__ = ( + {"comment": "People that are attended to building project meetings."}, + ) + + +class BuildDecisionBookProjectItems(CrudCollection): + """ + Builds class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "build_decision_book_project_items" + __exclude__fields__ = [] + + item_header: Mapped[str] = mapped_column( + String, nullable=False, comment="Item Header" + ) + item_comment: Mapped[str] = mapped_column( + Text, nullable=False, comment="Item Comment" + ) + attachment_pdf_path: Mapped[str] = mapped_column( + String, server_default="", nullable=True, comment="Attachment PDF Path" + ) + item_estimated_cost: Mapped[float] = mapped_column( + Numeric(16, 2), server_default="0", comment="Estimated Cost" + ) + item_short_comment: Mapped[str] = mapped_column( + String(24), + nullable=True, + comment="This field is reserved for use in grouping data or in the pivot heading.", + ) + + build_decision_book_project_id: Mapped[int] = mapped_column( + ForeignKey("build_decision_book_projects.id"), nullable=False + ) + build_decision_book_project_uu_id: Mapped[str] = mapped_column( + String, nullable=True, comment="Decision Book Project UUID" + ) + + __table_args__ = ( + {"comment": "Project Items related to decision taken at building meetings"}, + ) diff --git a/api_services/schemas/company/company.py b/api_services/schemas/company/company.py new file mode 100644 index 0000000..6f99f4b --- /dev/null +++ b/api_services/schemas/company/company.py @@ -0,0 +1,235 @@ +from typing import Any +from schemas.base_imports import ( + CrudCollection, + String, + Integer, + Boolean, + ForeignKey, + Index, + TIMESTAMP, + Numeric, + SmallInteger, + mapped_column, + Mapped, +) + +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): + from Schemas import Duties + + 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): + from Schemas import Duties + + 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/api_services/schemas/company/department.py b/api_services/schemas/company/department.py new file mode 100644 index 0000000..ffe7fd4 --- /dev/null +++ b/api_services/schemas/company/department.py @@ -0,0 +1,82 @@ +from schemas.base_imports import ( + CrudCollection, + String, + Integer, + Boolean, + ForeignKey, + Index, + TIMESTAMP, + Numeric, + SmallInteger, + mapped_column, + Mapped, +) + + +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/api_services/schemas/company/employee.py b/api_services/schemas/company/employee.py new file mode 100644 index 0000000..de9f3e1 --- /dev/null +++ b/api_services/schemas/company/employee.py @@ -0,0 +1,102 @@ +from schemas.base_imports import ( + CrudCollection, + String, + Integer, + Boolean, + ForeignKey, + Index, + TIMESTAMP, + Numeric, + SmallInteger, + mapped_column, + Mapped, +) + + +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/api_services/schemas/event/event.py b/api_services/schemas/event/event.py new file mode 100644 index 0000000..0a62d9b --- /dev/null +++ b/api_services/schemas/event/event.py @@ -0,0 +1,605 @@ +from schemas.base_imports import ( + CrudCollection, + String, + Integer, + Boolean, + ForeignKey, + Index, + TIMESTAMP, + Numeric, + SmallInteger, + mapped_column, + Mapped, +) + + +class Applications(CrudCollection): + """ + Applications class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "applications" + __exclude__fields__ = [] + + name: Mapped[str] = mapped_column( + String, nullable=False, comment="Application Name" + ) + site_url: Mapped[str] = mapped_column(String, nullable=False, comment="Site URL") + application_code: Mapped[str] = mapped_column( + String, nullable=False, comment="Application Code" + ) + application_type: Mapped[str] = mapped_column(String, comment="Application Type") + application_for: Mapped[str] = mapped_column( + String, server_default="EMP", comment="Application For" + ) + description: Mapped[str] = mapped_column(String, comment="Application Description") + + +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__ = [] + + function_code: Mapped[str] = mapped_column( + String, nullable=False, comment="function code", unique=True + ) + 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="") + 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_code: Mapped[str] = mapped_column( + String, nullable=False, comment="Event Type" + ) + 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 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"},) + + +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): + from Schemas import OccupantTypes + + 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 Service2Application(CrudCollection): + """ + Service2Application class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "services2applications" + __exclude__fields__ = [] + + application_id: Mapped[int] = mapped_column( + ForeignKey("applications.id"), nullable=False + ) + application_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Application 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" + ) + application_code: Mapped[str] = mapped_column( + String, nullable=False, comment="Application Code" + ) + site_url: Mapped[str] = mapped_column(String, nullable=False, comment="Site URL") + + __table_args__ = {"comment": "Service2Applications 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, db) -> dict[str:str]: + employee_events = cls.filter_all( + cls.employee_id == employee_id, + db=db, + ).data + service_ids = list(set([event.event_service_id for event in employee_events])) + active_event_ids = Service2Events.filter_all( + Service2Events.service_id.in_(service_ids), + 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) + events_dict = {} + for event in active_events: + if not event.endpoint_code in events_dict: + events_dict[str(event.endpoint_code)] = str(event.function_code) + else: + ValueError("Duplicate event code found for single endpoint") + return events_dict + + +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: int, db) -> dict[str:str]: + occupant_events = cls.filter_all( + cls.build_living_space_id == build_living_space_id, + db=db, + ).data + service_ids = list(set([event.event_service_id for event in occupant_events])) + active_event_ids = Service2Events.filter_all_system( + Service2Events.service_id.in_(service_ids), + 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) + events_dict = {} + for event in active_events: + if not event.endpoint_code in events_dict: + events_dict[str(event.endpoint_code)] = str(event.function_code) + else: + ValueError("Duplicate event code found for single endpoint") + return events_dict + + +class Application2Employee(CrudCollection): + """ + Application2Employee class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "application2employee" + __exclude__fields__ = [] + + employee_id: Mapped[int] = mapped_column(ForeignKey("employees.id"), nullable=False) + employee_uu_id = mapped_column(String, nullable=False, comment="Employee UUID") + service_id: Mapped[int] = mapped_column(ForeignKey("services.id"), nullable=False) + service_uu_id = mapped_column(String, nullable=False, comment="Service UUID") + + @classmethod + def get_application_codes(cls, employee_id: int, db) -> list[int]: + employee_services = cls.filter_all( + cls.employee_id == employee_id, + db=db, + ).data + service_ids = [service.service_id for service in employee_services] + active_applications = Service2Application.filter_all( + Service2Application.service_id.in_(service_ids), + db=db, + ).data + applications = Applications.filter_all( + Applications.id.in_( + [application.application_id for application in active_applications] + ), + db=db, + ).data + if extra_applications := Application2EmployeeExtra.filter_all( + Application2EmployeeExtra.employee_id == employee_id, + db=db, + ).data: + applications_extra = Applications.filter_all( + Applications.id.in_( + [application.application_id for application in extra_applications] + ), + db=db, + ).data + applications.extend(applications_extra) + applications_dict = {} + for application in applications: + if not application.site_url in applications_dict: + applications_dict[str(application.site_url)] = str(application.application_code) + else: + ValueError("Duplicate application code found for single endpoint") + return applications_dict + + __table_args__ = ( + Index( + "application2employee_employee_to_service", + employee_uu_id, + service_uu_id, + unique=True, + ), + {"comment": "Application to Employee Information"}, + ) + + +class Application2Occupant(CrudCollection): + """ + Application2Occupant class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "application2occupant" + __exclude__fields__ = [] + + build_living_space_id: Mapped[int] = mapped_column( + ForeignKey("build_living_space.id"), nullable=False + ) + build_living_space_uu_id = mapped_column( + String, nullable=False, comment="Build Living Space UUID" + ) + service_id: Mapped[int] = mapped_column(ForeignKey("services.id"), nullable=False) + service_uu_id = mapped_column(String, nullable=False, comment="Service UUID") + + @classmethod + def get_application_codes(cls, build_living_space_id: int, db) -> list[int]: + occupant_services = cls.filter_all( + cls.build_living_space_id == build_living_space_id, + db=db, + ).data + service_ids = [service.service_id for service in occupant_services] + active_applications = Service2Application.filter_all( + Service2Application.service_id.in_(service_ids), + db=db, + ).data + applications = Applications.filter_all( + Applications.id.in_( + [application.application_id for application in active_applications] + ), + db=db, + ).data + if extra_applications := Application2OccupantExtra.filter_all( + Application2OccupantExtra.build_living_space_id == build_living_space_id, + db=db, + ).data: + applications_extra = Applications.filter_all( + Applications.id.in_( + [application.application_id for application in extra_applications] + ), + db=db, + ).data + applications.extend(applications_extra) + applications_dict = {} + for application in applications: + if not application.site_url in applications_dict: + applications_dict[str(application.site_url)] = str(application.application_code) + else: + ValueError("Duplicate application code found for single endpoint") + return applications_dict + + __table_args__ = ( + Index( + "application2occupant_occupant_to_service", + build_living_space_uu_id, + service_uu_id, + unique=True, + ), + {"comment": "Application to Occupant Information"}, + ) + + +class Application2EmployeeExtra(CrudCollection): + """ + Application2EmployeeExtra class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "application2employee_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" + ) + application_id: Mapped[int] = mapped_column(ForeignKey("applications.id")) + application_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Application UUID" + ) + site_url: Mapped[str] = mapped_column(String, nullable=False, comment="Site URL") + application_code: Mapped[str] = mapped_column( + String, nullable=False, comment="Application Code" + ) + + __table_args__ = ( + Index( + "application_to_employee", + employee_id, + site_url, + application_id, + unique=True, + ), + {"comment": "Application2Employee Information"}, + ) + + +class Application2OccupantExtra(CrudCollection): + """ + Application2OccupantExtra class based on declarative_base and BaseMixin via session + """ + + __tablename__ = "application2occupant_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" + ) + application_id: Mapped[int] = mapped_column(ForeignKey("applications.id")) + application_uu_id: Mapped[str] = mapped_column( + String, nullable=False, comment="Application UUID" + ) + site_url: Mapped[str] = mapped_column(String, nullable=False, comment="Site URL") + application_code: Mapped[str] = mapped_column( + String, nullable=False, comment="Application Code" + ) + + __table_args__ = ( + Index( + "application_to_occupant", + build_living_space_id, + site_url, + application_id, + unique=True, + ), + {"comment": "Application2Occupant Information"}, + ) diff --git a/api_services/schemas/identity/identity.py b/api_services/schemas/identity/identity.py new file mode 100644 index 0000000..f78075e --- /dev/null +++ b/api_services/schemas/identity/identity.py @@ -0,0 +1,488 @@ +import arrow +from schemas.base_imports import ( + CrudCollection, + String, + Integer, + Boolean, + ForeignKey, + Index, + TIMESTAMP, + Text, + func, + BigInteger, + Numeric, + or_, + and_, + mapped_column, + Mapped, + relationship, +) + + +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(days=3)), + ) + + # users = relationship("Users", back_populates="tokens", foreign_keys=[user_id]) + + +class Credentials(CrudCollection): + """ + Credentials class to store user credentials + """ + + __tablename__ = "credentials" + __exclude__fields__ = [] + + credential_token: Mapped[str] = mapped_column( + String, server_default="", comment="Credential token for authentication" + ) + user_id: Mapped[int] = mapped_column( + ForeignKey("users.id"), nullable=False, comment="Foreign key to users table" + ) + user_uu_id: Mapped[str] = mapped_column( + String, server_default="", comment="User UUID", index=True + ) + 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 + ) + name: Mapped[str] = mapped_column( + String, server_default="", comment="Name of the user", index=True + ) + surname: Mapped[str] = mapped_column( + String, server_default="", comment="Surname of the user", index=True + ) + email: Mapped[str] = mapped_column( + String, server_default="", comment="Email address of the user", index=True + ) + phone: Mapped[str] = mapped_column( + String, server_default="", comment="Phone number of the user", index=True + ) + is_verified: Mapped[bool] = mapped_column( + Boolean, server_default="0", comment="Flag to check if user is verified" + ) + + def generate_token(self) -> str: + """ + Generate a unique token for the user + """ + name_token, rest_of_token = "", "" + if self.name and self.surname: + name_token = f"{self.name[0].upper()}{self.surname[0].upper()}" + return "" + + +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]) + default_language: Mapped[str] = mapped_column( + String, server_default="tr", comment="Default language of user" + ) + + +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"}, + ) + + + # @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) \ No newline at end of file diff --git a/api_services/schemas/others/enums.py b/api_services/schemas/others/enums.py new file mode 100644 index 0000000..8ff5294 --- /dev/null +++ b/api_services/schemas/others/enums.py @@ -0,0 +1,110 @@ +from schemas.base_imports import ( + CrudCollection, + UUID, + String, + text, + mapped_column, + Mapped, +) + + +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/api_services/schemas/rules/rules.py b/api_services/schemas/rules/rules.py new file mode 100644 index 0000000..f1cf8b1 --- /dev/null +++ b/api_services/schemas/rules/rules.py @@ -0,0 +1,37 @@ +from schemas.base_imports import ( + CrudCollection, + UUID, + String, + mapped_column, + Mapped, + Index, +) + + +class EndpointRestriction(CrudCollection): + """ + Initialize Endpoint Restriction with default values + """ + + __tablename__ = "endpoint_restriction" + __exclude__fields__ = [] + + operation_uu_id: Mapped[UUID] = mapped_column( + String, comment="UUID of the operation", nullable=False, unique=True + ) + endpoint_function: Mapped[str] = mapped_column( + String, comment="Function name of the API endpoint" + ) + endpoint_name: Mapped[str] = mapped_column( + String, comment="Name of the API endpoint" + ) + endpoint_method: Mapped[str] = mapped_column( + String, comment="HTTP method used by the endpoint" + ) + endpoint_desc: Mapped[str] = mapped_column( + String, server_default="", comment="Description of the endpoint" + ) + + __table_args__ = ( + Index("idx_endpoint_restriction_operation_uu_id", operation_uu_id, endpoint_method, endpoint_name, unique=True), + ) diff --git a/bank-services/README.md b/bank_services/README.md similarity index 100% rename from bank-services/README.md rename to bank_services/README.md diff --git a/bank-services/bank-email-service/README.md b/bank_services/bank-email-service/README.md similarity index 100% rename from bank-services/bank-email-service/README.md rename to bank_services/bank-email-service/README.md diff --git a/bank-services/bank-parser-service/README.md b/bank_services/bank-parser-service/README.md similarity index 100% rename from bank-services/bank-parser-service/README.md rename to bank_services/bank-parser-service/README.md diff --git a/bank-services/bank-routine-email-service/README.md b/bank_services/bank-routine-email-service/README.md similarity index 100% rename from bank-services/bank-routine-email-service/README.md rename to bank_services/bank-routine-email-service/README.md diff --git a/bank-services/bank-sender-service/README.md b/bank_services/bank-sender-service/README.md similarity index 100% rename from bank-services/bank-sender-service/README.md rename to bank_services/bank-sender-service/README.md diff --git a/bank-services/bank-service-depends/README.md b/bank_services/bank-service-depends/README.md similarity index 100% rename from bank-services/bank-service-depends/README.md rename to bank_services/bank-service-depends/README.md diff --git a/bank-services/bank-writer-service/README.md b/bank_services/bank-writer-service/README.md similarity index 100% rename from bank-services/bank-writer-service/README.md rename to bank_services/bank-writer-service/README.md diff --git a/bank-services/z-bank-templates/template_accounts.html b/bank_services/z-bank-templates/template_accounts.html similarity index 100% rename from bank-services/z-bank-templates/template_accounts.html rename to bank_services/z-bank-templates/template_accounts.html diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a81c58d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,47 @@ +services: + # auth_service: + # container_name: auth_service + # build: + # context: . + # dockerfile: api_services/api_builds/auth-service/Dockerfile + # env_file: + # - api_env.env + # environment: + # - API_PATH=app:app + # - API_HOST=0.0.0.0 + # - API_PORT=8001 + # - API_LOG_LEVEL=info + # - API_RELOAD=1 + # - API_APP_NAME=evyos-auth-api-gateway + # - API_TITLE=WAG API Auth Api Gateway + # - API_FORGOT_LINK=https://auth_service/forgot-password + # - API_DESCRIPTION=This api is serves as web auth api gateway only to evyos web services. + # - API_APP_URL=https://auth_service + # ports: + # - "8000:8000" + # restart: unless-stopped + # logging: + # driver: "json-file" + # options: + # max-size: "10m" + # max-file: "3" + + initializer_service: + container_name: initializer_service + build: + context: . + dockerfile: api_services/api_builds/initial-service/Dockerfile + environment: + - SET_ALEMBIC=0 + networks: + - wag-services + env_file: + - api_env.env + mem_limit: 512m + cpus: 0.5 + + +networks: + wag-services: + driver: bridge + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5d4e085 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,27 @@ +[project] +name = "prod-wag-backend-automate-services" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "alembic>=1.15.2", + "arrow>=1.3.0", + "cryptography>=44.0.2", + "faker>=37.1.0", + "fastapi>=0.115.12", + "pandas>=2.2.3", + "prometheus-fastapi-instrumentator>=7.1.0", + "psycopg2-binary>=2.9.10", + "pydantic-settings>=2.8.1", + "pymongo>=4.11.3", + "pytest>=8.3.5", + "redbox>=0.2.1", + "redis>=5.2.1", + "redmail>=0.6.0", + "requests>=2.32.3", + "sqlalchemy-mixins>=2.0.5", + "textdistance>=4.6.3", + "unidecode>=1.3.8", + "uvicorn>=0.34.0", +] diff --git a/web-services/client-frontend/README.md b/web_services/client-frontend/README.md similarity index 100% rename from web-services/client-frontend/README.md rename to web_services/client-frontend/README.md diff --git a/web-services/management-frontend/README.md b/web_services/management-frontend/README.md similarity index 100% rename from web-services/management-frontend/README.md rename to web_services/management-frontend/README.md diff --git a/web-services/web-controllers/README.md b/web_services/web-controllers/README.md similarity index 100% rename from web-services/web-controllers/README.md rename to web_services/web-controllers/README.md diff --git a/z-templates/a-service/a.txt b/z-templates/a-service/a.txt new file mode 100644 index 0000000..e69de29 diff --git a/z-templates/a-service/endpoints/index.py b/z-templates/a-service/endpoints/index.py new file mode 100644 index 0000000..940a9fa --- /dev/null +++ b/z-templates/a-service/endpoints/index.py @@ -0,0 +1,9 @@ + + +endpoints_index: dict = { + "Slot1": "", + "Slot2": "", + "Slot3": "", + "Slot4": "", + "Slot5": "", +} \ No newline at end of file diff --git a/z-templates/a-service/endpoints/routes.py b/z-templates/a-service/endpoints/routes.py new file mode 100644 index 0000000..b82ac19 --- /dev/null +++ b/z-templates/a-service/endpoints/routes.py @@ -0,0 +1,15 @@ +from fastapi import APIRouter + + +def get_routes() -> list[APIRouter]: + return [] + + +def get_safe_endpoint_urls() -> list[tuple[str, str]]: + return [ + ("/", "GET"), + ("/docs", "GET"), + ("/redoc", "GET"), + ("/openapi.json", "GET"), + ("/metrics", "GET"), + ] diff --git a/z-templates/a-service/events/index.py b/z-templates/a-service/events/index.py new file mode 100644 index 0000000..6bf28aa --- /dev/null +++ b/z-templates/a-service/events/index.py @@ -0,0 +1,9 @@ + + +events_index: dict = { + "Slot1": "", + "Slot2": "", + "Slot3": "", + "Slot4": "", + "Slot5": "", +}