diff --git a/ServicesApi/Builds/Initial/Dockerfile b/ServicesApi/Builds/Initial/Dockerfile index 9a930d3..f2a19db 100644 --- a/ServicesApi/Builds/Initial/Dockerfile +++ b/ServicesApi/Builds/Initial/Dockerfile @@ -12,12 +12,12 @@ COPY /pyproject.toml ./pyproject.toml 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_modules /api_modules +COPY /ServicesApi/Controllers /Controllers +COPY /ServicesApi/Schemas /Schemas +COPY /ServicesApi/Extensions /Extensions -COPY /api_services/api_builds/initial-service /initial-service -COPY /api_services/api_builds/initial-service / +COPY /ServicesApi/Builds/Initial /initial-service +COPY /ServicesApi/Builds/Initial / # Set Python path to include app directory ENV PYTHONPATH=/ PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1 diff --git a/ServicesApi/Builds/Initial/app.py b/ServicesApi/Builds/Initial/app.py index e0c0ffa..a30dbf7 100644 --- a/ServicesApi/Builds/Initial/app.py +++ b/ServicesApi/Builds/Initial/app.py @@ -1,6 +1,6 @@ import os -from api_controllers.postgres.engine import get_db +from 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 diff --git a/ServicesApi/Builds/Initial/init_enums.py b/ServicesApi/Builds/Initial/init_enums.py index abc181c..1841195 100644 --- a/ServicesApi/Builds/Initial/init_enums.py +++ b/ServicesApi/Builds/Initial/init_enums.py @@ -216,12 +216,12 @@ def init_api_enums_build_types(db_session): { "enum_class": "DebitTypes", "type_code": "DT-D", - "type_name": "Debit Sender", + "type_name": "Incoming Fund", }, { "enum_class": "DebitTypes", "type_code": "DT-R", - "type_name": "Credit Receiver", + "type_name": "Outgoing Fund", }, { "enum_class": "DebitTypes", diff --git a/ServicesApi/Controllers/Postgres/mixin.py b/ServicesApi/Controllers/Postgres/mixin.py index 8020a59..b6135a6 100644 --- a/ServicesApi/Controllers/Postgres/mixin.py +++ b/ServicesApi/Controllers/Postgres/mixin.py @@ -231,28 +231,18 @@ class CrudCollection(CrudMixin): __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" - ) + ref_int: Mapped[int] = mapped_column(Integer, nullable=True, index=True, comment="External reference ID") + ref_id: Mapped[str] = mapped_column(String(100), nullable=True, index=True, comment="External reference UUID") + + 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" - ) + 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" - ) + 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( diff --git a/ServicesApi/Schemas/account/account.py b/ServicesApi/Schemas/account/account.py index 9928a6f..b8671a0 100644 --- a/ServicesApi/Schemas/account/account.py +++ b/ServicesApi/Schemas/account/account.py @@ -451,6 +451,12 @@ class AccountRecords(CrudCollection): build_decision_book_uu_id: Mapped[str] = mapped_column( String, nullable=True, comment="Build Decision Book UU ID" ) + # payment_result_type = Mapped[int] = mapped_column( + # ForeignKey("api_enum_dropdown.id"), nullable=True + # ) + # payment_result_type_uu_id: Mapped[str] = mapped_column( + # String, nullable=True, comment="Payment Result Type UU ID" + # ) __table_args__ = ( Index("_budget_records_ndx_00", is_receipt_mail_send, bank_date), diff --git a/ServicesApi/Schemas/building/decision_book.py b/ServicesApi/Schemas/building/decision_book.py index 2030600..f65f78b 100644 --- a/ServicesApi/Schemas/building/decision_book.py +++ b/ServicesApi/Schemas/building/decision_book.py @@ -549,12 +549,9 @@ class BuildDecisionBookPayments(CrudCollection): ) 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" - ) + account_is_debit: Mapped[bool] = mapped_column(Boolean, nullable=True, server_default="1", comment="Is Debit") + 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) @@ -568,32 +565,18 @@ class BuildDecisionBookPayments(CrudCollection): 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" - ) + 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" - ) + 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( @@ -626,12 +609,15 @@ class BuildDecisionBookPayments(CrudCollection): __table_args__ = ( Index( "build_decision_book_payments_detail_ndx_00", + "uu_id", + "ref_id", build_decision_book_item_id, build_parts_id, payment_plan_time_periods, process_date, payment_types_id, account_records_id, + account_is_debit, unique=True, ), Index("build_decision_book_payments_detail_ndx_01", account_records_id), diff --git a/ServicesApi/__init__.py b/ServicesApi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ServicesBank/Finder/.dockerignore b/ServicesBank/Finder/.dockerignore new file mode 100644 index 0000000..1449c90 --- /dev/null +++ b/ServicesBank/Finder/.dockerignore @@ -0,0 +1,93 @@ +# Git +.git +.gitignore +.gitattributes + + +# CI +.codeclimate.yml +.travis.yml +.taskcluster.yml + +# Docker +docker-compose.yml +service_app/Dockerfile +.docker +.dockerignore + +# Byte-compiled / optimized / DLL files +**/__pycache__/ +**/*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +service_app/env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Virtual environment +service_app/.env +.venv/ +venv/ + +# PyCharm +.idea + +# Python mode for VIM +.ropeproject +**/.ropeproject + +# Vim swap files +**/*.swp + +# VS Code +.vscode/ + +test_application/ + + diff --git a/ServicesBank/Finder/BuildFromIban/.dockerignore b/ServicesBank/Finder/BuildFromIban/.dockerignore new file mode 100644 index 0000000..1449c90 --- /dev/null +++ b/ServicesBank/Finder/BuildFromIban/.dockerignore @@ -0,0 +1,93 @@ +# Git +.git +.gitignore +.gitattributes + + +# CI +.codeclimate.yml +.travis.yml +.taskcluster.yml + +# Docker +docker-compose.yml +service_app/Dockerfile +.docker +.dockerignore + +# Byte-compiled / optimized / DLL files +**/__pycache__/ +**/*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +service_app/env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Virtual environment +service_app/.env +.venv/ +venv/ + +# PyCharm +.idea + +# Python mode for VIM +.ropeproject +**/.ropeproject + +# Vim swap files +**/*.swp + +# VS Code +.vscode/ + +test_application/ + + diff --git a/ServicesBank/Finder/BuildFromIban/Dockerfile b/ServicesBank/Finder/BuildFromIban/Dockerfile new file mode 100644 index 0000000..e84a5ac --- /dev/null +++ b/ServicesBank/Finder/BuildFromIban/Dockerfile @@ -0,0 +1,33 @@ +FROM python:3.12-slim + +WORKDIR / + +# Set Python path to include app directory +ENV PYTHONPATH=/ PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1 + +# 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 + +# Install cron for scheduling tasks +RUN apt-get update && apt-get install -y cron + +# Copy application code +COPY /ServicesBank/Finder/BuildFromIban / +COPY /ServicesApi/Schemas /Schemas +COPY /ServicesApi/Controllers /Controllers + +# Create log file to grab cron logs +RUN touch /var/log/cron.log + +# Make entrypoint script executable +RUN chmod +x /entrypoint.sh +RUN chmod +x /run_app.sh + +# Use entrypoint script to update run_app.sh with environment variables and start cron +ENTRYPOINT ["/entrypoint.sh"] diff --git a/ServicesBank/Finder/BuildFromIban/README.md b/ServicesBank/Finder/BuildFromIban/README.md new file mode 100644 index 0000000..5d39bae --- /dev/null +++ b/ServicesBank/Finder/BuildFromIban/README.md @@ -0,0 +1,3 @@ +# Docs of Finder + +Finds people, living spaces, companies from AccountRecords diff --git a/ServicesBank/Finder/BuildFromIban/entrypoint.sh b/ServicesBank/Finder/BuildFromIban/entrypoint.sh new file mode 100644 index 0000000..a1aee28 --- /dev/null +++ b/ServicesBank/Finder/BuildFromIban/entrypoint.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Create environment file that will be available to cron jobs +echo "POSTGRES_USER=\"$POSTGRES_USER\"" >> /env.sh +echo "POSTGRES_PASSWORD=\"$POSTGRES_PASSWORD\"" >> /env.sh +echo "POSTGRES_DB=\"$POSTGRES_DB\"" >> /env.sh +echo "POSTGRES_HOST=\"$POSTGRES_HOST\"" >> /env.sh +echo "POSTGRES_PORT=$POSTGRES_PORT" >> /env.sh +echo "POSTGRES_ENGINE=\"$POSTGRES_ENGINE\"" >> /env.sh +echo "POSTGRES_POOL_PRE_PING=\"$POSTGRES_POOL_PRE_PING\"" >> /env.sh +echo "POSTGRES_POOL_SIZE=$POSTGRES_POOL_SIZE" >> /env.sh +echo "POSTGRES_MAX_OVERFLOW=$POSTGRES_MAX_OVERFLOW" >> /env.sh +echo "POSTGRES_POOL_RECYCLE=$POSTGRES_POOL_RECYCLE" >> /env.sh +echo "POSTGRES_POOL_TIMEOUT=$POSTGRES_POOL_TIMEOUT" >> /env.sh +echo "POSTGRES_ECHO=\"$POSTGRES_ECHO\"" >> /env.sh + +# Add Python environment variables +echo "PYTHONPATH=/" >> /env.sh +echo "PYTHONUNBUFFERED=1" >> /env.sh +echo "PYTHONDONTWRITEBYTECODE=1" >> /env.sh + +# Make the environment file available to cron +echo "*/5 * * * * /run_app.sh >> /var/log/cron.log 2>&1" > /tmp/crontab_list +crontab /tmp/crontab_list + +# Start cron +cron + +# Tail the log file +tail -f /var/log/cron.log diff --git a/ServicesBank/Finder/BuildFromIban/run_app.sh b/ServicesBank/Finder/BuildFromIban/run_app.sh new file mode 100644 index 0000000..83dcf19 --- /dev/null +++ b/ServicesBank/Finder/BuildFromIban/run_app.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# Source the environment file directly +. /env.sh + +# Re-export all variables to ensure they're available to the Python script +export POSTGRES_USER +export POSTGRES_PASSWORD +export POSTGRES_DB +export POSTGRES_HOST +export POSTGRES_PORT +export POSTGRES_ENGINE +export POSTGRES_POOL_PRE_PING +export POSTGRES_POOL_SIZE +export POSTGRES_MAX_OVERFLOW +export POSTGRES_POOL_RECYCLE +export POSTGRES_POOL_TIMEOUT +export POSTGRES_ECHO + +# Python environment variables +export PYTHONPATH +export PYTHONUNBUFFERED +export PYTHONDONTWRITEBYTECODE + +# env >> /var/log/cron.log +/usr/local/bin/python /runner.py diff --git a/ServicesBank/Finder/BuildFromIban/runner.py b/ServicesBank/Finder/BuildFromIban/runner.py new file mode 100644 index 0000000..d0476bf --- /dev/null +++ b/ServicesBank/Finder/BuildFromIban/runner.py @@ -0,0 +1,26 @@ +import arrow + +from Schemas import AccountRecords, BuildIbans + + +def account_find_build_from_iban(session): + AccountRecords.set_session(session) + BuildIbans.set_session(session) + + account_records_ibans = AccountRecords.query.filter(AccountRecords.build_id == None, AccountRecords.approved_record == False).distinct(AccountRecords.iban).all() + for account_records_iban in account_records_ibans: + found_iban: BuildIbans = BuildIbans.query.filter(BuildIbans.iban == account_records_iban.iban).first() + if not found_iban: + create_build_ibans = BuildIbans.create(iban=account_records_iban.iban, start_date=str(arrow.now().shift(days=-1))) + create_build_ibans.save() + else: + update_dict = {"build_id": found_iban.build_id, "build_uu_id": str(found_iban.build_uu_id)} + session.query(AccountRecords).filter(AccountRecords.iban == account_records_iban.iban).update(update_dict, synchronize_session=False) + session.commit() + + +if __name__ == "__main__": + print("Account Records Service is running...") + with AccountRecords.new_session() as session: + account_find_build_from_iban(session=session) + print("Account Records Service is finished...") diff --git a/ServicesBank/Finder/BuildLivingSpace/.dockerignore b/ServicesBank/Finder/BuildLivingSpace/.dockerignore new file mode 100644 index 0000000..1449c90 --- /dev/null +++ b/ServicesBank/Finder/BuildLivingSpace/.dockerignore @@ -0,0 +1,93 @@ +# Git +.git +.gitignore +.gitattributes + + +# CI +.codeclimate.yml +.travis.yml +.taskcluster.yml + +# Docker +docker-compose.yml +service_app/Dockerfile +.docker +.dockerignore + +# Byte-compiled / optimized / DLL files +**/__pycache__/ +**/*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +service_app/env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Virtual environment +service_app/.env +.venv/ +venv/ + +# PyCharm +.idea + +# Python mode for VIM +.ropeproject +**/.ropeproject + +# Vim swap files +**/*.swp + +# VS Code +.vscode/ + +test_application/ + + diff --git a/ServicesBank/Finder/BuildLivingSpace/Dockerfile b/ServicesBank/Finder/BuildLivingSpace/Dockerfile new file mode 100644 index 0000000..ed921c8 --- /dev/null +++ b/ServicesBank/Finder/BuildLivingSpace/Dockerfile @@ -0,0 +1,33 @@ +FROM python:3.12-slim + +WORKDIR / + +# Set Python path to include app directory +ENV PYTHONPATH=/ PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1 + +# 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 + +# Install cron for scheduling tasks +RUN apt-get update && apt-get install -y cron + +# Copy application code +COPY /ServicesBank/Finder/BuildLivingSpace / +COPY /ServicesApi/Schemas /Schemas +COPY /ServicesApi/Controllers /Controllers + +# Create log file to grab cron logs +RUN touch /var/log/cron.log + +# Make entrypoint script executable +RUN chmod +x /entrypoint.sh +RUN chmod +x /run_app.sh + +# Use entrypoint script to update run_app.sh with environment variables and start cron +ENTRYPOINT ["/entrypoint.sh"] diff --git a/ServicesBank/Finder/BuildLivingSpace/README.md b/ServicesBank/Finder/BuildLivingSpace/README.md new file mode 100644 index 0000000..5d39bae --- /dev/null +++ b/ServicesBank/Finder/BuildLivingSpace/README.md @@ -0,0 +1,3 @@ +# Docs of Finder + +Finds people, living spaces, companies from AccountRecords diff --git a/ServicesBank/Finder/BuildLivingSpace/configs.py b/ServicesBank/Finder/BuildLivingSpace/configs.py new file mode 100644 index 0000000..6f8eed1 --- /dev/null +++ b/ServicesBank/Finder/BuildLivingSpace/configs.py @@ -0,0 +1,8 @@ +class AccountConfig: + BEFORE_DAY = 30 + CATEGORIES = { + "DAIRE": ["daire", "dagire", "daare", "nolu daire", "no", "nolu dairenin"], + "APARTMAN": ["apartman", "aparman", "aprmn"], + "VILLA": ["villa", "vlla"], + "BINA": ["bina", "binna"], + } diff --git a/ServicesBank/Finder/BuildLivingSpace/entrypoint.sh b/ServicesBank/Finder/BuildLivingSpace/entrypoint.sh new file mode 100644 index 0000000..73f4e4d --- /dev/null +++ b/ServicesBank/Finder/BuildLivingSpace/entrypoint.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Create environment file that will be available to cron jobs +echo "POSTGRES_USER=\"$POSTGRES_USER\"" >> /env.sh +echo "POSTGRES_PASSWORD=\"$POSTGRES_PASSWORD\"" >> /env.sh +echo "POSTGRES_DB=\"$POSTGRES_DB\"" >> /env.sh +echo "POSTGRES_HOST=\"$POSTGRES_HOST\"" >> /env.sh +echo "POSTGRES_PORT=$POSTGRES_PORT" >> /env.sh +echo "POSTGRES_ENGINE=\"$POSTGRES_ENGINE\"" >> /env.sh +echo "POSTGRES_POOL_PRE_PING=\"$POSTGRES_POOL_PRE_PING\"" >> /env.sh +echo "POSTGRES_POOL_SIZE=$POSTGRES_POOL_SIZE" >> /env.sh +echo "POSTGRES_MAX_OVERFLOW=$POSTGRES_MAX_OVERFLOW" >> /env.sh +echo "POSTGRES_POOL_RECYCLE=$POSTGRES_POOL_RECYCLE" >> /env.sh +echo "POSTGRES_POOL_TIMEOUT=$POSTGRES_POOL_TIMEOUT" >> /env.sh +echo "POSTGRES_ECHO=\"$POSTGRES_ECHO\"" >> /env.sh + +# Add Python environment variables +echo "PYTHONPATH=/" >> /env.sh +echo "PYTHONUNBUFFERED=1" >> /env.sh +echo "PYTHONDONTWRITEBYTECODE=1" >> /env.sh + +# Make the environment file available to cron +echo "*/15 * * * * /run_app.sh >> /var/log/cron.log 2>&1" > /tmp/crontab_list +crontab /tmp/crontab_list + +# Start cron +cron + +# Tail the log file +tail -f /var/log/cron.log diff --git a/ServicesBank/Finder/BuildLivingSpace/parser.py b/ServicesBank/Finder/BuildLivingSpace/parser.py new file mode 100644 index 0000000..a4df439 --- /dev/null +++ b/ServicesBank/Finder/BuildLivingSpace/parser.py @@ -0,0 +1,319 @@ +import re +import textdistance + +from unidecode import unidecode +from gc import garbage +from Schemas import AccountRecords, People, Build, Companies, BuildIbanDescription +from regex_func import category_finder +from validations import Similarity + + +def parse_comment_to_split_with_star(account_record): + # Handle both ORM objects and dictionaries + try: + # Check if account_record is a dictionary or an ORM object + if isinstance(account_record, dict): + process_comment = str(account_record.get('process_comment', '')) + else: + process_comment = str(account_record.process_comment) + + if "*" in process_comment: + process_comment_cleaned = process_comment.replace("**", "*") + process_comments = process_comment_cleaned.split("*") + return len(process_comments), *process_comments + return 1, process_comment + except Exception as e: + # print(f"Error in parse_comment_to_split_with_star: {e}") + # Return a safe default if there's an error + return 1, "" + + +def remove_garbage_words(comment: str, garbage_word: str): + cleaned_comment = remove_spaces_from_string(comment.replace("*", " ")) + if garbage_word: + garbage_word = remove_spaces_from_string(garbage_word.replace("*", " ")) + for letter in garbage_word.split(" "): + cleaned_comment = unidecode(remove_spaces_from_string(cleaned_comment)) + cleaned_comment = cleaned_comment.replace(remove_spaces_from_string(letter), "") + return str(remove_spaces_from_string(cleaned_comment)).upper() + + +def remove_spaces_from_string(remove_string: str): + letter_list = [] + for letter in remove_string.split(" "): + if letter_ := "".join(i for i in letter if not i == " "): + letter_list.append(letter_) + return " ".join(letter_list).upper() + + +def get_garbage_words(comment: str, search_word: str): + garbage_words = unidecode(remove_spaces_from_string(comment)) + search_word = unidecode(remove_spaces_from_string(search_word)) + for word in search_word.split(" "): + garbage_words = garbage_words.replace(remove_spaces_from_string(unidecode(word)), "") + if cleaned_from_spaces := remove_spaces_from_string(garbage_words): + return str(unidecode(cleaned_from_spaces)).upper() + return None + + +def parse_comment_with_name_iban_description(account_record): + # Extract necessary data from account_record to avoid session detachment + if isinstance(account_record, dict): + iban = account_record.get('iban', '') + process_comment = account_record.get('process_comment', '') + else: + try: + iban = account_record.iban + process_comment = account_record.process_comment + except Exception as e: + # print(f"Error accessing account_record attributes: {e}") + return Similarity(similarity=0.0, garbage="", cleaned="") + + # Process the comment locally without depending on the account_record object + if "*" in process_comment: + process_comment_cleaned = str(process_comment.replace("**", "*")) + process_comments = process_comment_cleaned.split("*") + comments_list, comments_length = process_comments, len(process_comments) + else: + comments_list, comments_length = [process_comment], 1 + + # print("comments_list", comments_list, "comments_length", comments_length) + + with BuildIbanDescription.new_session() as session: + BuildIbanDescription.set_session(session) + Companies.set_session(session) + + iban_results = BuildIbanDescription.query.filter(BuildIbanDescription.iban == iban).all() + best_similarity = Similarity(similarity=0.0, garbage="", cleaned="") + + for comment in comments_list: + for iban_result in iban_results: + search_word = unidecode(iban_result.search_word) + garbage_words = get_garbage_words(comment, search_word) + cleaned_comment = remove_garbage_words(comment, garbage_words) + similarity_ratio = textdistance.jaro_winkler(cleaned_comment, search_word) + company = Companies.query.filter_by(id=iban_result.company_id).first() + + if float(similarity_ratio) > float(best_similarity.similarity): + best_similarity = Similarity(similarity=similarity_ratio, garbage=garbage_words, cleaned=cleaned_comment) + best_similarity.set_company(company) + best_similarity.set_found_from("Customer Public Name Description") + return best_similarity + + +def parse_comment_for_build_parts(comment: str, max_build_part: int = 200, parse: str = "DAIRE"): + results, results_list = category_finder(comment), [] + # print("results[parse]", results[parse]) + for result in results[parse] or []: + if digits := "".join([letter for letter in str(result) if letter.isdigit()]): + # print("digits", digits) + if int(digits) <= int(max_build_part): + results_list.append(int(digits)) + return results_list or None + + +def parse_comment_with_name(account_record, living_space_dict: dict = None): + # Extract necessary data from account_record to avoid session detachment + if isinstance(account_record, dict): + iban = account_record.get('iban', '') + process_comment = account_record.get('process_comment', '') + try: + currency_value = int(account_record.get('currency_value', 0)) + except (ValueError, TypeError): + currency_value = 0 + else: + try: + iban = account_record.iban + process_comment = account_record.process_comment + currency_value = int(account_record.currency_value) + except Exception as e: + # print(f"Error accessing account_record attributes: {e}") + return Similarity(similarity=0.0, garbage="", cleaned="") + + # Process the comment locally without depending on the account_record object + if "*" in process_comment: + process_comment_cleaned = str(process_comment.replace("**", "*")) + process_comments = process_comment_cleaned.split("*") + comments_list, comments_length = process_comments, len(process_comments) + else: + comments_list, comments_length = [process_comment], 1 + + # print("comments_list", comments_list, "comments_length", comments_length) + best_similarity = Similarity(similarity=0.0, garbage="", cleaned="") + + if currency_value > 0: # Build receive money from living space people + living_space_matches = dict(living_space_dict=living_space_dict, iban=iban, whole_comment=process_comment) + if comments_length == 1: + best_similarity = parse_comment_for_living_space(iban=iban, comment=comments_list[0], living_space_dict=living_space_dict) + best_similarity.set_send_person_id(best_similarity.customer_id) + living_space_matches["best_similarity"] = best_similarity + # if 0.5 < float(best_similarity['similarity']) < 0.8 + best_similarity = check_build_living_space_matches_with_build_parts(**living_space_matches) + return best_similarity + for comment in comments_list: + similarity_result = parse_comment_for_living_space(iban=iban, comment=comment, living_space_dict=living_space_dict) + if float(similarity_result.similarity) > float(best_similarity.similarity): + best_similarity = similarity_result + living_space_matches["best_similarity"] = best_similarity + # if 0.5 < float(best_similarity['similarity']) < 0.8: + best_similarity = check_build_living_space_matches_with_build_parts(**living_space_matches) + # print("last best_similarity", best_similarity) + return best_similarity + else: # Build pays money for service taken from company or individual + if not comments_length > 1: + best_similarity = parse_comment_for_company_or_individual(comment=comments_list[0]) + best_similarity.set_send_person_id(best_similarity.customer_id) + return best_similarity + for comment in comments_list: + similarity_result = parse_comment_for_company_or_individual(comment=comment) + if float(similarity_result.similarity) > float(best_similarity.similarity): + best_similarity = similarity_result + return best_similarity + + +def check_build_living_space_matches_with_build_parts(living_space_dict: dict, best_similarity: Similarity, iban: str, whole_comment: str): + if 0.6 < float(best_similarity.similarity) < 0.8: + build_parts_data = living_space_dict[iban]["build_parts"] + # Check if we have living space ID in the similarity object + living_space_id = getattr(best_similarity, 'living_space_id', None) + if living_space_id: + # Find the corresponding living space data + living_space_data = None + for ls in living_space_dict[iban]["living_space"]: + if ls.get('id') == living_space_id: + living_space_data = ls + break + + if living_space_data: + build_parts_id = living_space_data.get('build_parts_id') + parser_dict = dict(comment=str(whole_comment), max_build_part=len(build_parts_data)) + # print("build parts similarity", best_similarity, "parser_dict", parser_dict) + results_list = parse_comment_for_build_parts(**parser_dict) + # print("results_list", results_list) + if not results_list: + return best_similarity + + for build_part_data in build_parts_data: + # Get part_no directly if it exists in the dictionary + part_no = build_part_data.get('part_no') + + # If part_no doesn't exist, try to extract it from other attributes + if part_no is None: + # Try to get it from a name attribute if it exists + name = build_part_data.get('name', '') + if name and isinstance(name, str) and 'part' in name.lower(): + try: + part_no = int(name.lower().replace('part', '').strip()) + except (ValueError, TypeError): + pass + + # If we have a part_no, proceed with the comparison + if part_no is not None: + # print("part_no", part_no, " | ", results_list) + # print("build_part", build_part_data.get('id'), build_parts_id) + # print("cond", build_part_data.get('id') == build_parts_id) + # print("cond2", part_no in results_list) + + if build_part_data.get('id') == build_parts_id and part_no in results_list: + similarity = float(best_similarity.similarity) + best_similarity.set_similarity((1 - similarity) / 2 + similarity) + # print("similarity", best_similarity.similarity) + break + return best_similarity + + +def parse_comment_for_company_or_individual(comment: str): + # Extract all necessary data from Companies within the session + companies_data = [] + with Companies.new_session() as session: + Companies.set_session(session) + companies_list = Companies.query.filter(Companies.commercial_type != "Commercial").all() + + # Extract all needed data from companies while session is active + for company in companies_list: + company_data = { + 'id': company.id, + 'public_name': unidecode(company.public_name) + } + # Add any other needed attributes + if hasattr(company, 'commercial_type'): + company_data['commercial_type'] = company.commercial_type + companies_data.append(company_data) + + # Process the data outside the session + comment = unidecode(comment) + best_similarity = Similarity(similarity=0.0, garbage="", cleaned="") + + for company_data in companies_data: + search_word = company_data['public_name'] + garbage_words = get_garbage_words(comment, search_word) + cleaned_comment = remove_garbage_words(comment, garbage_words) + similarity_ratio = textdistance.jaro_winkler(cleaned_comment, search_word) + + if similarity_ratio > float(best_similarity.similarity): + best_similarity = Similarity(similarity=similarity_ratio, garbage=garbage_words, cleaned=cleaned_comment) + # Store company ID instead of the ORM object + best_similarity.set_company_id(company_data['id']) + best_similarity.set_found_from("Customer Public Name") + # print('cleaned_comment', cleaned_comment, '\n', 'search_word', search_word, '\n', 'best_similarity', best_similarity, '\n', + # 'company name', company_data['public_name'], '\n', 'similarity_ratio', similarity_ratio, '\n', 'garbage_words', garbage_words) + + return best_similarity + + +def parse_comment_for_living_space(iban: str, comment: str, living_space_dict: dict = None) -> Similarity: + comment = unidecode(comment) + best_similarity = Similarity(similarity=0.0, garbage="", cleaned="") + + if not iban in living_space_dict: + return best_similarity + + for person_data in living_space_dict[iban]["people"]: + # Extract name components from dictionary + first_name = unidecode(person_data.get('name', '')).upper() + last_name = unidecode(person_data.get('surname', '')).upper() + search_word_list = [ + remove_spaces_from_string("".join([f"{first_name} {last_name}"])), + remove_spaces_from_string("".join([f"{last_name} {first_name}"])), + ] + # We don't have middle_name in our dictionary, so skip that part + + cleaned_comment = unidecode(comment).upper() + for search_word in search_word_list: + if garbage_words := get_garbage_words(comment, unidecode(search_word)): + garbage_words = unidecode(garbage_words).upper() + cleaned_comment = unidecode(remove_garbage_words(comment, garbage_words)).upper() + similarity_ratio = textdistance.jaro_winkler(cleaned_comment, str(search_word).upper()) + if len(cleaned_comment) < len(f"{first_name}{last_name}"): + continue + if cleaned_comment and 0.9 < similarity_ratio <= 1: + pass + # print("cleaned comment dict", dict( + # garbage=garbage_words, cleaned=cleaned_comment, similarity=similarity_ratio, + # search_word=search_word, comment=comment, last_similarity=float(best_similarity.similarity)) + # ) + + if similarity_ratio > float(best_similarity.similarity): + # Use person_id from the dictionary data + person_id = person_data['id'] + for living_space_data in living_space_dict[iban]["living_space"]: + if living_space_data.get('person_id') == person_id: + # Create a dictionary with living space data + living_space_info = { + 'id': living_space_data.get('id'), + 'build_parts_id': living_space_data.get('build_parts_id'), + 'name': living_space_data.get('name') + } + best_similarity.set_living_space_id(living_space_data.get('id')) + best_similarity.set_found_from("Person Name") + best_similarity.set_similarity(similarity_ratio) + best_similarity.set_garbage(garbage_words) + best_similarity.set_cleaned(cleaned_comment) + best_similarity.set_customer_id(person_data['id']) + # Find matching build part + build_parts_id = living_space_data.get('build_parts_id') + for build_part_data in living_space_dict[iban]["build_parts"]: + if build_part_data.get('id') == build_parts_id: + best_similarity.set_build_part_id(build_part_data.get('id')) + break + return best_similarity diff --git a/ServicesBank/Finder/BuildLivingSpace/regex_func.py b/ServicesBank/Finder/BuildLivingSpace/regex_func.py new file mode 100644 index 0000000..059938b --- /dev/null +++ b/ServicesBank/Finder/BuildLivingSpace/regex_func.py @@ -0,0 +1,23 @@ +import re + +from difflib import get_close_matches +from configs import AccountConfig + + +def word_straighten(word, ref_list, threshold=0.8): + matches = get_close_matches(word, ref_list, n=1, cutoff=threshold) + return matches[0] if matches else word + + +def category_finder(text, output_template="{kategori} {numara}"): + categories = AccountConfig.CATEGORIES + result = {category: [] for category in categories} + for category, patterns in categories.items(): + words = re.split(r"\W+", text) + straighten_words = [word_straighten(word, patterns) for word in words] + straighten_text = " ".join(straighten_words) + pattern = r"(?:\b|\s|^)(?:" + "|".join(map(re.escape, patterns)) + r")(?:\s*|:|\-|\#)*(\d+)(?:\b|$)" + if founds_list := re.findall(pattern, straighten_text, re.IGNORECASE): + list_of_output = [output_template.format(kategori=category, numara=num) for num in founds_list] + result[category].extend([i for i in list_of_output if str(i).replace(" ", "")]) + return result diff --git a/ServicesBank/Finder/BuildLivingSpace/run_app.sh b/ServicesBank/Finder/BuildLivingSpace/run_app.sh new file mode 100644 index 0000000..f64912d --- /dev/null +++ b/ServicesBank/Finder/BuildLivingSpace/run_app.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# Source the environment file directly +. /env.sh + +# Re-export all variables to ensure they're available to the Python script +export POSTGRES_USER +export POSTGRES_PASSWORD +export POSTGRES_DB +export POSTGRES_HOST +export POSTGRES_PORT +export POSTGRES_ENGINE +export POSTGRES_POOL_PRE_PING +export POSTGRES_POOL_SIZE +export POSTGRES_MAX_OVERFLOW +export POSTGRES_POOL_RECYCLE +export POSTGRES_POOL_TIMEOUT +export POSTGRES_ECHO + +# Python environment variables +export PYTHONPATH +export PYTHONUNBUFFERED +export PYTHONDONTWRITEBYTECODE + +# Run the Python script +/usr/local/bin/python /runner.py diff --git a/ServicesBank/Finder/BuildLivingSpace/runner.py b/ServicesBank/Finder/BuildLivingSpace/runner.py new file mode 100644 index 0000000..6b9c10e --- /dev/null +++ b/ServicesBank/Finder/BuildLivingSpace/runner.py @@ -0,0 +1,187 @@ +from Schemas import AccountRecords, BuildIbans, BuildDecisionBook, Build, BuildLivingSpace, People, OccupantTypes, BuildParts, BuildDecisionBookPayments, ApiEnumDropdown +from Controllers.Postgres.engine import get_session_factory +from parser import parse_comment_with_name, parse_comment_with_name_iban_description +from validations import Similarity +import re +import time +from datetime import timedelta + + +def account_save_search_result(account_record_main_session: AccountRecords, similarity_result: Similarity): + + with AccountRecords.new_session() as session: + AccountRecords.set_session(session) + BuildParts.set_session(session) + Build.set_session(session) + BuildLivingSpace.set_session(session) + People.set_session(session) + + account_record = AccountRecords.query.filter_by(id=account_record_main_session.id).first() + if not account_record: + # print(f"Could not find account record with ID {account_record_main_session.id}") + return + + company_id = getattr(similarity_result, 'company_id', None) + living_space_id = getattr(similarity_result, 'living_space_id', None) + build_part_id = getattr(similarity_result, 'build_part_id', None) + customer_id = getattr(similarity_result, 'customer_id', None) + + part, build, found_customer = None, None, None + + if living_space_id: + found_customer = BuildLivingSpace.query.get(living_space_id) + if build_part_id: + part = BuildParts.query.get(build_part_id) + elif found_customer and hasattr(found_customer, 'build_parts_id'): + part = BuildParts.query.filter_by(id=found_customer.build_parts_id, human_livable=True).first() + + if part: + build = Build.query.filter_by(id=part.build_id).first() + + account_record.similarity = similarity_result.similarity + account_record.found_from = similarity_result.found_from + account_record.company_id = company_id + if company_id: + company = People.query.get(company_id) + account_record.company_uu_id = getattr(company, "uu_id", None) if company else None + + account_record.build_parts_id = getattr(part, "id", None) + account_record.build_parts_uu_id = getattr(part, "uu_id", None) if part else None + + if not account_record.build_id and build: + account_record.build_id = getattr(build, "id", None) + account_record.build_uu_id = getattr(build, "uu_id", None) + + account_record.living_space_id = living_space_id + if found_customer: + account_record.living_space_uu_id = getattr(found_customer, "uu_id", None) + if customer_id: + account_record.send_person_id = customer_id + customer = People.query.get(customer_id) + if customer: + account_record.send_person_uu_id = getattr(customer, "uu_id", None) + account_record.save() + + +if __name__ == "__main__": + # Start timer + start_time = time.time() + print("Build Living Space Service is running...") + + new_session = get_session_factory() + flat_id_list = [] + build_living_space_dict = {} + found_list = [] + account_records_ibans = [] + + with OccupantTypes.new_session() as occupant_types_session: + OccupantTypes.set_session(occupant_types_session) + flat_resident = OccupantTypes.query.filter_by(occupant_category_type="FL", occupant_code="FL-RES").first() + flat_owner = OccupantTypes.query.filter_by(occupant_category_type="FL", occupant_code="FL-OWN").first() + flat_tenant = OccupantTypes.query.filter_by(occupant_category_type="FL", occupant_code="FL-TEN").first() + flat_represent = OccupantTypes.query.filter_by(occupant_category_type="FL", occupant_code="FL-REP").first() + flat_id_list = [flat_resident.id, flat_owner.id, flat_tenant.id, flat_represent.id] + + AccountRecords.set_session(new_session) + BuildLivingSpace.set_session(new_session) + BuildParts.set_session(new_session) + People.set_session(new_session) + account_records_ibans = AccountRecords.query.filter(AccountRecords.build_decision_book_id != None).distinct(AccountRecords.iban).all() + + for account_records_iban in account_records_ibans: + if account_records_iban.iban not in build_living_space_dict: + build_parts = BuildParts.query.filter_by(build_id=account_records_iban.build_id, human_livable=True).all() + build_parts_data = [] + for bp in build_parts: + bp_dict = {'id': bp.id, 'build_id': bp.build_id, 'human_livable': bp.human_livable} + if hasattr(bp, 'part_no'): + bp_dict['part_no'] = bp.part_no + build_parts_data.append(bp_dict) + + living_spaces = BuildLivingSpace.query.filter( + BuildLivingSpace.build_parts_id.in_([bp.id for bp in build_parts]), BuildLivingSpace.occupant_type_id.in_(flat_id_list), + ).all() + living_spaces_data = [] + for ls in living_spaces: + ls_dict = {'id': ls.id, 'build_parts_id': ls.build_parts_id, 'occupant_type_id': ls.occupant_type_id, 'person_id': ls.person_id} + if hasattr(ls, 'name'): + ls_dict['name'] = ls.name + living_spaces_data.append(ls_dict) + + living_spaces_people = [ls.person_id for ls in living_spaces if ls.person_id] + people_list = People.query.filter(People.id.in_(living_spaces_people)).all() + people_data = [] + for p in people_list: + p_dict = {'id': p.id, 'name': p.firstname, 'surname': p.surname, 'middle_name': p.middle_name} + p_dict['full_name'] = f"{p.firstname} {p.surname}".strip() + people_data.append(p_dict) + build_living_space_dict[str(account_records_iban.iban)] = {"people": people_data, "living_space": living_spaces_data, "build_parts": build_parts_data} + + with AccountRecords.new_session() as query_session: + AccountRecords.set_session(query_session) + account_record_ids = [record.id for record in AccountRecords.query.filter(AccountRecords.build_decision_book_id != None).order_by(AccountRecords.bank_date.desc()).all()] + + for account_id in account_record_ids: + with AccountRecords.new_session() as record_session: + AccountRecords.set_session(record_session) + account_record = AccountRecords.query.filter_by(id=account_id).first() + if not account_record: + continue + account_iban = account_record.iban + account_process_comment = account_record.process_comment + account_currency_value = account_record.currency_value + account_similarity_value = float(account_record.similarity or 0.0) + account_build_id = account_record.build_id + + account_data = {"id": account_id, "iban": account_iban, "process_comment": account_process_comment, "currency_value": account_currency_value, + "similarity": account_similarity_value, "build_id": account_build_id} + + try: + similarity_result = parse_comment_with_name(account_record=account_data, living_space_dict=build_living_space_dict) + fs = float(similarity_result.similarity) + + if fs >= 0.8 and fs >= account_similarity_value: + found_list.append(similarity_result) + with AccountRecords.new_session() as save_session: + AccountRecords.set_session(save_session) + fresh_account = AccountRecords.query.filter_by(id=account_id).first() + if fresh_account: + account_save_search_result(account_record_main_session=fresh_account, similarity_result=similarity_result) + print("POSITIVE SIMILARITY RESULT:", { + 'similarity': similarity_result.similarity, 'found_from': similarity_result.found_from, 'garbage': similarity_result.garbage, + 'cleaned': similarity_result.cleaned, 'company_id': getattr(similarity_result, 'company_id', None), + 'living_space_id': getattr(similarity_result, 'living_space_id', None), 'build_part_id': getattr(similarity_result, 'build_part_id', None), + 'customer_id': getattr(similarity_result, 'customer_id', None) + }) + else: + similarity_result = parse_comment_with_name_iban_description(account_record=account_data) + fs = float(similarity_result.similarity) + + if fs >= 0.8 and fs > account_similarity_value: + found_list.append(similarity_result) + with AccountRecords.new_session() as save_session: + AccountRecords.set_session(save_session) + fresh_account = AccountRecords.query.filter_by(id=account_id).first() + if fresh_account: + account_save_search_result(account_record_main_session=fresh_account, similarity_result=similarity_result) + print("NEGATIVE SIMILARITY RESULT:", { + 'similarity': similarity_result.similarity, 'found_from': similarity_result.found_from, + 'garbage': similarity_result.garbage, 'cleaned': similarity_result.cleaned, + 'company_id': getattr(similarity_result, 'company_id', None), 'living_space_id': getattr(similarity_result, 'living_space_id', None), + 'build_part_id': getattr(similarity_result, 'build_part_id', None), 'customer_id': getattr(similarity_result, 'customer_id', None) + }) + except Exception as e: + # print(f"Error processing account {account_id}: {e}") + continue + + # Calculate elapsed time + end_time = time.time() + elapsed_time = end_time - start_time + elapsed_formatted = str(timedelta(seconds=int(elapsed_time))) + + print("Account Records Search : ", len(found_list), "/", len(account_record_ids)) + print(f"Total runtime: {elapsed_formatted} (HH:MM:SS)") + print(f"Total seconds: {elapsed_time:.2f}") + + new_session.close() + print("Build Living Space Service is finished...") diff --git a/ServicesBank/Finder/BuildLivingSpace/validations.py b/ServicesBank/Finder/BuildLivingSpace/validations.py new file mode 100644 index 0000000..9762537 --- /dev/null +++ b/ServicesBank/Finder/BuildLivingSpace/validations.py @@ -0,0 +1,49 @@ +from Schemas import BuildLivingSpace, People + +class Similarity: + + def __init__(self, similarity: float, garbage: str, cleaned: str): + self.similarity = similarity + self.garbage = garbage + self.cleaned = cleaned + self.living_space = None + self.living_space_id = None + self.build_part_id = None + self.company = None + self.company_id = None + self.found_from = None + self.send_person_id = None + self.customer_id = None + + def set_customer_id(self, customer_id: int): + self.customer_id = customer_id + + def set_living_space(self, living_space: BuildLivingSpace): + self.living_space = living_space + + def set_company(self, company: People): + self.company = company + + def set_found_from(self, found_from: str): + self.found_from = found_from + + def set_send_person_id(self, send_person_id: int): + self.send_person_id = send_person_id + + def set_similarity(self, similarity: float): + self.similarity = similarity + + def set_garbage(self, garbage: str): + self.garbage = garbage + + def set_cleaned(self, cleaned: str): + self.cleaned = cleaned + + def set_living_space_id(self, living_space_id: int): + self.living_space_id = living_space_id + + def set_build_part_id(self, build_part_id: int): + self.build_part_id = build_part_id + + def set_company_id(self, company_id: int): + self.company_id = company_id diff --git a/ServicesBank/Finder/DecisionBook/.dockerignore b/ServicesBank/Finder/DecisionBook/.dockerignore new file mode 100644 index 0000000..1449c90 --- /dev/null +++ b/ServicesBank/Finder/DecisionBook/.dockerignore @@ -0,0 +1,93 @@ +# Git +.git +.gitignore +.gitattributes + + +# CI +.codeclimate.yml +.travis.yml +.taskcluster.yml + +# Docker +docker-compose.yml +service_app/Dockerfile +.docker +.dockerignore + +# Byte-compiled / optimized / DLL files +**/__pycache__/ +**/*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +service_app/env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Virtual environment +service_app/.env +.venv/ +venv/ + +# PyCharm +.idea + +# Python mode for VIM +.ropeproject +**/.ropeproject + +# Vim swap files +**/*.swp + +# VS Code +.vscode/ + +test_application/ + + diff --git a/ServicesBank/Finder/DecisionBook/Dockerfile b/ServicesBank/Finder/DecisionBook/Dockerfile new file mode 100644 index 0000000..ca6826a --- /dev/null +++ b/ServicesBank/Finder/DecisionBook/Dockerfile @@ -0,0 +1,33 @@ +FROM python:3.12-slim + +WORKDIR / + +# Set Python path to include app directory +ENV PYTHONPATH=/ PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1 + +# 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 + +# Install cron for scheduling tasks +RUN apt-get update && apt-get install -y cron + +# Copy application code +COPY /ServicesBank/Finder/DecisionBook / +COPY /ServicesApi/Schemas /Schemas +COPY /ServicesApi/Controllers /Controllers + +# Create log file to grab cron logs +RUN touch /var/log/cron.log + +# Make entrypoint script executable +RUN chmod +x /entrypoint.sh +RUN chmod +x /run_app.sh + +# Use entrypoint script to update run_app.sh with environment variables and start cron +ENTRYPOINT ["/entrypoint.sh"] diff --git a/ServicesBank/Finder/DecisionBook/README.md b/ServicesBank/Finder/DecisionBook/README.md new file mode 100644 index 0000000..5d39bae --- /dev/null +++ b/ServicesBank/Finder/DecisionBook/README.md @@ -0,0 +1,3 @@ +# Docs of Finder + +Finds people, living spaces, companies from AccountRecords diff --git a/ServicesBank/Finder/DecisionBook/entrypoint.sh b/ServicesBank/Finder/DecisionBook/entrypoint.sh new file mode 100644 index 0000000..73f4e4d --- /dev/null +++ b/ServicesBank/Finder/DecisionBook/entrypoint.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Create environment file that will be available to cron jobs +echo "POSTGRES_USER=\"$POSTGRES_USER\"" >> /env.sh +echo "POSTGRES_PASSWORD=\"$POSTGRES_PASSWORD\"" >> /env.sh +echo "POSTGRES_DB=\"$POSTGRES_DB\"" >> /env.sh +echo "POSTGRES_HOST=\"$POSTGRES_HOST\"" >> /env.sh +echo "POSTGRES_PORT=$POSTGRES_PORT" >> /env.sh +echo "POSTGRES_ENGINE=\"$POSTGRES_ENGINE\"" >> /env.sh +echo "POSTGRES_POOL_PRE_PING=\"$POSTGRES_POOL_PRE_PING\"" >> /env.sh +echo "POSTGRES_POOL_SIZE=$POSTGRES_POOL_SIZE" >> /env.sh +echo "POSTGRES_MAX_OVERFLOW=$POSTGRES_MAX_OVERFLOW" >> /env.sh +echo "POSTGRES_POOL_RECYCLE=$POSTGRES_POOL_RECYCLE" >> /env.sh +echo "POSTGRES_POOL_TIMEOUT=$POSTGRES_POOL_TIMEOUT" >> /env.sh +echo "POSTGRES_ECHO=\"$POSTGRES_ECHO\"" >> /env.sh + +# Add Python environment variables +echo "PYTHONPATH=/" >> /env.sh +echo "PYTHONUNBUFFERED=1" >> /env.sh +echo "PYTHONDONTWRITEBYTECODE=1" >> /env.sh + +# Make the environment file available to cron +echo "*/15 * * * * /run_app.sh >> /var/log/cron.log 2>&1" > /tmp/crontab_list +crontab /tmp/crontab_list + +# Start cron +cron + +# Tail the log file +tail -f /var/log/cron.log diff --git a/ServicesBank/Finder/DecisionBook/run_app.sh b/ServicesBank/Finder/DecisionBook/run_app.sh new file mode 100644 index 0000000..83dcf19 --- /dev/null +++ b/ServicesBank/Finder/DecisionBook/run_app.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# Source the environment file directly +. /env.sh + +# Re-export all variables to ensure they're available to the Python script +export POSTGRES_USER +export POSTGRES_PASSWORD +export POSTGRES_DB +export POSTGRES_HOST +export POSTGRES_PORT +export POSTGRES_ENGINE +export POSTGRES_POOL_PRE_PING +export POSTGRES_POOL_SIZE +export POSTGRES_MAX_OVERFLOW +export POSTGRES_POOL_RECYCLE +export POSTGRES_POOL_TIMEOUT +export POSTGRES_ECHO + +# Python environment variables +export PYTHONPATH +export PYTHONUNBUFFERED +export PYTHONDONTWRITEBYTECODE + +# env >> /var/log/cron.log +/usr/local/bin/python /runner.py diff --git a/ServicesBank/Finder/DecisionBook/runner.py b/ServicesBank/Finder/DecisionBook/runner.py new file mode 100644 index 0000000..642ab37 --- /dev/null +++ b/ServicesBank/Finder/DecisionBook/runner.py @@ -0,0 +1,29 @@ +from sqlalchemy import cast, Date +from Schemas import AccountRecords, BuildIbans, BuildDecisionBook + + +def account_records_find_decision_book(session): + AccountRecords.set_session(session) + BuildIbans.set_session(session) + BuildDecisionBook.set_session(session) + + created_ibans, iban_build_dict = [], {} + filter_account_records = AccountRecords.build_id != None, AccountRecords.build_decision_book_id == None + account_records_list: list[AccountRecords] = AccountRecords.query.filter(*filter_account_records).order_by(AccountRecords.bank_date.desc()).all() + for account_record in account_records_list: + if found_iban := BuildIbans.query.filter(BuildIbans.iban == account_record.iban).first(): + if found_decision_book := BuildDecisionBook.query.filter( + BuildDecisionBook.build_id == found_iban.build_id, + cast(BuildDecisionBook.expiry_starts, Date) <= cast(account_record.bank_date, Date), + cast(BuildDecisionBook.expiry_ends, Date) >= cast(account_record.bank_date, Date), + ).first(): + account_record.build_decision_book_id = found_decision_book.id + account_record.build_decision_book_uu_id = str(found_decision_book.uu_id) + account_record.save() + + +if __name__ == "__main__": + print("DecisionBook Service is running...") + with AccountRecords.new_session() as session: + account_records_find_decision_book(session) + print("DecisionBook Service is finished...") diff --git a/ServicesBank/Finder/Dockerfile b/ServicesBank/Finder/Dockerfile new file mode 100644 index 0000000..9e3b1b7 --- /dev/null +++ b/ServicesBank/Finder/Dockerfile @@ -0,0 +1,32 @@ +FROM python:3.12-slim + +WORKDIR / + +# Set Python path to include app directory +ENV PYTHONPATH=/ PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1 + +# 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 + +# Install cron for scheduling tasks +RUN apt-get update && apt-get install -y cron + +# Copy application code +COPY /ServicesBank/Finder / +COPY /ServicesApi/Schemas /Schemas +COPY /ServicesApi/Controllers /Controllers + +# Create log file to grab cron logs +RUN touch /var/log/cron.log + +# Make entrypoint script executable +RUN chmod +x /entrypoint.sh + +# Use entrypoint script to update run_app.sh with environment variables and start cron +ENTRYPOINT ["/entrypoint.sh"] diff --git a/ServicesBank/Finder/Payment/.dockerignore b/ServicesBank/Finder/Payment/.dockerignore new file mode 100644 index 0000000..1449c90 --- /dev/null +++ b/ServicesBank/Finder/Payment/.dockerignore @@ -0,0 +1,93 @@ +# Git +.git +.gitignore +.gitattributes + + +# CI +.codeclimate.yml +.travis.yml +.taskcluster.yml + +# Docker +docker-compose.yml +service_app/Dockerfile +.docker +.dockerignore + +# Byte-compiled / optimized / DLL files +**/__pycache__/ +**/*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +service_app/env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Virtual environment +service_app/.env +.venv/ +venv/ + +# PyCharm +.idea + +# Python mode for VIM +.ropeproject +**/.ropeproject + +# Vim swap files +**/*.swp + +# VS Code +.vscode/ + +test_application/ + + diff --git a/ServicesBank/Finder/Payment/Dockerfile b/ServicesBank/Finder/Payment/Dockerfile new file mode 100644 index 0000000..ed88e33 --- /dev/null +++ b/ServicesBank/Finder/Payment/Dockerfile @@ -0,0 +1,33 @@ +FROM python:3.12-slim + +WORKDIR / + +# Set Python path to include app directory +ENV PYTHONPATH=/ PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1 + +# 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 + +# Install cron for scheduling tasks +RUN apt-get update && apt-get install -y cron + +# Copy application code +COPY /ServicesBank/Finder/Payment / +COPY /ServicesApi/Schemas /Schemas +COPY /ServicesApi/Controllers /Controllers + +# Create log file to grab cron logs +RUN touch /var/log/cron.log + +# Make entrypoint script executable +RUN chmod +x /entrypoint.sh +RUN chmod +x /run_app.sh + +# Use entrypoint script to update run_app.sh with environment variables and start cron +ENTRYPOINT ["/entrypoint.sh"] diff --git a/ServicesBank/Finder/Payment/README.md b/ServicesBank/Finder/Payment/README.md new file mode 100644 index 0000000..87b4266 --- /dev/null +++ b/ServicesBank/Finder/Payment/README.md @@ -0,0 +1,44 @@ +# Docs of Finder + +Finds people, living spaces, companies from AccountRecords + +start_time = perf_counter() +end_time = perf_counter() +elapsed = end_time - start_time +print(f'{elapsed:.3f} : seconds') +print('shallow_copy_list', len(shallow_copy_list)) +""" +""" +1. Stage (Incoming Money) +# BuildDecisionBookPayments are reverse records of AccountRecords +AccountRecords.approved_record == True +AccountRecords.living_space_id is not None +# AccountRecords.receive_debit = Credit Receiver (Incoming money from client) / Debit Sender (Debt to be paid by system) + +1.1 + AccountRecords.currency_value > 0 Received Money Transaction + + AccountRecords.currency_value > AccountRecords.remainder_balance () You have extra money in system account + Money consumed => AccountRecords.currency_value != abs(AccountRecords.remainder_balance) singluar iban + Some payment done but money not yet all money is consumed => AccountRecords.currency_value + AccountRecords.remainder_balance != 0 + + + AccountRecords.currency_value = AccountRecords.remainder_balance (There is no money that individual has in system) + AccountRecords.bank_date (Date money arrived) + AccountRecords.process_type (Type of bank transaction) + +1.2 + AccountRecords.currency_value < 0 Sent Money Transaction - + + +2. Stage (Payment Match Process) +Parse : BuildDecisionBookPayments.process_date (Year / Month / Day / Time) +BuildDecisionBookPayments.account_records_id == None ( Payment is not assigned to any account record) +BuildDecisionBookPayments.payment_types_id == debit_enum.id (Payment type is debit) + +2.1 Check current month has any payment to due Payment Month == Money Arrived Month +2.2 Check previous months has any payment to due Payment Month < Money Arrived Month + +3. Stage (Payment Assignment Process) +Do payment set left money to account record as AccountRecords.remainder_balance + + diff --git a/ServicesBank/Finder/Payment/draft/completed.py b/ServicesBank/Finder/Payment/draft/completed.py new file mode 100644 index 0000000..02d35d0 --- /dev/null +++ b/ServicesBank/Finder/Payment/draft/completed.py @@ -0,0 +1,774 @@ +import arrow +import time + +from decimal import Decimal +from datetime import datetime, timedelta +from Schemas import BuildDecisionBookPayments, AccountRecords, ApiEnumDropdown + +from time import perf_counter +from sqlalchemy import select, func, distinct, cast, Date, String, literal, desc, and_, or_ +from Controllers.Postgres.engine import get_session_factory +#from ServicesApi.Schemas.account.account import AccountRecords +#from ServicesApi.Schemas.building.decision_book import BuildDecisionBookPayments + + +class BuildDuesTypes: + + def __init__(self): + self.debit: ApiEnumDropdownShallowCopy = None + self.add_debit: ApiEnumDropdownShallowCopy = None + self.renovation: ApiEnumDropdownShallowCopy = None + self.lawyer_expence: ApiEnumDropdownShallowCopy = None + self.service_fee: ApiEnumDropdownShallowCopy = None + self.information: ApiEnumDropdownShallowCopy = None + + +class ApiEnumDropdownShallowCopy: + id: int + uuid: str + enum_class: str + key: str + value: str + + def __init__(self, id: int, uuid: str, enum_class: str, key: str, value: str): + self.id = id + self.uuid = uuid + self.enum_class = enum_class + self.key = key + self.value = value + + +def get_enums_from_database(): + build_dues_types = BuildDuesTypes() + with ApiEnumDropdown.new_session() as session: + + ApiEnumDropdown.set_session(session) + + debit_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-D").first() # Debit + add_debit_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-A").first() # Add Debit + renovation_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-R").first() # Renovation + late_payment_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-L").first() # Lawyer expence + service_fee_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-S").first() # Service fee + information_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-I").first() # Information + + build_dues_types.debit = ApiEnumDropdownShallowCopy( + debit_enum_shallow.id, str(debit_enum_shallow.uu_id), debit_enum_shallow.enum_class, debit_enum_shallow.key, debit_enum_shallow.value + ) + build_dues_types.add_debit = ApiEnumDropdownShallowCopy( + add_debit_enum_shallow.id, str(add_debit_enum_shallow.uu_id), add_debit_enum_shallow.enum_class, add_debit_enum_shallow.key, add_debit_enum_shallow.value + ) + build_dues_types.renovation = ApiEnumDropdownShallowCopy( + renovation_enum_shallow.id, str(renovation_enum_shallow.uu_id), renovation_enum_shallow.enum_class, renovation_enum_shallow.key, renovation_enum_shallow.value + ) + build_dues_types.lawyer_expence = ApiEnumDropdownShallowCopy( + late_payment_enum_shallow.id, str(late_payment_enum_shallow.uu_id), late_payment_enum_shallow.enum_class, late_payment_enum_shallow.key, late_payment_enum_shallow.value + ) + build_dues_types.service_fee = ApiEnumDropdownShallowCopy( + service_fee_enum_shallow.id, str(service_fee_enum_shallow.uu_id), service_fee_enum_shallow.enum_class, service_fee_enum_shallow.key, service_fee_enum_shallow.value + ) + build_dues_types.information = ApiEnumDropdownShallowCopy( + information_enum_shallow.id, str(information_enum_shallow.uu_id), information_enum_shallow.enum_class, information_enum_shallow.key, information_enum_shallow.value + ) + return [build_dues_types.debit, build_dues_types.lawyer_expence, build_dues_types.add_debit, build_dues_types.renovation, build_dues_types.service_fee, build_dues_types.information] + + +def generate_total_paid_amount_for_spesific_build_part_id(build_parts_id: int, session): + """ + Calculate the total amount paid for a specific build part ID. + + Args: + build_parts_id: The build part ID to calculate payments for + session: Database session + + Returns: + float: The total amount paid (absolute value) + """ + payment_query = session.query(func.sum(BuildDecisionBookPayments.payment_amount)).filter( + BuildDecisionBookPayments.build_parts_id == build_parts_id, + BuildDecisionBookPayments.account_is_debit == False, + cast(BuildDecisionBookPayments.process_date, Date) >= '2022-01-01' + ).scalar() + return payment_query if payment_query is not None else 0 + + +def generate_total_debt_amount_for_spesific_build_part_id(build_parts_id: int, session): + + # Use SQLAlchemy's func.sum to calculate the total debts + # For total debt, we want to include ALL debts, both processed and unprocessed + result = session.query( + func.sum(BuildDecisionBookPayments.payment_amount) + ).filter( + BuildDecisionBookPayments.build_parts_id == build_parts_id, + BuildDecisionBookPayments.account_is_debit == True, + cast(BuildDecisionBookPayments.process_date, Date) >= '2022-01-01' + ).scalar() + + # Return 0 if no debts found, otherwise return the absolute value of the sum + return abs(result) if result is not None else 0 + + +def generate_total_amount_that_user_has_in_account(account_record: AccountRecords, session): + # Get total amount that user has in account + result = session.query( + func.sum(AccountRecords.currency_value) + ).filter( + AccountRecords.build_parts_id == account_record.build_parts_id, + AccountRecords.currency_value > 0, + cast(AccountRecords.bank_date, Date) >= '2022-01-01' + ).scalar() + + # Return 0 if no payments found, otherwise return the absolute value of the sum + return abs(result) + + +def get_unpaid_debts(build_parts_id: int, session, debit_type, date_query: tuple): + """Find BuildDecisionBookPayments entries where the debt has NOT been fully paid for a specific debit type. + + This function identifies payments where the sum of payments is less than the debit amount, + meaning the debt has not been fully closed. + + Args: + build_parts_id: The build part ID to check + session: Database session + debit_type: The specific debit type to check + date_query: Tuple of date filters to apply + + Returns: + list: List of unpaid debt entries (full BuildDecisionBookPayments objects) + + query: + SELECT + bpf.ref_id, + bpf.process_date, + ABS(COALESCE(SUM(bpf.payment_amount), 0)) AS total_payments + FROM public.build_decision_book_payments AS bpf + GROUP BY + bpf.ref_id, + bpf.process_date + HAVING ABS(COALESCE(SUM(bpf.payment_amount), 0)) > 0 + order by bpf.process_date + """ + # Create a subquery for the payment sums without executing it separately + payment_sums_subquery = select( + BuildDecisionBookPayments.ref_id, + BuildDecisionBookPayments.process_date, + func.abs(func.coalesce(func.sum(BuildDecisionBookPayments.payment_amount), 0)).label("total_payments") + ).filter( + BuildDecisionBookPayments.build_parts_id == build_parts_id, + BuildDecisionBookPayments.payment_types_id == debit_type.id, + *date_query + ).group_by( + BuildDecisionBookPayments.ref_id, BuildDecisionBookPayments.process_date + ).having( + func.abs(func.coalesce(func.sum(BuildDecisionBookPayments.payment_amount), 0)) > 0 + ).order_by(BuildDecisionBookPayments.process_date.desc()) + + # Use the subquery directly in the main query + # query_results = session.query(BuildDecisionBookPayments).filter( + # BuildDecisionBookPayments.ref_id.in_(payment_sums_subquery), + # BuildDecisionBookPayments.build_parts_id == build_parts_id, + # BuildDecisionBookPayments.payment_types_id == debit_type.id, + # ).order_by(BuildDecisionBookPayments.process_date.desc()) + payment_sums = session.execute(payment_sums_subquery).all() + payment_sums_list = [] + for item in payment_sums: + payment_sums_list.append({"ref_id": item[0], "process_date": item[1], "total_payments": item[2]}) + return payment_sums + + +def _print_debt_details(debt, session): + """Helper function to print detailed information about an unpaid debt. + + Args: + debt: The BuildDecisionBookPayments object representing the debt + session: Database session + """ + # Get the sum of payments for this debt + payments_sum = session.query( + func.sum(BuildDecisionBookPayments.payment_amount) + ).filter( + BuildDecisionBookPayments.ref_id == debt.ref_id, + BuildDecisionBookPayments.account_is_debit == False + ).scalar() or 0 + + # Calculate remaining amount + debit_amount = abs(debt.payment_amount) + remaining = debit_amount - abs(payments_sum) + payment_percentage = (abs(payments_sum) / debit_amount) * 100 if debit_amount > 0 else 0 + + # Format the date for display + date_str = debt.process_date.strftime('%Y-%m-%d') if debt.process_date else 'Unknown date' + + +def analyze_payment_function(): + session_factory = get_session_factory() + session = session_factory() + # Set session for all models + AccountRecords.set_session(session) + BuildDecisionBookPayments.set_session(session) + + order_pay = get_enums_from_database() + # Get distinct build_parts_id values from account records with positive currency_value + # This avoids redundant processing of the same build_parts_id + distinct_build_parts = session.query( + distinct(AccountRecords.build_parts_id) + ).filter( + AccountRecords.build_parts_id.isnot(None), + AccountRecords.currency_value > 0, + AccountRecords.bank_date >= '2022-01-01' + ).order_by(AccountRecords.build_parts_id.desc()).all() + + start_time = time.time() + + for build_part_id_tuple in distinct_build_parts: + build_part_id = build_part_id_tuple[0] # Extract the ID from the tuple + process_date = datetime.now() + last_date_of_process_date = datetime(process_date.year, process_date.month, 1) - timedelta(days=1) + first_date_of_process_date = datetime(process_date.year, process_date.month, 1) + + print(f"\n{'=' * 50}") + print(f"ACCOUNT ANALYSIS FOR BUILD PART ID: {build_part_id}") + print(f"{'=' * 50}") + + # Calculate total paid amount for this build_part_id + total_amount_paid = generate_total_paid_amount_for_spesific_build_part_id(build_part_id, session) + + # Calculate total debt amount for this build_part_id + total_debt_amount = generate_total_debt_amount_for_spesific_build_part_id(build_part_id, session) + + # Get total amount in account for this build_part_id + account_record = AccountRecords() + account_record.build_parts_id = build_part_id + total_amount_in_account = generate_total_amount_that_user_has_in_account(account_record, session) + + # Calculate remaining amount to be paid + amount_need_to_paid = total_debt_amount - total_amount_paid + total_amount_that_user_need_to_transfer = abs(amount_need_to_paid) - abs(total_amount_in_account) + + # Print summary with clear descriptions + print(f"PAYMENT SUMMARY:") + print(f" • Total debt amount: {total_debt_amount:,.2f} TL") + print(f" • Amount already paid: {total_amount_paid:,.2f} TL") + print(f" • Remaining debt to be collected: {amount_need_to_paid:,.2f} TL") + print(f" • Current account balance: {total_amount_in_account:,.2f} TL") + + if total_amount_that_user_need_to_transfer > 0: + print(f" • Additional funds needed: {total_amount_that_user_need_to_transfer:,.2f} TL") + elif amount_need_to_paid <= 0: + print(f" • Account is fully paid with no outstanding debt") + else: + print(f" • Sufficient funds available to close all debt") + # Show debt coverage percentage + if total_debt_amount > 0: + # Calculate current coverage (already paid) + current_coverage_percentage = (total_amount_paid / total_debt_amount) * 100 + + # Calculate potential coverage (including available funds) + potential_coverage = min(100, ((total_amount_paid + total_amount_in_account) / total_debt_amount) * 100) + + # Display both percentages + print(f" • Current debt coverage: {current_coverage_percentage:.2f}%") + print(f" • Potential debt coverage with available funds: {potential_coverage:.2f}%") + + # Analyze unpaid debts for each payment type + print("\nUNPAID DEBTS ANALYSIS BY PAYMENT TYPE:") + for payment_type in order_pay: + # Get unpaid debts for current month + date_query_current = ( + BuildDecisionBookPayments.process_date >= first_date_of_process_date, + BuildDecisionBookPayments.process_date <= process_date + ) + date_query_previous = ( + BuildDecisionBookPayments.process_date < first_date_of_process_date, + ) + + current_unpaid_debts = get_unpaid_debts(build_parts_id=build_part_id, session=session, debit_type=payment_type, date_query=date_query_current) + + # Get unpaid debts from previous months + previous_unpaid_debts = get_unpaid_debts(build_parts_id=build_part_id, session=session, debit_type=payment_type, date_query=date_query_previous) + + # Calculate totals + current_total = sum(abs(debt[2]) for debt in current_unpaid_debts) + previous_total = sum(abs(debt[2]) for debt in previous_unpaid_debts) + grand_total = current_total + previous_total + + # Print summary for this payment type + if current_unpaid_debts or previous_unpaid_debts: + print(f" • {payment_type.key}: Total unpaid: {grand_total:,.2f} TL") + + # Current month details + if current_unpaid_debts: + print(f" - Current month: {len(current_unpaid_debts)} debts, {current_total:,.2f} TL") + # Show details of each unpaid debt if there aren't too many + # if len(current_unpaid_debts) <= 3: + # for debt in current_unpaid_debts: + # _print_debt_details(debt, session) + + # Previous months details + if previous_unpaid_debts: + print(f" - Previous months: {len(previous_unpaid_debts)} debts, {previous_total:,.2f} TL") + # Show details of each unpaid debt if there aren't too many + # if len(previous_unpaid_debts) <= 3: + # for debt in previous_unpaid_debts: + # _print_debt_details(debt, session) + else: + print(f" • {payment_type.key}: All debts paid") + print(f"{'=' * 50}\n") + + +def close_payment_book(payment_row_book, account_record, value, session): + """Create a credit entry in BuildDecisionBookPayments to close a debt. + + Args: + payment_row_book: The debit entry to be paid + account_record: The account record containing the funds + value: The amount to pay + session: Database session + + Returns: + The newly created payment record + """ + BuildDecisionBookPayments.set_session(session) + + # Create a new credit entry (payment) + new_row = BuildDecisionBookPayments.create( + ref_id=str(payment_row_book.uu_id), + payment_plan_time_periods=payment_row_book.payment_plan_time_periods, + period_time=payment_row_book.period_time, + currency=payment_row_book.currency, + account_records_id=account_record.id, + account_records_uu_id=str(account_record.uu_id), + build_parts_id=payment_row_book.build_parts_id, + build_parts_uu_id=str(payment_row_book.build_parts_uu_id), + payment_amount=abs(value), # Negative for credit entries + payment_types_id=payment_row_book.payment_types_id, + payment_types_uu_id=str(payment_row_book.payment_types_uu_id), + process_date_m=payment_row_book.process_date.month, + process_date_y=payment_row_book.process_date.year, + process_date=payment_row_book.process_date, + build_decision_book_item_id=payment_row_book.build_decision_book_item_id if payment_row_book.build_decision_book_item_id else None, + build_decision_book_item_uu_id=str(payment_row_book.build_decision_book_item_uu_id) if payment_row_book.build_decision_book_item_uu_id else None, + decision_book_project_id=payment_row_book.decision_book_project_id if payment_row_book.decision_book_project_id else None, + decision_book_project_uu_id=str(payment_row_book.decision_book_project_uu_id) if payment_row_book.decision_book_project_uu_id else None, + is_confirmed=True, + account_is_debit=False, + ) + + # Save the new payment record + saved_row = new_row.save() + # Update the original debt record to mark it as processed + # payment_row_book.account_records_id = account_record.id + # payment_row_book.account_records_uu_id = str(account_record.uu_id) + # payment_row_book.save() + # # Flush to ensure both records are saved to the database + # session.flush() + return saved_row + + +def update_account_remainder_if_spent(account_record, ref_id: str, session): + """Update the remainder_balance of an account after spending money. + + Args: + account_record: The account record to update + amount_spent: The amount spent in this transaction + session: Database session + + Returns: + bool: True if all money is spent, False otherwise + """ + AccountRecords.set_session(session) + BuildDecisionBookPayments.set_session(session) + sum_of_paid = session.query(func.sum(func.abs(BuildDecisionBookPayments.payment_amount))).filter( + BuildDecisionBookPayments.account_records_id == account_record.id, + BuildDecisionBookPayments.account_is_debit == False + ).scalar() + + debit_row = BuildDecisionBookPayments.query.filter_by(ref_id=ref_id).first() + account_record_to_update = AccountRecords.query.filter_by(id=account_record.id).first() + account_record_to_update.remainder_balance = sum_of_paid + account_record_to_update.save() + # Get the current remainder balance + if abs(sum_of_paid) == abs(account_record.currency_value): + return True + return False + + +def update_all_spent_accounts(session): + """Update remainder_balance for all accounts with payments. + + This function finds account records in BuildDecisionBookPayments and updates + their remainder_balance based on the sum of payments made, regardless of whether + all funds have been spent or not. + + Args: + session: Database session + """ + with AccountRecords.new_session() as session: + # Set sessions for models + AccountRecords.set_session(session) + BuildDecisionBookPayments.set_session(session) + + # Get distinct account_records_id values from BuildDecisionBookPayments + distinct_account_ids = session.query(BuildDecisionBookPayments.account_records_id).filter( + BuildDecisionBookPayments.account_records_id.isnot(None), + BuildDecisionBookPayments.account_is_debit == False # Credit entries (payments) + ).distinct().all() + + updated_count = 0 + + for account_id_tuple in distinct_account_ids: + account_id = account_id_tuple[0] + + # Get the account record + account = AccountRecords.query.filter_by(id=account_id).first() + if not account or not account.build_parts_id or account.currency_value <= 0: + continue + + # Calculate the sum of payments made using this account + # Note: payment_amount is negative for credit entries, so we need to use abs() to get the positive amount + payment_query = session.query(func.sum(func.abs(BuildDecisionBookPayments.payment_amount))).filter( + BuildDecisionBookPayments.account_records_id == account_id, + BuildDecisionBookPayments.account_is_debit == False # Credit entries (payments) + ) + + payment_sum = payment_query.scalar() or 0 + + # Update remainder_balance for ALL accounts, regardless of payment_sum value + threshold = Decimal('0.01') + fully_spent = payment_sum >= abs(account.currency_value) - threshold + status = "All funds spent" if fully_spent else "Partial payment" + + # Store the positive value in remainder_balance + account.remainder_balance = payment_sum + account.save() + updated_count += 1 + + print(f"\nTotal accounts updated: {updated_count}") + + +# def find_amount_to_pay_by_ref_id(ref_id, session): +# """Calculate the remaining amount to pay for a specific debt reference ID. + +# Args: +# ref_id: The reference ID of the debt (this is the uu_id of the debt record) +# session: Database session + +# Returns: +# float: The remaining amount to pay +# """ +# # Get the original debt amount - the debt is identified by its uu_id which is passed as ref_id +# debit = BuildDecisionBookPayments.query.filter( +# BuildDecisionBookPayments.uu_id == ref_id, +# BuildDecisionBookPayments.account_is_debit == True +# ).first() +# if not debit: +# return 0 # No debit found, nothing to pay + +# debit_amount = abs(debit.payment_amount) # Ensure positive value for debit amount + +# # Get the sum of payments already made for this debt +# # The ref_id in credit records points to the uu_id of the original debit +# # Note: payment_amount is negative for credit entries, so we use abs() to get positive values +# credit_amount = session.query( +# func.sum(func.abs(BuildDecisionBookPayments.payment_amount)) +# ).filter( +# BuildDecisionBookPayments.ref_id == str(ref_id), +# BuildDecisionBookPayments.account_is_debit == False +# ).scalar() or 0 +# # Calculate remaining amount to pay +# remaining = abs(debit_amount) - abs(credit_amount) +# # Ensure we don't return negative values +# if remaining < 0: +# return 0 + +# return remaining + + +def do_payments_of_this_month(): + """Process payments for the current month's unpaid debts. + This function retrieves account records with available funds and processes + payments for current month's unpaid debts in order of payment type priority. + """ + session_factory = get_session_factory() + session = session_factory() + + # Set session for all models + AccountRecords.set_session(session) + BuildDecisionBookPayments.set_session(session) + + # Get payment types in priority order + payment_type_list = get_enums_from_database() + + # Get account records with positive currency_value and available funds + account_records = AccountRecords.query.filter( + AccountRecords.build_parts_id.isnot(None), + AccountRecords.currency_value > 0, + or_( + AccountRecords.remainder_balance.is_(None), + AccountRecords.remainder_balance < AccountRecords.currency_value + ), + AccountRecords.bank_date >= '2022-01-01' + ).order_by(AccountRecords.build_parts_id.desc()).all() + + payments_made = 0 + total_amount_paid = 0 + process_date = datetime.now() + first_date_of_process_date = datetime(process_date.year, process_date.month, 1) + last_date_of_process_date_ = datetime(process_date.year, process_date.month, 1) + timedelta(days=31) + last_date_of_process_date = datetime(last_date_of_process_date_.year, last_date_of_process_date_.month, 1) - timedelta(days=1) + + # Current month date filter + date_query_tuple = ( + BuildDecisionBookPayments.process_date >= first_date_of_process_date, + BuildDecisionBookPayments.process_date <= last_date_of_process_date + ) + + fund_finished = lambda money_spend, money_in_account: money_spend == money_in_account + # Update all accounts with spent funds but zero remainder_balance + update_all_spent_accounts(session) + + for account_record in account_records: + # Initialize variables for this account + money_in_account = abs(account_record.currency_value) - abs(account_record.remainder_balance) + money_paid = 0 + build_parts_id = account_record.build_parts_id + # Process each payment type in priority order + for payment_type in payment_type_list: + # Check if all money is spent and update remainder balance if needed + if fund_finished(money_paid, money_in_account): + break + # Get unpaid debts for this payment type + unpaid_debts = get_unpaid_debts(build_parts_id=build_parts_id, session=session, debit_type=payment_type, date_query=date_query_tuple) + # Process each unpaid debt + for debt in unpaid_debts: + if not money_in_account > 0: + update_account_remainder_if_spent(account_record, debt[0], session) + break + # Check if all money is spent and update remainder balance if needed + # if fund_finished(money_paid, money_in_account): + # break + # Calculate remaining amount to pay + # Use debt.uu_id as the ref_id, not debt.ref_id + debt_to_pay = debt[2] + # Skip if nothing to pay + if debt_to_pay <= 0: + continue + # Determine amount to pay based on available funds + payment_amount = min(debt_to_pay, money_in_account) + # Make payment + debt_to_copy = BuildDecisionBookPayments.query.filter_by(ref_id=debt[0]).first() + close_payment_book(debt_to_copy, account_record, payment_amount, session) + # Update counters + money_in_account -= payment_amount + money_paid += payment_amount + payments_made += 1 + total_amount_paid += payment_amount + if not money_in_account > 0: + update_account_remainder_if_spent(account_record, debt[0], session) + break + + # if money_paid > 0: + # update_account_remainder_if_spent(account_record, money_paid, session) + + # Update all accounts with spent funds but zero remainder_balance + update_all_spent_accounts(session) + + # Commit all changes to the database + session.commit() + + # Print summary + print("\nCURRENT MONTH PAYMENT SUMMARY:") + print(f"Total payments made: {payments_made}") + print(f"Total amount paid: {total_amount_paid:,.2f} TL") + + +def do_payments_of_previos_months(): + """Process payments for previous months' unpaid debts. + + This function retrieves account records with available funds and processes + payments for previous months' unpaid debts in order of payment type priority. + """ + session_factory = get_session_factory() + session = session_factory() + + # Set session for all models + AccountRecords.set_session(session) + BuildDecisionBookPayments.set_session(session) + + # Get payment types in priority order + payment_type_list = get_enums_from_database() + + # Get account records with positive currency_value and available funds + account_query = AccountRecords.query.filter( + AccountRecords.build_parts_id.isnot(None), + AccountRecords.currency_value > 0, + AccountRecords.remainder_balance < AccountRecords.currency_value, + AccountRecords.bank_date >= '2022-01-01' + ).order_by(AccountRecords.bank_date.desc()) + + account_records = account_query.all() + + payments_made = 0 + total_amount_paid = 0 + # process_date = datetime.now() + # first_date_of_process_date = datetime(process_date.year, process_date.month, 1) + + # Previous months date filter + # date_query_tuple = (BuildDecisionBookPayments.process_date < first_date_of_process_date, ) + fund_finished = lambda money_spend, money_in_account: money_spend >= money_in_account + + # Update all accounts with spent funds but zero remainder_balance + update_all_spent_accounts(session) + + for account_record in account_records: + # Initialize variables for this account + process_date_begins = datetime(account_record.bank_date.year, account_record.bank_date.month, 1) + process_date_ends_ = datetime(account_record.bank_date.year, account_record.bank_date.month, 1)+ timedelta(days=31) + process_date_ends = datetime(process_date_ends_.year, process_date_ends_.month, 1)- timedelta(days=1) + + date_query_tuple = (BuildDecisionBookPayments.process_date >= process_date_begins, BuildDecisionBookPayments.process_date <= process_date_ends) + + money_in_account = abs(account_record.currency_value) - abs(account_record.remainder_balance) + money_paid = 0 + build_parts_id = account_record.build_parts_id + # Process each payment type in priority order + for payment_type in payment_type_list: + # Check if all money is spent and update remainder balance if needed + if fund_finished(money_paid, money_in_account): + break + # Get unpaid debts for this payment type + unpaid_debts = get_unpaid_debts(build_parts_id=build_parts_id, session=session, debit_type=payment_type, date_query=date_query_tuple) + if not len(unpaid_debts) > 0: + process_date = datetime.now() + process_date_ends = datetime(process_date.year, process_date.month, 1) + date_query_tuple = (BuildDecisionBookPayments.process_date < process_date_ends, ) + unpaid_debts = get_unpaid_debts(build_parts_id=build_parts_id, session=session, debit_type=payment_type, date_query=date_query_tuple) + + # Process each unpaid debt + for debt in unpaid_debts: + if not money_in_account > 0: + update_account_remainder_if_spent(account_record, debt[0], session) + break + # Check if all money is spent and update remainder balance if needed + if fund_finished(money_paid, money_in_account): + break + # Calculate remaining amount to pay + debt_to_pay = debt[2] + # Skip if nothing to pay + if debt_to_pay <= 0: + continue + # Determine amount to pay based on available funds + payment_amount = min(debt_to_pay, money_in_account) + # Make payment + try: + # Create payment record and update original debt + debt_to_copy = BuildDecisionBookPayments.query.filter_by(ref_id=debt[0]).first() + new_payment = close_payment_book(debt_to_copy, account_record, payment_amount, session) + # Verify the payment was created + if new_payment and new_payment.id: + # Update counters + money_in_account -= payment_amount + money_paid += payment_amount + payments_made += 1 + total_amount_paid += payment_amount + # Flush changes to ensure they are visible in subsequent queries + session.flush() + else: + session.rollback() + continue + except Exception as e: + print(f"Debt : {debt[0]} -> Exception: {e}") + session.rollback() + continue + if not money_in_account > 0: + update_account_remainder_if_spent(account_record, debt[0], session) + break + # Update all accounts with spent funds but zero remainder_balance + update_all_spent_accounts(session) + + # Commit all changes to the database + session.commit() + + # Print summary + print("\nPREVIOUS MONTHS PAYMENT SUMMARY:") + print(f"Total payments made: {payments_made}") + print(f"Total amount paid: {total_amount_paid:,.2f} TL") + + +# /draft/draft-first-work.py +if __name__ == "__main__": + start_time = perf_counter() + + print("\n===== PROCESSING PAYMENTS =====\n") + print("Starting payment processing at:", datetime.now()) + + # Process payments for current month first + print("\n1. Processing current month payments...") + do_payments_of_this_month() + + # Then process payments for previous months with remaining funds + print("\n2. Processing previous months payments...") + attempt = 4 + while True: + total_debit = BuildDecisionBookPayments.query.filter(BuildDecisionBookPayments.account_is_debit == True).count() + total_credit = BuildDecisionBookPayments.query.filter(BuildDecisionBookPayments.account_is_debit == False).count() + if total_debit != total_credit: + do_payments_of_previos_months() + attempt -= 1 + if attempt == 0: + break + else: + break + + print("\n===== PAYMENT PROCESSING COMPLETE =====\n") + print("Payment processing completed at:", datetime.now()) + + # Analyze the payment situation after processing payments + print("\n===== ANALYZING PAYMENT SITUATION AFTER PROCESSING =====\n") + analyze_payment_function() + + end_time = perf_counter() + print(f"\n{end_time - start_time:.3f} : seconds") + + + # # Create a subquery to get the sum of payments for each debit's uu_id + # # For credit entries, ref_id points to the original debit's uu_id + # payment_sums = session.query( + # BuildDecisionBookPayments.ref_id.label('original_debt_id'), + # func.sum(func.abs(BuildDecisionBookPayments.payment_amount)).label('payment_sum') + # ).filter( + # BuildDecisionBookPayments.account_is_debit == False # Credit entries only + # ).group_by(BuildDecisionBookPayments.ref_id).subquery() + + # # Main query to find debits with their payment sums + # query = session.query(BuildDecisionBookPayments) + + # # Join with payment sums - cast uu_id to string to match ref_id type + # query = query.outerjoin( + # payment_sums, + # func.cast(BuildDecisionBookPayments.uu_id, String) == payment_sums.c.original_debt_id + # ) + + # # Filter for debits of the specified build part and payment type + # query = query.filter( + # BuildDecisionBookPayments.build_parts_id == build_parts_id, + # BuildDecisionBookPayments.payment_types_id == debit_type.id, + # BuildDecisionBookPayments.account_is_debit == True, # Debit entries only + # ) + + # # Apply date filters if provided + # if date_query: + # for date_filter in date_query: + # query = query.filter(date_filter) + + # # Filter for debits that are not fully paid + # # (payment_sum < debit_amount or payment_sum is NULL) + # query = query.filter( + # or_( + # payment_sums.c.payment_sum.is_(None), + # func.coalesce(payment_sums.c.payment_sum, 0) < func.abs(BuildDecisionBookPayments.payment_amount) + # ) + # ) + + # # Execute the query and return the results + # results = query.order_by(BuildDecisionBookPayments.process_date).all()s \ No newline at end of file diff --git a/ServicesBank/Finder/Payment/draft/debug-payment.py b/ServicesBank/Finder/Payment/draft/debug-payment.py new file mode 100644 index 0000000..8b90804 --- /dev/null +++ b/ServicesBank/Finder/Payment/draft/debug-payment.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +# Debug script to test payment processing functions directly + +import sys +import os +from datetime import datetime +from time import perf_counter +from sqlalchemy import func + +# Import directly from the draft-first-work.py file in the same directory +sys.path.insert(0, '/draft') + +# Import the necessary functions and classes directly from the file +from draft_first_work import * # Import everything for simplicity + +def debug_find_unpaid_debts(build_parts_id, session): + """Find unpaid debts directly using raw SQL for debugging.""" + print(f"\nDEBUG: Finding unpaid debts for build part ID: {build_parts_id}") + + # Set session for the model + BuildDecisionBookPayments.set_session(session) + + # Get payment types + payment_types = get_enums_from_database() + + for payment_type in payment_types: + print(f"\nChecking payment type: {payment_type.key}") + + # Find debits that don't have account_records_id set (unpaid) + query = session.query(BuildDecisionBookPayments).filter( + BuildDecisionBookPayments.build_parts_id == build_parts_id, + BuildDecisionBookPayments.payment_types_id == payment_type.id, + BuildDecisionBookPayments.account_is_debit == True, + BuildDecisionBookPayments.account_records_id.is_(None) + ) + + # Print the SQL query + print(f"SQL Query: {query}") + + # Execute the query + unpaid_debts = query.all() + print(f"Found {len(unpaid_debts)} unpaid debts") + + # Print details of each unpaid debt + for i, debt in enumerate(unpaid_debts[:5]): # Limit to first 5 for brevity + print(f" Debt {i+1}:") + print(f" ID: {debt.id}") + print(f" UUID: {debt.uu_id}") + print(f" Amount: {abs(debt.payment_amount):,.2f} TL") + print(f" Process Date: {debt.process_date}") + print(f" Account Records ID: {debt.account_records_id}") + + # Check if any payments have been made for this debt + credit_query = session.query( + func.sum(func.abs(BuildDecisionBookPayments.payment_amount)) + ).filter( + BuildDecisionBookPayments.ref_id == str(debt.uu_id), + BuildDecisionBookPayments.account_is_debit == False + ) + + credit_amount = credit_query.scalar() or 0 + print(f" Payments made: {credit_amount:,.2f} TL") + print(f" Remaining to pay: {abs(debt.payment_amount) - credit_amount:,.2f} TL") + +def debug_process_payment(build_parts_id, session): + """Process a single payment for debugging purposes.""" + print(f"\nDEBUG: Processing payment for build part ID: {build_parts_id}") + + # Set session for all models + AccountRecords.set_session(session) + BuildDecisionBookPayments.set_session(session) + + # Get payment types in priority order + payment_type_list = get_enums_from_database() + + # Get account records with positive currency_value + account_query = AccountRecords.query.filter( + AccountRecords.build_parts_id == build_parts_id, + AccountRecords.currency_value > 0, + AccountRecords.bank_date >= '2022-01-01' + ).order_by(AccountRecords.id.desc()).limit(5) + + print(f"Account query: {account_query}") + account_records = account_query.all() + print(f"Found {len(account_records)} account records with funds") + + # Print account details + for i, account in enumerate(account_records): + available_funds = abs(account.currency_value) - abs(account.remainder_balance) + print(f" Account {i+1}: ID: {account.id}, Build Part ID: {account.build_parts_id}, Value: {account.currency_value:,.2f} TL, Available: {available_funds:,.2f} TL") + + if not account_records: + print("No account records found with funds. Cannot process payments.") + return + + # Use the first account with funds + account_record = account_records[0] + money_in_account = abs(account_record.currency_value) - abs(account_record.remainder_balance) + + print(f"\nUsing account ID: {account_record.id} with available funds: {money_in_account:,.2f} TL") + + # Get the first payment type + payment_type = payment_type_list[0] + print(f"Using payment type: {payment_type.key}") + + # Find unpaid debts for this payment type + query = session.query(BuildDecisionBookPayments).filter( + BuildDecisionBookPayments.build_parts_id == build_parts_id, + BuildDecisionBookPayments.payment_types_id == payment_type.id, + BuildDecisionBookPayments.account_is_debit == True, + BuildDecisionBookPayments.account_records_id.is_(None) + ).limit(1) + + print(f"Unpaid debt query: {query}") + unpaid_debt = query.first() + + if not unpaid_debt: + print(f"No unpaid debts found for payment type {payment_type.key}") + return + + print(f"\nFound unpaid debt:") + print(f" ID: {unpaid_debt.id}") + print(f" UUID: {unpaid_debt.uu_id}") + print(f" Amount: {abs(unpaid_debt.payment_amount):,.2f} TL") + + # Calculate amount to pay + debt_amount = abs(unpaid_debt.payment_amount) + payment_amount = min(debt_amount, money_in_account) + + print(f"\nProcessing payment:") + print(f" Debt amount: {debt_amount:,.2f} TL") + print(f" Available funds: {money_in_account:,.2f} TL") + print(f" Will pay: {payment_amount:,.2f} TL") + + # Make payment + try: + print("Creating payment record...") + before_state = session.query(BuildDecisionBookPayments).filter( + BuildDecisionBookPayments.uu_id == unpaid_debt.uu_id + ).first() + print(f"Before payment - account_records_id: {before_state.account_records_id}") + + new_payment = close_payment_book(unpaid_debt, account_record, payment_amount, session) + + # Verify the payment was created + after_state = session.query(BuildDecisionBookPayments).filter( + BuildDecisionBookPayments.uu_id == unpaid_debt.uu_id + ).first() + print(f"After payment - account_records_id: {after_state.account_records_id}") + + # Check if the new payment record was created + if new_payment: + print(f"New payment record created with ID: {new_payment.id}") + print(f"New payment amount: {abs(new_payment.payment_amount):,.2f} TL") + print(f"New payment ref_id: {new_payment.ref_id}") + print(f"New payment account_is_debit: {new_payment.account_is_debit}") + else: + print("Failed to create new payment record") + + # Commit the transaction + session.commit() + print("Transaction committed successfully") + + except Exception as e: + session.rollback() + print(f"Error making payment: {str(e)}") + print("Transaction rolled back") + +if __name__ == "__main__": + start_time = perf_counter() + + print("\n===== PAYMENT PROCESSING DEBUG =====\n") + + # Create a session + session_factory = get_session_factory() + session = session_factory() + + # Set the build part ID to debug + build_part_id = 14 # Change this to the build part ID you want to debug + + # Find unpaid debts + debug_find_unpaid_debts(build_part_id, session) + + # Process a single payment + debug_process_payment(build_part_id, session) + + end_time = perf_counter() + print(f"\nDebug completed in {end_time - start_time:.3f} seconds") diff --git a/ServicesBank/Finder/Payment/draft/debug_remainder.py b/ServicesBank/Finder/Payment/draft/debug_remainder.py new file mode 100644 index 0000000..5dbf065 --- /dev/null +++ b/ServicesBank/Finder/Payment/draft/debug_remainder.py @@ -0,0 +1,104 @@ +from Schemas import AccountRecords, BuildDecisionBookPayments +from Controllers.Postgres.engine import get_session_factory +from sqlalchemy import func +from decimal import Decimal + +def debug_remainder_balance(): + session_factory = get_session_factory() + session = session_factory() + + # Set sessions for models + AccountRecords.set_session(session) + BuildDecisionBookPayments.set_session(session) + + print("\n" + "=" * 50) + print("DEBUGGING REMAINDER BALANCE ISSUES") + print("=" * 50) + + # Get counts of accounts + total_accounts = session.query(AccountRecords).filter( + AccountRecords.currency_value > 0 + ).count() + + zero_remainder = session.query(AccountRecords).filter( + AccountRecords.currency_value > 0, + AccountRecords.remainder_balance == 0 + ).count() + + nonzero_remainder = session.query(AccountRecords).filter( + AccountRecords.currency_value > 0, + AccountRecords.remainder_balance != 0 + ).count() + + print(f"Total accounts with positive currency_value: {total_accounts}") + print(f"Accounts with zero remainder_balance: {zero_remainder}") + print(f"Accounts with non-zero remainder_balance: {nonzero_remainder}") + + # Get distinct account IDs with payments + distinct_account_ids = session.query(BuildDecisionBookPayments.account_records_id).filter( + BuildDecisionBookPayments.account_records_id.isnot(None), + BuildDecisionBookPayments.account_is_debit == False # Credit entries (payments) + ).distinct().all() + + print(f"\nDistinct account IDs with payments: {len(distinct_account_ids)}") + + # Sample some accounts with zero remainder_balance but have payments + print("\nSampling accounts with zero remainder_balance:") + sample_count = 0 + + for account_id_tuple in distinct_account_ids[:10]: # Check first 10 accounts with payments + account_id = account_id_tuple[0] + + # Get the account record + account = AccountRecords.query.get(account_id) + if not account or account.remainder_balance != 0: + continue + + # Calculate the sum of payments made using this account + payment_sum = session.query( + func.sum(BuildDecisionBookPayments.payment_amount) + ).filter( + BuildDecisionBookPayments.account_records_id == account_id, + BuildDecisionBookPayments.account_is_debit == False # Credit entries (payments) + ).scalar() or 0 + + print(f" Account {account_id}: Currency Value={abs(account.currency_value):,.2f} TL, Payments={abs(payment_sum):,.2f} TL, Remainder={account.remainder_balance}") + sample_count += 1 + + if sample_count == 0: + print(" No accounts found with zero remainder_balance that have payments") + + # Now let's fix a sample of accounts + print("\nFixing sample accounts with zero remainder_balance:") + fixed_count = 0 + + for account_id_tuple in distinct_account_ids[:5]: # Fix first 5 accounts with payments + account_id = account_id_tuple[0] + + # Get the account record + account = AccountRecords.query.get(account_id) + if not account or not account.build_parts_id or account.currency_value <= 0: + continue + + # Calculate the sum of payments made using this account + payment_sum = session.query( + func.sum(BuildDecisionBookPayments.payment_amount) + ).filter( + BuildDecisionBookPayments.account_records_id == account_id, + BuildDecisionBookPayments.account_is_debit == False # Credit entries (payments) + ).scalar() or 0 + + old_remainder = account.remainder_balance + + # Update remainder_balance for this account + account.remainder_balance = payment_sum + account.save() + fixed_count += 1 + + print(f" Fixed Account {account_id}: Old remainder={old_remainder}, New remainder={account.remainder_balance:,.2f} TL") + + print(f"\nTotal accounts fixed in this run: {fixed_count}") + print("=" * 50) + +if __name__ == "__main__": + debug_remainder_balance() diff --git a/ServicesBank/Finder/Payment/draft/draft-first-work.py b/ServicesBank/Finder/Payment/draft/draft-first-work.py new file mode 100644 index 0000000..66693f3 --- /dev/null +++ b/ServicesBank/Finder/Payment/draft/draft-first-work.py @@ -0,0 +1,346 @@ +import arrow +from decimal import Decimal +from datetime import datetime, timedelta +from Schemas import BuildDecisionBookPayments, AccountRecords, ApiEnumDropdown + +from time import perf_counter +import time +from sqlalchemy import cast, Date, String +from Controllers.Postgres.engine import get_session_factory +# from ServicesApi.Schemas.account.account import AccountRecords +# from ServicesApi.Schemas.building.decision_book import BuildDecisionBookPayments + +# Helper function to calculate available funds +def df_fund(account_income, total_paid): + return abs(account_income) - abs(total_paid) + +class BuildDuesTypes: + + def __init__(self): + self.debit: ApiEnumDropdownShallowCopy = None + self.add_debit: ApiEnumDropdownShallowCopy = None + self.renovation: ApiEnumDropdownShallowCopy = None + self.lawyer_expence: ApiEnumDropdownShallowCopy = None + self.service_fee: ApiEnumDropdownShallowCopy = None + self.information: ApiEnumDropdownShallowCopy = None + + +class ApiEnumDropdownShallowCopy: + id: int + uuid: str + enum_class: str + key: str + value: str + + def __init__(self, id: int, uuid: str, enum_class: str, key: str, value: str): + self.id = id + self.uuid = uuid + self.enum_class = enum_class + self.key = key + self.value = value + + +def find_master_payment_value(build_parts_id: int, process_date: datetime, session, debit_type): + BuildDecisionBookPayments.set_session(session) + book_payments_query = ( + BuildDecisionBookPayments.process_date_m == process_date.month, BuildDecisionBookPayments.process_date_y == process_date.year, + BuildDecisionBookPayments.build_parts_id == build_parts_id, BuildDecisionBookPayments.account_records_id.is_(None), + BuildDecisionBookPayments.account_is_debit == True, BuildDecisionBookPayments.payment_types_id == debit_type.id, + ) + debit_row = BuildDecisionBookPayments.query.filter(*book_payments_query).order_by(BuildDecisionBookPayments.process_date.desc()).first() + if not debit_row: + return 0, None, None + return abs(debit_row.payment_amount), debit_row, str(debit_row.ref_id) + + +def calculate_paid_amount_for_master(ref_id: str, session, debit_amount): + """Calculate how much has been paid for a given payment reference. + + Args: + ref_id: The reference ID to check payments for + session: Database session + debit_amount: Original debit amount + + Returns: + float: Remaining amount to pay (debit_amount - total_paid) + """ + BuildDecisionBookPayments.set_session(session) + paid_rows = BuildDecisionBookPayments.query.filter( + BuildDecisionBookPayments.ref_id == ref_id, + BuildDecisionBookPayments.account_records_id.isnot(None), + BuildDecisionBookPayments.account_is_debit == False + ).order_by(BuildDecisionBookPayments.process_date.desc()).all() + + if not paid_rows: + return debit_amount + + total_paid = sum([abs(paid_row.payment_amount) for paid_row in paid_rows]) + remaining = abs(debit_amount) - abs(total_paid) + return remaining + + +def find_master_payment_value_previous(build_parts_id: int, process_date: datetime, session, debit_type): + BuildDecisionBookPayments.set_session(session) + parse_process_date = datetime(process_date.year, process_date.month, 1) - timedelta(days=1) + book_payments_query = ( + BuildDecisionBookPayments.process_date < parse_process_date, + BuildDecisionBookPayments.build_parts_id == build_parts_id, BuildDecisionBookPayments.account_records_id.is_(None), + BuildDecisionBookPayments.account_is_debit == True, BuildDecisionBookPayments.payment_types_id == debit_type.id, + ) + debit_rows = BuildDecisionBookPayments.query.filter(*book_payments_query).order_by(BuildDecisionBookPayments.process_date.desc()).all() + return debit_rows + + +def find_amount_to_pay(build_parts_id: int, process_date: datetime, session, debit_type): + # debit -negative value that need to be pay + debit, debit_row, debit_row_ref_id = find_master_payment_value(build_parts_id=build_parts_id, process_date=process_date, session=session, debit_type=debit_type) + # Is there any payment done for this ref_id ? + return calculate_paid_amount_for_master(ref_id=debit_row_ref_id, session=session, debit_amount=debit), debit_row + + +def calculate_total_debt_for_account(build_parts_id: int, session): + """Calculate the total debt and total paid amount for an account regardless of process date.""" + BuildDecisionBookPayments.set_session(session) + + # Get all debits for this account + all_debits = BuildDecisionBookPayments.query.filter( + BuildDecisionBookPayments.build_parts_id == build_parts_id, + BuildDecisionBookPayments.account_records_id.is_(None), + BuildDecisionBookPayments.account_is_debit == True + ).all() + + total_debt = sum([abs(debit.payment_amount) for debit in all_debits]) + + # Get all payments for this account's debits + total_paid = 0 + for debit in all_debits: + payments = BuildDecisionBookPayments.query.filter( + BuildDecisionBookPayments.ref_id == debit.ref_id, + BuildDecisionBookPayments.account_is_debit == False + ).all() + + if payments: + total_paid += sum([abs(payment.payment_amount) for payment in payments]) + + return total_debt, total_paid + + +def refresh_book_payment(account_record: AccountRecords): + """Update the remainder_balance of an account record based on attached payments. + + This function calculates the total of all payments attached to an account record + and updates the remainder_balance field accordingly. The remainder_balance represents + funds that have been received but not yet allocated to specific debits. + + Args: + account_record: The account record to update + + Returns: + float: The total payment amount + """ + total_payment = 0 + all_payment_attached = BuildDecisionBookPayments.query.filter( + BuildDecisionBookPayments.account_records_id == account_record.id, + BuildDecisionBookPayments.account_is_debit == False, + ).all() + + if all_payment_attached: + total_payment = sum([abs(row.payment_amount) for row in all_payment_attached]) + + # Always update the remainder_balance, even if no payments are attached + # This ensures we track unallocated funds properly + old_balance = account_record.remainder_balance + account_record.update(remainder_balance=total_payment) + account_record.save() + + return total_payment + + +def close_payment_book(payment_row_book, account_record, value, session): + BuildDecisionBookPayments.set_session(session) + new_row = BuildDecisionBookPayments.create( + ref_id=str(payment_row_book.uu_id), + payment_plan_time_periods=payment_row_book.payment_plan_time_periods, + period_time=payment_row_book.period_time, + currency=payment_row_book.currency, + account_records_id=account_record.id, + account_records_uu_id=str(account_record.uu_id), + build_parts_id=payment_row_book.build_parts_id, + build_parts_uu_id=str(payment_row_book.build_parts_uu_id), + payment_amount=value, + payment_types_id=payment_row_book.payment_types_id, + payment_types_uu_id=str(payment_row_book.payment_types_uu_id), + process_date_m=payment_row_book.process_date.month, + process_date_y=payment_row_book.process_date.year, + process_date=payment_row_book.process_date, + build_decision_book_item_id=payment_row_book.build_decision_book_item_id, + build_decision_book_item_uu_id=str(payment_row_book.build_decision_book_item_uu_id), + decision_book_project_id=payment_row_book.decision_book_project_id, + decision_book_project_uu_id=str(payment_row_book.decision_book_project_uu_id), + is_confirmed=True, + account_is_debit=False, + ) + return new_row.save() + + +def get_enums_from_database(): + build_dues_types = BuildDuesTypes() + with ApiEnumDropdown.new_session() as session: + + ApiEnumDropdown.set_session(session) + + debit_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-D").first() # Debit + add_debit_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-A").first() # Add Debit + renovation_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-R").first() # Renovation + late_payment_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-L").first() # Lawyer expence + service_fee_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-S").first() # Service fee + information_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-I").first() # Information + + build_dues_types.debit = ApiEnumDropdownShallowCopy( + debit_enum_shallow.id, str(debit_enum_shallow.uu_id), debit_enum_shallow.enum_class, debit_enum_shallow.key, debit_enum_shallow.value + ) + build_dues_types.add_debit = ApiEnumDropdownShallowCopy( + add_debit_enum_shallow.id, str(add_debit_enum_shallow.uu_id), add_debit_enum_shallow.enum_class, add_debit_enum_shallow.key, add_debit_enum_shallow.value + ) + build_dues_types.renovation = ApiEnumDropdownShallowCopy( + renovation_enum_shallow.id, str(renovation_enum_shallow.uu_id), renovation_enum_shallow.enum_class, renovation_enum_shallow.key, renovation_enum_shallow.value + ) + build_dues_types.lawyer_expence = ApiEnumDropdownShallowCopy( + late_payment_enum_shallow.id, str(late_payment_enum_shallow.uu_id), late_payment_enum_shallow.enum_class, late_payment_enum_shallow.key, late_payment_enum_shallow.value + ) + build_dues_types.service_fee = ApiEnumDropdownShallowCopy( + service_fee_enum_shallow.id, str(service_fee_enum_shallow.uu_id), service_fee_enum_shallow.enum_class, service_fee_enum_shallow.key, service_fee_enum_shallow.value + ) + build_dues_types.information = ApiEnumDropdownShallowCopy( + information_enum_shallow.id, str(information_enum_shallow.uu_id), information_enum_shallow.enum_class, information_enum_shallow.key, information_enum_shallow.value + ) + return [build_dues_types.debit, build_dues_types.lawyer_expence, build_dues_types.add_debit, build_dues_types.renovation, build_dues_types.service_fee, build_dues_types.information] + + +def payment_function(): + session_factory = get_session_factory() + session = session_factory() + # Set session for all models + AccountRecords.set_session(session) + BuildDecisionBookPayments.set_session(session) + order_pay = get_enums_from_database() + + # Get account records with positive currency_value regardless of remainder_balance + # This ensures accounts with unallocated funds are processed + account_records = AccountRecords.query.filter( + AccountRecords.build_parts_id.isnot(None), + AccountRecords.currency_value > 0, + AccountRecords.bank_date >= '2022-01-01' + ).order_by(AccountRecords.build_parts_id.desc()).all() + + start_time = time.time() + + for account_record in account_records: + incoming_total_money = abs(account_record.currency_value) + total_paid = refresh_book_payment(account_record) + available_fund = df_fund(incoming_total_money, total_paid) + + + # Calculate total debt and payment status for this account + total_debt, already_paid = calculate_total_debt_for_account(account_record.build_parts_id, session) + remaining_debt = total_debt - already_paid + + # Skip accounts with no debt and zero remainder balance + if remaining_debt <= 0 and account_record.remainder_balance == 0: + continue + + # Skip accounts with no available funds + if not available_fund > 0.0: + continue + + + process_date = datetime.now() + + # Try to pay current month first + for debit_type in order_pay: + amount_to_pay, debit_row = find_amount_to_pay( + build_parts_id=account_record.build_parts_id, + process_date=process_date, + session=session, + debit_type=debit_type + ) + + if amount_to_pay > 0 and debit_row: + if amount_to_pay >= available_fund: + close_payment_book( + payment_row_book=debit_row, + account_record=account_record, + value=available_fund, + session=session + ) + total_paid = refresh_book_payment(account_record) + available_fund = df_fund(incoming_total_money, total_paid) + else: + close_payment_book( + payment_row_book=debit_row, + account_record=account_record, + value=amount_to_pay, + session=session + ) + total_paid = refresh_book_payment(account_record) + available_fund = df_fund(incoming_total_money, total_paid) + + if not available_fund > 0.0: + continue + + # Try to pay previous unpaid debts + should_continue = False + for debit_type in order_pay: + debit_rows = find_master_payment_value_previous( + build_parts_id=account_record.build_parts_id, + process_date=process_date, + session=session, + debit_type=debit_type + ) + + if not debit_rows: + continue + + for debit_row in debit_rows: + amount_to_pay = calculate_paid_amount_for_master( + ref_id=debit_row.ref_id, + session=session, + debit_amount=debit_row.payment_amount + ) + + # Skip if already fully paid + if not amount_to_pay > 0: + continue + + if amount_to_pay >= available_fund: + close_payment_book( + payment_row_book=debit_row, + account_record=account_record, + value=available_fund, + session=session + ) + total_paid = refresh_book_payment(account_record) + available_fund = df_fund(incoming_total_money, total_paid) + should_continue = True + break + else: + close_payment_book( + payment_row_book=debit_row, + account_record=account_record, + value=amount_to_pay, + session=session + ) + total_paid = refresh_book_payment(account_record) + available_fund = df_fund(incoming_total_money, total_paid) + if should_continue or not available_fund > 0.0: + break + if not available_fund > 0.0: + continue # Changed from break to continue to process next account record + + +if __name__ == "__main__": + start_time = perf_counter() + payment_function() + end_time = perf_counter() + elapsed = end_time - start_time + print(f'{elapsed:.3f} : seconds') diff --git a/ServicesBank/Finder/Payment/draft/draft.py b/ServicesBank/Finder/Payment/draft/draft.py new file mode 100644 index 0000000..0800980 --- /dev/null +++ b/ServicesBank/Finder/Payment/draft/draft.py @@ -0,0 +1,493 @@ + +class AccountRecordsShallowCopy: + id: int + uuid: str + iban: str + currency_value: Decimal + remainder_balance: Decimal + bank_date: datetime + process_type: int + receive_debit: int + receive_debit_uuid: str + living_space_id: int + living_space_uuid: str + build_id: int + build_uuid: str + build_parts_id: int + build_parts_uuid: str + + +class BuildDecisionBookPaymentsShallowCopy: + + def __init__(self): + self.id: int + self.uuid: str + self.payment_plan_time_periods: str + self.process_date: datetime + self.payment_amount: float + self.currency: str + self.payment_types_id: int + self.payment_types_uu_id: str + self.period_time: str + self.process_date_y: int + self.process_date_m: int + self.build_decision_book_item_id: int + self.build_decision_book_item_uu_id: str + self.build_parts_id: int + self.build_parts_uu_id: str + self.decision_book_project_id: int + self.decision_book_project_uu_id: str + self.account_records_id: int + self.account_records_uu_id: str + + @classmethod + def convert_row_to_shallow_copy(cls, row: BuildDecisionBookPayments): + shallow_copy = cls() + shallow_copy.id = row.id + shallow_copy.uuid = str(row.uu_id) + shallow_copy.ref_id = str(row.ref_id) + shallow_copy.payment_plan_time_periods = row.payment_plan_time_periods + shallow_copy.process_date = row.process_date + shallow_copy.payment_amount = row.payment_amount + shallow_copy.currency = row.currency + shallow_copy.payment_types_id = row.payment_types_id + shallow_copy.payment_types_uu_id = str(row.payment_types_uu_id) + shallow_copy.period_time = row.period_time + shallow_copy.process_date_y = row.process_date_y + shallow_copy.process_date_m = row.process_date_m + shallow_copy.build_decision_book_item_id = row.build_decision_book_item_id + shallow_copy.build_decision_book_item_uu_id = str(row.build_decision_book_item_uu_id) + shallow_copy.build_parts_id = row.build_parts_id + shallow_copy.build_parts_uu_id = str(row.build_parts_uu_id) + shallow_copy.decision_book_project_id = row.decision_book_project_id + shallow_copy.decision_book_project_uu_id = str(row.decision_book_project_uu_id) + shallow_copy.account_records_id = row.account_records_id + shallow_copy.account_records_uu_id = str(row.account_records_uu_id) + return shallow_copy + + +class ApiEnumDropdownShallowCopy: + id: int + uuid: str + enum_class: str + key: str + value: str + + def __init__(self, id: int, uuid: str, enum_class: str, key: str, value: str): + self.id = id + self.uuid = uuid + self.enum_class = enum_class + self.key = key + self.value = value + + +class BuildDuesTypes: + + def __init__(self): + self.debit: ApiEnumDropdownShallowCopy = None + self.add_debit: ApiEnumDropdownShallowCopy = None + self.renovation: ApiEnumDropdownShallowCopy = None + self.lawyer_expence: ApiEnumDropdownShallowCopy = None + self.service_fee: ApiEnumDropdownShallowCopy = None + self.information: ApiEnumDropdownShallowCopy = None + + +class PaymentsRows: + + def __init__(self): + self.debit: list[BuildDecisionBookPaymentsShallowCopy] = [] + self.add_debit: list[BuildDecisionBookPaymentsShallowCopy] = [] + self.renovation: list[BuildDecisionBookPaymentsShallowCopy] = [] + self.lawyer_expence: list[BuildDecisionBookPaymentsShallowCopy] = [] + self.service_fee: list[BuildDecisionBookPaymentsShallowCopy] = [] + self.information: list[BuildDecisionBookPaymentsShallowCopy] = [] + + +class PaidRow: + + def __init__(self, uuid: str, amount: Decimal, closed: bool, left_payment: Decimal | None = None): + self.uuid: str = uuid + self.amount: Decimal = amount + self.closed: bool = closed + self.left_payment: Decimal = Decimal(0) + if not self.closed: + self.left_payment = left_payment + if not self.check_transaction_is_valid(): + raise ValueError(f"Record uuid: {self.uuid} tries to pay more than its debt in records.") + + def check_transaction_is_valid(self): + with BuildDecisionBookPayments.new_session() as session: + BuildDecisionBookPayments.set_session(session) + payment_row = BuildDecisionBookPayments.query.filter( + BuildDecisionBookPayments.ref_id == self.uuid, + cast(BuildDecisionBookPayments.uu_id, String) == cast(BuildDecisionBookPayments.ref_id, String), + BuildDecisionBookPayments.account_records_id.is_(None), + ).first() + if not payment_row: + return False + already_paid = BuildDecisionBookPayments.query.filter( + BuildDecisionBookPayments.ref_id == self.uuid, + cast(BuildDecisionBookPayments.uu_id, String) != cast(BuildDecisionBookPayments.ref_id, String), + BuildDecisionBookPayments.account_records_id.isnot(None), + ).all() + already_paid = sum([abs(row.payment_amount) for row in already_paid]) + left_amount = abs(payment_row.payment_amount) - abs(already_paid) + if left_amount < self.amount: + print(f"left_amount: {left_amount}, self.amount: {self.amount}. Record uuid: {self.uuid} tries to pay more than its debt in records.") + return False + return True + def get_dict(self): + return { + "uuid": self.uuid, + "amount": self.amount, + "closed": self.closed, + "left_payment": self.left_payment, + } + def __str__(self): + return f"{self.uuid} = Paid: {self.amount} Left: {self.left_payment}" + + +class PaymentActions: + + def __init__(self, initial_money: Decimal): + self.initial_money: Decimal = initial_money + self.consumed_money: Decimal = Decimal(0) + self.remaining_money: Decimal = self.initial_money + self.paid_list: list[PaidRow] = [] + + def is_money_consumed(self): + return self.consumed_money == self.initial_money + + def consume(self, payment_due: Decimal, payment_uuid: str): + left_payment = Decimal(0) + if self.remaining_money >= payment_due: + self.consumed_money += abs(payment_due) + self.remaining_money = abs(self.remaining_money) - abs(payment_due) + paid_row = PaidRow(payment_uuid, payment_due, True) + self.paid_list.append(paid_row) + elif self.remaining_money < payment_due: + self.consumed_money = self.remaining_money + self.remaining_money = Decimal(0) + left_payment = abs(payment_due) - abs(self.consumed_money) + paid_row = PaidRow(payment_uuid, self.consumed_money, False, left_payment) + self.paid_list.append(paid_row) + return left_payment + + +def row_iteration_account_records(): + shallow_copy_list = [] + build_dues_types = BuildDuesTypes() + + with ApiEnumDropdown.new_session() as session: + + ApiEnumDropdown.set_session(session) + + debit_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-D").first() # Debit + add_debit_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-A").first() # Add Debit + renovation_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-R").first() # Renovation + late_payment_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-L").first() # Lawyer expence + service_fee_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-S").first() # Service fee + information_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-I").first() # Information + + build_dues_types.debit = ApiEnumDropdownShallowCopy( + debit_enum_shallow.id, str(debit_enum_shallow.uu_id), debit_enum_shallow.enum_class, debit_enum_shallow.key, debit_enum_shallow.value + ) + build_dues_types.add_debit = ApiEnumDropdownShallowCopy( + add_debit_enum_shallow.id, str(add_debit_enum_shallow.uu_id), add_debit_enum_shallow.enum_class, add_debit_enum_shallow.key, add_debit_enum_shallow.value + ) + build_dues_types.renovation = ApiEnumDropdownShallowCopy( + renovation_enum_shallow.id, str(renovation_enum_shallow.uu_id), renovation_enum_shallow.enum_class, renovation_enum_shallow.key, renovation_enum_shallow.value + ) + build_dues_types.lawyer_expence = ApiEnumDropdownShallowCopy( + late_payment_enum_shallow.id, str(late_payment_enum_shallow.uu_id), late_payment_enum_shallow.enum_class, late_payment_enum_shallow.key, late_payment_enum_shallow.value + ) + build_dues_types.service_fee = ApiEnumDropdownShallowCopy( + service_fee_enum_shallow.id, str(service_fee_enum_shallow.uu_id), service_fee_enum_shallow.enum_class, service_fee_enum_shallow.key, service_fee_enum_shallow.value + ) + build_dues_types.information = ApiEnumDropdownShallowCopy( + information_enum_shallow.id, str(information_enum_shallow.uu_id), information_enum_shallow.enum_class, information_enum_shallow.key, information_enum_shallow.value + ) + + with AccountRecords.new_session() as session: + + AccountRecords.set_session(session) + account_records: list[AccountRecords] = AccountRecords.query.filter( + AccountRecords.approved_record == True, AccountRecords.living_space_id.isnot(None), + (AccountRecords.remainder_balance + AccountRecords.currency_value) > 0, + cast(AccountRecords.bank_date, Date) > cast("2022-01-01", Date), + # AccountRecords.currency_value > 0, + ).all() + + for account_record in account_records: + shallow_copy = AccountRecordsShallowCopy() + shallow_copy.id = account_record.id + shallow_copy.uuid = str(account_record.uu_id) + shallow_copy.iban = account_record.iban + shallow_copy.currency_value = account_record.currency_value + shallow_copy.remainder_balance = account_record.remainder_balance + shallow_copy.bank_date = arrow.get(account_record.bank_date).datetime + shallow_copy.process_type = account_record.process_type + shallow_copy.receive_debit = account_record.receive_debit + shallow_copy.receive_debit_uuid = str(account_record.receive_debit_uu_id) + shallow_copy.living_space_id = account_record.living_space_id + shallow_copy.living_space_uuid = str(account_record.living_space_uu_id) + shallow_copy.build_id = account_record.build_id + shallow_copy.build_uuid = str(account_record.build_uu_id) + shallow_copy.build_parts_id = account_record.build_parts_id + shallow_copy.build_parts_uuid = str(account_record.build_parts_uu_id) + shallow_copy_list.append(shallow_copy) + + return shallow_copy_list, build_dues_types + + +def check_payment_stage_debit(account_shallow_copy: AccountRecordsShallowCopy, build_dues_types: BuildDuesTypes, payments_rows: PaymentsRows, payment_actions: PaymentActions): + account_records_bank_date = account_shallow_copy.bank_date + for payment_row in payments_rows.debit: + payment_actions.consume(payment_row.payment_amount, payment_row.uuid) + if payment_actions.is_money_consumed(): + return + return + + +def check_payment_stage_add_debit(account_shallow_copy: AccountRecordsShallowCopy, build_dues_types: BuildDuesTypes, payments_rows: PaymentsRows, payment_actions: PaymentActions): + account_records_bank_date = account_shallow_copy.bank_date + for payment_row in payments_rows.add_debit: + payment_actions.consume(payment_row.payment_amount, payment_row.uuid) + if payment_actions.is_money_consumed(): + return + return + + +def check_payment_stage_renovation(account_shallow_copy: AccountRecordsShallowCopy, build_dues_types: BuildDuesTypes, payments_rows: PaymentsRows, payment_actions: PaymentActions): + account_records_bank_date = account_shallow_copy.bank_date + for payment_row in payments_rows.renovation: + payment_actions.consume(payment_row.payment_amount, payment_row.uuid) + if payment_actions.is_money_consumed(): + return + return + + +def check_payment_stage_lawyer_expence(account_shallow_copy: AccountRecordsShallowCopy, build_dues_types: BuildDuesTypes, payments_rows: PaymentsRows, payment_actions: PaymentActions): + account_records_bank_date = account_shallow_copy.bank_date + for payment_row in payments_rows.lawyer_expence: + payment_actions.consume(payment_row.payment_amount, payment_row.uuid) + if payment_actions.is_money_consumed(): + return + return + + +def check_payment_stage_service_fee(account_shallow_copy: AccountRecordsShallowCopy, build_dues_types: BuildDuesTypes, payments_rows: PaymentsRows, payment_actions: PaymentActions): + account_records_bank_date = account_shallow_copy.bank_date + for payment_row in payments_rows.service_fee: + payment_actions.consume(payment_row.payment_amount, payment_row.uuid) + if payment_actions.is_money_consumed(): + return + return + + +def check_payment_stage_information(account_shallow_copy: AccountRecordsShallowCopy, build_dues_types: BuildDuesTypes, payments_rows: PaymentsRows, payment_actions: PaymentActions): + account_records_bank_date = account_shallow_copy.bank_date + for payment_row in payments_rows.information: + payment_actions.consume(payment_row.payment_amount, payment_row.uuid) + if payment_actions.is_money_consumed(): + return + return + + +def close_account_records(account_shallow_copy: AccountRecordsShallowCopy, payment_actions: PaymentActions, records_to_close: int): + if payment_actions.is_money_consumed(): + print(f'payment_actions.is_money_consumed() : {payment_actions.is_money_consumed()}') + for paid_row in payment_actions.paid_list: + print(f'paid_row item : {paid_row.get_dict()}') + print(f'payment_actions.consumed_money : {payment_actions.consumed_money}') + print(f'payment_actions.initial_money : {payment_actions.initial_money}') + print(f'payment_actions.remaining_money : {payment_actions.remaining_money}') + + with BuildDecisionBookPayments.new_session() as session: + BuildDecisionBookPayments.set_session(session) + for payment_row in payment_actions.paid_list: + print(f'payment_row : {payment_row}') + payment_row_book = BuildDecisionBookPayments.query.filter( + BuildDecisionBookPayments.uu_id == payment_row.uuid, + cast(BuildDecisionBookPayments.ref_id, String) == cast(BuildDecisionBookPayments.uu_id, String), + BuildDecisionBookPayments.account_records_id.is_(None), + ).first() + if not payment_row_book: + raise ValueError(f"Payment row not found for uuid: {payment_row.uuid}") + new_row = BuildDecisionBookPayments.create( + ref_id=str(payment_row_book.uu_id), + payment_plan_time_periods=payment_row_book.payment_plan_time_periods, + period_time=payment_row_book.period_time, + currency=payment_row_book.currency, + account_records_id=account_shallow_copy.id, + account_records_uu_id=str(account_shallow_copy.uuid), + build_parts_id=payment_row_book.build_parts_id, + build_parts_uu_id=str(payment_row_book.build_parts_uu_id), + payment_amount=abs(payment_row.amount), + payment_types_id=payment_row_book.payment_types_id, + payment_types_uu_id=str(payment_row_book.payment_types_uu_id), + process_date_m=payment_row_book.process_date.month, + process_date_y=payment_row_book.process_date.year, + process_date=payment_row_book.process_date, + build_decision_book_item_id=payment_row_book.build_decision_book_item_id, + build_decision_book_item_uu_id=str(payment_row_book.build_decision_book_item_uu_id), + decision_book_project_id=payment_row_book.decision_book_project_id, + decision_book_project_uu_id=str(payment_row_book.decision_book_project_uu_id), + is_confirmed=True, + ) + new_row.save() + account_record = AccountRecords.query.filter_by(id=account_shallow_copy.id).first() + account_record.remainder_balance = - abs(account_record.remainder_balance) - abs(payment_actions.consumed_money) + account_record.save() + records_to_close += 1 + return records_to_close, True + return records_to_close, False + + +def any_function(shallow_copy_list, build_dues_types): + error_records, not_closed_records, records_to_close = 0, 0, 0 + shallow_copy_list, build_dues_types = row_iteration_account_records() + for index, shallow_copy in enumerate(shallow_copy_list): + initial_amount = abs(shallow_copy.currency_value) - abs(shallow_copy.remainder_balance) + if initial_amount == 0: + # print(f'AC: {shallow_copy.uuid} initial_amount : {initial_amount} must be greater than 0 to spend money on payments') + not_closed_records += 1 + continue + if initial_amount < 0: + print(f'AC: {shallow_copy.uuid} initial_amount : {initial_amount} wrong calculation is saved on account records Remainder Balance : {shallow_copy.remainder_balance} Currency Value : {shallow_copy.currency_value}') + error_records += 1 + continue + + payment_actions = PaymentActions(initial_amount) + # print(f'initial_amount : {initial_amount}') + payments_rows = PaymentsRows() + with BuildDecisionBookPayments.new_session() as session: + BuildDecisionBookPayments.set_session(session) + book_payments_query = ( + BuildDecisionBookPayments.process_date_m == shallow_copy.bank_date.month, BuildDecisionBookPayments.process_date_y == shallow_copy.bank_date.year, + BuildDecisionBookPayments.build_parts_id == shallow_copy.build_parts_id, BuildDecisionBookPayments.account_records_id.is_(None), + cast(BuildDecisionBookPayments.ref_id, String) == cast(BuildDecisionBookPayments.uu_id, String), + ) + query_db = BuildDecisionBookPayments.query.filter( + *book_payments_query, BuildDecisionBookPayments.payment_types_id == build_dues_types.debit.id).order_by(BuildDecisionBookPayments.process_date.desc()).all() + payments_rows.debit: list[BuildDecisionBookPaymentsShallowCopy] = [BuildDecisionBookPaymentsShallowCopy.convert_row_to_shallow_copy(row) for row in query_db] + query_db = BuildDecisionBookPayments.query.filter( + *book_payments_query, BuildDecisionBookPayments.payment_types_id == build_dues_types.add_debit.id).order_by(BuildDecisionBookPayments.process_date.desc()).all() + payments_rows.add_debit: list[BuildDecisionBookPaymentsShallowCopy] = [BuildDecisionBookPaymentsShallowCopy.convert_row_to_shallow_copy(row) for row in query_db] + query_db = BuildDecisionBookPayments.query.filter( + *book_payments_query, BuildDecisionBookPayments.payment_types_id == build_dues_types.renovation.id).order_by(BuildDecisionBookPayments.process_date.desc()).all() + payments_rows.renovation: list[BuildDecisionBookPaymentsShallowCopy] = [BuildDecisionBookPaymentsShallowCopy.convert_row_to_shallow_copy(row) for row in query_db] + query_db = BuildDecisionBookPayments.query.filter( + *book_payments_query, BuildDecisionBookPayments.payment_types_id == build_dues_types.lawyer_expence.id).order_by(BuildDecisionBookPayments.process_date.desc()).all() + payments_rows.lawyer_expence: list[BuildDecisionBookPaymentsShallowCopy] = [BuildDecisionBookPaymentsShallowCopy.convert_row_to_shallow_copy(row) for row in query_db] + query_db = BuildDecisionBookPayments.query.filter( + *book_payments_query, BuildDecisionBookPayments.payment_types_id == build_dues_types.service_fee.id).order_by(BuildDecisionBookPayments.process_date.desc()).all() + payments_rows.service_fee: list[BuildDecisionBookPaymentsShallowCopy] = [BuildDecisionBookPaymentsShallowCopy.convert_row_to_shallow_copy(row) for row in query_db] + query_db = BuildDecisionBookPayments.query.filter( + *book_payments_query, BuildDecisionBookPayments.payment_types_id == build_dues_types.information.id).order_by(BuildDecisionBookPayments.process_date.desc()).all() + payments_rows.information: list[BuildDecisionBookPaymentsShallowCopy] = [BuildDecisionBookPaymentsShallowCopy.convert_row_to_shallow_copy(row) for row in query_db] + + if len(payments_rows.debit) > 0: + # print(f'{shallow_copy.uuid} debit', len(payments_rows.debit)) + records_to_close += 1 + if len(payments_rows.add_debit) > 0: + # print(f'{shallow_copy.uuid} add_debit', len(payments_rows.add_debit)) + records_to_close += 1 + if len(payments_rows.renovation) > 0: + # print(f'{shallow_copy.uuid} renovation', len(payments_rows.renovation)) + records_to_close += 1 + if len(payments_rows.lawyer_expence) > 0: + # print(f'{shallow_copy.uuid} lawyer_expence', len(payments_rows.lawyer_expence)) + records_to_close += 1 + if len(payments_rows.service_fee) > 0: + # print(f'{shallow_copy.uuid} service_fee', len(payments_rows.service_fee)) + records_to_close += 1 + if len(payments_rows.information) > 0: + # print(f'{shallow_copy.uuid} information', len(payments_rows.information)) + records_to_close += 1 + + # continue + check_payment_stage_debit(account_shallow_copy=shallow_copy, build_dues_types=build_dues_types, payments_rows=payments_rows, payment_actions=payment_actions) + records_to_close, is_money_consumed = close_account_records(account_shallow_copy=shallow_copy, payment_actions=payment_actions, records_to_close=records_to_close) + if is_money_consumed: + continue + check_payment_stage_add_debit(account_shallow_copy=shallow_copy, build_dues_types=build_dues_types, payments_rows=payments_rows, payment_actions=payment_actions) + records_to_close, is_money_consumed = close_account_records(account_shallow_copy=shallow_copy, payment_actions=payment_actions, records_to_close=records_to_close) + if is_money_consumed: + continue + check_payment_stage_renovation(account_shallow_copy=shallow_copy, build_dues_types=build_dues_types, payments_rows=payments_rows, payment_actions=payment_actions) + records_to_close, is_money_consumed = close_account_records(account_shallow_copy=shallow_copy, payment_actions=payment_actions, records_to_close=records_to_close) + if is_money_consumed: + continue + check_payment_stage_lawyer_expence(account_shallow_copy=shallow_copy, build_dues_types=build_dues_types, payments_rows=payments_rows, payment_actions=payment_actions) + records_to_close, is_money_consumed = close_account_records(account_shallow_copy=shallow_copy, payment_actions=payment_actions, records_to_close=records_to_close) + if is_money_consumed: + continue + check_payment_stage_service_fee(account_shallow_copy=shallow_copy, build_dues_types=build_dues_types, payments_rows=payments_rows, payment_actions=payment_actions) + records_to_close, is_money_consumed = close_account_records(account_shallow_copy=shallow_copy, payment_actions=payment_actions, records_to_close=records_to_close) + if is_money_consumed: + continue + check_payment_stage_information(account_shallow_copy=shallow_copy, build_dues_types=build_dues_types, payments_rows=payments_rows, payment_actions=payment_actions) + records_to_close, is_money_consumed = close_account_records(account_shallow_copy=shallow_copy, payment_actions=payment_actions, records_to_close=records_to_close) + if is_money_consumed: + continue + + # with BuildDecisionBookPayments.new_session() as session: + # BuildDecisionBookPayments.set_session(session) + # book_payments_query = ( + # BuildDecisionBookPayments.process_date_m < shallow_copy.bank_date.month, BuildDecisionBookPayments.process_date_y == shallow_copy.bank_date.year, + # BuildDecisionBookPayments.build_parts_id == shallow_copy.build_parts_id, BuildDecisionBookPayments.account_records_id.is_(None), + # BuildDecisionBookPayments.ref_id == BuildDecisionBookPayments.uu_id, + # ) + # query_db = BuildDecisionBookPayments.query.filter( + # *book_payments_query, BuildDecisionBookPayments.payment_types_id == build_dues_types.debit.id).order_by(BuildDecisionBookPayments.process_date.desc()).all() + # payments_rows.debit: list[BuildDecisionBookPaymentsShallowCopy] = [BuildDecisionBookPaymentsShallowCopy.convert_row_to_shallow_copy(row) for row in query_db] + # query_db = BuildDecisionBookPayments.query.filter( + # *book_payments_query, BuildDecisionBookPayments.payment_types_id == build_dues_types.add_debit.id).order_by(BuildDecisionBookPayments.process_date.desc()).all() + # payments_rows.add_debit: list[BuildDecisionBookPaymentsShallowCopy] = [BuildDecisionBookPaymentsShallowCopy.convert_row_to_shallow_copy(row) for row in query_db] + # query_db = BuildDecisionBookPayments.query.filter( + # *book_payments_query, BuildDecisionBookPayments.payment_types_id == build_dues_types.renovation.id).order_by(BuildDecisionBookPayments.process_date.desc()).all() + # payments_rows.renovation: list[BuildDecisionBookPaymentsShallowCopy] = [BuildDecisionBookPaymentsShallowCopy.convert_row_to_shallow_copy(row) for row in query_db] + # query_db = BuildDecisionBookPayments.query.filter( + # *book_payments_query, BuildDecisionBookPayments.payment_types_id == build_dues_types.lawyer_expence.id).order_by(BuildDecisionBookPayments.process_date.desc()).all() + # payments_rows.lawyer_expence: list[BuildDecisionBookPaymentsShallowCopy] = [BuildDecisionBookPaymentsShallowCopy.convert_row_to_shallow_copy(row) for row in query_db] + # query_db = BuildDecisionBookPayments.query.filter( + # *book_payments_query, BuildDecisionBookPayments.payment_types_id == build_dues_types.service_fee.id).order_by(BuildDecisionBookPayments.process_date.desc()).all() + # payments_rows.service_fee: list[BuildDecisionBookPaymentsShallowCopy] = [BuildDecisionBookPaymentsShallowCopy.convert_row_to_shallow_copy(row) for row in query_db] + # query_db = BuildDecisionBookPayments.query.filter( + # *book_payments_query, BuildDecisionBookPayments.payment_types_id == build_dues_types.information.id).order_by(BuildDecisionBookPayments.process_date.desc()).all() + # payments_rows.information: list[BuildDecisionBookPaymentsShallowCopy] = [BuildDecisionBookPaymentsShallowCopy.convert_row_to_shallow_copy(row) for row in query_db] + + # check_payment_stage_debit(account_shallow_copy=shallow_copy, build_dues_types=build_dues_types, payments_rows=payments_rows, payment_actions=payment_actions) + # records_to_close, is_money_consumed = close_account_records(account_shallow_copy=shallow_copy, payment_actions=payment_actions, records_to_close=records_to_close) + # if is_money_consumed: + # continue + # check_payment_stage_add_debit(account_shallow_copy=shallow_copy, build_dues_types=build_dues_types, payments_rows=payments_rows, payment_actions=payment_actions) + # records_to_close, is_money_consumed = close_account_records(account_shallow_copy=shallow_copy, payment_actions=payment_actions, records_to_close=records_to_close) + # if is_money_consumed: + # continue + # check_payment_stage_renovation(account_shallow_copy=shallow_copy, build_dues_types=build_dues_types, payments_rows=payments_rows, payment_actions=payment_actions) + # records_to_close, is_money_consumed = close_account_records(account_shallow_copy=shallow_copy, payment_actions=payment_actions, records_to_close=records_to_close) + # if is_money_consumed: + # continue + # check_payment_stage_lawyer_expence(account_shallow_copy=shallow_copy, build_dues_types=build_dues_types, payments_rows=payments_rows, payment_actions=payment_actions) + # records_to_close, is_money_consumed = close_account_records(account_shallow_copy=shallow_copy, payment_actions=payment_actions, records_to_close=records_to_close) + # if is_money_consumed: + # continue + # check_payment_stage_service_fee(account_shallow_copy=shallow_copy, build_dues_types=build_dues_types, payments_rows=payments_rows, payment_actions=payment_actions) + # records_to_close, is_money_consumed = close_account_records(account_shallow_copy=shallow_copy, payment_actions=payment_actions, records_to_close=records_to_close) + # if is_money_consumed: + # continue + # check_payment_stage_information(account_shallow_copy=shallow_copy, build_dues_types=build_dues_types, payments_rows=payments_rows, payment_actions=payment_actions) + # records_to_close, is_money_consumed = close_account_records(account_shallow_copy=shallow_copy, payment_actions=payment_actions, records_to_close=records_to_close) + # if is_money_consumed: + # continue + +""" + build_decision_book_item_id, type=null, pos=10 + build_parts_id, type=null, pos=12 + payment_plan_time_periods, type=null, pos=1 + process_date, type=null, pos=2 + payment_types_id, type=null, pos=5 + account_records_id, type=null, pos=16 +""" \ No newline at end of file diff --git a/ServicesBank/Finder/Payment/draft/second.py b/ServicesBank/Finder/Payment/draft/second.py new file mode 100644 index 0000000..29b52ce --- /dev/null +++ b/ServicesBank/Finder/Payment/draft/second.py @@ -0,0 +1,348 @@ +import arrow +from decimal import Decimal +from datetime import datetime, timedelta +from Schemas import BuildDecisionBookPayments, AccountRecords, ApiEnumDropdown + +from time import perf_counter +from sqlalchemy import cast, Date, String +from Controllers.Postgres.engine import get_session_factory +# from ServicesApi.Schemas.account.account import AccountRecords +# from ServicesApi.Schemas.building.decision_book import BuildDecisionBookPayments + +class BuildDuesTypes: + + def __init__(self): + self.debit: ApiEnumDropdownShallowCopy = None + self.add_debit: ApiEnumDropdownShallowCopy = None + self.renovation: ApiEnumDropdownShallowCopy = None + self.lawyer_expence: ApiEnumDropdownShallowCopy = None + self.service_fee: ApiEnumDropdownShallowCopy = None + self.information: ApiEnumDropdownShallowCopy = None + + +class ApiEnumDropdownShallowCopy: + id: int + uuid: str + enum_class: str + key: str + value: str + + def __init__(self, id: int, uuid: str, enum_class: str, key: str, value: str): + self.id = id + self.uuid = uuid + self.enum_class = enum_class + self.key = key + self.value = value + + +def find_master_payment_value(build_parts_id: int, process_date: datetime, session, debit_type): + BuildDecisionBookPayments.set_session(session) + book_payments_query = ( + BuildDecisionBookPayments.process_date_m == process_date.month, BuildDecisionBookPayments.process_date_y == process_date.year, + BuildDecisionBookPayments.build_parts_id == build_parts_id, BuildDecisionBookPayments.account_records_id.is_(None), + BuildDecisionBookPayments.account_is_debit == True, BuildDecisionBookPayments.payment_types_id == debit_type.id, + ) + debit_row = BuildDecisionBookPayments.query.filter(*book_payments_query).order_by(BuildDecisionBookPayments.process_date.desc()).first() + if not debit_row: + print(f'No record of master payment is found for :{process_date.strftime("%Y-%m-%d %H:%M:%S")} ') + return 0, None, None + return abs(debit_row.payment_amount), debit_row, str(debit_row.ref_id) + + +def calculate_paid_amount_for_master(ref_id: str, session, debit_amount): + """Calculate how much has been paid for a given payment reference. + + Args: + ref_id: The reference ID to check payments for + session: Database session + debit_amount: Original debit amount + + Returns: + float: Remaining amount to pay (debit_amount - total_paid) + """ + BuildDecisionBookPayments.set_session(session) + paid_rows = BuildDecisionBookPayments.query.filter( + BuildDecisionBookPayments.ref_id == ref_id, + BuildDecisionBookPayments.account_records_id.isnot(None), + BuildDecisionBookPayments.account_is_debit == False + ).order_by(BuildDecisionBookPayments.process_date.desc()).all() + + if not paid_rows: + return debit_amount + + total_paid = sum([abs(paid_row.payment_amount) for paid_row in paid_rows]) + remaining = abs(debit_amount) - abs(total_paid) + return remaining + + +def find_master_payment_value_previous(build_parts_id: int, process_date: datetime, session, debit_type): + BuildDecisionBookPayments.set_session(session) + parse_process_date = datetime(process_date.year, process_date.month, 1) - timedelta(days=1) + book_payments_query = ( + BuildDecisionBookPayments.process_date < parse_process_date, + BuildDecisionBookPayments.build_parts_id == build_parts_id, BuildDecisionBookPayments.account_records_id.is_(None), + BuildDecisionBookPayments.account_is_debit == True, BuildDecisionBookPayments.payment_types_id == debit_type.id, + ) + debit_rows = BuildDecisionBookPayments.query.filter(*book_payments_query).order_by(BuildDecisionBookPayments.process_date.desc()).all() + return debit_rows + + +def find_amount_to_pay(build_parts_id: int, process_date: datetime, session, debit_type): + # debit -negative value that need to be pay + debit, debit_row, debit_row_ref_id = find_master_payment_value(build_parts_id=build_parts_id, process_date=process_date, session=session, debit_type=debit_type) + # Is there any payment done for this ref_id ? + return calculate_paid_amount_for_master(ref_id=debit_row_ref_id, session=session, debit_amount=debit), debit_row + + +def calculate_total_debt_for_account(build_parts_id: int, session): + """Calculate the total debt and total paid amount for an account regardless of process date.""" + BuildDecisionBookPayments.set_session(session) + + # Get all debits for this account + all_debits = BuildDecisionBookPayments.query.filter( + BuildDecisionBookPayments.build_parts_id == build_parts_id, + BuildDecisionBookPayments.account_records_id.is_(None), + BuildDecisionBookPayments.account_is_debit == True + ).all() + + total_debt = sum([abs(debit.payment_amount) for debit in all_debits]) + + # Get all payments for this account's debits + total_paid = 0 + for debit in all_debits: + payments = BuildDecisionBookPayments.query.filter( + BuildDecisionBookPayments.ref_id == debit.ref_id, + BuildDecisionBookPayments.account_is_debit == False + ).all() + + if payments: + total_paid += sum([abs(payment.payment_amount) for payment in payments]) + + return total_debt, total_paid + + +def refresh_book_payment(account_record: AccountRecords): + """Update the remainder_balance of an account record based on attached payments. + + This function calculates the total of all payments attached to an account record + and updates the remainder_balance field accordingly. The remainder_balance represents + funds that have been received but not yet allocated to specific debits. + + Args: + account_record: The account record to update + + Returns: + float: The total payment amount + """ + total_payment = 0 + all_payment_attached = BuildDecisionBookPayments.query.filter( + BuildDecisionBookPayments.account_records_id == account_record.id, + BuildDecisionBookPayments.account_is_debit == False, + ).all() + + if all_payment_attached: + total_payment = sum([abs(row.payment_amount) for row in all_payment_attached]) + + old_balance = account_record.remainder_balance + account_record.update(remainder_balance=total_payment) + account_record.save() + + # Only print if there's a change in balance + if old_balance != total_payment: + print(f"Account {account_record.id}: Updated remainder_balance {old_balance} → {total_payment}") + + return total_payment + + +def close_payment_book(payment_row_book, account_record, value, session): + BuildDecisionBookPayments.set_session(session) + new_row = BuildDecisionBookPayments.create( + ref_id=str(payment_row_book.uu_id), + payment_plan_time_periods=payment_row_book.payment_plan_time_periods, + period_time=payment_row_book.period_time, + currency=payment_row_book.currency, + account_records_id=account_record.id, + account_records_uu_id=str(account_record.uu_id), + build_parts_id=payment_row_book.build_parts_id, + build_parts_uu_id=str(payment_row_book.build_parts_uu_id), + payment_amount=value, + payment_types_id=payment_row_book.payment_types_id, + payment_types_uu_id=str(payment_row_book.payment_types_uu_id), + process_date_m=payment_row_book.process_date.month, + process_date_y=payment_row_book.process_date.year, + process_date=payment_row_book.process_date, + build_decision_book_item_id=payment_row_book.build_decision_book_item_id, + build_decision_book_item_uu_id=str(payment_row_book.build_decision_book_item_uu_id), + decision_book_project_id=payment_row_book.decision_book_project_id, + decision_book_project_uu_id=str(payment_row_book.decision_book_project_uu_id), + is_confirmed=True, + account_is_debit=False, + ) + return new_row.save() + + +def get_enums_from_database(): + build_dues_types = BuildDuesTypes() + with ApiEnumDropdown.new_session() as session: + + ApiEnumDropdown.set_session(session) + + debit_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-D").first() # Debit + add_debit_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-A").first() # Add Debit + renovation_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-R").first() # Renovation + late_payment_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-L").first() # Lawyer expence + service_fee_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-S").first() # Service fee + information_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-I").first() # Information + + build_dues_types.debit = ApiEnumDropdownShallowCopy( + debit_enum_shallow.id, str(debit_enum_shallow.uu_id), debit_enum_shallow.enum_class, debit_enum_shallow.key, debit_enum_shallow.value + ) + build_dues_types.add_debit = ApiEnumDropdownShallowCopy( + add_debit_enum_shallow.id, str(add_debit_enum_shallow.uu_id), add_debit_enum_shallow.enum_class, add_debit_enum_shallow.key, add_debit_enum_shallow.value + ) + build_dues_types.renovation = ApiEnumDropdownShallowCopy( + renovation_enum_shallow.id, str(renovation_enum_shallow.uu_id), renovation_enum_shallow.enum_class, renovation_enum_shallow.key, renovation_enum_shallow.value + ) + build_dues_types.lawyer_expence = ApiEnumDropdownShallowCopy( + late_payment_enum_shallow.id, str(late_payment_enum_shallow.uu_id), late_payment_enum_shallow.enum_class, late_payment_enum_shallow.key, late_payment_enum_shallow.value + ) + build_dues_types.service_fee = ApiEnumDropdownShallowCopy( + service_fee_enum_shallow.id, str(service_fee_enum_shallow.uu_id), service_fee_enum_shallow.enum_class, service_fee_enum_shallow.key, service_fee_enum_shallow.value + ) + build_dues_types.information = ApiEnumDropdownShallowCopy( + information_enum_shallow.id, str(information_enum_shallow.uu_id), information_enum_shallow.enum_class, information_enum_shallow.key, information_enum_shallow.value + ) + return [build_dues_types.debit, build_dues_types.lawyer_expence, build_dues_types.add_debit, build_dues_types.renovation, build_dues_types.service_fee, build_dues_types.information] + + +def payment_function(): + session_factory = get_session_factory() + session = session_factory() + ApiEnumDropdown.set_session(session) + order_pay = ApiEnumDropdown.query.filter(ApiEnumDropdown.enum_type == 'order_pay').order_by(ApiEnumDropdown.id.asc()).all() + + # Get account records with positive currency_value regardless of remainder_balance + # This ensures accounts with unallocated funds are processed + account_records = AccountRecords.query.filter( + AccountRecords.build_parts_id.isnot(None), + AccountRecords.currency_value > 0, + AccountRecords.bank_date >= '2022-01-01' + ).order_by(AccountRecords.build_parts_id.desc()).all() + + start_time = time.time() + + for account_record in account_records: + incoming_total_money = abs(account_record.currency_value) + total_paid = refresh_book_payment(account_record) + available_fund = df_fund(incoming_total_money, total_paid) + + # Calculate total debt and payment status for this account + total_debt, already_paid = calculate_total_debt_for_account(account_record.build_parts_id, session) + remaining_debt = total_debt - already_paid + + # Log account status with debt information + if remaining_debt <= 0 and account_record.remainder_balance == 0: + # Skip accounts with no debt and zero remainder balance + continue + + if not available_fund > 0.0: + # Skip accounts with no available funds + continue + + process_date = datetime.now() + + # Try to pay current month first + for debit_type in order_pay: + amount_to_pay, debit_row = find_amount_to_pay( + build_parts_id=account_record.build_parts_id, + process_date=process_date, + session=session, + debit_type=debit_type + ) + + if amount_to_pay > 0 and debit_row: + if amount_to_pay >= available_fund: + close_payment_book( + payment_row_book=debit_row, + account_record=account_record, + value=available_fund, + session=session + ) + total_paid = refresh_book_payment(account_record) + available_fund = df_fund(incoming_total_money, total_paid) + else: + close_payment_book( + payment_row_book=debit_row, + account_record=account_record, + value=amount_to_pay, + session=session + ) + total_paid = refresh_book_payment(account_record) + available_fund = df_fund(incoming_total_money, total_paid) + + if not available_fund > 0.0: + continue + + # Try to pay previous unpaid debts + should_continue = False + for debit_type in order_pay: + debit_rows = find_master_payment_value_previous( + build_parts_id=account_record.build_parts_id, + process_date=process_date, + session=session, + debit_type=debit_type + ) + + if not debit_rows: + continue + + for debit_row in debit_rows: + amount_to_pay = calculate_paid_amount_for_master( + ref_id=debit_row.ref_id, + session=session, + debit_amount=debit_row.payment_amount + ) + + # Skip if already fully paid + if not amount_to_pay > 0: + continue + + if amount_to_pay >= available_fund: + close_payment_book( + payment_row_book=debit_row, + account_record=account_record, + value=available_fund, + session=session + ) + total_paid = refresh_book_payment(account_record) + available_fund = df_fund(incoming_total_money, total_paid) + should_continue = True + break + else: + close_payment_book( + payment_row_book=debit_row, + account_record=account_record, + value=amount_to_pay, + session=session + ) + total_paid = refresh_book_payment(account_record) + available_fund = df_fund(incoming_total_money, total_paid) + + print(f"Account {account_record.id}: {available_fund} funds remaining after payment") + if should_continue or not available_fund > 0.0: + break + if not available_fund > 0.0: + continue # Changed from break to continue to process next account record + + +if __name__ == "__main__": + + start_time = perf_counter() + payment_function() + end_time = perf_counter() + elapsed = end_time - start_time + print(f'{elapsed:.3f} : seconds') + # print(f'not_closed_records : {not_closed_records}') + # print(f'error_records : {error_records}') + # print(f'records_to_close : {records_to_close}') + # print(f"Total: {not_closed_records + error_records + records_to_close} / {len(shallow_copy_list)}") diff --git a/ServicesBank/Finder/Payment/draft/update_remainder_balance.py b/ServicesBank/Finder/Payment/draft/update_remainder_balance.py new file mode 100644 index 0000000..b65da17 --- /dev/null +++ b/ServicesBank/Finder/Payment/draft/update_remainder_balance.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from sqlalchemy import create_engine, func +from sqlalchemy.orm import sessionmaker +from sqlalchemy.ext.declarative import declarative_base +import os +from datetime import datetime +import time + +# Get database connection details from environment variables +DB_HOST = os.environ.get('DB_HOST', 'postgres') +DB_PORT = os.environ.get('DB_PORT', '5432') +DB_NAME = os.environ.get('DB_NAME', 'evyos') +DB_USER = os.environ.get('DB_USER', 'evyos') +DB_PASS = os.environ.get('DB_PASS', 'evyos') + +# Create SQLAlchemy engine and session +engine = create_engine(f'postgresql://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}') +Session = sessionmaker(bind=engine) +session = Session() + +print("\n" + "=" * 50) +print("UPDATING ACCOUNTS WITH ZERO REMAINDER_BALANCE") +print("=" * 50) + +try: + # First, get the count of accounts that need updating + count_query = """SELECT COUNT(*) FROM account_records + WHERE build_parts_id IS NOT NULL + AND currency_value > 0 + AND (remainder_balance = 0 OR remainder_balance IS NULL) + AND bank_date >= '2022-01-01'""" + count_result = session.execute(count_query).scalar() + print(f"Found {count_result} accounts with zero remainder_balance") + + # Then update all those accounts + update_query = """UPDATE account_records + SET remainder_balance = currency_value + WHERE build_parts_id IS NOT NULL + AND currency_value > 0 + AND (remainder_balance = 0 OR remainder_balance IS NULL) + AND bank_date >= '2022-01-01'""" + result = session.execute(update_query) + session.commit() + print(f"Updated {result.rowcount} accounts with zero remainder_balance") + + # Verify the update + verify_query = """SELECT COUNT(*) FROM account_records + WHERE build_parts_id IS NOT NULL + AND currency_value > 0 + AND (remainder_balance = 0 OR remainder_balance IS NULL) + AND bank_date >= '2022-01-01'""" + verify_result = session.execute(verify_query).scalar() + print(f"Remaining accounts with zero remainder_balance: {verify_result}") + +except Exception as e: + print(f"Error updating accounts: {str(e)}") + session.rollback() +finally: + session.close() + +print("=" * 50) diff --git a/ServicesBank/Finder/Payment/entrypoint.sh b/ServicesBank/Finder/Payment/entrypoint.sh new file mode 100644 index 0000000..6508655 --- /dev/null +++ b/ServicesBank/Finder/Payment/entrypoint.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Create environment file that will be available to cron jobs +echo "POSTGRES_USER=\"$POSTGRES_USER\"" >> /env.sh +echo "POSTGRES_PASSWORD=\"$POSTGRES_PASSWORD\"" >> /env.sh +echo "POSTGRES_DB=\"$POSTGRES_DB\"" >> /env.sh +echo "POSTGRES_HOST=\"$POSTGRES_HOST\"" >> /env.sh +echo "POSTGRES_PORT=$POSTGRES_PORT" >> /env.sh +echo "POSTGRES_ENGINE=\"$POSTGRES_ENGINE\"" >> /env.sh +echo "POSTGRES_POOL_PRE_PING=\"$POSTGRES_POOL_PRE_PING\"" >> /env.sh +echo "POSTGRES_POOL_SIZE=$POSTGRES_POOL_SIZE" >> /env.sh +echo "POSTGRES_MAX_OVERFLOW=$POSTGRES_MAX_OVERFLOW" >> /env.sh +echo "POSTGRES_POOL_RECYCLE=$POSTGRES_POOL_RECYCLE" >> /env.sh +echo "POSTGRES_POOL_TIMEOUT=$POSTGRES_POOL_TIMEOUT" >> /env.sh +echo "POSTGRES_ECHO=\"$POSTGRES_ECHO\"" >> /env.sh + +# Add Python environment variables +echo "PYTHONPATH=/" >> /env.sh +echo "PYTHONUNBUFFERED=1" >> /env.sh +echo "PYTHONDONTWRITEBYTECODE=1" >> /env.sh + +# Make the environment file available to cron +echo "*/30 * * * * /run_app.sh >> /var/log/cron.log 2>&1" > /tmp/crontab_list +crontab /tmp/crontab_list + +# Start cron +cron + +# Tail the log file +tail -f /var/log/cron.log diff --git a/ServicesBank/Finder/Payment/old_runner.py b/ServicesBank/Finder/Payment/old_runner.py new file mode 100644 index 0000000..8599ff8 --- /dev/null +++ b/ServicesBank/Finder/Payment/old_runner.py @@ -0,0 +1,326 @@ +import sys +import arrow +from decimal import Decimal +from Schemas import BuildDecisionBookPayments, AccountRecords, ApiEnumDropdown + +# Counters for tracking conditions +counter_current_currency_not_positive = 0 # Track when current_currency_value <= 0 +counter_net_amount_not_positive = 0 # Track when net_amount <= 0 +counter_account_records_updated = 0 # Track number of account records updated +counter_payments_found = 0 # Track how many payments were found +counter_found_payment_skips = 0 # Track how many times we skip due to found_payment +counter_payment_exceptions = 0 # Track exceptions during payment processing +counter_missing_build_parts_id = 0 # Track accounts with missing build_parts_id +counter_null_payment_types = 0 # Track payments with null payment_types_id + + +def pay_the_registration(account_record, receive_enum, debit_enum, is_old_record: bool = False, session=None): + # If no session is provided, create a new one + if session is None: + with AccountRecords.new_session() as new_session: + AccountRecords.set_session(new_session) + BuildDecisionBookPayments.set_session(new_session) + return _process_payment(account_record, receive_enum, debit_enum, is_old_record, new_session) + else: + # Use the provided session + AccountRecords.set_session(session) + BuildDecisionBookPayments.set_session(session) + return _process_payment(account_record, receive_enum, debit_enum, is_old_record, session) + + +def _process_payment(account_record, receive_enum, debit_enum, is_old_record, session): + """Internal function to process payments with a given session""" + current_currency_value = float(Decimal(account_record.currency_value)) - float(Decimal(account_record.remainder_balance)) + if not current_currency_value > 0: + global counter_current_currency_not_positive + counter_current_currency_not_positive += 1 + return current_currency_value + + # Check if account_record has build_parts_id + if account_record.build_parts_id is None: + global counter_missing_build_parts_id + counter_missing_build_parts_id += 1 + return current_currency_value + + process_date = arrow.get(account_record.bank_date) + account_bank_date_year, account_bank_date_month = (process_date.date().year, process_date.date().month) + + # First, try to find payments with null payment_types_id + payment_arguments_debit = [ + BuildDecisionBookPayments.build_parts_id == account_record.build_parts_id, + BuildDecisionBookPayments.account_records_id == None, + ] + + # Add date filters if not processing old records + if not is_old_record: + payment_arguments_debit.extend([BuildDecisionBookPayments.process_date_y == int(account_bank_date_year), BuildDecisionBookPayments.process_date_m == int(account_bank_date_month)]) + + # First try with debit_enum.id + payments = BuildDecisionBookPayments.query.filter(*payment_arguments_debit, BuildDecisionBookPayments.payment_types_id == debit_enum.id).order_by(BuildDecisionBookPayments.process_date.asc()).all() + + # If no payments found, try with null payment_types_id + if len(payments) == 0: + payments = BuildDecisionBookPayments.query.filter(*payment_arguments_debit, BuildDecisionBookPayments.payment_types_id == None).order_by(BuildDecisionBookPayments.process_date.asc()).all() + if len(payments) > 0: + global counter_null_payment_types + counter_null_payment_types += len(payments) + + global counter_payments_found + counter_payments_found += len(payments) + + # Debug: Print info about the first few payments found (if any) + if len(payments) > 0 and account_record.id % 100 == 0: # Only print for every 100th record to avoid too much output + print(f"DEBUG: Found {len(payments)} payments for account_record {account_record.id}") + if len(payments) > 0: + sample_payment = payments[0] + print(f" Sample payment: ID={getattr(sample_payment, 'id', 'N/A')}, amount={getattr(sample_payment, 'payment_amount', 'N/A')}") + + if len(payments) == 0: + # No payments found for this account record + return current_currency_value + for payment in payments: + if not current_currency_value > 0: + return current_currency_value + payment_arguments_receive = [ + BuildDecisionBookPayments.build_parts_id == account_record.build_parts_id, + BuildDecisionBookPayments.payment_plan_time_periods == payment.payment_plan_time_periods, + BuildDecisionBookPayments.payment_types_id == receive_enum.id, + BuildDecisionBookPayments.build_decision_book_item_id == payment.build_decision_book_item_id, + BuildDecisionBookPayments.decision_book_project_id == payment.decision_book_project_id, + BuildDecisionBookPayments.process_date == payment.process_date, + ] + if not is_old_record: + payment_arguments_receive.extend([BuildDecisionBookPayments.process_date_y == int(account_bank_date_year), BuildDecisionBookPayments.process_date_m == int(account_bank_date_month)]) + + payment_received = BuildDecisionBookPayments.query.filter(*payment_arguments_receive).all() + sum_of_payment_received = sum([abs(payment.payment_amount) for payment in payment_received]) + net_amount = float(abs(Decimal(payment.payment_amount))) - float(abs(Decimal(sum_of_payment_received))) + if not net_amount > 0: + global counter_net_amount_not_positive + counter_net_amount_not_positive += 1 + continue + if float(abs(current_currency_value)) < float(abs(net_amount)): + net_amount = float(current_currency_value) + process_date = arrow.get(payment.process_date) + try: + found_payment = BuildDecisionBookPayments.query.filter_by( + build_parts_id=payment.build_parts_id, payment_plan_time_periods=payment.payment_plan_time_periods, + payment_types_id=receive_enum.id, build_decision_book_item_id=payment.build_decision_book_item_id, + decision_book_project_id=payment.decision_book_project_id, process_date=str(process_date), + ).first() + if found_payment: + global counter_found_payment_skips + counter_found_payment_skips += 1 + continue + created_book_payment = BuildDecisionBookPayments.create( + payment_plan_time_periods=payment.payment_plan_time_periods, payment_amount=float(abs(net_amount)), + payment_types_id=receive_enum.id, payment_types_uu_id=str(receive_enum.uu_id), + process_date=str(process_date), process_date_m=process_date.date().month, process_date_y=process_date.date().year, + period_time=f"{process_date.year}-{str(process_date.month).zfill(2)}", build_parts_id=payment.build_parts_id, + build_parts_uu_id=str(payment.build_parts_uu_id), account_records_id=account_record.id, + account_records_uu_id=str(account_record.uu_id), build_decision_book_item_id=payment.build_decision_book_item_id, + build_decision_book_item_uu_id=str(payment.build_decision_book_item_uu_id), decision_book_project_id=payment.decision_book_project_id, + decision_book_project_uu_id=str(payment.decision_book_project_uu_id), + ) + created_book_payment.save() + created_payment_amount = float(Decimal(created_book_payment.payment_amount)) + remainder_balance = float(Decimal(account_record.remainder_balance)) + float(abs(created_payment_amount)) + account_record.update(remainder_balance=remainder_balance) + account_record.save() + + global counter_account_records_updated + counter_account_records_updated += 1 + if current_currency_value >= abs(net_amount): + current_currency_value -= abs(net_amount) + except Exception as e: + print("Exception of decision payment:", e) + global counter_payment_exceptions + counter_payment_exceptions += 1 + return current_currency_value + + +def create_direct_payment(account_record, receive_enum, session): + """ + Create a direct payment record for an account record without relying on matching BuildDecisionBookPayments + """ + try: + # Calculate the amount to process + payment_amount = float(Decimal(account_record.currency_value)) - float(Decimal(account_record.remainder_balance)) + if payment_amount <= 0: + return False + + # Get process date information + process_date = arrow.get(account_record.bank_date) + process_date_y = process_date.date().year + process_date_m = process_date.date().month + period_time = f"{process_date_y}-{str(process_date_m).zfill(2)}" + + # Check if a payment already exists for this account record + existing_payment = BuildDecisionBookPayments.query.filter_by(account_records_id=account_record.id, payment_types_id=receive_enum.id).first() + if existing_payment: # Skip if payment already exists + return False + + # Create a new payment record directly + created_book_payment = BuildDecisionBookPayments.create( + payment_plan_time_periods=1, # Default value + payment_types_id=receive_enum.id, payment_types_uu_id=str(receive_enum.uu_id), payment_amount=payment_amount, process_date=str(process_date), process_date_y=process_date_y, + process_date_m=process_date_m, period_time=period_time, account_records_id=account_record.id, account_records_uu_id=str(account_record.uu_id), + build_parts_id=account_record.build_parts_id, build_parts_uu_id=str(account_record.build_parts_uu_id) if hasattr(account_record, 'build_parts_uu_id') and account_record.build_parts_uu_id else None, + decision_book_project_id=getattr(account_record, 'decision_book_project_id', None), + decision_book_project_uu_id=str(account_record.decision_book_project_uu_id) if hasattr(account_record, 'decision_book_project_uu_id') and account_record.decision_book_project_uu_id else None, + ) + created_book_payment.save() + + # Update the account record + remainder_balance = float(Decimal(account_record.remainder_balance)) + float(abs(payment_amount)) + account_record.update(remainder_balance=remainder_balance) + account_record.save() + + global counter_account_records_updated + counter_account_records_updated += 1 + return True + except Exception as e: + print(f"Exception in create_direct_payment for account {account_record.id}: {e}") + global counter_payment_exceptions + counter_payment_exceptions += 1 + return False + + +def send_accounts_to_decision_payment(): + with ApiEnumDropdown.new_session() as session: + # Set the session for all models that will be used + ApiEnumDropdown.set_session(session) + AccountRecords.set_session(session) + BuildDecisionBookPayments.set_session(session) + + try: + # Get required enum values + receive_enum = ApiEnumDropdown.query.filter_by(enum_class="DebitTypes", key="DT-R").first() + debit_enum = ApiEnumDropdown.query.filter_by(enum_class="DebitTypes", key="DT-D").first() + if not receive_enum or not debit_enum: + print("Error: Could not find required enum values") + return + + # Check if there are any BuildDecisionBookPayments records at all + total_payments = BuildDecisionBookPayments.query.count() + print(f"\n--- DEBUG: Database Statistics ---") + print(f"Total BuildDecisionBookPayments records in database: {total_payments}") + + # Check how many have payment_types_id = debit_enum.id + debit_payments = BuildDecisionBookPayments.query.filter_by(payment_types_id=debit_enum.id).count() + print(f"BuildDecisionBookPayments with payment_types_id={debit_enum.id} (DT-D): {debit_payments}") + + # Check how many have account_records_id = None + null_account_payments = BuildDecisionBookPayments.query.filter(BuildDecisionBookPayments.account_records_id == None).count() + print(f"BuildDecisionBookPayments with account_records_id=None: {null_account_payments}") + + # Check a sample payment record + sample_payment = BuildDecisionBookPayments.query.first() + if sample_payment: + print("\n--- Sample BuildDecisionBookPayment ---") + print(f"ID: {getattr(sample_payment, 'id', 'N/A')}") + print(f"payment_types_id: {getattr(sample_payment, 'payment_types_id', 'N/A')}") + print(f"build_parts_id: {getattr(sample_payment, 'build_parts_id', 'N/A')}") + print(f"account_records_id: {getattr(sample_payment, 'account_records_id', 'N/A')}") + print(f"process_date_y: {getattr(sample_payment, 'process_date_y', 'N/A')}") + print(f"process_date_m: {getattr(sample_payment, 'process_date_m', 'N/A')}") + else: + print("No BuildDecisionBookPayment records found in the database!") + + # Check a sample account record + sample_account = AccountRecords.query.filter(AccountRecords.remainder_balance < AccountRecords.currency_value, AccountRecords.receive_debit == receive_enum.id).first() + if sample_account: + print("\n--- Sample AccountRecord ---") + print(f"ID: {getattr(sample_account, 'id', 'N/A')}") + print(f"build_parts_id: {getattr(sample_account, 'build_parts_id', 'N/A')}") + print(f"bank_date: {getattr(sample_account, 'bank_date', 'N/A')}") + + # Try to find payments for this specific account record + if sample_account.bank_date: + process_date = arrow.get(sample_account.bank_date) + account_bank_date_year, account_bank_date_month = (process_date.date().year, process_date.date().month) + + print("\n--- Checking for payments for sample account ---") + print(f"Looking for payments with build_parts_id={sample_account.build_parts_id}, payment_types_id={debit_enum.id}, account_records_id=None") + + # Try without date filters first + basic_payments = BuildDecisionBookPayments.query.filter( + BuildDecisionBookPayments.build_parts_id == sample_account.build_parts_id, BuildDecisionBookPayments.payment_types_id == debit_enum.id, + BuildDecisionBookPayments.account_records_id == None + ).count() + print(f"Found {basic_payments} payments without date filters") + + # Now try with date filters + dated_payments = BuildDecisionBookPayments.query.filter( + BuildDecisionBookPayments.build_parts_id == sample_account.build_parts_id, BuildDecisionBookPayments.payment_types_id == debit_enum.id, + BuildDecisionBookPayments.account_records_id == None, BuildDecisionBookPayments.process_date_y == int(account_bank_date_year), + BuildDecisionBookPayments.process_date_m == int(account_bank_date_month) + ).count() + print(f"Found {dated_payments} payments with date filters (year={account_bank_date_year}, month={account_bank_date_month})") + else: + print("No matching AccountRecord found for debugging!") + + # Query for account records that need payment processing + # Note: We removed the approved_record condition as it was too restrictive + account_records_list = AccountRecords.query.filter(AccountRecords.remainder_balance < AccountRecords.currency_value, AccountRecords.receive_debit == receive_enum.id + ).order_by(AccountRecords.bank_date.desc()).all() + + print(f"\nProcessing {len(account_records_list)} account records") + + # Track how many records were processed with each method + traditional_method_count = 0 + direct_method_count = 0 + + for account_record in account_records_list: + # Try the traditional method first + current_currency_value = pay_the_registration(account_record, receive_enum, debit_enum, False, session) + + if current_currency_value > 0: + # If first pass found payments, try with old records too + pay_the_registration(account_record, receive_enum, debit_enum, True, session) + traditional_method_count += 1 + else: + # If traditional method didn't work, try direct payment for all records + # This will handle records with missing build_parts_id + if create_direct_payment(account_record, receive_enum, session): + direct_method_count += 1 + if direct_method_count % 10 == 0: # Only print every 10th record to avoid too much output + print(f"Direct payment created for account_record {account_record.id}") + + # Refresh the account record to get updated values + session.refresh(account_record) + + # Update status if the remainder balance equals the currency value + if abs(float(Decimal(account_record.remainder_balance))) == abs(float(Decimal(account_record.currency_value))): + account_record.update(status_id=97) + account_record.save() + + print(f"\nProcessed with traditional method: {traditional_method_count} records") + print(f"Processed with direct payment method: {direct_method_count} records") + + print("Payment processing completed successfully") + except Exception as e: + print(f"Error in send_accounts_to_decision_payment: {e}") + import traceback + traceback.print_exc() + # Rollback the session in case of error + session.rollback() + return + +if __name__ == "__main__": + print("Payment Service is running...") + try: + send_accounts_to_decision_payment() + + # Print counter statistics + print("\n--- Processing Statistics ---") + print(f"Records where current_currency_value <= 0: {counter_current_currency_not_positive}") + print(f"Records where net_amount <= 0: {counter_net_amount_not_positive}") + print(f"Account records updated: {counter_account_records_updated}") + print(f"Total payments found: {counter_payments_found}") + print(f"Skips due to found_payment: {counter_found_payment_skips}") + print(f"Payment exceptions: {counter_payment_exceptions}") + + except Exception as e: + print(f"Error: {e}") + print("Payment Service is finished...") diff --git a/ServicesBank/Finder/Payment/run_app.sh b/ServicesBank/Finder/Payment/run_app.sh new file mode 100644 index 0000000..83dcf19 --- /dev/null +++ b/ServicesBank/Finder/Payment/run_app.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# Source the environment file directly +. /env.sh + +# Re-export all variables to ensure they're available to the Python script +export POSTGRES_USER +export POSTGRES_PASSWORD +export POSTGRES_DB +export POSTGRES_HOST +export POSTGRES_PORT +export POSTGRES_ENGINE +export POSTGRES_POOL_PRE_PING +export POSTGRES_POOL_SIZE +export POSTGRES_MAX_OVERFLOW +export POSTGRES_POOL_RECYCLE +export POSTGRES_POOL_TIMEOUT +export POSTGRES_ECHO + +# Python environment variables +export PYTHONPATH +export PYTHONUNBUFFERED +export PYTHONDONTWRITEBYTECODE + +# env >> /var/log/cron.log +/usr/local/bin/python /runner.py diff --git a/ServicesBank/Finder/Payment/runner.py b/ServicesBank/Finder/Payment/runner.py new file mode 100644 index 0000000..f9f2d7f --- /dev/null +++ b/ServicesBank/Finder/Payment/runner.py @@ -0,0 +1,854 @@ +import arrow +import calendar +import time + +from decimal import Decimal +from datetime import datetime, timedelta + +from Schemas import BuildDecisionBookPayments, AccountRecords, ApiEnumDropdown, Build, BuildDecisionBook +from time import perf_counter +from sqlalchemy import select, func, distinct, cast, Date, String, literal, desc, and_, or_ +from Controllers.Postgres.engine import get_session_factory +#from ServicesApi.Schemas.account.account import AccountRecords +#from ServicesApi.Schemas.building.decision_book import BuildDecisionBookPayments + + +def find_last_day_of_month(date_value): + today = date_value.date() + _, last_day = calendar.monthrange(today.year, today.month) + return datetime(today.year, today.month, last_day, 23, 59, 59) + +def find_first_day_of_month(date_value): + today = date_value.date() + return datetime(today.year, today.month, 1) + + +class BuildDuesTypes: + + def __init__(self): + self.debit: ApiEnumDropdownShallowCopy = None + self.add_debit: ApiEnumDropdownShallowCopy = None + self.renovation: ApiEnumDropdownShallowCopy = None + self.lawyer_expence: ApiEnumDropdownShallowCopy = None + self.service_fee: ApiEnumDropdownShallowCopy = None + self.information: ApiEnumDropdownShallowCopy = None + + +class ApiEnumDropdownShallowCopy: + id: int + uuid: str + enum_class: str + key: str + value: str + + def __init__(self, id: int, uuid: str, enum_class: str, key: str, value: str): + self.id = id + self.uuid = uuid + self.enum_class = enum_class + self.key = key + self.value = value + + +def get_enums_from_database(): + build_dues_types = BuildDuesTypes() + with ApiEnumDropdown.new_session() as session: + + ApiEnumDropdown.set_session(session) + + debit_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-D").first() # Debit + add_debit_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-A").first() # Add Debit + renovation_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-R").first() # Renovation + late_payment_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-L").first() # Lawyer expence + service_fee_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-S").first() # Service fee + information_enum_shallow = ApiEnumDropdown.query.filter_by(enum_class="BuildDuesTypes", key="BDT-I").first() # Information + + build_dues_types.debit = ApiEnumDropdownShallowCopy( + debit_enum_shallow.id, str(debit_enum_shallow.uu_id), debit_enum_shallow.enum_class, debit_enum_shallow.key, debit_enum_shallow.value + ) + build_dues_types.add_debit = ApiEnumDropdownShallowCopy( + add_debit_enum_shallow.id, str(add_debit_enum_shallow.uu_id), add_debit_enum_shallow.enum_class, add_debit_enum_shallow.key, add_debit_enum_shallow.value + ) + build_dues_types.renovation = ApiEnumDropdownShallowCopy( + renovation_enum_shallow.id, str(renovation_enum_shallow.uu_id), renovation_enum_shallow.enum_class, renovation_enum_shallow.key, renovation_enum_shallow.value + ) + build_dues_types.lawyer_expence = ApiEnumDropdownShallowCopy( + late_payment_enum_shallow.id, str(late_payment_enum_shallow.uu_id), late_payment_enum_shallow.enum_class, late_payment_enum_shallow.key, late_payment_enum_shallow.value + ) + build_dues_types.service_fee = ApiEnumDropdownShallowCopy( + service_fee_enum_shallow.id, str(service_fee_enum_shallow.uu_id), service_fee_enum_shallow.enum_class, service_fee_enum_shallow.key, service_fee_enum_shallow.value + ) + build_dues_types.information = ApiEnumDropdownShallowCopy( + information_enum_shallow.id, str(information_enum_shallow.uu_id), information_enum_shallow.enum_class, information_enum_shallow.key, information_enum_shallow.value + ) + return [build_dues_types.debit, build_dues_types.lawyer_expence, build_dues_types.add_debit, build_dues_types.renovation, build_dues_types.service_fee, build_dues_types.information] + + +def generate_total_paid_amount_for_spesific_build_part_id(build_parts_id: int, session): + """ + Calculate the total amount paid for a specific build part ID. + + Args: + build_parts_id: The build part ID to calculate payments for + session: Database session + + Returns: + float: The total amount paid (absolute value) + """ + payment_query = session.query(func.sum(BuildDecisionBookPayments.payment_amount)).filter( + BuildDecisionBookPayments.build_parts_id == build_parts_id, + BuildDecisionBookPayments.account_is_debit == False, + cast(BuildDecisionBookPayments.process_date, Date) >= '2022-01-01' + ).scalar() + return payment_query if payment_query is not None else 0 + + +def generate_total_debt_amount_for_spesific_build_part_id(build_parts_id: int, session): + + # Use SQLAlchemy's func.sum to calculate the total debts + # For total debt, we want to include ALL debts, both processed and unprocessed + result = session.query( + func.sum(BuildDecisionBookPayments.payment_amount) + ).filter( + BuildDecisionBookPayments.build_parts_id == build_parts_id, + BuildDecisionBookPayments.account_is_debit == True, + cast(BuildDecisionBookPayments.process_date, Date) >= '2022-01-01' + ).scalar() + + # Return 0 if no debts found, otherwise return the absolute value of the sum + return abs(result) if result is not None else 0 + + +def generate_total_amount_that_user_has_in_account(account_record: AccountRecords, session): + # Get total amount that user has in account + result = session.query( + func.sum(AccountRecords.currency_value) + ).filter( + AccountRecords.build_parts_id == account_record.build_parts_id, + AccountRecords.currency_value > 0, + cast(AccountRecords.bank_date, Date) >= '2022-01-01' + ).scalar() + + # Return 0 if no payments found, otherwise return the absolute value of the sum + return abs(result) + + + +def _print_debt_details(debt, session): + """Helper function to print detailed information about an unpaid debt. + + Args: + debt: The BuildDecisionBookPayments object representing the debt + session: Database session + """ + # Get the sum of payments for this debt + payments_sum = session.query( + func.sum(BuildDecisionBookPayments.payment_amount) + ).filter( + BuildDecisionBookPayments.ref_id == debt.ref_id, + BuildDecisionBookPayments.account_is_debit == False + ).scalar() or 0 + + # Calculate remaining amount + debit_amount = abs(debt.payment_amount) + remaining = debit_amount - abs(payments_sum) + payment_percentage = (abs(payments_sum) / debit_amount) * 100 if debit_amount > 0 else 0 + + # Format the date for display + date_str = debt.process_date.strftime('%Y-%m-%d') if debt.process_date else 'Unknown date' + + +def analyze_payment_function(): + session_factory = get_session_factory() + session = session_factory() + # Set session for all models + AccountRecords.set_session(session) + BuildDecisionBookPayments.set_session(session) + + order_pay = get_enums_from_database() + # Get distinct build_parts_id values from account records with positive currency_value + # This avoids redundant processing of the same build_parts_id + distinct_build_parts = session.query( + distinct(AccountRecords.build_parts_id) + ).filter( + AccountRecords.build_parts_id.isnot(None), + AccountRecords.currency_value > 0, + AccountRecords.bank_date >= '2022-01-01' + ).order_by(AccountRecords.build_parts_id.desc()).all() + + start_time = time.time() + + for build_part_id_tuple in distinct_build_parts: + build_part_id = build_part_id_tuple[0] # Extract the ID from the tuple + process_date = datetime.now() + last_date_of_process_date = datetime(process_date.year, process_date.month, 1) - timedelta(days=1) + first_date_of_process_date = datetime(process_date.year, process_date.month, 1) + + print(f"\n{'=' * 50}") + print(f"ACCOUNT ANALYSIS FOR BUILD PART ID: {build_part_id}") + print(f"{'=' * 50}") + + # Calculate total paid amount for this build_part_id + total_amount_paid = generate_total_paid_amount_for_spesific_build_part_id(build_part_id, session) + + # Calculate total debt amount for this build_part_id + total_debt_amount = generate_total_debt_amount_for_spesific_build_part_id(build_part_id, session) + + # Get total amount in account for this build_part_id + account_record = AccountRecords() + account_record.build_parts_id = build_part_id + total_amount_in_account = generate_total_amount_that_user_has_in_account(account_record, session) + + # Calculate remaining amount to be paid + amount_need_to_paid = total_debt_amount - total_amount_paid + total_amount_that_user_need_to_transfer = abs(amount_need_to_paid) - abs(total_amount_in_account) + + # Print summary with clear descriptions + print(f"PAYMENT SUMMARY:") + print(f" • Total debt amount: {total_debt_amount:,.2f} TL") + print(f" • Amount already paid: {total_amount_paid:,.2f} TL") + print(f" • Remaining debt to be collected: {amount_need_to_paid:,.2f} TL") + print(f" • Current account balance: {total_amount_in_account:,.2f} TL") + + if total_amount_that_user_need_to_transfer > 0: + print(f" • Additional funds needed: {total_amount_that_user_need_to_transfer:,.2f} TL") + elif amount_need_to_paid <= 0: + print(f" • Account is fully paid with no outstanding debt") + else: + print(f" • Sufficient funds available to close all debt") + # Show debt coverage percentage + if total_debt_amount > 0: + # Calculate current coverage (already paid) + current_coverage_percentage = (total_amount_paid / total_debt_amount) * 100 + + # Calculate potential coverage (including available funds) + potential_coverage = min(100, ((total_amount_paid + total_amount_in_account) / total_debt_amount) * 100) + + # Display both percentages + print(f" • Current debt coverage: {current_coverage_percentage:.2f}%") + print(f" • Potential debt coverage with available funds: {potential_coverage:.2f}%") + + # Analyze unpaid debts for each payment type + print("\nUNPAID DEBTS ANALYSIS BY PAYMENT TYPE:") + for payment_type in order_pay: + # Get unpaid debts for current month + date_query_current = ( + BuildDecisionBookPayments.process_date >= first_date_of_process_date, + BuildDecisionBookPayments.process_date <= process_date + ) + date_query_previous = ( + BuildDecisionBookPayments.process_date < first_date_of_process_date, + ) + + current_unpaid_debts = get_unpaid_debts(build_parts_id=build_part_id, session=session, debit_type=payment_type, date_query=date_query_current) + + # Get unpaid debts from previous months + previous_unpaid_debts = get_unpaid_debts(build_parts_id=build_part_id, session=session, debit_type=payment_type, date_query=date_query_previous) + + # Calculate totals + current_total = sum(abs(debt[2]) for debt in current_unpaid_debts) + previous_total = sum(abs(debt[2]) for debt in previous_unpaid_debts) + grand_total = current_total + previous_total + + # Print summary for this payment type + if current_unpaid_debts or previous_unpaid_debts: + print(f" • {payment_type.key}: Total unpaid: {grand_total:,.2f} TL") + + # Current month details + if current_unpaid_debts: + print(f" - Current month: {len(current_unpaid_debts)} debts, {current_total:,.2f} TL") + # Show details of each unpaid debt if there aren't too many + # if len(current_unpaid_debts) <= 3: + # for debt in current_unpaid_debts: + # _print_debt_details(debt, session) + + # Previous months details + if previous_unpaid_debts: + print(f" - Previous months: {len(previous_unpaid_debts)} debts, {previous_total:,.2f} TL") + # Show details of each unpaid debt if there aren't too many + # if len(previous_unpaid_debts) <= 3: + # for debt in previous_unpaid_debts: + # _print_debt_details(debt, session) + else: + print(f" • {payment_type.key}: All debts paid") + print(f"{'=' * 50}\n") + + +def close_payment_book(payment_row_book, account_record, value, session): + """Create a credit entry in BuildDecisionBookPayments to close a debt. + + Args: + payment_row_book: The debit entry to be paid + account_record: The account record containing the funds + value: The amount to pay + session: Database session + + Returns: + The newly created payment record + """ + BuildDecisionBookPayments.set_session(session) + + # Create a new credit entry (payment) + new_row = BuildDecisionBookPayments.create( + ref_id=str(payment_row_book.uu_id), + payment_plan_time_periods=payment_row_book.payment_plan_time_periods, + period_time=payment_row_book.period_time, + currency=payment_row_book.currency, + account_records_id=account_record.id, + account_records_uu_id=str(account_record.uu_id), + build_parts_id=payment_row_book.build_parts_id, + build_parts_uu_id=str(payment_row_book.build_parts_uu_id), + payment_amount=abs(value), # Negative for credit entries + payment_types_id=payment_row_book.payment_types_id, + payment_types_uu_id=str(payment_row_book.payment_types_uu_id), + process_date_m=payment_row_book.process_date.month, + process_date_y=payment_row_book.process_date.year, + process_date=payment_row_book.process_date, + build_decision_book_item_id=payment_row_book.build_decision_book_item_id if payment_row_book.build_decision_book_item_id else None, + build_decision_book_item_uu_id=str(payment_row_book.build_decision_book_item_uu_id) if payment_row_book.build_decision_book_item_uu_id else None, + decision_book_project_id=payment_row_book.decision_book_project_id if payment_row_book.decision_book_project_id else None, + decision_book_project_uu_id=str(payment_row_book.decision_book_project_uu_id) if payment_row_book.decision_book_project_uu_id else None, + build_decision_book_id=payment_row_book.build_decision_book_id if payment_row_book.build_decision_book_id else None, + build_decision_book_uu_id=str(payment_row_book.build_decision_book_uu_id) if payment_row_book.build_decision_book_uu_id else None, + is_confirmed=True, + account_is_debit=False, + ) + + # Save the new payment record + saved_row = new_row.save() + session.commit() + session.refresh(saved_row) + session.flush() + return saved_row + + +def update_account_remainder_if_spent(account_record, ref_id: str, session): + """Update the remainder_balance of an account after spending money. + + Args: + account_record: The account record to update + amount_spent: The amount spent in this transaction + session: Database session + + Returns: + bool: True if all money is spent, False otherwise + """ + AccountRecords.set_session(session) + BuildDecisionBookPayments.set_session(session) + session.commit() + sum_of_paid = session.query(func.sum(func.abs(BuildDecisionBookPayments.payment_amount))).filter( + BuildDecisionBookPayments.account_records_id == account_record.id, + BuildDecisionBookPayments.account_is_debit == False + ).scalar() + if not sum_of_paid: + return False + debit_row = BuildDecisionBookPayments.query.filter_by(ref_id=ref_id).first() + account_record_to_update = AccountRecords.query.filter_by(id=account_record.id).first() + account_record_to_update.remainder_balance = (-1 * abs(sum_of_paid)) or 0 + account_record_to_update.save() + session.commit() + session.refresh(account_record_to_update) + # Get the current remainder balance + if abs(sum_of_paid) == abs(account_record_to_update.currency_value): + return True + return False + + +def update_all_spent_accounts(session): + """Update remainder_balance for all accounts with payments. + + This function finds account records in BuildDecisionBookPayments and updates + their remainder_balance based on the sum of payments made, regardless of whether + all funds have been spent or not. + + Args: + session: Database session + """ + with AccountRecords.new_session() as session: + # Set sessions for models + AccountRecords.set_session(session) + BuildDecisionBookPayments.set_session(session) + + # Get distinct account_records_id values from BuildDecisionBookPayments + distinct_account_ids = session.query(BuildDecisionBookPayments.account_records_id).filter( + BuildDecisionBookPayments.account_records_id.isnot(None), + BuildDecisionBookPayments.account_is_debit == False # Credit entries (payments) + ).distinct().all() + + updated_count = 0 + + for account_id_tuple in distinct_account_ids: + account_id = account_id_tuple[0] + + # Get the account record + account = AccountRecords.query.filter_by(id=account_id).first() + if not account or not account.build_parts_id or account.currency_value <= 0: + continue + + # Calculate the sum of payments made using this account + # Note: payment_amount is negative for credit entries, so we need to use abs() to get the positive amount + payment_query = session.query(func.sum(func.abs(BuildDecisionBookPayments.payment_amount))).filter( + BuildDecisionBookPayments.account_records_id == account_id, + BuildDecisionBookPayments.account_is_debit == False # Credit entries (payments) + ) + + payment_sum = payment_query.scalar() or 0 + + # Update remainder_balance for ALL accounts, regardless of payment_sum value + threshold = Decimal('0.01') + fully_spent = abs(payment_sum) >= abs(account.currency_value) - threshold + status = "All funds spent" if fully_spent else "Partial payment" + + # Store the positive value in remainder_balance + account.remainder_balance = -1 * abs(payment_sum) + account.save() + updated_count += 1 + session.commit() + print(f"\nTotal accounts updated: {updated_count}") + + +# def find_amount_to_pay_by_ref_id(ref_id, session): +# """Calculate the remaining amount to pay for a specific debt reference ID. + +# Args: +# ref_id: The reference ID of the debt (this is the uu_id of the debt record) +# session: Database session + +# Returns: +# float: The remaining amount to pay +# """ +# # Get the original debt amount - the debt is identified by its uu_id which is passed as ref_id +# debit = BuildDecisionBookPayments.query.filter( +# BuildDecisionBookPayments.uu_id == ref_id, +# BuildDecisionBookPayments.account_is_debit == True +# ).first() +# if not debit: +# return 0 # No debit found, nothing to pay + +# debit_amount = abs(debit.payment_amount) # Ensure positive value for debit amount + +# # Get the sum of payments already made for this debt +# # The ref_id in credit records points to the uu_id of the original debit +# # Note: payment_amount is negative for credit entries, so we use abs() to get positive values +# credit_amount = session.query( +# func.sum(func.abs(BuildDecisionBookPayments.payment_amount)) +# ).filter( +# BuildDecisionBookPayments.ref_id == str(ref_id), +# BuildDecisionBookPayments.account_is_debit == False +# ).scalar() or 0 +# # Calculate remaining amount to pay +# remaining = abs(debit_amount) - abs(credit_amount) +# # Ensure we don't return negative values +# if remaining < 0: +# return 0 + +# return remaining + +def get_unpaid_debts_via_build_parts(build_parts_id: int, session, debit_type, date_query: tuple): + """Find BuildDecisionBookPayments entries where the debt has NOT been fully paid for a specific debit type. + + This function identifies payments where the sum of payments is less than the debit amount, + meaning the debt has not been fully closed. + + Args: + build_parts_id: The build part ID to check + session: Database session + debit_type: The specific debit type to check + date_query: Tuple of date filters to apply + + Returns: + list: List of unpaid debt entries (full BuildDecisionBookPayments objects) + + query: + SELECT + bpf.ref_id, + bpf.process_date, + ABS(COALESCE(SUM(bpf.payment_amount), 0)) AS total_payments + FROM public.build_decision_book_payments AS bpf + GROUP BY + bpf.ref_id, + bpf.process_date + HAVING ABS(COALESCE(SUM(bpf.payment_amount), 0)) > 0 + order by bpf.process_date + """ + # Create a subquery for the payment sums without executing it separately + payment_sums_subquery = select( + BuildDecisionBookPayments.ref_id, + BuildDecisionBookPayments.process_date, + func.abs(func.coalesce(func.sum(BuildDecisionBookPayments.payment_amount), 0)).label("total_payments") + ).filter( + BuildDecisionBookPayments.build_parts_id == build_parts_id, + BuildDecisionBookPayments.payment_types_id == debit_type.id, + *date_query + ).group_by( + BuildDecisionBookPayments.ref_id, BuildDecisionBookPayments.process_date + ).having( + func.abs(func.coalesce(func.sum(BuildDecisionBookPayments.payment_amount), 0)) > 0 + ).order_by(BuildDecisionBookPayments.process_date.desc()) + + # Use the subquery directly in the main query + payment_sums = session.execute(payment_sums_subquery).all() + payment_sums_list = [] + for item in payment_sums: + payment_sums_list.append({"ref_id": item[0], "process_date": item[1], "total_payments": item[2]}) + return payment_sums + + +def get_unpaid_debts(session, debit_type, date_query: tuple): + """Find BuildDecisionBookPayments entries where the debt has NOT been fully paid for a specific debit type. + + This function identifies payments where the sum of payments is less than the debit amount, + meaning the debt has not been fully closed. + + Args: + build_parts_id: The build part ID to check + session: Database session + debit_type: The specific debit type to check + date_query: Tuple of date filters to apply + + Returns: + list: List of unpaid debt entries (full BuildDecisionBookPayments objects) + + query: + SELECT + bpf.ref_id, + bpf.process_date, + ABS(COALESCE(SUM(bpf.payment_amount), 0)) AS total_payments + FROM public.build_decision_book_payments AS bpf + GROUP BY + bpf.ref_id, + bpf.process_date + HAVING ABS(COALESCE(SUM(bpf.payment_amount), 0)) > 0 + order by bpf.process_date + """ + # Create a subquery for the payment sums without executing it separately + do_payments_set = lambda ref, date, total: {"ref_id": ref, "process_date": date, "total_payments": total} + payment_sums_subquery = select(BuildDecisionBookPayments.ref_id, BuildDecisionBookPayments.process_date, + func.abs(func.coalesce(func.sum(BuildDecisionBookPayments.payment_amount), 0)).label("total_payments") + ).filter(BuildDecisionBookPayments.payment_types_id == debit_type.id, *date_query + ).group_by(BuildDecisionBookPayments.ref_id, BuildDecisionBookPayments.process_date + ).having(func.abs(func.coalesce(func.sum(BuildDecisionBookPayments.payment_amount), 0)) > 0 + ).order_by(BuildDecisionBookPayments.process_date.asc()) + + payment_sums = session.execute(payment_sums_subquery).all() + return [do_payments_set(item[0], item[1], item[2]) for item in payment_sums] + + +def do_payments_of_this_month(build_id: int = 1): + """Process payments for the current month's unpaid debts. + This function retrieves account records with available funds and processes + payments for current month's unpaid debts in order of payment type priority. + """ + session_factory = get_session_factory() + session = session_factory() + + # Set session for all models + AccountRecords.set_session(session) + BuildDecisionBookPayments.set_session(session) + Build.set_session(session) + BuildDecisionBook.set_session(session) + + # Get payment types in priority order + payment_type_list = get_enums_from_database() + fund_finished = lambda money_spend, money_in_account: money_spend == money_in_account + payments_made, total_amount_paid, paid_count = 0, 0, 0 + target_build = Build.query.filter(Build.id == build_id).first() + if not target_build: + raise ValueError(f"Build with id {build_id} not found") + + now = datetime.now() + decision_book = BuildDecisionBook.query.filter( + BuildDecisionBook.build_id == build_id, cast(BuildDecisionBook.expiry_starts, Date) <= now.date(), + cast(BuildDecisionBook.expiry_ends, Date) >= now.date(), BuildDecisionBook.decision_type == "RBM" + ).first() + if not decision_book: + raise ValueError(f"Decision book not found for build with id {build_id}") + + period_date_start = decision_book.expiry_starts + period_date_end = decision_book.expiry_ends + period_id = decision_book.id + + first_date_of_process_date = find_first_day_of_month(now) + last_date_of_process_date = find_last_day_of_month(now) + + # Current month date filter + date_query_tuple = ( + cast(BuildDecisionBookPayments.process_date, Date) >= first_date_of_process_date.date(), cast(BuildDecisionBookPayments.process_date, Date) <= last_date_of_process_date.date() + ) + + update_all_spent_accounts(session) + + for payment_type in payment_type_list: + unpaid_debts = get_unpaid_debts(session, payment_type, date_query_tuple) + for unpaid_debt in unpaid_debts: + amount_to_pay = unpaid_debt["total_payments"] + ref_id = unpaid_debt["ref_id"] + process_date = unpaid_debt["process_date"] + debit_row = BuildDecisionBookPayments.query.filter(BuildDecisionBookPayments.uu_id == ref_id).first() + build_parts_id = debit_row.build_parts_id + money_to_pay_rows = AccountRecords.query.filter( + AccountRecords.build_parts_id == build_parts_id, AccountRecords.currency_value > 0, AccountRecords.currency_value > func.abs(AccountRecords.remainder_balance), + cast(AccountRecords.bank_date, Date) >= first_date_of_process_date.date(), cast(AccountRecords.bank_date, Date) <= last_date_of_process_date.date(), + ).order_by(AccountRecords.bank_date.asc()).all() + for money_to_pay_row in money_to_pay_rows: + update_account_remainder_if_spent(account_record=money_to_pay_row, ref_id=debit_row.ref_id, session=session) + money_to_pay_row = AccountRecords.query.filter(AccountRecords.id == money_to_pay_row.id).first() + available_money = abs(money_to_pay_row.currency_value) - abs(money_to_pay_row.remainder_balance) + if available_money > amount_to_pay: + print('NOT All money is spent amount_to_pay:', amount_to_pay, 'debit_row payment_amount:', debit_row.payment_amount) + close_payment_book(debit_row, money_to_pay_row, amount_to_pay, session) + total_amount_paid += amount_to_pay + paid_count += 1 + payments_made += 1 + update_account_remainder_if_spent(account_record=money_to_pay_row, ref_id=debit_row.ref_id, session=session) + break + elif available_money <= amount_to_pay: + print('All money is spent amount_to_pay:', amount_to_pay, 'debit_row payment_amount:', debit_row.payment_amount) + close_payment_book(debit_row, money_to_pay_row, available_money, session) + total_amount_paid += available_money + paid_count += 1 + payments_made += 1 + update_account_remainder_if_spent(account_record=money_to_pay_row, ref_id=debit_row.ref_id, session=session) + continue + else: + print(f"Something else happened available_money: {available_money}, amount_to_pay: {amount_to_pay}") + + update_all_spent_accounts(session) + + print('payments_made', payments_made) + print('total_amount_paid', total_amount_paid) + print('paid_count', paid_count) + + +def do_payments_of_previos_months(build_id: int = 1): + """Process payments for previous months' unpaid debts. + + This function retrieves account records with available funds and processes + payments for previous months' unpaid debts in order of payment type priority. + """ + """Process payments for the current month's unpaid debts. + This function retrieves account records with available funds and processes + payments for current month's unpaid debts in order of payment type priority. + """ + session_factory = get_session_factory() + session = session_factory() + + # Set session for all models + AccountRecords.set_session(session) + BuildDecisionBookPayments.set_session(session) + Build.set_session(session) + BuildDecisionBook.set_session(session) + + # Get payment types in priority order + payment_type_list = get_enums_from_database() + fund_finished = lambda money_spend, money_in_account: money_spend == money_in_account + payments_made, total_amount_paid, paid_count = 0, 0, 0 + target_build = Build.query.filter(Build.id == build_id).first() + if not target_build: + raise ValueError(f"Build with id {build_id} not found") + + now = datetime.now() + decision_book = BuildDecisionBook.query.filter( + BuildDecisionBook.build_id == build_id, + cast(BuildDecisionBook.expiry_starts, Date) <= now.date(), + cast(BuildDecisionBook.expiry_ends, Date) >= now.date(), + BuildDecisionBook.decision_type == "RBM" + ).first() + if not decision_book: + raise ValueError(f"Decision book not found for build with id {build_id}") + early_date = datetime(now.year - 1, now.month, now.day) + early_decision_book = BuildDecisionBook.query.filter( + BuildDecisionBook.build_id == build_id, + cast(BuildDecisionBook.expiry_starts, Date) <= early_date.date(), + cast(BuildDecisionBook.expiry_ends, Date) >= early_date.date(), + BuildDecisionBook.decision_type == "RBM" + ).first() + + period_date_start = decision_book.expiry_starts + period_date_end = decision_book.expiry_ends + period_id = decision_book.id + early_period_date_start = early_decision_book.expiry_starts + early_period_date_end = early_decision_book.expiry_ends + early_period_id = early_decision_book.id + + first_date_of_process_date = arrow.get(period_date_start).datetime + last_date_of_process_date = now + + # Current month date filter + date_query_tuple = ( + cast(BuildDecisionBookPayments.process_date, Date) >= first_date_of_process_date.date(), + cast(BuildDecisionBookPayments.process_date, Date) <= last_date_of_process_date.date() + ) + + update_all_spent_accounts(session) + + for payment_type in payment_type_list: + unpaid_debts = get_unpaid_debts(session, payment_type, date_query_tuple) + print('length unpaid debts: ', len(unpaid_debts)) + for unpaid_debt in unpaid_debts: + amount_to_pay = unpaid_debt["total_payments"] + ref_id = unpaid_debt["ref_id"] + process_date = unpaid_debt["process_date"] + debit_row = BuildDecisionBookPayments.query.filter(BuildDecisionBookPayments.uu_id == ref_id).first() + build_parts_id = debit_row.build_parts_id + money_to_pay_rows = AccountRecords.query.filter( + AccountRecords.build_parts_id == build_parts_id, AccountRecords.currency_value > 0, AccountRecords.currency_value > func.abs(AccountRecords.remainder_balance), + cast(AccountRecords.bank_date, Date) >= find_first_day_of_month(process_date).date(), cast(AccountRecords.bank_date, Date) <= find_last_day_of_month(process_date).date(), + ).order_by(AccountRecords.bank_date.asc()).all() + if not money_to_pay_rows: + money_to_pay_rows = AccountRecords.query.filter( + AccountRecords.build_parts_id == build_parts_id, AccountRecords.currency_value > 0, AccountRecords.currency_value > func.abs(AccountRecords.remainder_balance), + cast(AccountRecords.bank_date, Date) >= first_date_of_process_date.date(), cast(AccountRecords.bank_date, Date) <= last_date_of_process_date.date(), + ).order_by(AccountRecords.bank_date.asc()).all() + for money_to_pay_row in money_to_pay_rows: + update_account_remainder_if_spent(account_record=money_to_pay_row, ref_id=debit_row.ref_id, session=session) + money_to_pay_row = AccountRecords.query.filter(AccountRecords.id == money_to_pay_row.id).first() + available_money = abs(money_to_pay_row.currency_value) - abs(money_to_pay_row.remainder_balance) + if available_money > amount_to_pay: + print('NOT All money is spent amount_to_pay:', amount_to_pay, 'debit_row payment_amount:', debit_row.payment_amount) + close_payment_book(debit_row, money_to_pay_row, amount_to_pay, session) + total_amount_paid += amount_to_pay + paid_count += 1 + payments_made += 1 + update_account_remainder_if_spent(account_record=money_to_pay_row, ref_id=debit_row.ref_id, session=session) + break + elif available_money <= amount_to_pay: + print('All money is spent amount_to_pay:', available_money, 'debit_row payment_amount:', debit_row.payment_amount) + close_payment_book(debit_row, money_to_pay_row, available_money, session) + total_amount_paid += available_money + paid_count += 1 + payments_made += 1 + update_account_remainder_if_spent(account_record=money_to_pay_row, ref_id=debit_row.ref_id, session=session) + continue + else: + print(f"Something else happened available_money: {available_money}, amount_to_pay: {amount_to_pay}") + + print('This years decision book payments') + print('payments_made', payments_made) + print('total_amount_paid', total_amount_paid) + print('paid_count', paid_count) + payments_made, total_amount_paid, paid_count = 0, 0, 0 + update_all_spent_accounts(session) + first_date_of_process_date = arrow.get(early_period_date_start).datetime + last_date_of_process_date = arrow.get(early_period_date_end).datetime + + # Early month date filter + date_query_tuple = ( + cast(BuildDecisionBookPayments.process_date, Date) >= first_date_of_process_date.date(), cast(BuildDecisionBookPayments.process_date, Date) <= last_date_of_process_date.date() + ) + + for payment_type in payment_type_list: + unpaid_debts = get_unpaid_debts(session, payment_type, date_query_tuple) + print('length unpaid debts: ', len(unpaid_debts)) + for unpaid_debt in unpaid_debts: + amount_to_pay = unpaid_debt["total_payments"] + ref_id = unpaid_debt["ref_id"] + process_date = unpaid_debt["process_date"] + debit_row = BuildDecisionBookPayments.query.filter(BuildDecisionBookPayments.uu_id == ref_id).first() + build_parts_id = debit_row.build_parts_id + first_date_of_process_date = find_first_day_of_month(process_date) + last_date_of_process_date = find_last_day_of_month(process_date) + money_to_pay_rows = AccountRecords.query.filter( + AccountRecords.build_parts_id == build_parts_id, AccountRecords.currency_value > 0, AccountRecords.currency_value > func.abs(AccountRecords.remainder_balance), + cast(AccountRecords.bank_date, Date) >= first_date_of_process_date.date(), cast(AccountRecords.bank_date, Date) <= last_date_of_process_date.date(), + ).order_by(AccountRecords.bank_date.asc()).all() + if not money_to_pay_rows: + money_to_pay_rows = AccountRecords.query.filter( + AccountRecords.build_parts_id == build_parts_id, AccountRecords.currency_value > 0, AccountRecords.currency_value > func.abs(AccountRecords.remainder_balance), + cast(AccountRecords.bank_date, Date) >= first_date_of_process_date.date(), cast(AccountRecords.bank_date, Date) <= last_date_of_process_date.date(), + ).order_by(AccountRecords.bank_date.asc()).all() + for money_to_pay_row in money_to_pay_rows: + update_account_remainder_if_spent(account_record=money_to_pay_row, ref_id=debit_row.ref_id, session=session) + money_to_pay_row = AccountRecords.query.filter(AccountRecords.id == money_to_pay_row.id).first() + available_money = abs(money_to_pay_row.currency_value) - abs(money_to_pay_row.remainder_balance) + if available_money > amount_to_pay: + print('NOT All money is spent amount_to_pay:', amount_to_pay, 'debit_row payment_amount:', debit_row.payment_amount) + close_payment_book(debit_row, money_to_pay_row, amount_to_pay, session) + total_amount_paid += amount_to_pay + paid_count += 1 + payments_made += 1 + update_account_remainder_if_spent(account_record=money_to_pay_row, ref_id=debit_row.ref_id, session=session) + break + elif available_money <= amount_to_pay: + print('All money is spent amount_to_pay:', available_money, 'debit_row payment_amount:', debit_row.payment_amount) + close_payment_book(debit_row, money_to_pay_row, available_money, session) + total_amount_paid += available_money + paid_count += 1 + payments_made += 1 + update_account_remainder_if_spent(account_record=money_to_pay_row, ref_id=debit_row.ref_id, session=session) + continue + else: + print(f"Something else happened available_money: {available_money}, amount_to_pay: {amount_to_pay}") + + update_all_spent_accounts(session) + print('Early years decision book payments') + print('payments_made', payments_made) + print('total_amount_paid', total_amount_paid) + print('paid_count', paid_count) + + +if __name__ == "__main__": + start_time = perf_counter() + + print("\n===== PROCESSING PAYMENTS =====\n") + print("Starting payment processing at:", datetime.now()) + + # Process payments for current month first + print("\n1. Processing current month payments...") + do_payments_of_this_month() + + # Process payments for previous months + print("\n2. Processing previous months payments...") + do_payments_of_previos_months() + + print("\n===== PAYMENT PROCESSING COMPLETE =====\n") + print("Payment processing completed at:", datetime.now()) + + # Analyze the payment situation after processing payments + print("\n===== ANALYZING PAYMENT SITUATION AFTER PROCESSING =====\n") + # analyze_payment_function() + + end_time = perf_counter() + print(f"\n{end_time - start_time:.3f} : seconds") + + + # # Create a subquery to get the sum of payments for each debit's uu_id + # # For credit entries, ref_id points to the original debit's uu_id + # payment_sums = session.query( + # BuildDecisionBookPayments.ref_id.label('original_debt_id'), + # func.sum(func.abs(BuildDecisionBookPayments.payment_amount)).label('payment_sum') + # ).filter( + # BuildDecisionBookPayments.account_is_debit == False # Credit entries only + # ).group_by(BuildDecisionBookPayments.ref_id).subquery() + + # # Main query to find debits with their payment sums + # query = session.query(BuildDecisionBookPayments) + + # # Join with payment sums - cast uu_id to string to match ref_id type + # query = query.outerjoin( + # payment_sums, + # func.cast(BuildDecisionBookPayments.uu_id, String) == payment_sums.c.original_debt_id + # ) + + # # Filter for debits of the specified build part and payment type + # query = query.filter( + # BuildDecisionBookPayments.build_parts_id == build_parts_id, + # BuildDecisionBookPayments.payment_types_id == debit_type.id, + # BuildDecisionBookPayments.account_is_debit == True, # Debit entries only + # ) + + # # Apply date filters if provided + # if date_query: + # for date_filter in date_query: + # query = query.filter(date_filter) + + # # Filter for debits that are not fully paid + # # (payment_sum < debit_amount or payment_sum is NULL) + # query = query.filter( + # or_( + # payment_sums.c.payment_sum.is_(None), + # func.coalesce(payment_sums.c.payment_sum, 0) < func.abs(BuildDecisionBookPayments.payment_amount) + # ) + # ) + + # # Execute the query and return the results + # results = query.order_by(BuildDecisionBookPayments.process_date).all() \ No newline at end of file diff --git a/ServicesBank/Finder/README.md b/ServicesBank/Finder/README.md new file mode 100644 index 0000000..5d39bae --- /dev/null +++ b/ServicesBank/Finder/README.md @@ -0,0 +1,3 @@ +# Docs of Finder + +Finds people, living spaces, companies from AccountRecords diff --git a/ServicesBank/Finder/account_record_parser.py b/ServicesBank/Finder/account_record_parser.py new file mode 100644 index 0000000..2adb15b --- /dev/null +++ b/ServicesBank/Finder/account_record_parser.py @@ -0,0 +1,452 @@ +import re +import textdistance + +from unidecode import unidecode +from datetime import datetime +from Schemas import BuildIbanDescription, BuildIbans, BuildDecisionBook, BuildLivingSpace, AccountRecords, Companies, People +from gc import garbage +from typing import Optional +from pydantic import BaseModel +from regex_func import category_finder + + +class InsertBudgetRecord(BaseModel): + iban: str + bank_date: str = datetime.now().__str__() + receive_debit: str = "debit" + budget_type: str = "B" + currency_value: float = 0 + balance: float = 0 + bank_reference_code: str = "" + currency: str = "TL" + channel_branch: str = "" + process_name: str = "" + process_type: str = "" + process_comment: str = "" + + add_xcomment: Optional[str] = None + project_no: Optional[str] = None + company_id: Optional[str] = None + customer_id: Optional[str] = None + + send_person_id: Optional[int] = None + send_company_id: Optional[int] = None + build_id: Optional[int] = None + build_decision_book_id: Optional[int] = None + build_parts_id: Optional[int] = None + build_db_item_id: Optional[int] = None + dues_type: Optional[str] = "D" + period_time: Optional[str] = "" + approving_accounting_record: Optional[bool] = False + approving_accounting_person: Optional[str] = None + accounting_receipt_date: Optional[str] = "1900-01-01 00:00:00" + accounting_receipt_number: Optional[int] = 0 + + +def strip_time_date(date_str): + return datetime.strptime(date_str, "%Y-%m-%d") + + +def strip_date_to_valid(date_str): + return datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S") + + +def find_iban_in_comment(iban: str, comment: str, living_space_dict: dict = None): + with BuildIbanDescription.new_session() as session: + BuildIbanDescription.set_session(session) + iban_results = BuildIbanDescription.query.filter(BuildIbanDescription.iban == iban).all() + sm_dict_extended, sm_dict_digit = {}, {} + for iban_result in iban_results or []: + extended_candidate_parts, digit_part, candidate_parts = [], [], comment.split(" ") + for part in candidate_parts: + if part.lower() not in ["no", "daire", "nolu"]: + extended_candidate_parts.append(part) + # if part.isdigit(): + # digit_part.append(part) + if extended_candidate_parts: + if all(candidate_part.lower() in comment.lower() for candidate_part in extended_candidate_parts): + similarity_ratio = textdistance.jaro_winkler(unidecode(str(iban_result.search_word)), comment) + found = False + name_list = (unidecode(str(iban_result.search_word)).replace(".", " ").split(" ")) + for name in name_list: + if len(name) > 3 and name.lower() in comment.lower(): + found = True + break + if not found: + similarity_ratio = 0.1 + sm_dict_extended[f"{iban_result.id}"] = similarity_ratio + if sm_dict_extended: + result = sorted(sm_dict_extended.items(), key=lambda item: item[1], reverse=True)[0] + if float(result[1]) >= 0.5: + iban_result = BuildIbanDescription.query.filter(BuildIbanDescription.id == int(result[0]), system=True).first() + return {"company_id": iban_result.company_id, "customer_id": iban_result.customer_id, "found_from": "Name", "similarity": result[1]} + return {"company_id": None, "customer_id": None, "found_from": None, "similarity": 0.0} + + +def remove_spaces_from_string(remove_string: str): + letter_list = [] + for letter in remove_string.split(" "): + if letter_ := "".join(i for i in letter if not i == " "): + letter_list.append(letter_) + return " ".join(letter_list).upper() + + +def get_garbage_words(comment: str, search_word: str): + garbage_words = unidecode(remove_spaces_from_string(comment)) + search_word = unidecode(remove_spaces_from_string(search_word)) + for word in search_word.split(" "): + garbage_words = garbage_words.replace(remove_spaces_from_string(unidecode(word)), "") + if cleaned_from_spaces := remove_spaces_from_string(garbage_words): + return str(unidecode(cleaned_from_spaces)).upper() + return None + + +def remove_garbage_words(comment: str, garbage_word: str): + cleaned_comment = remove_spaces_from_string(comment.replace("*", " ")) + if garbage_word: + garbage_word = remove_spaces_from_string(garbage_word.replace("*", " ")) + for letter in garbage_word.split(" "): + cleaned_comment = unidecode(remove_spaces_from_string(cleaned_comment)) + cleaned_comment = cleaned_comment.replace(remove_spaces_from_string(letter), "") + return str(remove_spaces_from_string(cleaned_comment)).upper() + + +def check_is_comment_is_build(comment: str): + has_build_words = False + candidate_parts = remove_spaces_from_string(comment.replace("*", " ")).split(" ") + for candidate_part in candidate_parts: + candidate_part = remove_spaces_from_string(candidate_part).replace(":", "") + for build_word in ["no", "daire", "apt", "apartman"]: + if unidecode(candidate_part).upper() in unidecode(build_word).upper(): + has_build_words = True + break + return has_build_words + + +def get_list_of_build_words(comment: str): + build_words = [] + candidate_parts = remove_spaces_from_string(comment.replace("*", " ")) + for build_word in ["no", "nolu", "daire", "apt", "apartman"]: + if unidecode(build_word).upper() in unidecode(candidate_parts).upper(): + st = unidecode(candidate_parts).upper().index(unidecode(build_word).upper()) + et = st + len(build_word) + st = st - 5 if st > 5 else 0 + et = et + 5 if et + 5 <= len(candidate_parts) else len(candidate_parts) + number_digit = "".join(letter for letter in str(candidate_parts[st:et]) if letter.isdigit()) + if number_digit: + rt_dict = {"garbage": candidate_parts[st:et],"number": int(number_digit) if number_digit else None } + build_words.append(rt_dict) + return build_words + + +def generate_pattern(word): + if len(word) < 1: + raise ValueError("The word must have at least 1 character.") + add_string, add_match = "\d{1,3}$\s?$", f"{{1, {len(word)}}}" + adda_string = "d{1,3}$\s?\^[" + return adda_string + f"{word}]" + add_match + rf"{word}(?:e|é|ı|i|ğr)?" + add_string + + +def test_pattern(word, test_cases): # Generate the pattern + pattern = generate_pattern(word) + for test in test_cases: # Test the regex pattern on each input and print results + if re.match(pattern, test, re.IGNORECASE): + print(f"'{test}' matches the pattern.", "*" * 60) + else: + print(f"'{test}' does NOT match the pattern.") + + +def parse_comment_for_living_space(iban: str, comment: str, living_space_dict: dict = None): + comment = unidecode(comment) + best_similarity = dict(company=None, living_space=None, found_from=None, similarity=0.0, garbage="", cleaned="",) + if not iban in living_space_dict: + return best_similarity + for person in living_space_dict[iban]["people"]: + firstname = person.get("firstname") + surname = person.get("surname") + middle_name = person.get("middle_name") + + first_name = unidecode(firstname).upper() + last_name = unidecode(surname).upper() + search_word_list = [ + remove_spaces_from_string("".join([f"{first_name} {last_name}"])), + remove_spaces_from_string("".join([f"{last_name} {first_name}"])), + ] + if middle_name := unidecode(middle_name).upper(): + search_word_list.append(remove_spaces_from_string(f"{first_name} {middle_name} {last_name}")) + search_word_list.append(remove_spaces_from_string(f"{last_name} {middle_name} {first_name}")) + + cleaned_comment = unidecode(comment).upper() + for search_word in search_word_list: + garbage_words = get_garbage_words(comment, unidecode(search_word)) + if garbage_words: + garbage_words = unidecode(garbage_words).upper() + cleaned_comment = unidecode(remove_garbage_words(comment, garbage_words)).upper() + similarity_ratio = textdistance.jaro_winkler(cleaned_comment, str(search_word).upper()) + if len(cleaned_comment) < len(f"{first_name}{last_name}"): + continue + if cleaned_comment and 0.9 < similarity_ratio <= 1: + print("cleaned comment dict", dict(garbage=garbage_words, cleaned=cleaned_comment, similarity=similarity_ratio, search_word=search_word, comment=comment, last_similarity=float(best_similarity["similarity"]))) + if similarity_ratio > float(best_similarity["similarity"]): + for living_space in living_space_dict[iban]["living_space"]: + if living_space.get("person_id") == person.get("id"): + best_similarity = {"company": None,"living_space": living_space,"found_from": "Person Name","similarity": similarity_ratio,"garbage": garbage_words,"cleaned": cleaned_comment,} + return best_similarity + + +def parse_comment_for_build_parts(comment: str, max_build_part: int = 200, parse: str = "DAIRE"): + results, results_list = category_finder(comment), [] + print("results[parse]", results[parse]) + for result in results[parse] or []: + if digits := "".join([letter for letter in str(result) if letter.isdigit()]): + print("digits", digits) + if int(digits) <= int(max_build_part): + results_list.append(int(digits)) + return results_list or None + + +def parse_comment_for_company_or_individual(comment: str): + with Companies.new_session() as session: + Companies.set_session(session) + companies_list = Companies.query.filter(Companies.commercial_type != "Commercial").all() + comment = unidecode(comment) + best_similarity = dict(company=None,living_space=None,found_from=None,similarity=0.0,garbage="",cleaned="",) + for company in companies_list: + search_word = unidecode(company.public_name) + garbage_words = get_garbage_words(comment, search_word) + cleaned_comment = remove_garbage_words(comment, garbage_words) + similarity_ratio = textdistance.jaro_winkler(cleaned_comment, search_word) + if similarity_ratio > float(best_similarity["similarity"]): + best_similarity = {"company": company,"living_space": None,"found_from": "Customer Public Name","similarity": similarity_ratio,"garbage": garbage_words,"cleaned": cleaned_comment,} + # print( + # 'cleaned_comment', cleaned_comment, '\n' + # 'search_word', search_word, '\n' + # 'best_similarity', best_similarity, '\n' + # 'company name', company.public_name, '\n' + # 'similarity_ratio', similarity_ratio, '\n' + # 'garbage_words', garbage_words + # ) + return best_similarity + + +def parse_comment_to_split_with_star(account_record: dict): + if "*" in account_record.get("process_comment", ""): + process_comment = str(account_record.get("process_comment", "").replace("**", "*")) + process_comments = process_comment.split("*") + return len(process_comments), *process_comments + return 1, account_record.get("process_comment", "") + + +def check_build_living_space_matches_with_build_parts(living_space_dict: dict, best_similarity: dict, iban: str, whole_comment: str): + if 0.6 < float(best_similarity["similarity"]) < 0.8: + build_parts = living_space_dict[iban]["build_parts"] + if best_similarity["living_space"]: + build_parts_id = best_similarity["living_space"].build_parts_id + parser_dict = dict(comment=str(whole_comment), max_build_part=len(build_parts)) + print("build parts similarity", best_similarity, "parser_dict", parser_dict) + results_list = parse_comment_for_build_parts(**parser_dict) + print("results_list", results_list) + if not results_list: + return best_similarity + for build_part in build_parts: + print("part_no", int(build_part.part_no), " | ", results_list) + print("build_part", int(build_part.id), int(build_parts_id)) + print("cond", int(build_part.id) == int(build_parts_id)) + print("cond2", int(build_part.part_no) in results_list) + if (int(build_part.id) == int(build_parts_id) and int(build_part.part_no) in results_list): + similarity = float(best_similarity["similarity"]) + best_similarity["similarity"] = (1 - similarity) / 2 + similarity + print("similarity", best_similarity["similarity"]) + break + return best_similarity + + +def parse_comment_with_name(account_record: dict, living_space_dict: dict = None): + + comments = parse_comment_to_split_with_star(account_record=account_record) + best_similarity = {"similarity": 0.0} + comments_list, comments_length = comments[1:], int(comments[0]) + + if int(account_record.get("currency_value", 0)) > 0: # Build receive money from living space people + living_space_matches = dict(living_space_dict=living_space_dict, iban=account_record.get("iban", None), whole_comment=account_record.get("process_comment", None)) + if comments_length == 1: + best_similarity = parse_comment_for_living_space(iban=account_record.get("iban", None), comment=comments_list[0], living_space_dict=living_space_dict) + best_similarity["send_person_id"] = best_similarity.get("customer_id", None) + living_space_matches["best_similarity"] = best_similarity + # if 0.5 < float(best_similarity['similarity']) < 0.8 + best_similarity = check_build_living_space_matches_with_build_parts(**living_space_matches) + return best_similarity + for comment in comments_list: + similarity_result = parse_comment_for_living_space(iban=account_record.get("iban", None), comment=comment, living_space_dict=living_space_dict) + if float(similarity_result["similarity"]) > float(best_similarity["similarity"]): + best_similarity = similarity_result + living_space_matches["best_similarity"] = best_similarity + # if 0.5 < float(best_similarity['similarity']) < 0.8: + best_similarity = check_build_living_space_matches_with_build_parts(**living_space_matches) + print("last best_similarity", best_similarity) + return best_similarity + else: # Build pays money for service taken from company or individual + if not comments_length > 1: + best_similarity = parse_comment_for_company_or_individual(comment=comments_list[0]) + best_similarity["send_person_id"] = best_similarity.get("customer_id", None) + return best_similarity + for comment in comments_list: + similarity_result = parse_comment_for_company_or_individual(comment=comment) + if float(similarity_result["similarity"]) > float(best_similarity["similarity"]): + best_similarity = similarity_result + return best_similarity + + +def parse_comment_with_name_iban_description(account_record: dict): + with AccountRecords.new_session() as session: + BuildIbanDescription.set_session(session) + Companies.set_session(session) + + comments = parse_comment_to_split_with_star(account_record=account_record) + comments_list, comments_length = comments[1:], int(comments[0]) + iban_results = BuildIbanDescription.query.filter(BuildIbanDescription.iban == account_record.get("iban", "")).all() + best_similarity = dict(company=None,living_space=None,found_from=None,similarity=0.0,garbage="",cleaned="",) + for comment in comments_list: + for iban_result in iban_results: + search_word = unidecode(iban_result.search_word) + garbage_words = get_garbage_words(comment, search_word) + cleaned_comment = remove_garbage_words(comment, garbage_words) + similarity_ratio = textdistance.jaro_winkler(cleaned_comment, search_word) + company = Companies.query.filter_by(id=iban_result.company_id).first() + if float(similarity_ratio) > float(best_similarity["similarity"]): + best_similarity = {"company": company,"living_space": None,"found_from": "Customer Public Name Description","similarity": similarity_ratio,"garbage": garbage_words,"cleaned": cleaned_comment,} + return best_similarity + + # "decision_book_project_id": None, + # "build_parts_id": None, + # "decision_book_project_id": iban_result.decision_book_project_id, + # "build_parts_id": iban_result.build_parts_id, + + # print('account_record.process_comment', account_record.process_comment) + # test_pattern( + # word=unidecode("no"), + # test_cases=[account_record.process_comment] + # ) + # test_pattern(word="daire", test_cases=comments_list) + + # sm_dict_extended, sm_dict_digit = {}, {} + # iban_results = BuildIbanDescription.filter_all( + # BuildIbanDescription.iban == iban, system=True + # ).data + # for iban_result in iban_results or []: + # candidate_parts = comment.split(" ") + # extended_candidate_parts, digit_part = [], [] + # for part in candidate_parts: + # if part.lower() not in ["no", "daire", "nolu"]: + # extended_candidate_parts.append(part) + # if extended_candidate_parts: + # if all( + # candidate_part.lower() in comment.lower() + # for candidate_part in extended_candidate_parts + # ): + # similarity_ratio = textdistance.jaro_winkler( + # unidecode(str(iban_result.search_word)), comment + # ) + # found = False + # name_list = ( + # unidecode(str(iban_result.search_word)).replace(".", " ").split(" ") + # ) + # for name in name_list: + # if len(name) > 3 and name.lower() in comment.lower(): + # found = True + # break + # + # if not found: + # similarity_ratio = 0.1 + # sm_dict_extended[f"{iban_result.id}"] = similarity_ratio + # if sm_dict_extended: + # result = sorted( + # sm_dict_extended.items(), key=lambda item: item[1], reverse=True + # )[0] + # if float(result[1]) >= 0.5: + # iban_result = BuildIbanDescription.filter_one( + # BuildIbanDescription.id == int(result[0]), system=True + # ).data + # return { + # "company_id": iban_result.company_id, + # "customer_id": iban_result.customer_id, + # "found_from": "Name", + # "similarity": result[1], + # } + # return { + # "company_id": None, + # "customer_id": None, + # "found_from": None, + # "similarity": 0.0, + # } + + +# +# def wag_insert_budget_record(data): +# similarity_result = parse_comment_with_name(data["iban"], data["process_comment"]) +# build_iban = BuildIbans.find_one(iban=data["iban"]) +# +# if payload := InsertBudgetRecord(**data): +# payload_dict = payload.model_dump(exclude_unset=True, exclude_none=True) +# decision_books = BuildDecisionBook.select_only( +# BuildDecisionBook.period_start_date +# < strip_date_to_valid(payload_dict["bank_date"]), +# BuildDecisionBook.period_stop_date +# > strip_date_to_valid(payload_dict["bank_date"]), +# select_args=[BuildDecisionBook.id], +# order_by=[BuildDecisionBook.expiry_ends.desc()], +# ) +# payload_dict["build_id"] = getattr( +# BuildIbans.find_one(iban=data["iban"]), "build_id", None +# ) +# living_space, count = BuildLivingSpace.find_living_from_customer_id( +# similarity_result.get("customer_id", None), +# strip_date_to_valid(payload_dict["bank_date"]), +# ) +# # living_space, count = BuildLivingSpace.filter( +# # or_( +# # BuildLivingSpace.owner_person_id +# # == similarity_result.get("customer_id", None), +# # BuildLivingSpace.life_person_id +# # == similarity_result.get("customer_id", None), +# # ), +# # BuildLivingSpace.start_date +# # < strip_date_to_valid(payload_dict["bank_date"]) - timedelta(days=30), +# # BuildLivingSpace.stop_date +# # > strip_date_to_valid(payload_dict["bank_date"]) + timedelta(days=30), +# # BuildLivingSpace.active == True, +# # BuildLivingSpace.deleted == False, +# # ) +# payload_dict["build_decision_book_id"] = ( +# decision_books[0][0].id if decision_books else None +# ) +# payload_dict["company_id"] = similarity_result.get("company_id", None) +# payload_dict["customer_id"] = similarity_result.get("customer_id", None) +# payload_dict["send_person_id"] = similarity_result.get("send_person_id", None) +# +# payload_dict["build_parts_id"] = ( +# living_space[0].build_parts_id if living_space else None +# ) +# +# payload_dict["bank_date_y"] = strip_date_to_valid( +# payload_dict["bank_date"] +# ).year +# payload_dict["bank_date_m"] = strip_date_to_valid( +# payload_dict["bank_date"] +# ).month +# payload_dict["bank_date_d"] = strip_date_to_valid(payload_dict["bank_date"]).day +# payload_dict["bank_date_w"] = strip_date_to_valid( +# payload_dict["bank_date"] +# ).isocalendar()[2] +# payload_dict["build_id"] = build_iban.build_id if build_iban else None +# payload_dict["replication_id"] = 55 +# payload_dict["receive_debit"] = ( +# "R" if payload_dict["currency_value"] < 0 else "D" +# ) +# data, found = AccountRecords.find_or_create( +# **payload_dict, +# found_from=similarity_result.get("found_from", None), +# similarity=similarity_result.get("similarity", 0.0), +# ) +# data.payment_budget_record_close() +# return data, found diff --git a/ServicesBank/Finder/app_accounts.py b/ServicesBank/Finder/app_accounts.py new file mode 100644 index 0000000..7270142 --- /dev/null +++ b/ServicesBank/Finder/app_accounts.py @@ -0,0 +1,266 @@ +import sys +import arrow + +if "/service_account_records" not in list(sys.path): + sys.path.append("/service_account_records") + +from decimal import Decimal +from pydantic import BaseModel +from typing import Optional, Union +from sqlalchemy import func, cast, Date +from Schemas import AccountRecords, BuildIbans, BuildDecisionBook, Build, BuildLivingSpace, People, OccupantTypes, BuildParts, BuildDecisionBookPayments, ApiEnumDropdown +from account_record_parser import parse_comment_with_name, parse_comment_with_name_iban_description +# from ServicesApi.Schemas.account.account import AccountRecords +# from ServicesApi.Schemas.building.build import BuildIbans, BuildDecisionBook, BuildParts, BuildLivingSpace, Build +# from ServicesApi.Schemas.identity.identity import People, OccupantTypes +# from ServicesApi.Schemas.others.enums import ApiEnumDropdown +# from ServicesApi.Schemas.building.decision_book import BuildDecisionBookPayments + + +# AccountRecords.approved_record = False + +def account_find_build_from_iban(session): + created_ibans = [] + AccountRecords.set_session(session) + BuildIbans.set_session(session) + + account_records_ibans = AccountRecords.query.filter(AccountRecords.build_id == None, AccountRecords.approved_record == False).distinct(AccountRecords.iban).all() + for account_records_iban in account_records_ibans: + found_iban: BuildIbans = BuildIbans.query.filter(BuildIbans.iban == account_records_iban.iban).first() + if not found_iban: + create_build_ibans = BuildIbans.create(iban=account_records_iban.iban, start_date=str(arrow.now().shift(days=-1))) + create_build_ibans.save() + created_ibans.append(account_records_iban.iban) + else: + update_dict = {"build_id": found_iban.build_id, "build_uu_id": str(found_iban.build_uu_id)} + session.query(AccountRecords).filter(AccountRecords.iban == account_records_iban.iban).update(update_dict, synchronize_session=False) + session.commit() + + +def account_records_find_decision_book(session): + AccountRecords.set_session(session) + BuildIbans.set_session(session) + BuildDecisionBook.set_session(session) + + created_ibans, iban_build_dict = [], {} + account_records_list: list[AccountRecords] = AccountRecords.query.filter(AccountRecords.build_id != None, AccountRecords.build_decision_book_id == None).order_by(AccountRecords.bank_date.desc()).all() + for account_record in account_records_list: + if found_iban := BuildIbans.query.filter(BuildIbans.iban == account_record.iban).first(): + if found_decision_book := BuildDecisionBook.query.filter( + BuildDecisionBook.build_id == found_iban.build_id, + cast(BuildDecisionBook.expiry_starts, Date) <= cast(account_record.bank_date, Date), + cast(BuildDecisionBook.expiry_ends, Date) >= cast(account_record.bank_date, Date), + ).first(): + account_record.build_decision_book_id = found_decision_book.id + account_record.build_decision_book_uu_id = str(found_decision_book.uu_id) + account_record.save() + + +def account_get_people_and_living_space_info_via_iban() -> dict: + build_living_space_dict = {} + AccountRecords.set_session(session) + OccupantTypes.set_session(session) + BuildParts.set_session(session) + BuildLivingSpace.set_session(session) + People.set_session(session) + + account_records_ibans = AccountRecords.query.filter(AccountRecords.build_decision_book_id != None).distinct(AccountRecords.iban).all() + + flat_resident = OccupantTypes.query.filter_by(occupant_category_type="FL", occupant_code="FL-RES").first() + flat_owner = OccupantTypes.query.filter_by(occupant_category_type="FL", occupant_code="FL-OWN").first() + flat_tenant = OccupantTypes.query.filter_by(occupant_category_type="FL", occupant_code="FL-TEN").first() + flat_represent = OccupantTypes.query.filter_by(occupant_category_type="FL", occupant_code="FL-REP").first() + + for account_records_iban in account_records_ibans: + if account_records_iban.iban not in build_living_space_dict: + build_parts = BuildParts.query.filter_by(build_id=account_records_iban.build_id, human_livable=True).all() + living_spaces = BuildLivingSpace.query.filter( + BuildLivingSpace.build_parts_id.in_([build_parts.id for build_parts in build_parts]), + BuildLivingSpace.occupant_type_id.in_([flat_resident.id, flat_owner.id, flat_tenant.id, flat_represent.id]), + ).all() + living_spaces_people = [living_space.person_id for living_space in living_spaces if living_space.person_id] + people_list = People.query.filter(People.id.in_(living_spaces_people)).all() + people_list_dict = [] + for people in people_list: + people_list_dict.append({"id": people.id, "uu_id": str(people.uu_id), "firstname": people.firstname, "surname": people.surname, "middle_name": people.middle_name}) + living_spaces_dict = [] + for living_space in living_spaces: + living_spaces_dict.append({"id": living_space.id, "uu_id": str(living_space.uu_id), "person_id": living_space.person_id, "person_uu_id": str(living_space.person_uu_id)}) + build_parts_dict = [] + for build_parts in build_parts: + build_parts_dict.append({"id": build_parts.id, "uu_id": str(build_parts.uu_id)}) + build_living_space_dict[str(account_records_iban.iban)] = {"people": list(people_list_dict), "living_space": list(living_spaces_dict), "build_parts": list(build_parts_dict)} + return build_living_space_dict + + +def account_records_search(): + build_living_space_dict = account_get_people_and_living_space_info_via_iban() + found_list = [] + with AccountRecords.new_session() as session: + AccountRecords.set_session(session) + + account_records_list: list[AccountRecords] = AccountRecords.query.filter(AccountRecords.build_decision_book_id != None).all() + for account_record in account_records_list: + account_record_dict = account_record.get_dict() + similarity_result = parse_comment_with_name(account_record=account_record_dict, living_space_dict=build_living_space_dict) + fs, ac = float(similarity_result.get("similarity", 0)), float(account_record_dict.get("similarity", 0)) + if fs >= 0.8 and fs >= ac: + print("similarity_result positive", similarity_result) + found_list.append(similarity_result) + account_save_search_result(account_record=account_record_dict, similarity_result=similarity_result) + else: + similarity_result = parse_comment_with_name_iban_description(account_record=account_record_dict) + fs, ac = float(similarity_result.get("similarity", 0)), float(account_record_dict.get("similarity", 0)) + if fs >= 0.8 and fs > ac: + print("similarity_result negative", similarity_result) + found_list.append(similarity_result) + account_save_search_result(account_record=account_record_dict, similarity_result=similarity_result) + print("Account Records Search : ", len(found_list), "/", len(account_records_list)) + return + + +def account_save_search_result(account_record, similarity_result): + BuildParts.set_session(session) + Build.set_session(session) + BuildLivingSpace.set_session(session) + + found_company = similarity_result.get("company", None) + found_customer, part, build = (similarity_result.get("living_space", None), None, None) + if found_customer: + found_living_space = BuildLivingSpace.query.filter_by(id=int(found_customer.get("id"))).first() + part = BuildParts.query.filter_by(id=found_living_space.build_parts_id, human_livable=True).first() + if part: + build = Build.query.filter_by(id=part.build_id).first() + + account_record_dict = { + "similarity": similarity_result.get("similarity", 0.00), + "found_from": similarity_result.get("found_from", None), + "company_id": getattr(found_company, "id", None), + "company_uu_id": str(getattr(found_company, "uu_id", None)) if getattr(found_company, "uu_id", None) else None, + "build_parts_id": getattr(part, "id", None) if getattr(part, "id", None) else None, + "build_parts_uu_id": str(getattr(part, "uu_id", None)) if getattr(part, "uu_id", None) else None, + "living_space_id": getattr(found_customer, "id", None), + "living_space_uu_id": str(getattr(found_customer, "uu_id", None)) if getattr(found_customer, "uu_id", None) else None, + } + if not account_record.get("build_id") and build: + account_record_dict.update({"build_id": getattr(build, "id", None), "build_uu_id": str(getattr(build, "uu_id", None)) if getattr(build, "uu_id", None) else None}) + AccountRecords.query.filter_by(uu_id=str(account_record.get("uu_id"))).update(account_record_dict) + session.commit() + + +def pay_the_registration(account_record, receive_enum, debit_enum, is_old_record: bool = False): + with AccountRecords.new_session() as session: + AccountRecords.set_session(session) + BuildDecisionBookPayments.set_session(session) + + current_currency_value = float(Decimal(account_record.currency_value)) - float(Decimal(account_record.remainder_balance)) + if not current_currency_value > 0: + return current_currency_value + + process_date = arrow.get(account_record.bank_date) + account_bank_date_year, account_bank_date_month = (process_date.date().year, process_date.date().month) + payment_arguments_debit = [ + BuildDecisionBookPayments.build_parts_id == account_record.build_parts_id, + BuildDecisionBookPayments.payment_types_id == debit_enum.id, + BuildDecisionBookPayments.account_records_id == None, + ] + if not is_old_record: + payment_arguments_debit.extend([BuildDecisionBookPayments.process_date_y == int(account_bank_date_year), BuildDecisionBookPayments.process_date_m == int(account_bank_date_month)]) + payments = BuildDecisionBookPayments.query.filter(*payment_arguments_debit).order_by(BuildDecisionBookPayments.process_date.asc()).all() + for payment in payments: + if not current_currency_value > 0: + return current_currency_value + payment_arguments_receive = [ + BuildDecisionBookPayments.build_parts_id == account_record.build_parts_id, + BuildDecisionBookPayments.payment_plan_time_periods == payment.payment_plan_time_periods, + BuildDecisionBookPayments.payment_types_id == receive_enum.id, + BuildDecisionBookPayments.build_decision_book_item_id == payment.build_decision_book_item_id, + BuildDecisionBookPayments.decision_book_project_id == payment.decision_book_project_id, + BuildDecisionBookPayments.process_date == payment.process_date, + ] + if not is_old_record: + payment_arguments_receive.extend([BuildDecisionBookPayments.process_date_y == int(account_bank_date_year), BuildDecisionBookPayments.process_date_m == int(account_bank_date_month)]) + + payment_received = BuildDecisionBookPayments.query.filter(*payment_arguments_receive).all() + sum_of_payment_received = sum([abs(payment.payment_amount) for payment in payment_received]) + net_amount = float(abs(Decimal(payment.payment_amount))) - float(abs(Decimal(sum_of_payment_received))) + if not net_amount > 0: + continue + if float(abs(current_currency_value)) < float(abs(net_amount)): + net_amount = float(current_currency_value) + process_date = arrow.get(payment.process_date) + try: + found_payment = BuildDecisionBookPayments.query.filter_by( + build_parts_id=payment.build_parts_id, + payment_plan_time_periods=payment.payment_plan_time_periods, + payment_types_id=receive_enum.id, + build_decision_book_item_id=payment.build_decision_book_item_id, + decision_book_project_id=payment.decision_book_project_id, + process_date=str(process_date), + ).first() + if found_payment: + continue + created_book_payment = BuildDecisionBookPayments.create( + payment_plan_time_periods=payment.payment_plan_time_periods, + payment_amount=float(abs(net_amount)), + payment_types_id=receive_enum.id, + payment_types_uu_id=str(receive_enum.uu_id), + process_date=str(process_date), + process_date_m=process_date.date().month, + process_date_y=process_date.date().year, + period_time=f"{process_date.year}-{str(process_date.month).zfill(2)}", + build_parts_id=payment.build_parts_id, + build_parts_uu_id=str(payment.build_parts_uu_id), + account_records_id=account_record.id, + account_records_uu_id=str(account_record.uu_id), + build_decision_book_item_id=payment.build_decision_book_item_id, + build_decision_book_item_uu_id=str(payment.build_decision_book_item_uu_id), + decision_book_project_id=payment.decision_book_project_id, + decision_book_project_uu_id=str(payment.decision_book_project_uu_id), + ) + created_book_payment.save_and_confirm() + created_payment_amount = float(Decimal(created_book_payment.payment_amount)) + remainder_balance = float(Decimal(account_record.remainder_balance)) + float(abs(created_payment_amount)) + account_record.update(remainder_balance=remainder_balance) + account_record.save() + if current_currency_value >= abs(net_amount): + current_currency_value -= abs(net_amount) + except Exception as e: + print("Exception of decision payment ln:300", e) + return current_currency_value + + +def send_accounts_to_decision_payment(): + with ApiEnumDropdown.new_session() as session: + ApiEnumDropdown.set_session(session) + AccountRecords.set_session(session) + + receive_enum = ApiEnumDropdown.query.filter_by(enum_class="DebitTypes", key="DT-R").first() + debit_enum = ApiEnumDropdown.query.filter_by(enum_class="DebitTypes", key="DT-D").first() + account_records_list: list[AccountRecords] = AccountRecords.query.filter( + AccountRecords.remainder_balance < AccountRecords.currency_value, + AccountRecords.approved_record == True, + AccountRecords.receive_debit == receive_enum.id, + ).order_by(AccountRecords.bank_date.desc()).limit(1000).offset(0).all() + for account_record in account_records_list: + current_currency_value = pay_the_registration(account_record, receive_enum, debit_enum) + if current_currency_value > 0: + pay_the_registration(account_record, receive_enum, debit_enum, True) + if abs(float(Decimal(account_record.remainder_balance))) == abs(float(Decimal(account_record.currency_value))): + account_record.update(status_id=97) + account_record.save() + # # # todo If the payment is more than the amount, then create a new account record with the remaining amount + return + + +def account_records_service() -> None: + print("Account Records Service is running...") + account_find_build_from_iban(session=session) + account_records_find_decision_book(session=session) + account_records_search(session=session) + send_accounts_to_decision_payment(session=session) + print("Account Records Service is finished...") + + +if __name__ == "__main__": + account_records_service() diff --git a/ServicesBank/Finder/configs.py b/ServicesBank/Finder/configs.py new file mode 100644 index 0000000..6f8eed1 --- /dev/null +++ b/ServicesBank/Finder/configs.py @@ -0,0 +1,8 @@ +class AccountConfig: + BEFORE_DAY = 30 + CATEGORIES = { + "DAIRE": ["daire", "dagire", "daare", "nolu daire", "no", "nolu dairenin"], + "APARTMAN": ["apartman", "aparman", "aprmn"], + "VILLA": ["villa", "vlla"], + "BINA": ["bina", "binna"], + } diff --git a/ServicesBank/Finder/entrypoint.sh b/ServicesBank/Finder/entrypoint.sh new file mode 100644 index 0000000..d1bc6ca --- /dev/null +++ b/ServicesBank/Finder/entrypoint.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# Create environment file that will be available to cron jobs +echo "# Environment variables for cron jobs" > /env.sh +echo "EMAIL_HOST=\"$EMAIL_HOST\"" >> /env.sh +echo "EMAIL_USERNAME=\"$EMAIL_USERNAME\"" >> /env.sh +echo "EMAIL_PASSWORD=\"$EMAIL_PASSWORD\"" >> /env.sh +echo "EMAIL_PORT=$EMAIL_PORT" >> /env.sh +echo "EMAIL_SEND=$EMAIL_SEND" >> /env.sh +echo "DB_HOST=\"$DB_HOST\"" >> /env.sh +echo "DB_USER=\"$DB_USER\"" >> /env.sh +echo "DB_PASSWORD=\"$DB_PASSWORD\"" >> /env.sh +echo "DB_PORT=$DB_PORT" >> /env.sh +echo "DB_NAME=\"$DB_NAME\"" >> /env.sh + +# Add Python environment variables +echo "PYTHONPATH=/" >> /env.sh +echo "PYTHONUNBUFFERED=1" >> /env.sh +echo "PYTHONDONTWRITEBYTECODE=1" >> /env.sh + +# Make the environment file available to cron +echo "0 8 * * * /run_app.sh >> /var/log/cron.log 2>&1" > /tmp/crontab_list +crontab /tmp/crontab_list + +# Start cron +cron + +# Tail the log file +tail -f /var/log/cron.log diff --git a/ServicesBank/Finder/regex_func.py b/ServicesBank/Finder/regex_func.py new file mode 100644 index 0000000..ca587ea --- /dev/null +++ b/ServicesBank/Finder/regex_func.py @@ -0,0 +1,28 @@ +import sys + +if "/service_account_records" not in list(sys.path): + sys.path.append("/service_account_records") + +import re + +from difflib import get_close_matches +from configs import AccountConfig + + +def word_straighten(word, ref_list, threshold=0.8): + matches = get_close_matches(word, ref_list, n=1, cutoff=threshold) + return matches[0] if matches else word + + +def category_finder(text, output_template="{kategori} {numara}"): + categories = AccountConfig.CATEGORIES + result = {category: [] for category in categories} + for category, patterns in categories.items(): + words = re.split(r"\W+", text) + straighten_words = [word_straighten(word, patterns) for word in words] + straighten_text = " ".join(straighten_words) + pattern = (r"(?:\b|\s|^)(?:" + "|".join(map(re.escape, patterns)) + r")(?:\s*|:|\-|\#)*(\d+)(?:\b|$)") + if founds_list := re.findall(pattern, straighten_text, re.IGNORECASE): + list_of_output = [output_template.format(kategori=category, numara=num) for num in founds_list] + result[category].extend([i for i in list_of_output if str(i).replace(" ", "")]) + return result diff --git a/docker-compose.yml b/docker-compose.yml index b035a48..dd60895 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,27 +31,27 @@ services: # cpus: 1 # mem_limit: 2048m - tester_service: - container_name: tester_service - build: - context: . - dockerfile: ServicesApi/Builds/TestApi/Dockerfile - env_file: - - api_env.env - networks: - - wag-services - environment: - - API_PATH=app:app - - API_HOST=0.0.0.0 - - API_PORT=8005 - - API_LOG_LEVEL=info - - API_RELOAD=1 - - API_APP_NAME=evyos-tester-api-gateway - - API_TITLE=WAG API Tester Api Gateway - - API_DESCRIPTION=This api is serves as web tester api gateway only to evyos web services. - - API_APP_URL=https://tester_service - ports: - - "8005:8005" + # tester_service: + # container_name: tester_service + # build: + # context: . + # dockerfile: ServicesApi/Builds/TestApi/Dockerfile + # env_file: + # - api_env.env + # networks: + # - wag-services + # environment: + # - API_PATH=app:app + # - API_HOST=0.0.0.0 + # - API_PORT=8005 + # - API_LOG_LEVEL=info + # - API_RELOAD=1 + # - API_APP_NAME=evyos-tester-api-gateway + # - API_TITLE=WAG API Tester Api Gateway + # - API_DESCRIPTION=This api is serves as web tester api gateway only to evyos web services. + # - API_APP_URL=https://tester_service + # ports: + # - "8005:8005" # account_service: # container_name: account_service @@ -187,7 +187,69 @@ services: - API_APP_URL=https://management_service ports: - "8003:8003" - # restart: unless-stopped + # restart: unless-stopped python3 app_accounts.py + + # finder_build_from_iban_service: + # container_name: finder_build_from_iban_service + # env_file: + # - api_env.env + # build: + # context: . + # dockerfile: ServicesBank/Finder/BuildFromIban/Dockerfile + # networks: + # - wag-services + # logging: + # driver: "json-file" + # options: + # max-size: "10m" + # max-file: "3" + + # finder_build_living_space_service: + # container_name: finder_build_living_space_service + # env_file: + # - api_env.env + # build: + # context: . + # dockerfile: ServicesBank/Finder/BuildLivingSpace/Dockerfile + # networks: + # - wag-services + # logging: + # driver: "json-file" + # options: + # max-size: "10m" + # max-file: "3" + + # finder_decision_book_service: + # container_name: finder_decision_book_service + # env_file: + # - api_env.env + # build: + # context: . + # dockerfile: ServicesBank/Finder/DecisionBook/Dockerfile + # networks: + # - wag-services + # logging: + # driver: "json-file" + # options: + # max-size: "10m" + # max-file: "3" + + finder_payment_service: + container_name: finder_payment_service + env_file: + - api_env.env + build: + context: . + dockerfile: ServicesBank/Finder/Payment/Dockerfile + networks: + - wag-services + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + # cpus: 0.25 + # mem_limit: 512m # address_service: # container_name: address_service @@ -281,9 +343,9 @@ services: # container_name: initializer_service # build: # context: . - # dockerfile: ServicesApi/Builds/initial_service/Dockerfile + # dockerfile: ServicesApi/Builds/Initial/Dockerfile # environment: - # - SET_ALEMBIC=0 + # - SET_ALEMBIC=1 # networks: # - wag-services # env_file: diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 11e071f..0000000 --- a/package-lock.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "production-evyos-systems-and-services-3", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "@tanstack/react-query": "^5.80.7" - } - }, - "node_modules/@tanstack/query-core": { - "version": "5.80.7", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.80.7.tgz", - "integrity": "sha512-s09l5zeUKC8q7DCCCIkVSns8zZrK4ZDT6ryEjxNBFi68G4z2EBobBS7rdOY3r6W1WbUDpc1fe5oY+YO/+2UVUg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/react-query": { - "version": "5.80.7", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.80.7.tgz", - "integrity": "sha512-u2F0VK6+anItoEvB3+rfvTO9GEh2vb00Je05OwlUe/A0lkJBgW1HckiY3f9YZa+jx6IOe4dHPh10dyp9aY3iRQ==", - "license": "MIT", - "dependencies": { - "@tanstack/query-core": "5.80.7" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^18 || ^19" - } - }, - "node_modules/react": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", - "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 4d769c5..0000000 --- a/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "dependencies": { - "@tanstack/react-query": "^5.80.7" - } -} diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..d158b0e --- /dev/null +++ b/uv.lock @@ -0,0 +1,910 @@ +version = 1 +revision = 2 +requires-python = ">=3.12" + +[[package]] +name = "alembic" +version = "1.16.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/35/116797ff14635e496bbda0c168987f5326a6555b09312e9b817e360d1f56/alembic-1.16.2.tar.gz", hash = "sha256:e53c38ff88dadb92eb22f8b150708367db731d58ad7e9d417c9168ab516cbed8", size = 1963563, upload-time = "2025-06-16T18:05:08.566Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/e2/88e425adac5ad887a087c38d04fe2030010572a3e0e627f8a6e8c33eeda8/alembic-1.16.2-py3-none-any.whl", hash = "sha256:5f42e9bd0afdbd1d5e3ad856c01754530367debdebf21ed6894e34af52b3bb03", size = 242717, upload-time = "2025-06-16T18:05:10.27Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, +] + +[[package]] +name = "arrow" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "types-python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/00/0f6e8fcdb23ea632c866620cc872729ff43ed91d284c866b515c6342b173/arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85", size = 131960, upload-time = "2023-09-30T22:11:18.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/ed/e97229a566617f2ae958a6b13e7cc0f585470eac730a73e9e82c32a3cdd2/arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80", size = 66419, upload-time = "2023-09-30T22:11:16.072Z" }, +] + +[[package]] +name = "certifi" +version = "2025.6.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cryptography" +version = "45.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/c8/a2a376a8711c1e11708b9c9972e0c3223f5fc682552c82d8db844393d6ce/cryptography-45.0.4.tar.gz", hash = "sha256:7405ade85c83c37682c8fe65554759800a4a8c54b2d96e0f8ad114d31b808d57", size = 744890, upload-time = "2025-06-10T00:03:51.297Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/1c/92637793de053832523b410dbe016d3f5c11b41d0cf6eef8787aabb51d41/cryptography-45.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:425a9a6ac2823ee6e46a76a21a4e8342d8fa5c01e08b823c1f19a8b74f096069", size = 7055712, upload-time = "2025-06-10T00:02:38.826Z" }, + { url = "https://files.pythonhosted.org/packages/ba/14/93b69f2af9ba832ad6618a03f8a034a5851dc9a3314336a3d71c252467e1/cryptography-45.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:680806cf63baa0039b920f4976f5f31b10e772de42f16310a6839d9f21a26b0d", size = 4205335, upload-time = "2025-06-10T00:02:41.64Z" }, + { url = "https://files.pythonhosted.org/packages/67/30/fae1000228634bf0b647fca80403db5ca9e3933b91dd060570689f0bd0f7/cryptography-45.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4ca0f52170e821bc8da6fc0cc565b7bb8ff8d90d36b5e9fdd68e8a86bdf72036", size = 4431487, upload-time = "2025-06-10T00:02:43.696Z" }, + { url = "https://files.pythonhosted.org/packages/6d/5a/7dffcf8cdf0cb3c2430de7404b327e3db64735747d641fc492539978caeb/cryptography-45.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f3fe7a5ae34d5a414957cc7f457e2b92076e72938423ac64d215722f6cf49a9e", size = 4208922, upload-time = "2025-06-10T00:02:45.334Z" }, + { url = "https://files.pythonhosted.org/packages/c6/f3/528729726eb6c3060fa3637253430547fbaaea95ab0535ea41baa4a6fbd8/cryptography-45.0.4-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:25eb4d4d3e54595dc8adebc6bbd5623588991d86591a78c2548ffb64797341e2", size = 3900433, upload-time = "2025-06-10T00:02:47.359Z" }, + { url = "https://files.pythonhosted.org/packages/d9/4a/67ba2e40f619e04d83c32f7e1d484c1538c0800a17c56a22ff07d092ccc1/cryptography-45.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ce1678a2ccbe696cf3af15a75bb72ee008d7ff183c9228592ede9db467e64f1b", size = 4464163, upload-time = "2025-06-10T00:02:49.412Z" }, + { url = "https://files.pythonhosted.org/packages/7e/9a/b4d5aa83661483ac372464809c4b49b5022dbfe36b12fe9e323ca8512420/cryptography-45.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:49fe9155ab32721b9122975e168a6760d8ce4cffe423bcd7ca269ba41b5dfac1", size = 4208687, upload-time = "2025-06-10T00:02:50.976Z" }, + { url = "https://files.pythonhosted.org/packages/db/b7/a84bdcd19d9c02ec5807f2ec2d1456fd8451592c5ee353816c09250e3561/cryptography-45.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2882338b2a6e0bd337052e8b9007ced85c637da19ef9ecaf437744495c8c2999", size = 4463623, upload-time = "2025-06-10T00:02:52.542Z" }, + { url = "https://files.pythonhosted.org/packages/d8/84/69707d502d4d905021cac3fb59a316344e9f078b1da7fb43ecde5e10840a/cryptography-45.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:23b9c3ea30c3ed4db59e7b9619272e94891f8a3a5591d0b656a7582631ccf750", size = 4332447, upload-time = "2025-06-10T00:02:54.63Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ee/d4f2ab688e057e90ded24384e34838086a9b09963389a5ba6854b5876598/cryptography-45.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0a97c927497e3bc36b33987abb99bf17a9a175a19af38a892dc4bbb844d7ee2", size = 4572830, upload-time = "2025-06-10T00:02:56.689Z" }, + { url = "https://files.pythonhosted.org/packages/70/d4/994773a261d7ff98034f72c0e8251fe2755eac45e2265db4c866c1c6829c/cryptography-45.0.4-cp311-abi3-win32.whl", hash = "sha256:e00a6c10a5c53979d6242f123c0a97cff9f3abed7f064fc412c36dc521b5f257", size = 2932769, upload-time = "2025-06-10T00:02:58.467Z" }, + { url = "https://files.pythonhosted.org/packages/5a/42/c80bd0b67e9b769b364963b5252b17778a397cefdd36fa9aa4a5f34c599a/cryptography-45.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:817ee05c6c9f7a69a16200f0c90ab26d23a87701e2a284bd15156783e46dbcc8", size = 3410441, upload-time = "2025-06-10T00:03:00.14Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0b/2488c89f3a30bc821c9d96eeacfcab6ff3accc08a9601ba03339c0fd05e5/cryptography-45.0.4-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:964bcc28d867e0f5491a564b7debb3ffdd8717928d315d12e0d7defa9e43b723", size = 7031836, upload-time = "2025-06-10T00:03:01.726Z" }, + { url = "https://files.pythonhosted.org/packages/fe/51/8c584ed426093aac257462ae62d26ad61ef1cbf5b58d8b67e6e13c39960e/cryptography-45.0.4-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6a5bf57554e80f75a7db3d4b1dacaa2764611ae166ab42ea9a72bcdb5d577637", size = 4195746, upload-time = "2025-06-10T00:03:03.94Z" }, + { url = "https://files.pythonhosted.org/packages/5c/7d/4b0ca4d7af95a704eef2f8f80a8199ed236aaf185d55385ae1d1610c03c2/cryptography-45.0.4-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:46cf7088bf91bdc9b26f9c55636492c1cce3e7aaf8041bbf0243f5e5325cfb2d", size = 4424456, upload-time = "2025-06-10T00:03:05.589Z" }, + { url = "https://files.pythonhosted.org/packages/1d/45/5fabacbc6e76ff056f84d9f60eeac18819badf0cefc1b6612ee03d4ab678/cryptography-45.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7bedbe4cc930fa4b100fc845ea1ea5788fcd7ae9562e669989c11618ae8d76ee", size = 4198495, upload-time = "2025-06-10T00:03:09.172Z" }, + { url = "https://files.pythonhosted.org/packages/55/b7/ffc9945b290eb0a5d4dab9b7636706e3b5b92f14ee5d9d4449409d010d54/cryptography-45.0.4-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:eaa3e28ea2235b33220b949c5a0d6cf79baa80eab2eb5607ca8ab7525331b9ff", size = 3885540, upload-time = "2025-06-10T00:03:10.835Z" }, + { url = "https://files.pythonhosted.org/packages/7f/e3/57b010282346980475e77d414080acdcb3dab9a0be63071efc2041a2c6bd/cryptography-45.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7ef2dde4fa9408475038fc9aadfc1fb2676b174e68356359632e980c661ec8f6", size = 4452052, upload-time = "2025-06-10T00:03:12.448Z" }, + { url = "https://files.pythonhosted.org/packages/37/e6/ddc4ac2558bf2ef517a358df26f45bc774a99bf4653e7ee34b5e749c03e3/cryptography-45.0.4-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6a3511ae33f09094185d111160fd192c67aa0a2a8d19b54d36e4c78f651dc5ad", size = 4198024, upload-time = "2025-06-10T00:03:13.976Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c0/85fa358ddb063ec588aed4a6ea1df57dc3e3bc1712d87c8fa162d02a65fc/cryptography-45.0.4-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:06509dc70dd71fa56eaa138336244e2fbaf2ac164fc9b5e66828fccfd2b680d6", size = 4451442, upload-time = "2025-06-10T00:03:16.248Z" }, + { url = "https://files.pythonhosted.org/packages/33/67/362d6ec1492596e73da24e669a7fbbaeb1c428d6bf49a29f7a12acffd5dc/cryptography-45.0.4-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5f31e6b0a5a253f6aa49be67279be4a7e5a4ef259a9f33c69f7d1b1191939872", size = 4325038, upload-time = "2025-06-10T00:03:18.4Z" }, + { url = "https://files.pythonhosted.org/packages/53/75/82a14bf047a96a1b13ebb47fb9811c4f73096cfa2e2b17c86879687f9027/cryptography-45.0.4-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:944e9ccf67a9594137f942d5b52c8d238b1b4e46c7a0c2891b7ae6e01e7c80a4", size = 4560964, upload-time = "2025-06-10T00:03:20.06Z" }, + { url = "https://files.pythonhosted.org/packages/cd/37/1a3cba4c5a468ebf9b95523a5ef5651244693dc712001e276682c278fc00/cryptography-45.0.4-cp37-abi3-win32.whl", hash = "sha256:c22fe01e53dc65edd1945a2e6f0015e887f84ced233acecb64b4daadb32f5c97", size = 2924557, upload-time = "2025-06-10T00:03:22.563Z" }, + { url = "https://files.pythonhosted.org/packages/2a/4b/3256759723b7e66380397d958ca07c59cfc3fb5c794fb5516758afd05d41/cryptography-45.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:627ba1bc94f6adf0b0a2e35d87020285ead22d9f648c7e75bb64f367375f3b22", size = 3395508, upload-time = "2025-06-10T00:03:24.586Z" }, +] + +[[package]] +name = "dnspython" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197, upload-time = "2024-10-05T20:14:59.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632, upload-time = "2024-10-05T20:14:57.687Z" }, +] + +[[package]] +name = "faker" +version = "37.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/f9/66af4019ee952fc84b8fe5b523fceb7f9e631ed8484417b6f1e3092f8290/faker-37.4.0.tar.gz", hash = "sha256:7f69d579588c23d5ce671f3fa872654ede0e67047820255f43a4aa1925b89780", size = 1901976, upload-time = "2025-06-11T17:59:30.818Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/5e/c8c3c5ea0896ab747db2e2889bf5a6f618ed291606de6513df56ad8670a8/faker-37.4.0-py3-none-any.whl", hash = "sha256:cb81c09ebe06c32a10971d1bbdb264bb0e22b59af59548f011ac4809556ce533", size = 1942992, upload-time = "2025-06-11T17:59:28.698Z" }, +] + +[[package]] +name = "fastapi" +version = "0.115.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/64/ec0788201b5554e2a87c49af26b77a4d132f807a0fa9675257ac92c6aa0e/fastapi-0.115.13.tar.gz", hash = "sha256:55d1d25c2e1e0a0a50aceb1c8705cd932def273c102bff0b1c1da88b3c6eb307", size = 295680, upload-time = "2025-06-17T11:49:45.575Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/4a/e17764385382062b0edbb35a26b7cf76d71e27e456546277a42ba6545c6e/fastapi-0.115.13-py3-none-any.whl", hash = "sha256:0a0cab59afa7bab22f5eb347f8c9864b681558c278395e94035a741fc10cd865", size = 95315, upload-time = "2025-06-17T11:49:44.106Z" }, +] + +[[package]] +name = "greenlet" +version = "3.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/92/bb85bd6e80148a4d2e0c59f7c0c2891029f8fd510183afc7d8d2feeed9b6/greenlet-3.2.3.tar.gz", hash = "sha256:8b0dd8ae4c0d6f5e54ee55ba935eeb3d735a9b58a8a1e5b5cbab64e01a39f365", size = 185752, upload-time = "2025-06-05T16:16:09.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/94/ad0d435f7c48debe960c53b8f60fb41c2026b1d0fa4a99a1cb17c3461e09/greenlet-3.2.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:25ad29caed5783d4bd7a85c9251c651696164622494c00802a139c00d639242d", size = 271992, upload-time = "2025-06-05T16:11:23.467Z" }, + { url = "https://files.pythonhosted.org/packages/93/5d/7c27cf4d003d6e77749d299c7c8f5fd50b4f251647b5c2e97e1f20da0ab5/greenlet-3.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88cd97bf37fe24a6710ec6a3a7799f3f81d9cd33317dcf565ff9950c83f55e0b", size = 638820, upload-time = "2025-06-05T16:38:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7e/807e1e9be07a125bb4c169144937910bf59b9d2f6d931578e57f0bce0ae2/greenlet-3.2.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:baeedccca94880d2f5666b4fa16fc20ef50ba1ee353ee2d7092b383a243b0b0d", size = 653046, upload-time = "2025-06-05T16:41:36.343Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ab/158c1a4ea1068bdbc78dba5a3de57e4c7aeb4e7fa034320ea94c688bfb61/greenlet-3.2.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:be52af4b6292baecfa0f397f3edb3c6092ce071b499dd6fe292c9ac9f2c8f264", size = 647701, upload-time = "2025-06-05T16:48:19.604Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0d/93729068259b550d6a0288da4ff72b86ed05626eaf1eb7c0d3466a2571de/greenlet-3.2.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0cc73378150b8b78b0c9fe2ce56e166695e67478550769536a6742dca3651688", size = 649747, upload-time = "2025-06-05T16:13:04.628Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f6/c82ac1851c60851302d8581680573245c8fc300253fc1ff741ae74a6c24d/greenlet-3.2.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:706d016a03e78df129f68c4c9b4c4f963f7d73534e48a24f5f5a7101ed13dbbb", size = 605461, upload-time = "2025-06-05T16:12:50.792Z" }, + { url = "https://files.pythonhosted.org/packages/98/82/d022cf25ca39cf1200650fc58c52af32c90f80479c25d1cbf57980ec3065/greenlet-3.2.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:419e60f80709510c343c57b4bb5a339d8767bf9aef9b8ce43f4f143240f88b7c", size = 1121190, upload-time = "2025-06-05T16:36:48.59Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e1/25297f70717abe8104c20ecf7af0a5b82d2f5a980eb1ac79f65654799f9f/greenlet-3.2.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:93d48533fade144203816783373f27a97e4193177ebaaf0fc396db19e5d61163", size = 1149055, upload-time = "2025-06-05T16:12:40.457Z" }, + { url = "https://files.pythonhosted.org/packages/1f/8f/8f9e56c5e82eb2c26e8cde787962e66494312dc8cb261c460e1f3a9c88bc/greenlet-3.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:7454d37c740bb27bdeddfc3f358f26956a07d5220818ceb467a483197d84f849", size = 297817, upload-time = "2025-06-05T16:29:49.244Z" }, + { url = "https://files.pythonhosted.org/packages/b1/cf/f5c0b23309070ae93de75c90d29300751a5aacefc0a3ed1b1d8edb28f08b/greenlet-3.2.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:500b8689aa9dd1ab26872a34084503aeddefcb438e2e7317b89b11eaea1901ad", size = 270732, upload-time = "2025-06-05T16:10:08.26Z" }, + { url = "https://files.pythonhosted.org/packages/48/ae/91a957ba60482d3fecf9be49bc3948f341d706b52ddb9d83a70d42abd498/greenlet-3.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a07d3472c2a93117af3b0136f246b2833fdc0b542d4a9799ae5f41c28323faef", size = 639033, upload-time = "2025-06-05T16:38:53.983Z" }, + { url = "https://files.pythonhosted.org/packages/6f/df/20ffa66dd5a7a7beffa6451bdb7400d66251374ab40b99981478c69a67a8/greenlet-3.2.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:8704b3768d2f51150626962f4b9a9e4a17d2e37c8a8d9867bbd9fa4eb938d3b3", size = 652999, upload-time = "2025-06-05T16:41:37.89Z" }, + { url = "https://files.pythonhosted.org/packages/51/b4/ebb2c8cb41e521f1d72bf0465f2f9a2fd803f674a88db228887e6847077e/greenlet-3.2.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5035d77a27b7c62db6cf41cf786cfe2242644a7a337a0e155c80960598baab95", size = 647368, upload-time = "2025-06-05T16:48:21.467Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6a/1e1b5aa10dced4ae876a322155705257748108b7fd2e4fae3f2a091fe81a/greenlet-3.2.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2d8aa5423cd4a396792f6d4580f88bdc6efcb9205891c9d40d20f6e670992efb", size = 650037, upload-time = "2025-06-05T16:13:06.402Z" }, + { url = "https://files.pythonhosted.org/packages/26/f2/ad51331a157c7015c675702e2d5230c243695c788f8f75feba1af32b3617/greenlet-3.2.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c724620a101f8170065d7dded3f962a2aea7a7dae133a009cada42847e04a7b", size = 608402, upload-time = "2025-06-05T16:12:51.91Z" }, + { url = "https://files.pythonhosted.org/packages/26/bc/862bd2083e6b3aff23300900a956f4ea9a4059de337f5c8734346b9b34fc/greenlet-3.2.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:873abe55f134c48e1f2a6f53f7d1419192a3d1a4e873bace00499a4e45ea6af0", size = 1119577, upload-time = "2025-06-05T16:36:49.787Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/1fc0cc068cfde885170e01de40a619b00eaa8f2916bf3541744730ffb4c3/greenlet-3.2.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:024571bbce5f2c1cfff08bf3fbaa43bbc7444f580ae13b0099e95d0e6e67ed36", size = 1147121, upload-time = "2025-06-05T16:12:42.527Z" }, + { url = "https://files.pythonhosted.org/packages/27/1a/199f9587e8cb08a0658f9c30f3799244307614148ffe8b1e3aa22f324dea/greenlet-3.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5195fb1e75e592dd04ce79881c8a22becdfa3e6f500e7feb059b1e6fdd54d3e3", size = 297603, upload-time = "2025-06-05T16:20:12.651Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ca/accd7aa5280eb92b70ed9e8f7fd79dc50a2c21d8c73b9a0856f5b564e222/greenlet-3.2.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:3d04332dddb10b4a211b68111dabaee2e1a073663d117dc10247b5b1642bac86", size = 271479, upload-time = "2025-06-05T16:10:47.525Z" }, + { url = "https://files.pythonhosted.org/packages/55/71/01ed9895d9eb49223280ecc98a557585edfa56b3d0e965b9fa9f7f06b6d9/greenlet-3.2.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8186162dffde068a465deab08fc72c767196895c39db26ab1c17c0b77a6d8b97", size = 683952, upload-time = "2025-06-05T16:38:55.125Z" }, + { url = "https://files.pythonhosted.org/packages/ea/61/638c4bdf460c3c678a0a1ef4c200f347dff80719597e53b5edb2fb27ab54/greenlet-3.2.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f4bfbaa6096b1b7a200024784217defedf46a07c2eee1a498e94a1b5f8ec5728", size = 696917, upload-time = "2025-06-05T16:41:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/22/cc/0bd1a7eb759d1f3e3cc2d1bc0f0b487ad3cc9f34d74da4b80f226fde4ec3/greenlet-3.2.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:ed6cfa9200484d234d8394c70f5492f144b20d4533f69262d530a1a082f6ee9a", size = 692443, upload-time = "2025-06-05T16:48:23.113Z" }, + { url = "https://files.pythonhosted.org/packages/67/10/b2a4b63d3f08362662e89c103f7fe28894a51ae0bc890fabf37d1d780e52/greenlet-3.2.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:02b0df6f63cd15012bed5401b47829cfd2e97052dc89da3cfaf2c779124eb892", size = 692995, upload-time = "2025-06-05T16:13:07.972Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c6/ad82f148a4e3ce9564056453a71529732baf5448ad53fc323e37efe34f66/greenlet-3.2.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86c2d68e87107c1792e2e8d5399acec2487a4e993ab76c792408e59394d52141", size = 655320, upload-time = "2025-06-05T16:12:53.453Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4f/aab73ecaa6b3086a4c89863d94cf26fa84cbff63f52ce9bc4342b3087a06/greenlet-3.2.3-cp314-cp314-win_amd64.whl", hash = "sha256:8c47aae8fbbfcf82cc13327ae802ba13c9c36753b67e760023fd116bc124a62a", size = 301236, upload-time = "2025-06-05T16:15:20.111Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "mako" +version = "1.3.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, +] + +[[package]] +name = "numpy" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/19/d7c972dfe90a353dbd3efbbe1d14a5951de80c99c9dc1b93cd998d51dc0f/numpy-2.3.1.tar.gz", hash = "sha256:1ec9ae20a4226da374362cca3c62cd753faf2f951440b0e3b98e93c235441d2b", size = 20390372, upload-time = "2025-06-21T12:28:33.469Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/56/71ad5022e2f63cfe0ca93559403d0edef14aea70a841d640bd13cdba578e/numpy-2.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2959d8f268f3d8ee402b04a9ec4bb7604555aeacf78b360dc4ec27f1d508177d", size = 20896664, upload-time = "2025-06-21T12:15:30.845Z" }, + { url = "https://files.pythonhosted.org/packages/25/65/2db52ba049813670f7f987cc5db6dac9be7cd95e923cc6832b3d32d87cef/numpy-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:762e0c0c6b56bdedfef9a8e1d4538556438288c4276901ea008ae44091954e29", size = 14131078, upload-time = "2025-06-21T12:15:52.23Z" }, + { url = "https://files.pythonhosted.org/packages/57/dd/28fa3c17b0e751047ac928c1e1b6990238faad76e9b147e585b573d9d1bd/numpy-2.3.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:867ef172a0976aaa1f1d1b63cf2090de8b636a7674607d514505fb7276ab08fc", size = 5112554, upload-time = "2025-06-21T12:16:01.434Z" }, + { url = "https://files.pythonhosted.org/packages/c9/fc/84ea0cba8e760c4644b708b6819d91784c290288c27aca916115e3311d17/numpy-2.3.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:4e602e1b8682c2b833af89ba641ad4176053aaa50f5cacda1a27004352dde943", size = 6646560, upload-time = "2025-06-21T12:16:11.895Z" }, + { url = "https://files.pythonhosted.org/packages/61/b2/512b0c2ddec985ad1e496b0bd853eeb572315c0f07cd6997473ced8f15e2/numpy-2.3.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8e333040d069eba1652fb08962ec5b76af7f2c7bce1df7e1418c8055cf776f25", size = 14260638, upload-time = "2025-06-21T12:16:32.611Z" }, + { url = "https://files.pythonhosted.org/packages/6e/45/c51cb248e679a6c6ab14b7a8e3ead3f4a3fe7425fc7a6f98b3f147bec532/numpy-2.3.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:e7cbf5a5eafd8d230a3ce356d892512185230e4781a361229bd902ff403bc660", size = 16632729, upload-time = "2025-06-21T12:16:57.439Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ff/feb4be2e5c09a3da161b412019caf47183099cbea1132fd98061808c2df2/numpy-2.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5f1b8f26d1086835f442286c1d9b64bb3974b0b1e41bb105358fd07d20872952", size = 15565330, upload-time = "2025-06-21T12:17:20.638Z" }, + { url = "https://files.pythonhosted.org/packages/bc/6d/ceafe87587101e9ab0d370e4f6e5f3f3a85b9a697f2318738e5e7e176ce3/numpy-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ee8340cb48c9b7a5899d1149eece41ca535513a9698098edbade2a8e7a84da77", size = 18361734, upload-time = "2025-06-21T12:17:47.938Z" }, + { url = "https://files.pythonhosted.org/packages/2b/19/0fb49a3ea088be691f040c9bf1817e4669a339d6e98579f91859b902c636/numpy-2.3.1-cp312-cp312-win32.whl", hash = "sha256:e772dda20a6002ef7061713dc1e2585bc1b534e7909b2030b5a46dae8ff077ab", size = 6320411, upload-time = "2025-06-21T12:17:58.475Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3e/e28f4c1dd9e042eb57a3eb652f200225e311b608632bc727ae378623d4f8/numpy-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:cfecc7822543abdea6de08758091da655ea2210b8ffa1faf116b940693d3df76", size = 12734973, upload-time = "2025-06-21T12:18:17.601Z" }, + { url = "https://files.pythonhosted.org/packages/04/a8/8a5e9079dc722acf53522b8f8842e79541ea81835e9b5483388701421073/numpy-2.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:7be91b2239af2658653c5bb6f1b8bccafaf08226a258caf78ce44710a0160d30", size = 10191491, upload-time = "2025-06-21T12:18:33.585Z" }, + { url = "https://files.pythonhosted.org/packages/d4/bd/35ad97006d8abff8631293f8ea6adf07b0108ce6fec68da3c3fcca1197f2/numpy-2.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25a1992b0a3fdcdaec9f552ef10d8103186f5397ab45e2d25f8ac51b1a6b97e8", size = 20889381, upload-time = "2025-06-21T12:19:04.103Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/df5923874d8095b6062495b39729178eef4a922119cee32a12ee1bd4664c/numpy-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7dea630156d39b02a63c18f508f85010230409db5b2927ba59c8ba4ab3e8272e", size = 14152726, upload-time = "2025-06-21T12:19:25.599Z" }, + { url = "https://files.pythonhosted.org/packages/8c/0f/a1f269b125806212a876f7efb049b06c6f8772cf0121139f97774cd95626/numpy-2.3.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bada6058dd886061f10ea15f230ccf7dfff40572e99fef440a4a857c8728c9c0", size = 5105145, upload-time = "2025-06-21T12:19:34.782Z" }, + { url = "https://files.pythonhosted.org/packages/6d/63/a7f7fd5f375b0361682f6ffbf686787e82b7bbd561268e4f30afad2bb3c0/numpy-2.3.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:a894f3816eb17b29e4783e5873f92faf55b710c2519e5c351767c51f79d8526d", size = 6639409, upload-time = "2025-06-21T12:19:45.228Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0d/1854a4121af895aab383f4aa233748f1df4671ef331d898e32426756a8a6/numpy-2.3.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:18703df6c4a4fee55fd3d6e5a253d01c5d33a295409b03fda0c86b3ca2ff41a1", size = 14257630, upload-time = "2025-06-21T12:20:06.544Z" }, + { url = "https://files.pythonhosted.org/packages/50/30/af1b277b443f2fb08acf1c55ce9d68ee540043f158630d62cef012750f9f/numpy-2.3.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5902660491bd7a48b2ec16c23ccb9124b8abfd9583c5fdfa123fe6b421e03de1", size = 16627546, upload-time = "2025-06-21T12:20:31.002Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ec/3b68220c277e463095342d254c61be8144c31208db18d3fd8ef02712bcd6/numpy-2.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:36890eb9e9d2081137bd78d29050ba63b8dab95dff7912eadf1185e80074b2a0", size = 15562538, upload-time = "2025-06-21T12:20:54.322Z" }, + { url = "https://files.pythonhosted.org/packages/77/2b/4014f2bcc4404484021c74d4c5ee8eb3de7e3f7ac75f06672f8dcf85140a/numpy-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a780033466159c2270531e2b8ac063704592a0bc62ec4a1b991c7c40705eb0e8", size = 18360327, upload-time = "2025-06-21T12:21:21.053Z" }, + { url = "https://files.pythonhosted.org/packages/40/8d/2ddd6c9b30fcf920837b8672f6c65590c7d92e43084c25fc65edc22e93ca/numpy-2.3.1-cp313-cp313-win32.whl", hash = "sha256:39bff12c076812595c3a306f22bfe49919c5513aa1e0e70fac756a0be7c2a2b8", size = 6312330, upload-time = "2025-06-21T12:25:07.447Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c8/beaba449925988d415efccb45bf977ff8327a02f655090627318f6398c7b/numpy-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d5ee6eec45f08ce507a6570e06f2f879b374a552087a4179ea7838edbcbfa42", size = 12731565, upload-time = "2025-06-21T12:25:26.444Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c3/5c0c575d7ec78c1126998071f58facfc124006635da75b090805e642c62e/numpy-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:0c4d9e0a8368db90f93bd192bfa771ace63137c3488d198ee21dfb8e7771916e", size = 10190262, upload-time = "2025-06-21T12:25:42.196Z" }, + { url = "https://files.pythonhosted.org/packages/ea/19/a029cd335cf72f79d2644dcfc22d90f09caa86265cbbde3b5702ccef6890/numpy-2.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b0b5397374f32ec0649dd98c652a1798192042e715df918c20672c62fb52d4b8", size = 20987593, upload-time = "2025-06-21T12:21:51.664Z" }, + { url = "https://files.pythonhosted.org/packages/25/91/8ea8894406209107d9ce19b66314194675d31761fe2cb3c84fe2eeae2f37/numpy-2.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c5bdf2015ccfcee8253fb8be695516ac4457c743473a43290fd36eba6a1777eb", size = 14300523, upload-time = "2025-06-21T12:22:13.583Z" }, + { url = "https://files.pythonhosted.org/packages/a6/7f/06187b0066eefc9e7ce77d5f2ddb4e314a55220ad62dd0bfc9f2c44bac14/numpy-2.3.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d70f20df7f08b90a2062c1f07737dd340adccf2068d0f1b9b3d56e2038979fee", size = 5227993, upload-time = "2025-06-21T12:22:22.53Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ec/a926c293c605fa75e9cfb09f1e4840098ed46d2edaa6e2152ee35dc01ed3/numpy-2.3.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:2fb86b7e58f9ac50e1e9dd1290154107e47d1eef23a0ae9145ded06ea606f992", size = 6736652, upload-time = "2025-06-21T12:22:33.629Z" }, + { url = "https://files.pythonhosted.org/packages/e3/62/d68e52fb6fde5586650d4c0ce0b05ff3a48ad4df4ffd1b8866479d1d671d/numpy-2.3.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:23ab05b2d241f76cb883ce8b9a93a680752fbfcbd51c50eff0b88b979e471d8c", size = 14331561, upload-time = "2025-06-21T12:22:55.056Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ec/b74d3f2430960044bdad6900d9f5edc2dc0fb8bf5a0be0f65287bf2cbe27/numpy-2.3.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ce2ce9e5de4703a673e705183f64fd5da5bf36e7beddcb63a25ee2286e71ca48", size = 16693349, upload-time = "2025-06-21T12:23:20.53Z" }, + { url = "https://files.pythonhosted.org/packages/0d/15/def96774b9d7eb198ddadfcbd20281b20ebb510580419197e225f5c55c3e/numpy-2.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c4913079974eeb5c16ccfd2b1f09354b8fed7e0d6f2cab933104a09a6419b1ee", size = 15642053, upload-time = "2025-06-21T12:23:43.697Z" }, + { url = "https://files.pythonhosted.org/packages/2b/57/c3203974762a759540c6ae71d0ea2341c1fa41d84e4971a8e76d7141678a/numpy-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:010ce9b4f00d5c036053ca684c77441f2f2c934fd23bee058b4d6f196efd8280", size = 18434184, upload-time = "2025-06-21T12:24:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/22/8a/ccdf201457ed8ac6245187850aff4ca56a79edbea4829f4e9f14d46fa9a5/numpy-2.3.1-cp313-cp313t-win32.whl", hash = "sha256:6269b9edfe32912584ec496d91b00b6d34282ca1d07eb10e82dfc780907d6c2e", size = 6440678, upload-time = "2025-06-21T12:24:21.596Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7e/7f431d8bd8eb7e03d79294aed238b1b0b174b3148570d03a8a8a8f6a0da9/numpy-2.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:2a809637460e88a113e186e87f228d74ae2852a2e0c44de275263376f17b5bdc", size = 12870697, upload-time = "2025-06-21T12:24:40.644Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ca/af82bf0fad4c3e573c6930ed743b5308492ff19917c7caaf2f9b6f9e2e98/numpy-2.3.1-cp313-cp313t-win_arm64.whl", hash = "sha256:eccb9a159db9aed60800187bc47a6d3451553f0e1b08b068d8b277ddfbb9b244", size = 10260376, upload-time = "2025-06-21T12:24:56.884Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/51/48f713c4c728d7c55ef7444ba5ea027c26998d96d1a40953b346438602fc/pandas-2.3.0.tar.gz", hash = "sha256:34600ab34ebf1131a7613a260a61dbe8b62c188ec0ea4c296da7c9a06b004133", size = 4484490, upload-time = "2025-06-05T03:27:54.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/46/24192607058dd607dbfacdd060a2370f6afb19c2ccb617406469b9aeb8e7/pandas-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2eb4728a18dcd2908c7fccf74a982e241b467d178724545a48d0caf534b38ebf", size = 11573865, upload-time = "2025-06-05T03:26:46.774Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cc/ae8ea3b800757a70c9fdccc68b67dc0280a6e814efcf74e4211fd5dea1ca/pandas-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9d8c3187be7479ea5c3d30c32a5d73d62a621166675063b2edd21bc47614027", size = 10702154, upload-time = "2025-06-05T16:50:14.439Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ba/a7883d7aab3d24c6540a2768f679e7414582cc389876d469b40ec749d78b/pandas-2.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ff730713d4c4f2f1c860e36c005c7cefc1c7c80c21c0688fd605aa43c9fcf09", size = 11262180, upload-time = "2025-06-05T16:50:17.453Z" }, + { url = "https://files.pythonhosted.org/packages/01/a5/931fc3ad333d9d87b10107d948d757d67ebcfc33b1988d5faccc39c6845c/pandas-2.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba24af48643b12ffe49b27065d3babd52702d95ab70f50e1b34f71ca703e2c0d", size = 11991493, upload-time = "2025-06-05T03:26:51.813Z" }, + { url = "https://files.pythonhosted.org/packages/d7/bf/0213986830a92d44d55153c1d69b509431a972eb73f204242988c4e66e86/pandas-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:404d681c698e3c8a40a61d0cd9412cc7364ab9a9cc6e144ae2992e11a2e77a20", size = 12470733, upload-time = "2025-06-06T00:00:18.651Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0e/21eb48a3a34a7d4bac982afc2c4eb5ab09f2d988bdf29d92ba9ae8e90a79/pandas-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6021910b086b3ca756755e86ddc64e0ddafd5e58e076c72cb1585162e5ad259b", size = 13212406, upload-time = "2025-06-05T03:26:55.992Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d9/74017c4eec7a28892d8d6e31ae9de3baef71f5a5286e74e6b7aad7f8c837/pandas-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:094e271a15b579650ebf4c5155c05dcd2a14fd4fdd72cf4854b2f7ad31ea30be", size = 10976199, upload-time = "2025-06-05T03:26:59.594Z" }, + { url = "https://files.pythonhosted.org/packages/d3/57/5cb75a56a4842bbd0511c3d1c79186d8315b82dac802118322b2de1194fe/pandas-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c7e2fc25f89a49a11599ec1e76821322439d90820108309bf42130d2f36c983", size = 11518913, upload-time = "2025-06-05T03:27:02.757Z" }, + { url = "https://files.pythonhosted.org/packages/05/01/0c8785610e465e4948a01a059562176e4c8088aa257e2e074db868f86d4e/pandas-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c6da97aeb6a6d233fb6b17986234cc723b396b50a3c6804776351994f2a658fd", size = 10655249, upload-time = "2025-06-05T16:50:20.17Z" }, + { url = "https://files.pythonhosted.org/packages/e8/6a/47fd7517cd8abe72a58706aab2b99e9438360d36dcdb052cf917b7bf3bdc/pandas-2.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb32dc743b52467d488e7a7c8039b821da2826a9ba4f85b89ea95274f863280f", size = 11328359, upload-time = "2025-06-05T03:27:06.431Z" }, + { url = "https://files.pythonhosted.org/packages/2a/b3/463bfe819ed60fb7e7ddffb4ae2ee04b887b3444feee6c19437b8f834837/pandas-2.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:213cd63c43263dbb522c1f8a7c9d072e25900f6975596f883f4bebd77295d4f3", size = 12024789, upload-time = "2025-06-05T03:27:09.875Z" }, + { url = "https://files.pythonhosted.org/packages/04/0c/e0704ccdb0ac40aeb3434d1c641c43d05f75c92e67525df39575ace35468/pandas-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1d2b33e68d0ce64e26a4acc2e72d747292084f4e8db4c847c6f5f6cbe56ed6d8", size = 12480734, upload-time = "2025-06-06T00:00:22.246Z" }, + { url = "https://files.pythonhosted.org/packages/e9/df/815d6583967001153bb27f5cf075653d69d51ad887ebbf4cfe1173a1ac58/pandas-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:430a63bae10b5086995db1b02694996336e5a8ac9a96b4200572b413dfdfccb9", size = 13223381, upload-time = "2025-06-05T03:27:15.641Z" }, + { url = "https://files.pythonhosted.org/packages/79/88/ca5973ed07b7f484c493e941dbff990861ca55291ff7ac67c815ce347395/pandas-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4930255e28ff5545e2ca404637bcc56f031893142773b3468dc021c6c32a1390", size = 10970135, upload-time = "2025-06-05T03:27:24.131Z" }, + { url = "https://files.pythonhosted.org/packages/24/fb/0994c14d1f7909ce83f0b1fb27958135513c4f3f2528bde216180aa73bfc/pandas-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f925f1ef673b4bd0271b1809b72b3270384f2b7d9d14a189b12b7fc02574d575", size = 12141356, upload-time = "2025-06-05T03:27:34.547Z" }, + { url = "https://files.pythonhosted.org/packages/9d/a2/9b903e5962134497ac4f8a96f862ee3081cb2506f69f8e4778ce3d9c9d82/pandas-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78ad363ddb873a631e92a3c063ade1ecfb34cae71e9a2be6ad100f875ac1042", size = 11474674, upload-time = "2025-06-05T03:27:39.448Z" }, + { url = "https://files.pythonhosted.org/packages/81/3a/3806d041bce032f8de44380f866059437fb79e36d6b22c82c187e65f765b/pandas-2.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951805d146922aed8357e4cc5671b8b0b9be1027f0619cea132a9f3f65f2f09c", size = 11439876, upload-time = "2025-06-05T03:27:43.652Z" }, + { url = "https://files.pythonhosted.org/packages/15/aa/3fc3181d12b95da71f5c2537c3e3b3af6ab3a8c392ab41ebb766e0929bc6/pandas-2.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a881bc1309f3fce34696d07b00f13335c41f5f5a8770a33b09ebe23261cfc67", size = 11966182, upload-time = "2025-06-05T03:27:47.652Z" }, + { url = "https://files.pythonhosted.org/packages/37/e7/e12f2d9b0a2c4a2cc86e2aabff7ccfd24f03e597d770abfa2acd313ee46b/pandas-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e1991bbb96f4050b09b5f811253c4f3cf05ee89a589379aa36cd623f21a31d6f", size = 12547686, upload-time = "2025-06-06T00:00:26.142Z" }, + { url = "https://files.pythonhosted.org/packages/39/c2/646d2e93e0af70f4e5359d870a63584dacbc324b54d73e6b3267920ff117/pandas-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bb3be958022198531eb7ec2008cfc78c5b1eed51af8600c6c5d9160d89d8d249", size = 13231847, upload-time = "2025-06-05T03:27:51.465Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "prod-wag-backend-automate-services" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "alembic" }, + { name = "arrow" }, + { name = "cryptography" }, + { name = "faker" }, + { name = "fastapi" }, + { name = "pandas" }, + { name = "prometheus-fastapi-instrumentator" }, + { name = "psycopg2-binary" }, + { name = "pydantic-settings" }, + { name = "pymongo" }, + { name = "pytest" }, + { name = "redbox" }, + { name = "redis" }, + { name = "redmail" }, + { name = "requests" }, + { name = "sqlalchemy-mixins" }, + { name = "textdistance" }, + { name = "unidecode" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "alembic", specifier = ">=1.15.2" }, + { name = "arrow", specifier = ">=1.3.0" }, + { name = "cryptography", specifier = ">=44.0.2" }, + { name = "faker", specifier = ">=37.1.0" }, + { name = "fastapi", specifier = ">=0.115.12" }, + { name = "pandas", specifier = ">=2.2.3" }, + { name = "prometheus-fastapi-instrumentator", specifier = ">=7.1.0" }, + { name = "psycopg2-binary", specifier = ">=2.9.10" }, + { name = "pydantic-settings", specifier = ">=2.8.1" }, + { name = "pymongo", specifier = ">=4.11.3" }, + { name = "pytest", specifier = ">=8.3.5" }, + { name = "redbox", specifier = ">=0.2.1" }, + { name = "redis", specifier = ">=5.2.1" }, + { name = "redmail", specifier = ">=0.6.0" }, + { name = "requests", specifier = ">=2.32.3" }, + { name = "sqlalchemy-mixins", specifier = ">=2.0.5" }, + { name = "textdistance", specifier = ">=4.6.3" }, + { name = "unidecode", specifier = ">=1.3.8" }, + { name = "uvicorn", specifier = ">=0.34.0" }, +] + +[[package]] +name = "prometheus-client" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/cf/40dde0a2be27cc1eb41e333d1a674a74ce8b8b0457269cc640fd42b07cf7/prometheus_client-0.22.1.tar.gz", hash = "sha256:190f1331e783cf21eb60bca559354e0a4d4378facecf78f5428c39b675d20d28", size = 69746, upload-time = "2025-06-02T14:29:01.152Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/ae/ec06af4fe3ee72d16973474f122541746196aaa16cea6f66d18b963c6177/prometheus_client-0.22.1-py3-none-any.whl", hash = "sha256:cca895342e308174341b2cbf99a56bef291fbc0ef7b9e5412a0f26d653ba7094", size = 58694, upload-time = "2025-06-02T14:29:00.068Z" }, +] + +[[package]] +name = "prometheus-fastapi-instrumentator" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prometheus-client" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/6d/24d53033cf93826aa7857699a4450c1c67e5b9c710e925b1ed2b320c04df/prometheus_fastapi_instrumentator-7.1.0.tar.gz", hash = "sha256:be7cd61eeea4e5912aeccb4261c6631b3f227d8924542d79eaf5af3f439cbe5e", size = 20220, upload-time = "2025-03-19T19:35:05.351Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/72/0824c18f3bc75810f55dacc2dd933f6ec829771180245ae3cc976195dec0/prometheus_fastapi_instrumentator-7.1.0-py3-none-any.whl", hash = "sha256:978130f3c0bb7b8ebcc90d35516a6fe13e02d2eb358c8f83887cdef7020c31e9", size = 19296, upload-time = "2025-03-19T19:35:04.323Z" }, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/bdc8274dc0585090b4e3432267d7be4dfbfd8971c0fa59167c711105a6bf/psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", size = 385764, upload-time = "2024-10-16T11:24:58.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/7d/465cc9795cf76f6d329efdafca74693714556ea3891813701ac1fee87545/psycopg2_binary-2.9.10-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0", size = 3044771, upload-time = "2024-10-16T11:20:35.234Z" }, + { url = "https://files.pythonhosted.org/packages/8b/31/6d225b7b641a1a2148e3ed65e1aa74fc86ba3fee850545e27be9e1de893d/psycopg2_binary-2.9.10-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a", size = 3275336, upload-time = "2024-10-16T11:20:38.742Z" }, + { url = "https://files.pythonhosted.org/packages/30/b7/a68c2b4bff1cbb1728e3ec864b2d92327c77ad52edcd27922535a8366f68/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539", size = 2851637, upload-time = "2024-10-16T11:20:42.145Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b1/cfedc0e0e6f9ad61f8657fd173b2f831ce261c02a08c0b09c652b127d813/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526", size = 3082097, upload-time = "2024-10-16T11:20:46.185Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/0a8e4153c9b769f59c02fb5e7914f20f0b2483a19dae7bf2db54b743d0d0/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1", size = 3264776, upload-time = "2024-10-16T11:20:50.879Z" }, + { url = "https://files.pythonhosted.org/packages/10/db/d09da68c6a0cdab41566b74e0a6068a425f077169bed0946559b7348ebe9/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e", size = 3020968, upload-time = "2024-10-16T11:20:56.819Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/4d6f8c255f0dfffb410db2b3f9ac5218d959a66c715c34cac31081e19b95/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f", size = 2872334, upload-time = "2024-10-16T11:21:02.411Z" }, + { url = "https://files.pythonhosted.org/packages/05/f7/20d7bf796593c4fea95e12119d6cc384ff1f6141a24fbb7df5a668d29d29/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00", size = 2822722, upload-time = "2024-10-16T11:21:09.01Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e4/0c407ae919ef626dbdb32835a03b6737013c3cc7240169843965cada2bdf/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5", size = 2920132, upload-time = "2024-10-16T11:21:16.339Z" }, + { url = "https://files.pythonhosted.org/packages/2d/70/aa69c9f69cf09a01da224909ff6ce8b68faeef476f00f7ec377e8f03be70/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47", size = 2959312, upload-time = "2024-10-16T11:21:25.584Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/213e59854fafe87ba47814bf413ace0dcee33a89c8c8c814faca6bc7cf3c/psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64", size = 1025191, upload-time = "2024-10-16T11:21:29.912Z" }, + { url = "https://files.pythonhosted.org/packages/92/29/06261ea000e2dc1e22907dbbc483a1093665509ea586b29b8986a0e56733/psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0", size = 1164031, upload-time = "2024-10-16T11:21:34.211Z" }, + { url = "https://files.pythonhosted.org/packages/3e/30/d41d3ba765609c0763505d565c4d12d8f3c79793f0d0f044ff5a28bf395b/psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d", size = 3044699, upload-time = "2024-10-16T11:21:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/35/44/257ddadec7ef04536ba71af6bc6a75ec05c5343004a7ec93006bee66c0bc/psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb", size = 3275245, upload-time = "2024-10-16T11:21:51.989Z" }, + { url = "https://files.pythonhosted.org/packages/1b/11/48ea1cd11de67f9efd7262085588790a95d9dfcd9b8a687d46caf7305c1a/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7", size = 2851631, upload-time = "2024-10-16T11:21:57.584Z" }, + { url = "https://files.pythonhosted.org/packages/62/e0/62ce5ee650e6c86719d621a761fe4bc846ab9eff8c1f12b1ed5741bf1c9b/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d", size = 3082140, upload-time = "2024-10-16T11:22:02.005Z" }, + { url = "https://files.pythonhosted.org/packages/27/ce/63f946c098611f7be234c0dd7cb1ad68b0b5744d34f68062bb3c5aa510c8/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73", size = 3264762, upload-time = "2024-10-16T11:22:06.412Z" }, + { url = "https://files.pythonhosted.org/packages/43/25/c603cd81402e69edf7daa59b1602bd41eb9859e2824b8c0855d748366ac9/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673", size = 3020967, upload-time = "2024-10-16T11:22:11.583Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d6/8708d8c6fca531057fa170cdde8df870e8b6a9b136e82b361c65e42b841e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f", size = 2872326, upload-time = "2024-10-16T11:22:16.406Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ac/5b1ea50fc08a9df82de7e1771537557f07c2632231bbab652c7e22597908/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", size = 2822712, upload-time = "2024-10-16T11:22:21.366Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fc/504d4503b2abc4570fac3ca56eb8fed5e437bf9c9ef13f36b6621db8ef00/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", size = 2920155, upload-time = "2024-10-16T11:22:25.684Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d1/323581e9273ad2c0dbd1902f3fb50c441da86e894b6e25a73c3fda32c57e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", size = 2959356, upload-time = "2024-10-16T11:22:30.562Z" }, + { url = "https://files.pythonhosted.org/packages/08/50/d13ea0a054189ae1bc21af1d85b6f8bb9bbc5572991055d70ad9006fe2d6/psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", size = 2569224, upload-time = "2025-01-04T20:09:19.234Z" }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pymongo" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/5a/d664298bf54762f0c89b8aa2c276868070e06afb853b4a8837de5741e5f9/pymongo-4.13.2.tar.gz", hash = "sha256:0f64c6469c2362962e6ce97258ae1391abba1566a953a492562d2924b44815c2", size = 2167844, upload-time = "2025-06-16T18:16:30.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/e0/0e187750e23eed4227282fcf568fdb61f2b53bbcf8cbe3a71dde2a860d12/pymongo-4.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ec89516622dfc8b0fdff499612c0bd235aa45eeb176c9e311bcc0af44bf952b6", size = 912004, upload-time = "2025-06-16T18:15:14.299Z" }, + { url = "https://files.pythonhosted.org/packages/57/c2/9b79795382daaf41e5f7379bffdef1880d68160adea352b796d6948cb5be/pymongo-4.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f30eab4d4326df54fee54f31f93e532dc2918962f733ee8e115b33e6fe151d92", size = 911698, upload-time = "2025-06-16T18:15:16.334Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e4/f04dc9ed5d1d9dbc539dc2d8758dd359c5373b0e06fcf25418b2c366737c/pymongo-4.13.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cce9428d12ba396ea245fc4c51f20228cead01119fcc959e1c80791ea45f820", size = 1690357, upload-time = "2025-06-16T18:15:18.358Z" }, + { url = "https://files.pythonhosted.org/packages/bb/de/41478a7d527d38f1b98b084f4a78bbb805439a6ebd8689fbbee0a3dfacba/pymongo-4.13.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac9241b727a69c39117c12ac1e52d817ea472260dadc66262c3fdca0bab0709b", size = 1754593, upload-time = "2025-06-16T18:15:20.096Z" }, + { url = "https://files.pythonhosted.org/packages/df/d9/8fa2eb110291e154f4312779b1a5b815090b8b05a59ecb4f4a32427db1df/pymongo-4.13.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3efc4c515b371a9fa1d198b6e03340985bfe1a55ae2d2b599a714934e7bc61ab", size = 1723637, upload-time = "2025-06-16T18:15:22.048Z" }, + { url = "https://files.pythonhosted.org/packages/27/7b/9863fa60a4a51ea09f5e3cd6ceb231af804e723671230f2daf3bd1b59c2b/pymongo-4.13.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f57a664aa74610eb7a52fa93f2cf794a1491f4f76098343485dd7da5b3bcff06", size = 1693613, upload-time = "2025-06-16T18:15:24.866Z" }, + { url = "https://files.pythonhosted.org/packages/9b/89/a42efa07820a59089836f409a63c96e7a74e33313e50dc39c554db99ac42/pymongo-4.13.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3dcb0b8cdd499636017a53f63ef64cf9b6bd3fd9355796c5a1d228e4be4a4c94", size = 1652745, upload-time = "2025-06-16T18:15:27.078Z" }, + { url = "https://files.pythonhosted.org/packages/6a/cf/2c77d1acda61d281edd3e3f00d5017d3fac0c29042c769efd3b8018cb469/pymongo-4.13.2-cp312-cp312-win32.whl", hash = "sha256:bf43ae07804d7762b509f68e5ec73450bb8824e960b03b861143ce588b41f467", size = 883232, upload-time = "2025-06-16T18:15:29.169Z" }, + { url = "https://files.pythonhosted.org/packages/d2/4f/727f59156e3798850c3c2901f106804053cb0e057ed1bd9883f5fa5aa8fa/pymongo-4.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:812a473d584bcb02ab819d379cd5e752995026a2bb0d7713e78462b6650d3f3a", size = 903304, upload-time = "2025-06-16T18:15:31.346Z" }, + { url = "https://files.pythonhosted.org/packages/e0/95/b44b8e24b161afe7b244f6d43c09a7a1f93308cad04198de1c14c67b24ce/pymongo-4.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6044ca0eb74d97f7d3415264de86a50a401b7b0b136d30705f022f9163c3124", size = 966232, upload-time = "2025-06-16T18:15:33.057Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/d4d59799a52033acb187f7bd1f09bc75bebb9fd12cef4ba2964d235ad3f9/pymongo-4.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dd326bcb92d28d28a3e7ef0121602bad78691b6d4d1f44b018a4616122f1ba8b", size = 965935, upload-time = "2025-06-16T18:15:34.826Z" }, + { url = "https://files.pythonhosted.org/packages/07/a8/67502899d89b317ea9952e4769bc193ca15efee561b24b38a86c59edde6f/pymongo-4.13.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfb0c21bdd58e58625c9cd8de13e859630c29c9537944ec0a14574fdf88c2ac4", size = 1954070, upload-time = "2025-06-16T18:15:36.576Z" }, + { url = "https://files.pythonhosted.org/packages/da/3b/0dac5d81d1af1b96b3200da7ccc52fc261a35efb7d2ac493252eb40a2b11/pymongo-4.13.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9c7d345d57f17b1361008aea78a37e8c139631a46aeb185dd2749850883c7ba", size = 2031424, upload-time = "2025-06-16T18:15:38.723Z" }, + { url = "https://files.pythonhosted.org/packages/31/ed/7a5af49a153224ca7e31e9915703e612ad9c45808cc39540e9dd1a2a7537/pymongo-4.13.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8860445a8da1b1545406fab189dc20319aff5ce28e65442b2b4a8f4228a88478", size = 1995339, upload-time = "2025-06-16T18:15:40.474Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e9/9c72eceae8439c4f1bdebc4e6b290bf035e3f050a80eeb74abb5e12ef8e2/pymongo-4.13.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01c184b612f67d5a4c8f864ae7c40b6cc33c0e9bb05e39d08666f8831d120504", size = 1956066, upload-time = "2025-06-16T18:15:42.272Z" }, + { url = "https://files.pythonhosted.org/packages/ac/79/9b019c47923395d5fced03856996465fb9340854b0f5a2ddf16d47e2437c/pymongo-4.13.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ea8c62d5f3c6529407c12471385d9a05f9fb890ce68d64976340c85cd661b", size = 1905642, upload-time = "2025-06-16T18:15:43.978Z" }, + { url = "https://files.pythonhosted.org/packages/93/2f/ebf56c7fa9298fa2f9716e7b66cf62b29e7fc6e11774f3b87f55d214d466/pymongo-4.13.2-cp313-cp313-win32.whl", hash = "sha256:d13556e91c4a8cb07393b8c8be81e66a11ebc8335a40fa4af02f4d8d3b40c8a1", size = 930184, upload-time = "2025-06-16T18:15:46.899Z" }, + { url = "https://files.pythonhosted.org/packages/76/2f/49c35464cbd5d116d950ff5d24b4b20491aaae115d35d40b945c33b29250/pymongo-4.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:cfc69d7bc4d4d5872fd1e6de25e6a16e2372c7d5556b75c3b8e2204dce73e3fb", size = 955111, upload-time = "2025-06-16T18:15:48.85Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/b17c8b5329b1842b7847cf0fa224ef0a272bf2e5126360f4da8065c855a1/pymongo-4.13.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a457d2ac34c05e9e8a6bb724115b093300bf270f0655fb897df8d8604b2e3700", size = 1022735, upload-time = "2025-06-16T18:15:50.672Z" }, + { url = "https://files.pythonhosted.org/packages/83/e6/66fec65a7919bf5f35be02e131b4dc4bf3152b5e8d78cd04b6d266a44514/pymongo-4.13.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:02f131a6e61559613b1171b53fbe21fed64e71b0cb4858c47fc9bc7c8e0e501c", size = 1022740, upload-time = "2025-06-16T18:15:53.218Z" }, + { url = "https://files.pythonhosted.org/packages/17/92/cda7383df0d5e71dc007f172c1ecae6313d64ea05d82bbba06df7f6b3e49/pymongo-4.13.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c942d1c6334e894271489080404b1a2e3b8bd5de399f2a0c14a77d966be5bc9", size = 2282430, upload-time = "2025-06-16T18:15:55.356Z" }, + { url = "https://files.pythonhosted.org/packages/84/da/285e05eb1d617b30dc7a7a98ebeb264353a8903e0e816a4eec6487c81f18/pymongo-4.13.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:850168d115680ab66a0931a6aa9dd98ed6aa5e9c3b9a6c12128049b9a5721bc5", size = 2369470, upload-time = "2025-06-16T18:15:57.5Z" }, + { url = "https://files.pythonhosted.org/packages/89/c0/c0d5eae236de9ca293497dc58fc1e4872382223c28ec223f76afc701392c/pymongo-4.13.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af7dfff90647ee77c53410f7fe8ca4fe343f8b768f40d2d0f71a5602f7b5a541", size = 2328857, upload-time = "2025-06-16T18:15:59.59Z" }, + { url = "https://files.pythonhosted.org/packages/2b/5a/d8639fba60def128ce9848b99c56c54c8a4d0cd60342054cd576f0bfdf26/pymongo-4.13.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8057f9bc9c94a8fd54ee4f5e5106e445a8f406aff2df74746f21c8791ee2403", size = 2280053, upload-time = "2025-06-16T18:16:02.166Z" }, + { url = "https://files.pythonhosted.org/packages/a1/69/d56f0897cc4932a336820c5d2470ffed50be04c624b07d1ad6ea75aaa975/pymongo-4.13.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51040e1ba78d6671f8c65b29e2864483451e789ce93b1536de9cc4456ede87fa", size = 2219378, upload-time = "2025-06-16T18:16:04.108Z" }, + { url = "https://files.pythonhosted.org/packages/04/1e/427e7f99801ee318b6331062d682d3816d7e1d6b6013077636bd75d49c87/pymongo-4.13.2-cp313-cp313t-win32.whl", hash = "sha256:7ab86b98a18c8689514a9f8d0ec7d9ad23a949369b31c9a06ce4a45dcbffcc5e", size = 979460, upload-time = "2025-06-16T18:16:06.128Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9c/00301a6df26f0f8d5c5955192892241e803742e7c3da8c2c222efabc0df6/pymongo-4.13.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c38168263ed94a250fc5cf9c6d33adea8ab11c9178994da1c3481c2a49d235f8", size = 1011057, upload-time = "2025-06-16T18:16:07.917Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "redbox" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/33/42dbfd394d8099079d31dc0f98afca62bc6cc9635ea1ccab1029fefdc6ff/redbox-0.2.1.tar.gz", hash = "sha256:17005f8cfe8acba992b649e5682b2dd4bff937d67df3fd8496e187cae4f19d60", size = 219953, upload-time = "2022-10-01T18:35:17.755Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/24/9f8330b5ce5a64cd97ae2d3a00d1d5cb9096c54ac2e56a05d7a5812709b8/redbox-0.2.1-py3-none-any.whl", hash = "sha256:14906668345c7e76db367d6d40347c2dcb5de2a5167f96d08f06f95c0a908f71", size = 16507, upload-time = "2022-10-01T18:35:15.546Z" }, +] + +[[package]] +name = "redis" +version = "6.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/9a/0551e01ba52b944f97480721656578c8a7c46b51b99d66814f85fe3a4f3e/redis-6.2.0.tar.gz", hash = "sha256:e821f129b75dde6cb99dd35e5c76e8c49512a5a0d8dfdc560b2fbd44b85ca977", size = 4639129, upload-time = "2025-05-28T05:01:18.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/67/e60968d3b0e077495a8fee89cf3f2373db98e528288a48f1ee44967f6e8c/redis-6.2.0-py3-none-any.whl", hash = "sha256:c8ddf316ee0aab65f04a11229e94a64b2618451dab7a67cb2f77eb799d872d5e", size = 278659, upload-time = "2025-05-28T05:01:16.955Z" }, +] + +[[package]] +name = "redmail" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/96/36c740474cadc1b8a6e735334a0c67c02ea7169d29ffde48eb6c74f3abaa/redmail-0.6.0.tar.gz", hash = "sha256:0447cbd76deb2788b2d831c12e22b513587e99f725071d9951a01b0f2b8d0a72", size = 448832, upload-time = "2023-02-25T10:20:52.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/67/3e0005b255a9d02448c5529af450b6807403e9af7b82636123273906ea37/redmail-0.6.0-py3-none-any.whl", hash = "sha256:8e64a680ffc8aaf8054312bf8b216da8fed20669181b77b1f1ccbdf4ee064427", size = 46948, upload-time = "2023-02-25T10:20:50.438Z" }, +] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.41" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/66/45b165c595ec89aa7dcc2c1cd222ab269bc753f1fc7a1e68f8481bd957bf/sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9", size = 9689424, upload-time = "2025-05-14T17:10:32.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/2a/f1f4e068b371154740dd10fb81afb5240d5af4aa0087b88d8b308b5429c2/sqlalchemy-2.0.41-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:81f413674d85cfd0dfcd6512e10e0f33c19c21860342a4890c3a2b59479929f9", size = 2119645, upload-time = "2025-05-14T17:55:24.854Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e8/c664a7e73d36fbfc4730f8cf2bf930444ea87270f2825efbe17bf808b998/sqlalchemy-2.0.41-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:598d9ebc1e796431bbd068e41e4de4dc34312b7aa3292571bb3674a0cb415dd1", size = 2107399, upload-time = "2025-05-14T17:55:28.097Z" }, + { url = "https://files.pythonhosted.org/packages/5c/78/8a9cf6c5e7135540cb682128d091d6afa1b9e48bd049b0d691bf54114f70/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a104c5694dfd2d864a6f91b0956eb5d5883234119cb40010115fd45a16da5e70", size = 3293269, upload-time = "2025-05-14T17:50:38.227Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/f74add3978c20de6323fb11cb5162702670cc7a9420033befb43d8d5b7a4/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6145afea51ff0af7f2564a05fa95eb46f542919e6523729663a5d285ecb3cf5e", size = 3303364, upload-time = "2025-05-14T17:51:49.829Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d4/c990f37f52c3f7748ebe98883e2a0f7d038108c2c5a82468d1ff3eec50b7/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b46fa6eae1cd1c20e6e6f44e19984d438b6b2d8616d21d783d150df714f44078", size = 3229072, upload-time = "2025-05-14T17:50:39.774Z" }, + { url = "https://files.pythonhosted.org/packages/15/69/cab11fecc7eb64bc561011be2bd03d065b762d87add52a4ca0aca2e12904/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41836fe661cc98abfae476e14ba1906220f92c4e528771a8a3ae6a151242d2ae", size = 3268074, upload-time = "2025-05-14T17:51:51.736Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/0c19ec16858585d37767b167fc9602593f98998a68a798450558239fb04a/sqlalchemy-2.0.41-cp312-cp312-win32.whl", hash = "sha256:a8808d5cf866c781150d36a3c8eb3adccfa41a8105d031bf27e92c251e3969d6", size = 2084514, upload-time = "2025-05-14T17:55:49.915Z" }, + { url = "https://files.pythonhosted.org/packages/7f/23/4c2833d78ff3010a4e17f984c734f52b531a8c9060a50429c9d4b0211be6/sqlalchemy-2.0.41-cp312-cp312-win_amd64.whl", hash = "sha256:5b14e97886199c1f52c14629c11d90c11fbb09e9334fa7bb5f6d068d9ced0ce0", size = 2111557, upload-time = "2025-05-14T17:55:51.349Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ad/2e1c6d4f235a97eeef52d0200d8ddda16f6c4dd70ae5ad88c46963440480/sqlalchemy-2.0.41-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443", size = 2115491, upload-time = "2025-05-14T17:55:31.177Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8d/be490e5db8400dacc89056f78a52d44b04fbf75e8439569d5b879623a53b/sqlalchemy-2.0.41-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc", size = 2102827, upload-time = "2025-05-14T17:55:34.921Z" }, + { url = "https://files.pythonhosted.org/packages/a0/72/c97ad430f0b0e78efaf2791342e13ffeafcbb3c06242f01a3bb8fe44f65d/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1", size = 3225224, upload-time = "2025-05-14T17:50:41.418Z" }, + { url = "https://files.pythonhosted.org/packages/5e/51/5ba9ea3246ea068630acf35a6ba0d181e99f1af1afd17e159eac7e8bc2b8/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a", size = 3230045, upload-time = "2025-05-14T17:51:54.722Z" }, + { url = "https://files.pythonhosted.org/packages/78/2f/8c14443b2acea700c62f9b4a8bad9e49fc1b65cfb260edead71fd38e9f19/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c153265408d18de4cc5ded1941dcd8315894572cddd3c58df5d5b5705b3fa28d", size = 3159357, upload-time = "2025-05-14T17:50:43.483Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b2/43eacbf6ccc5276d76cea18cb7c3d73e294d6fb21f9ff8b4eef9b42bbfd5/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f67766965996e63bb46cfbf2ce5355fc32d9dd3b8ad7e536a920ff9ee422e23", size = 3197511, upload-time = "2025-05-14T17:51:57.308Z" }, + { url = "https://files.pythonhosted.org/packages/fa/2e/677c17c5d6a004c3c45334ab1dbe7b7deb834430b282b8a0f75ae220c8eb/sqlalchemy-2.0.41-cp313-cp313-win32.whl", hash = "sha256:bfc9064f6658a3d1cadeaa0ba07570b83ce6801a1314985bf98ec9b95d74e15f", size = 2082420, upload-time = "2025-05-14T17:55:52.69Z" }, + { url = "https://files.pythonhosted.org/packages/e9/61/e8c1b9b6307c57157d328dd8b8348ddc4c47ffdf1279365a13b2b98b8049/sqlalchemy-2.0.41-cp313-cp313-win_amd64.whl", hash = "sha256:82ca366a844eb551daff9d2e6e7a9e5e76d2612c8564f58db6c19a726869c1df", size = 2108329, upload-time = "2025-05-14T17:55:54.495Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fc/9ba22f01b5cdacc8f5ed0d22304718d2c758fce3fd49a5372b886a86f37c/sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", size = 1911224, upload-time = "2025-05-14T17:39:42.154Z" }, +] + +[[package]] +name = "sqlalchemy-mixins" +version = "2.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/90/a920aa06a038677dde522dd8d7bc168eedd5fd3331ba1c759bf91ccd28d3/sqlalchemy_mixins-2.0.5.tar.gz", hash = "sha256:85197fc3682c4bf9c35671fb3d10282a0973b19cd2ff2b6791d601cbfb0fb89e", size = 20186, upload-time = "2023-08-29T13:37:22.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/4d/8d97e3ec646e8732ea8d33fb33068cab97d0bc5a0e3f46c93174e2d3d3eb/sqlalchemy_mixins-2.0.5-py3-none-any.whl", hash = "sha256:9067b630744741b472aa91d92494cc5612ed2d29c66729a5a4a1d3fbbeccd448", size = 17578, upload-time = "2023-08-29T13:37:20.475Z" }, +] + +[[package]] +name = "starlette" +version = "0.46.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload-time = "2025-04-13T13:56:17.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" }, +] + +[[package]] +name = "textdistance" +version = "4.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/68/97ac72dd781301d6a52140066c68400c96f1a91f69737959e414844749b0/textdistance-4.6.3.tar.gz", hash = "sha256:d6dabc50b4ea832cdcf0e1e6021bd0c7fcd9ade155888d79bb6a3c31fce2dc6f", size = 32710, upload-time = "2024-07-16T09:34:54.09Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/c2/c62601c858010b0513a6434b9be19bd740533a6e861eddfd30b7258d92a0/textdistance-4.6.3-py3-none-any.whl", hash = "sha256:0cb1b2cc8e3339ddc3e0f8c870e49fb49de6ecc42a718917308b3c971f34aa56", size = 31263, upload-time = "2024-07-16T09:34:51.082Z" }, +] + +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20250516" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/88/d65ed807393285204ab6e2801e5d11fbbea811adcaa979a2ed3b67a5ef41/types_python_dateutil-2.9.0.20250516.tar.gz", hash = "sha256:13e80d6c9c47df23ad773d54b2826bd52dbbb41be87c3f339381c1700ad21ee5", size = 13943, upload-time = "2025-05-16T03:06:58.385Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/3f/b0e8db149896005adc938a1e7f371d6d7e9eca4053a29b108978ed15e0c2/types_python_dateutil-2.9.0.20250516-py3-none-any.whl", hash = "sha256:2b2b3f57f9c6a61fba26a9c0ffb9ea5681c9b83e69cd897c6b5f668d9c0cab93", size = 14356, upload-time = "2025-05-16T03:06:57.249Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +] + +[[package]] +name = "unidecode" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/7d/a8a765761bbc0c836e397a2e48d498305a865b70a8600fd7a942e85dcf63/Unidecode-1.4.0.tar.gz", hash = "sha256:ce35985008338b676573023acc382d62c264f307c8f7963733405add37ea2b23", size = 200149, upload-time = "2025-04-24T08:45:03.798Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/b7/559f59d57d18b44c6d1250d2eeaa676e028b9c527431f5d0736478a73ba1/Unidecode-1.4.0-py3-none-any.whl", hash = "sha256:c3c7606c27503ad8d501270406e345ddb480a7b5f38827eafe4fa82a137f0021", size = 235837, upload-time = "2025-04-24T08:45:01.609Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.34.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/ad/713be230bcda622eaa35c28f0d328c3675c371238470abdea52417f17a8e/uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a", size = 76631, upload-time = "2025-06-01T07:48:17.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/0d/8adfeaa62945f90d19ddc461c55f4a50c258af7662d34b6a3d5d1f8646f6/uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885", size = 62431, upload-time = "2025-06-01T07:48:15.664Z" }, +]