From 1f8db23f75a11a6b56ec55f9621f974f5819152a Mon Sep 17 00:00:00 2001 From: Berkay Date: Thu, 31 Jul 2025 17:26:30 +0300 Subject: [PATCH] services api --- .gitignore | 2 +- ServicesApi/Builds/Auth/events/auth/events.py | 2 +- ServicesApi/Builds/Initial/app.py | 2 +- .../Extensions/Middlewares/token_provider.py | 1 - ServicesApi/Schemas/__init__.py | 4 + ServicesApi/Schemas/account/account.py | 7 +- ServicesApi/Schemas/identity/identity.py | 60 +- ServicesBank/Depends/config.py | 31 + ServicesBank/Depends/template_accounts.html | 54 ++ ServicesBank/Email/Dockerfile | 17 + ServicesBank/Email/README.md | 84 +++ ServicesBank/Email/app.py | 169 +++++ ServicesBank/Email/pyproject.toml | 12 + .../.dockerignore | 0 ServicesBank/Finder/BuildExtractor/Dockerfile | 33 + ServicesBank/Finder/BuildExtractor/README.md | 3 + .../entrypoint.sh | 0 .../run_app.sh | 0 ServicesBank/Finder/BuildExtractor/runner.py | 66 ++ .../BuildLivingSpace => Parser}/.dockerignore | 0 ServicesBank/Parser/Dockerfile | 24 + ServicesBank/Parser/README.md | 3 + .../DecisionBook => Parser}/entrypoint.sh | 0 .../DecisionBook => Parser}/run_app.sh | 0 ServicesBank/Parser/runner.py | 636 ++++++++++++++++++ ServicesBank/RoutineEmail/Dockerfile | 26 + ServicesBank/RoutineEmail/README.md | 69 ++ ServicesBank/RoutineEmail/app.py | 157 +++++ ServicesBank/RoutineEmail/entrypoint.sh | 29 + ServicesBank/RoutineEmail/pyproject.toml | 17 + ServicesBank/RoutineEmail/run_app.sh | 24 + ServicesBank/RoutineEmail/templates/a.txt | 0 bank-services-docker-compose copy.yml | 161 +++++ docker-compose.yml | 105 +-- .../XBuildFromIban}/.dockerignore | 0 .../XBuildFromIban}/Dockerfile | 0 .../XBuildFromIban}/README.md | 0 .../XBuildFromIban}/entrypoint.sh | 0 trash/XBuildFromIban/run_app.sh | 26 + .../XBuildFromIban}/runner.py | 0 trash/XBuildLivingSpace/.dockerignore | 93 +++ .../XBuildLivingSpace}/Dockerfile | 0 .../XBuildLivingSpace}/README.md | 0 .../XBuildLivingSpace}/configs.py | 0 trash/XBuildLivingSpace/entrypoint.sh | 30 + .../XBuildLivingSpace}/parser.py | 0 .../XBuildLivingSpace}/regex_func.py | 0 .../XBuildLivingSpace}/run_app.sh | 0 .../XBuildLivingSpace}/runner.py | 0 .../XBuildLivingSpace}/validations.py | 0 trash/XDecisionBook/.dockerignore | 93 +++ .../XDecisionBook}/Dockerfile | 0 .../XDecisionBook}/README.md | 0 trash/XDecisionBook/entrypoint.sh | 30 + trash/XDecisionBook/run_app.sh | 26 + .../XDecisionBook}/runner.py | 0 56 files changed, 1976 insertions(+), 120 deletions(-) create mode 100644 ServicesBank/Depends/config.py create mode 100644 ServicesBank/Depends/template_accounts.html create mode 100644 ServicesBank/Email/Dockerfile create mode 100644 ServicesBank/Email/README.md create mode 100644 ServicesBank/Email/app.py create mode 100644 ServicesBank/Email/pyproject.toml rename ServicesBank/Finder/{BuildFromIban => BuildExtractor}/.dockerignore (100%) create mode 100644 ServicesBank/Finder/BuildExtractor/Dockerfile create mode 100644 ServicesBank/Finder/BuildExtractor/README.md rename ServicesBank/Finder/{BuildLivingSpace => BuildExtractor}/entrypoint.sh (100%) rename ServicesBank/Finder/{BuildFromIban => BuildExtractor}/run_app.sh (100%) create mode 100644 ServicesBank/Finder/BuildExtractor/runner.py rename ServicesBank/{Finder/BuildLivingSpace => Parser}/.dockerignore (100%) create mode 100644 ServicesBank/Parser/Dockerfile create mode 100644 ServicesBank/Parser/README.md rename ServicesBank/{Finder/DecisionBook => Parser}/entrypoint.sh (100%) rename ServicesBank/{Finder/DecisionBook => Parser}/run_app.sh (100%) create mode 100644 ServicesBank/Parser/runner.py create mode 100644 ServicesBank/RoutineEmail/Dockerfile create mode 100644 ServicesBank/RoutineEmail/README.md create mode 100644 ServicesBank/RoutineEmail/app.py create mode 100644 ServicesBank/RoutineEmail/entrypoint.sh create mode 100644 ServicesBank/RoutineEmail/pyproject.toml create mode 100644 ServicesBank/RoutineEmail/run_app.sh create mode 100644 ServicesBank/RoutineEmail/templates/a.txt create mode 100644 bank-services-docker-compose copy.yml rename {ServicesBank/Finder/DecisionBook => trash/XBuildFromIban}/.dockerignore (100%) rename {ServicesBank/Finder/BuildFromIban => trash/XBuildFromIban}/Dockerfile (100%) rename {ServicesBank/Finder/BuildFromIban => trash/XBuildFromIban}/README.md (100%) rename {ServicesBank/Finder/BuildFromIban => trash/XBuildFromIban}/entrypoint.sh (100%) create mode 100644 trash/XBuildFromIban/run_app.sh rename {ServicesBank/Finder/BuildFromIban => trash/XBuildFromIban}/runner.py (100%) create mode 100644 trash/XBuildLivingSpace/.dockerignore rename {ServicesBank/Finder/BuildLivingSpace => trash/XBuildLivingSpace}/Dockerfile (100%) rename {ServicesBank/Finder/BuildLivingSpace => trash/XBuildLivingSpace}/README.md (100%) rename {ServicesBank/Finder/BuildLivingSpace => trash/XBuildLivingSpace}/configs.py (100%) create mode 100644 trash/XBuildLivingSpace/entrypoint.sh rename {ServicesBank/Finder/BuildLivingSpace => trash/XBuildLivingSpace}/parser.py (100%) rename {ServicesBank/Finder/BuildLivingSpace => trash/XBuildLivingSpace}/regex_func.py (100%) rename {ServicesBank/Finder/BuildLivingSpace => trash/XBuildLivingSpace}/run_app.sh (100%) rename {ServicesBank/Finder/BuildLivingSpace => trash/XBuildLivingSpace}/runner.py (100%) rename {ServicesBank/Finder/BuildLivingSpace => trash/XBuildLivingSpace}/validations.py (100%) create mode 100644 trash/XDecisionBook/.dockerignore rename {ServicesBank/Finder/DecisionBook => trash/XDecisionBook}/Dockerfile (100%) rename {ServicesBank/Finder/DecisionBook => trash/XDecisionBook}/README.md (100%) create mode 100644 trash/XDecisionBook/entrypoint.sh create mode 100644 trash/XDecisionBook/run_app.sh rename {ServicesBank/Finder/DecisionBook => trash/XDecisionBook}/runner.py (100%) diff --git a/.gitignore b/.gitignore index f8ab384..dba3cf9 100644 --- a/.gitignore +++ b/.gitignore @@ -164,4 +164,4 @@ yarn-error.log* next-env.d.ts # Project specific -ServicesBank/Zemberek/ \ No newline at end of file +trash/Zemberek/ \ No newline at end of file diff --git a/ServicesApi/Builds/Auth/events/auth/events.py b/ServicesApi/Builds/Auth/events/auth/events.py index 4e2404c..9a69314 100644 --- a/ServicesApi/Builds/Auth/events/auth/events.py +++ b/ServicesApi/Builds/Auth/events/auth/events.py @@ -386,7 +386,7 @@ class LoginHandler: ).join(BuildParts, BuildParts.id == BuildLivingSpace.build_parts_id ).join(Build, Build.id == BuildParts.build_id ).join(People, People.id == BuildLivingSpace.person_id - ).filter(BuildLivingSpace.uu_id == data.uuid) + ).filter(BuildLivingSpace.uu_id == data.uuid) selected_build_living_space_first = selected_build_living_space_query.first() if not selected_build_living_space_first: diff --git a/ServicesApi/Builds/Initial/app.py b/ServicesApi/Builds/Initial/app.py index 554067b..8819c74 100644 --- a/ServicesApi/Builds/Initial/app.py +++ b/ServicesApi/Builds/Initial/app.py @@ -17,7 +17,7 @@ if __name__ == "__main__": with get_db() as db_session: if set_alembic: generate_alembic(session=db_session) - + exit() try: create_one_address(db_session=db_session) except Exception as e: diff --git a/ServicesApi/Extensions/Middlewares/token_provider.py b/ServicesApi/Extensions/Middlewares/token_provider.py index 93e32ab..bf80569 100644 --- a/ServicesApi/Extensions/Middlewares/token_provider.py +++ b/ServicesApi/Extensions/Middlewares/token_provider.py @@ -26,7 +26,6 @@ class TokenProvider: return OccupantTokenObject(**redis_object) raise ValueError("Invalid user type") - @classmethod def get_login_token_from_redis( cls, token: Optional[str] = None, user_uu_id: Optional[str] = None diff --git a/ServicesApi/Schemas/__init__.py b/ServicesApi/Schemas/__init__.py index 8eacc2f..c64d3df 100644 --- a/ServicesApi/Schemas/__init__.py +++ b/ServicesApi/Schemas/__init__.py @@ -7,6 +7,8 @@ from Schemas.account.account import ( AccountRecordExchanges, AccountRecords, AccountDelayInterest, + AccountRecordsPredict, + AccountRecordsModelTrain, ) from Schemas.account.iban import ( BuildIbans, @@ -126,6 +128,8 @@ __all__ = [ "AccountRecordExchanges", "AccountRecords", "AccountDelayInterest", + "AccountRecordsPredict", + "AccountRecordsModelTrain", "BuildIbans", "BuildIbanDescription", "RelationshipEmployee2PostCode", diff --git a/ServicesApi/Schemas/account/account.py b/ServicesApi/Schemas/account/account.py index 1428c78..f284cd4 100644 --- a/ServicesApi/Schemas/account/account.py +++ b/ServicesApi/Schemas/account/account.py @@ -1,6 +1,7 @@ from Schemas.base_imports import ( CrudCollection, String, + Text, Integer, Boolean, ForeignKey, @@ -384,9 +385,9 @@ class AccountRecordsPredict(CrudCollection): account_records_id: Mapped[int] = mapped_column(ForeignKey("account_records.id"), nullable=False) account_records_uu_id: Mapped[str] = mapped_column(String(100), nullable=False) - prediction_model: Mapped[str] = mapped_column(String(10), nullable=False) - prediction_result: Mapped[int] = mapped_column(Integer, nullable=False) - prediction_field: Mapped[str] = mapped_column(String(10), nullable=False, server_default="") + prediction_model: Mapped[str] = mapped_column(String, nullable=False) + prediction_result: Mapped[str] = mapped_column(Text, nullable=False) + prediction_field: Mapped[str] = mapped_column(String, nullable=False, server_default="") treshold: Mapped[float] = mapped_column(Numeric(18, 6), nullable=True) is_first_prediction: Mapped[bool] = mapped_column(Boolean, server_default="0") is_approved: Mapped[bool] = mapped_column(Boolean, server_default="0") diff --git a/ServicesApi/Schemas/identity/identity.py b/ServicesApi/Schemas/identity/identity.py index 0e1d868..220aee9 100644 --- a/ServicesApi/Schemas/identity/identity.py +++ b/ServicesApi/Schemas/identity/identity.py @@ -211,49 +211,22 @@ class People(CrudCollection): "tax_no", ] - firstname: Mapped[str] = mapped_column( - String, nullable=False, comment="First name of the person" - ) - surname: Mapped[str] = mapped_column( - String(24), nullable=False, comment="Surname of the person" - ) - middle_name: Mapped[str] = mapped_column( - String, server_default="", comment="Middle name of the person" - ) - sex_code: Mapped[str] = mapped_column( - String(1), nullable=False, comment="Sex code of the person (e.g., M/F)" - ) - person_ref: Mapped[str] = mapped_column( - String, server_default="", comment="Reference ID for the person" - ) - person_tag: Mapped[str] = mapped_column( - String, server_default="", comment="Unique tag for the person" - ) + firstname: Mapped[str] = mapped_column(String, nullable=False, comment="First name of the person") + surname: Mapped[str] = mapped_column(String(24), nullable=False, comment="Surname of the person") + middle_name: Mapped[str] = mapped_column(String, server_default="", comment="Middle name of the person") + birthname: Mapped[str] = mapped_column(String, nullable=True, server_default="", comment="Birth name of the person") + sex_code: Mapped[str] = mapped_column(String(1), nullable=False, comment="Sex code of the person (e.g., M/F)") + person_ref: Mapped[str] = mapped_column(String, server_default="", comment="Reference ID for the person") + person_tag: Mapped[str] = mapped_column(String, server_default="", comment="Unique tag for the person") # ENCRYPT DATA - father_name: Mapped[str] = mapped_column( - String, server_default="", comment="Father's name of the person" - ) - mother_name: Mapped[str] = mapped_column( - String, server_default="", comment="Mother's name of the person" - ) - country_code: Mapped[str] = mapped_column( - String(4), server_default="TR", comment="Country code of the person" - ) - national_identity_id: Mapped[str] = mapped_column( - String, server_default="", comment="National identity ID of the person" - ) - birth_place: Mapped[str] = mapped_column( - String, server_default="", comment="Birth place of the person" - ) - birth_date: Mapped[TIMESTAMP] = mapped_column( - TIMESTAMP(timezone=True), - server_default="1900-01-01", - comment="Birth date of the person", - ) - tax_no: Mapped[str] = mapped_column( - String, server_default="", comment="Tax number of the person" - ) + father_name: Mapped[str] = mapped_column(String, server_default="", comment="Father's name of the person") + mother_name: Mapped[str] = mapped_column(String, server_default="", comment="Mother's name of the person") + country_code: Mapped[str] = mapped_column(String(4), server_default="TR", comment="Country code of the person") + national_identity_id: Mapped[str] = mapped_column(String, server_default="", comment="National identity ID of the person") + birth_place: Mapped[str] = mapped_column(String, server_default="", comment="Birth place of the person") + birth_date: Mapped[TIMESTAMP] = mapped_column(TIMESTAMP(timezone=True), server_default="1900-01-01", comment="Birth date of the person") + tax_no: Mapped[str] = mapped_column(String, server_default="", comment="Tax number of the person") # Receive at Create person # language = mapped_column( # String, comment="Language code of the person" @@ -262,10 +235,7 @@ class People(CrudCollection): # String, comment="Currency code of the person" # ) - # ENCRYPT DATA - user = relationship( - "Users", back_populates="person", foreign_keys="Users.person_id" - ) + user = relationship("Users", back_populates="person", foreign_keys="Users.person_id") __table_args__ = ( Index( diff --git a/ServicesBank/Depends/config.py b/ServicesBank/Depends/config.py new file mode 100644 index 0000000..2a9b87e --- /dev/null +++ b/ServicesBank/Depends/config.py @@ -0,0 +1,31 @@ +import os + + +class Config: + + MAILBOX: str = os.getenv("MAILBOX", "bilgilendirme@ileti.isbank.com.tr") + MAIN_MAIL: str = os.getenv("MAIN_MAIL", "karatay.berkay@gmail.com") + INFO_MAIL: str = os.getenv("INFO_MAIL", "mehmet.karatay@hotmail.com") + EMAIL_HOST: str = os.getenv("EMAIL_HOST", "10.10.2.34") + EMAIL_SENDER_USERNAME: str = os.getenv("EMAIL_SENDER_USERNAME", "karatay@mehmetkaratay.com.tr") + EMAIL_USERNAME: str = os.getenv("EMAIL_USERNAME", "isbank@mehmetkaratay.com.tr") + EMAIL_PASSWORD: str = os.getenv("EMAIL_PASSWORD", "system") + AUTHORIZE_IBAN: str = os.getenv("AUTHORIZE_IBAN", "4245-0093333") + SERVICE_TIMING: int = int(os.getenv("SERVICE_TIMING", 900)) + EMAIL_PORT: int = int(os.getenv("EMAIL_PORT", 993)) + EMAIL_SEND_PORT: int = int(os.getenv("EMAIL_SEND_PORT", 587)) + EMAIL_SLEEP: int = int(os.getenv("EMAIL_SLEEP", 60)) + EMAIL_SEND: bool = bool(os.getenv("EMAIL_SEND", False)) + + +class EmailConfig: + + EMAIL_HOST: str = os.getenv("EMAIL_HOST", "10.10.2.34") + EMAIL_USERNAME: str = Config.EMAIL_SENDER_USERNAME + EMAIL_PASSWORD: str = Config.EMAIL_PASSWORD + EMAIL_PORT: int = Config.EMAIL_SEND_PORT + EMAIL_SEND: bool = Config.EMAIL_SEND + + @classmethod + def as_dict(cls): + return dict(host=EmailConfig.EMAIL_HOST, port=EmailConfig.EMAIL_PORT, username=EmailConfig.EMAIL_USERNAME, password=EmailConfig.EMAIL_PASSWORD) diff --git a/ServicesBank/Depends/template_accounts.html b/ServicesBank/Depends/template_accounts.html new file mode 100644 index 0000000..67c03d1 --- /dev/null +++ b/ServicesBank/Depends/template_accounts.html @@ -0,0 +1,54 @@ + + + + + + Gelen Banka Kayıtları + + + +

Günaydın, Admin

+
+

Banka Kayıtları : {{today}}

+

Son Bakiye : {{bank_balance}}

+

{{"Status : İkinci Bakiye Hatalı" if balance_error else "Status :OK"}}

+ + + + {% for header in headers %} + + {% endfor %} + + + + {% for row in rows %} + + {% for cell in row %} + + {% endfor %} + + {% endfor %} + +
{{ header }}
{{ cell }}
+

Teşekkür ederiz,
Evyos Yönetim
Saygılarımızla

+ + \ No newline at end of file diff --git a/ServicesBank/Email/Dockerfile b/ServicesBank/Email/Dockerfile new file mode 100644 index 0000000..7615823 --- /dev/null +++ b/ServicesBank/Email/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.12-slim + +WORKDIR / + +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 /ServicesBank/Email/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 /ServicesBank/Email / +COPY /ServicesApi/Controllers /ServicesApi/Controllers +COPY /ServicesBank/Depends/config.py /ServicesBank/Depends/config.py + +ENV PYTHONPATH=/ PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1 + +CMD ["poetry", "run", "python", "app.py"] diff --git a/ServicesBank/Email/README.md b/ServicesBank/Email/README.md new file mode 100644 index 0000000..4647acf --- /dev/null +++ b/ServicesBank/Email/README.md @@ -0,0 +1,84 @@ +# Email Service + +## Overview +The Email Service is the first component in a Redis pub/sub processing chain for bank-related email automation. It monitors a specified mailbox for emails with attachments, filters them based on IBAN criteria, and publishes the data to a Redis channel for further processing. + +## Features + +### Email Processing +- Connects to a configured mailbox using IMAP +- Implements smart date-based filtering: + - Checks emails from the past 14 days on the first run of each day + - Checks emails from the past 7 days on subsequent runs within the same day +- Extracts attachments from emails +- Filters attachments based on IBAN criteria +- Uses a context manager to ensure emails are properly handled even during errors + +### Redis Integration +- Publishes messages to a Redis pub/sub channel ("CollectedData") +- Each message contains: + - Unique UUID + - Timestamp + - Initial stage marker ("red") + - Attachment payload and metadata +- Connects to an external Redis server + +### Error Handling +- Robust error management with context managers +- Automatic marking of emails as unread if processing fails +- Comprehensive logging + +## Configuration + +### Environment Variables +``` +EMAIL_HOST=10.10.2.34 +EMAIL_USERNAME=isbank@mehmetkaratay.com.tr +EMAIL_PASSWORD=system +EMAIL_SLEEP=60 +AUTHORIZE_IBAN=4245-0093333 +REDIS_HOST=10.10.2.15 +REDIS_PORT=6379 +REDIS_PASSWORD=your_strong_password_here +``` + +## Deployment + +### Docker +The service is containerized using Docker and can be deployed using the provided Dockerfile and docker-compose configuration. + +```bash +# Build and start the service +docker compose -f bank-services-docker-compose.yml up -d --build + +# View logs +docker compose -f bank-services-docker-compose.yml logs -f email_service + +# Stop the service +docker compose -f bank-services-docker-compose.yml down +``` + +### Service Management +The `check_bank_services.sh` script provides a simple way to restart the service: + +```bash +./check_bank_services.sh +``` + +## Architecture + +### Redis Pub/Sub Chain +This service is the first in a multi-stage processing chain: +1. **Email Service** (this service): Reads emails, extracts attachments, publishes to Redis with stage="red" +2. **Processor Service**: Subscribes to stage="red" messages, processes data, republishes with stage="processed" +3. **Writer Service**: Subscribes to stage="processed" messages, writes data to final destination, marks as stage="completed" + +## Development + +### Dependencies +- Python 3.12 +- Redbox (email library) +- Redis + +### State Management +The service maintains a state file at `/tmp/email_service_last_run.json` to track when it last ran, enabling the smart date-based filtering feature. diff --git a/ServicesBank/Email/app.py b/ServicesBank/Email/app.py new file mode 100644 index 0000000..0c65776 --- /dev/null +++ b/ServicesBank/Email/app.py @@ -0,0 +1,169 @@ +import time +import arrow +import os +import json +import base64 + +from uuid import uuid4 +from datetime import datetime, timedelta +from typing import TypeVar +from redbox import EmailBox +from redbox.query import FROM, UNSEEN, OR, SINCE + +from ServicesApi.Controllers.Redis.Broadcast.actions import redis_pubsub +from ServicesBank.Depends.config import Config + + +authorized_iban = Config.AUTHORIZE_IBAN +authorized_iban_cleaned = authorized_iban.replace("-", "") +REDIS_CHANNEL = "reader" +delimiter = "|" + +# banks_mails = mailbox.search(from_=filter_mail, unseen=True) bununla denemeyin +# banks_mails = mailbox.search(FROM(filter_mail) & UNSEEN) + + +T = TypeVar("T") + + +class EmailProcessingContext: + """Context manager for email processing that marks emails as unread if an error occurs.""" + + def __init__(self, email_message, mark_as_read: bool = True): + self.email_message = email_message + self.mark_as_read = mark_as_read + self.success = False + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if exc_type is not None or not self.success: + try: + if hasattr(self.email_message, "mark_as_unread"): + self.email_message.mark_as_unread() + print(f"[EMAIL_SERVICE] Marked email as UNREAD due to processing error: {exc_val if exc_val else 'Unknown error'}") + except Exception as e: + print(f"[EMAIL_SERVICE] Failed to mark email as unread: {str(e)}") + elif self.mark_as_read: + try: + if hasattr(self.email_message, "mark_as_read"): + self.email_message.mark_as_read() + except Exception as e: + print(f"[EMAIL_SERVICE] Failed to mark email as read: {str(e)}") + return False + + +def publish_payload_to_redis(payload, filename: str, mail_info: dict) -> bool: + if isinstance(payload, bytes): + encoded_payload = base64.b64encode(payload).decode("utf-8") + is_base64 = True + else: + encoded_payload = payload + is_base64 = False + + message = { + "filename": filename, "payload": encoded_payload, "is_base64": is_base64, "stage": "red", + "created_at": str(arrow.now()), "uuid": str(uuid4()), **mail_info, + } + + result = redis_pubsub.publisher.publish(REDIS_CHANNEL, message) + if result.status: + print(f"[EMAIL_SERVICE] Published message with filename: {filename} to channel: {REDIS_CHANNEL}") + return True + else: + print(f"[EMAIL_SERVICE] Publish error: {result.error}") + return False + + +def read_email_and_publish_to_redis(email_message, mail_info: dict) -> bool: + if email_message.is_multipart(): # Check if email has multipart content + for part in email_message.walk(): # Each part can be an attachment + content_disposition = part.get("Content-Disposition") + if content_disposition and "attachment" in content_disposition: + if filename := part.get_filename(): + is_iban_in_filename = authorized_iban_cleaned in str(filename) + if is_iban_in_filename: + if payload := part.get_payload(decode=True): + return publish_payload_to_redis(payload=payload, filename=filename, mail_info=mail_info) + else: + content_disposition = email_message.get("Content-Disposition") + if content_disposition and "attachment" in content_disposition: + if filename := email_message.get_filename(): + is_iban_in_filename = authorized_iban_cleaned in str(filename) + if is_iban_in_filename: + payload = email_message.get_payload(decode=True) + return publish_payload_to_redis(payload=payload, filename=filename, mail_info=mail_info) + return False + + +def app(): + + host = Config.EMAIL_HOST + port = Config.EMAIL_PORT + username = Config.EMAIL_USERNAME + password = Config.EMAIL_PASSWORD + + box = EmailBox(host=host, port=port, username=username, password=password) + if not box: + return Exception("Mailbox not found") + + box.connect() + mail_folders = box.mailfolders + filter_mail = OR(FROM(Config.MAILBOX), FROM(Config.MAIN_MAIL)) + filter_print = f"{Config.MAILBOX} & {Config.MAIN_MAIL}" + last_run_file = "/tmp/email_service_last_run.json" + current_date = datetime.now().strftime("%Y-%m-%d") + days_to_check, full_check = 7, 90 + + try: + if os.path.exists(last_run_file): + with open(last_run_file, "r") as f: + last_run_data = json.load(f) + last_run_date = last_run_data.get("last_run_date") + if last_run_date != current_date: + days_to_check = full_check + print(f"[EMAIL_SERVICE] First run of the day. Checking emails from the past {days_to_check} days") + else: + print(f"[EMAIL_SERVICE] Subsequent run today. Checking emails from the past {days_to_check} days") + else: + days_to_check = full_check + print(f"[EMAIL_SERVICE] First run detected. Checking emails from the past {days_to_check} days") + except Exception as e: + print(f"[EMAIL_SERVICE] Error reading last run file: {str(e)}. Using default of {days_to_check} days") + + try: + with open(last_run_file, "w") as f: + json.dump({"last_run_date": current_date}, f) + except Exception as e: + print(f"[EMAIL_SERVICE] Error writing last run file: {str(e)}") + + check_since_date = (datetime.now() - timedelta(days=days_to_check)).strftime("%d-%b-%Y") + for folder in mail_folders: + if folder.name == "INBOX": + banks_mails = folder.search(filter_mail & SINCE(check_since_date)) + print(f"[EMAIL_SERVICE] Reading mailbox [{username}] with mail sender [{filter_print}] since {check_since_date} with count: {len(banks_mails)}") + for banks_mail in banks_mails or []: + if email_message := banks_mail.email: + with EmailProcessingContext(banks_mail) as ctx: + try: + headers = {k.lower(): v for k, v in banks_mail.headers.items()} + mail_info = {"from": headers["from"], "to": headers["to"], "subject": headers["subject"], "date": str(headers["date"])} + success = read_email_and_publish_to_redis(email_message=email_message, mail_info=mail_info) + ctx.success = success + if success: + print(f"[EMAIL_SERVICE] Successfully processed email with subject: {mail_info['subject']}") + else: + print(f"[EMAIL_SERVICE] No matching attachments found in email with subject: {mail_info['subject']}") + except Exception as e: + print(f"[EMAIL_SERVICE] Error processing email: {str(e)}") + + +if __name__ == "__main__": + print("=== Starting Email Service with Redis Pub/Sub ===") + print(f"Publishing to channel: {REDIS_CHANNEL}") + time.sleep(20) + while True: + print("\n[EMAIL_SERVICE] Checking for new emails...") + app() + time.sleep(Config.EMAIL_SLEEP) diff --git a/ServicesBank/Email/pyproject.toml b/ServicesBank/Email/pyproject.toml new file mode 100644 index 0000000..e6943ae --- /dev/null +++ b/ServicesBank/Email/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "emailservice" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "arrow>=1.3.0", + "redbox>=0.2.1", + "redis>=5.2.1", + "pydantic-settings>=2.8.1", +] diff --git a/ServicesBank/Finder/BuildFromIban/.dockerignore b/ServicesBank/Finder/BuildExtractor/.dockerignore similarity index 100% rename from ServicesBank/Finder/BuildFromIban/.dockerignore rename to ServicesBank/Finder/BuildExtractor/.dockerignore diff --git a/ServicesBank/Finder/BuildExtractor/Dockerfile b/ServicesBank/Finder/BuildExtractor/Dockerfile new file mode 100644 index 0000000..f061a84 --- /dev/null +++ b/ServicesBank/Finder/BuildExtractor/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/BuildExtractor / +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/BuildExtractor/README.md b/ServicesBank/Finder/BuildExtractor/README.md new file mode 100644 index 0000000..aa05ba7 --- /dev/null +++ b/ServicesBank/Finder/BuildExtractor/README.md @@ -0,0 +1,3 @@ +# Docs of Build Extractor + +Finds build_id, decision_book_id, living_space_id from AccountRecords diff --git a/ServicesBank/Finder/BuildLivingSpace/entrypoint.sh b/ServicesBank/Finder/BuildExtractor/entrypoint.sh similarity index 100% rename from ServicesBank/Finder/BuildLivingSpace/entrypoint.sh rename to ServicesBank/Finder/BuildExtractor/entrypoint.sh diff --git a/ServicesBank/Finder/BuildFromIban/run_app.sh b/ServicesBank/Finder/BuildExtractor/run_app.sh similarity index 100% rename from ServicesBank/Finder/BuildFromIban/run_app.sh rename to ServicesBank/Finder/BuildExtractor/run_app.sh diff --git a/ServicesBank/Finder/BuildExtractor/runner.py b/ServicesBank/Finder/BuildExtractor/runner.py new file mode 100644 index 0000000..d01ba03 --- /dev/null +++ b/ServicesBank/Finder/BuildExtractor/runner.py @@ -0,0 +1,66 @@ +import arrow + +from Schemas import AccountRecords, BuildIbans, BuildDecisionBook +from Controllers.Postgres.engine import get_session_factory +from sqlalchemy import cast, Date + + +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() + + +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("Build Extractor Service is running...") + session_factory = get_session_factory() + session = session_factory() + + try: + account_find_build_from_iban(session=session) + except Exception as e: + print(f"Error occured on find build : {e}") + session.rollback() + + try: + account_records_find_decision_book(session=session) + except Exception as e: + print(f"Error occured on find decision book : {e}") + session.rollback() + + session.close() + session_factory.remove() + print("Build Extractor Service is finished...") diff --git a/ServicesBank/Finder/BuildLivingSpace/.dockerignore b/ServicesBank/Parser/.dockerignore similarity index 100% rename from ServicesBank/Finder/BuildLivingSpace/.dockerignore rename to ServicesBank/Parser/.dockerignore diff --git a/ServicesBank/Parser/Dockerfile b/ServicesBank/Parser/Dockerfile new file mode 100644 index 0000000..e915be7 --- /dev/null +++ b/ServicesBank/Parser/Dockerfile @@ -0,0 +1,24 @@ +FROM python:3.12-slim + +WORKDIR / + +ENV PYTHONPATH=/ PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1 + +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 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 + +RUN apt-get update && apt-get install -y cron + +COPY /ServicesBank/Parser / +COPY /ServicesApi/Schemas /Schemas +COPY /ServicesApi/Controllers /Controllers + +RUN touch /var/log/cron.log + +RUN chmod +x /entrypoint.sh +RUN chmod +x /run_app.sh + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/ServicesBank/Parser/README.md b/ServicesBank/Parser/README.md new file mode 100644 index 0000000..aa05ba7 --- /dev/null +++ b/ServicesBank/Parser/README.md @@ -0,0 +1,3 @@ +# Docs of Build Extractor + +Finds build_id, decision_book_id, living_space_id from AccountRecords diff --git a/ServicesBank/Finder/DecisionBook/entrypoint.sh b/ServicesBank/Parser/entrypoint.sh similarity index 100% rename from ServicesBank/Finder/DecisionBook/entrypoint.sh rename to ServicesBank/Parser/entrypoint.sh diff --git a/ServicesBank/Finder/DecisionBook/run_app.sh b/ServicesBank/Parser/run_app.sh similarity index 100% rename from ServicesBank/Finder/DecisionBook/run_app.sh rename to ServicesBank/Parser/run_app.sh diff --git a/ServicesBank/Parser/runner.py b/ServicesBank/Parser/runner.py new file mode 100644 index 0000000..2b9414c --- /dev/null +++ b/ServicesBank/Parser/runner.py @@ -0,0 +1,636 @@ +import re +import arrow + +from json import loads, dumps +from unidecode import unidecode +from difflib import SequenceMatcher +from itertools import permutations +from time import perf_counter +from sqlalchemy import text as sqlalchemy_text + +from Controllers.Postgres.engine import get_session_factory +from Schemas.account.account import AccountRecordsPredict, AccountRecords + + +def clean_text(text): + text = str(text) + text = re.sub(r'\d{8,}', ' ', text) + # text = re.sub(r'\b[A-Za-z0-9]*?[0-9]+[A-Za-z0-9]*?[A-Za-z]+[A-Za-z0-9]*\b|\b[A-Za-z0-9]*?[A-Za-z]+[A-Za-z0-9]*?[0-9]+[A-Za-z0-9]*\b', ' ', text) + text = text.replace("/", " ") + text = text.replace("_", " ") + text_remove_underscore = text.replace("-", " ").replace("+", " ") + text_remove_asterisk = text_remove_underscore.replace("*", " ") + text_remove_comma = text_remove_asterisk.replace(",", " ") + text_remove_dots = text_remove_comma.replace(".", " ") + text_remove_dots = re.sub(r'\s+', ' ', text_remove_dots) + text_remove_dots = text_remove_dots.strip() + return text_remove_dots + + +def normalize_text(text): + text = text.replace('İ', 'i') + text = text.replace('I', 'ı') + text = text.replace('Ş', 'ş') + text = text.replace('Ğ', 'ğ') + text = text.replace('Ü', 'ü') + text = text.replace('Ö', 'ö') + text = text.replace('Ç', 'ç') + return unidecode(text).lower() + + +def get_person_initials(person): + parts = [person.get("firstname", ""), person.get("middle_name", ""), person.get("surname", ""), person.get("birthname", "")] + return [unidecode(p.strip())[0].upper() for p in parts if p] + + +def get_text_initials(matched_text): + return [unidecode(word.strip())[0].upper() for word in matched_text.split() if word.strip()] + + +def generate_dictonary_of_patterns(people): + + """ + completly remove middle_name instead do regex firstName + SomeWord + surname + """ + patterns_dict = {} + + for person in people: + person_id = person.get('id') + firstname = person.get('firstname', '').strip() if person.get('firstname') else "" + middle_name = person.get('middle_name', '').strip() if person.get('middle_name') else "" + surname = person.get('surname', '').strip() if person.get('surname') else "" + birthname = person.get('birthname', '').strip() if person.get('birthname') else "" + + if not firstname or not surname: + continue + + name_parts = { + 'firstname': { + 'orig': firstname, + 'norm': normalize_text(firstname) if firstname else "", + 'init': normalize_text(firstname)[0] if firstname else "" + }, + 'surname': { + 'orig': surname, + 'norm': normalize_text(surname) if surname else "", + 'init': normalize_text(surname)[0] if surname else "" + } + } + + if middle_name: + name_parts['middle_name'] = { + 'orig': middle_name, + 'norm': normalize_text(middle_name) if middle_name else "", + 'init': normalize_text(middle_name)[0] if middle_name else "" + } + + if birthname and normalize_text(birthname) != normalize_text(surname): + name_parts['birthname'] = { + 'orig': birthname, + 'norm': normalize_text(birthname), + 'init': normalize_text(birthname)[0] if birthname else "" + } + + person_patterns = set() + + def create_pattern(parts, formats, separators=None): + if separators is None: + separators = [""] + + patterns = [] + for fmt in formats: + for sep in separators: + pattern_parts = [] + for part_type, part_name in fmt: + if part_name in parts and part_type in parts[part_name]: + pattern_parts.append(re.escape(parts[part_name][part_type])) + if pattern_parts: + patterns.append(r"\b" + sep.join(pattern_parts) + r"\b") + return patterns + + name_formats = [ + [('orig', 'firstname'), ('orig', 'surname')], + [('norm', 'firstname'), ('norm', 'surname')], + [('orig', 'surname'), ('orig', 'firstname')], + [('norm', 'surname'), ('norm', 'firstname')], + ] + if 'middle_name' in name_parts: + name_formats = [ + [('orig', 'firstname'), ('orig', 'middle_name'), ('orig', 'surname')], + [('norm', 'firstname'), ('norm', 'middle_name'), ('norm', 'surname')], + ] + + person_patterns.update(create_pattern(name_parts, name_formats, [" ", ""])) + + if 'middle_name' in name_parts: + middle_name_formats = [ + [('orig', 'firstname'), ('orig', 'middle_name')], + [('norm', 'firstname'), ('norm', 'middle_name')], + [('orig', 'middle_name'), ('orig', 'surname')], + [('norm', 'middle_name'), ('norm', 'surname')], + ] + person_patterns.update(create_pattern(name_parts, middle_name_formats, [" ", ""])) + + if 'birthname' in name_parts and name_parts['surname']['orig'] != name_parts['birthname']['orig']: + birthname_formats = [ + [('orig', 'firstname'), ('orig', 'birthname')], + [('norm', 'firstname'), ('norm', 'birthname')], + [('orig', 'birthname'), ('orig', 'firstname')], + [('norm', 'birthname'), ('norm', 'firstname')], + ] + person_patterns.update(create_pattern(name_parts, birthname_formats, [" ", ""])) + + initial_formats = [ + [('init', 'firstname'), ('init', 'middle_name'), ('init', 'surname')], + [('init', 'firstname'), ('init', 'surname')], + ] + person_patterns.update(create_pattern(name_parts, initial_formats, ["", ".", " ", ". "])) + + if 'middle_name' in name_parts: + triple_initial_formats = [ + [('init', 'firstname'), ('init', 'middle_name'), ('init', 'surname')], + ] + person_patterns.update(create_pattern(name_parts, triple_initial_formats, ["", ".", " ", ". "])) + + compiled_patterns = [re.compile(pattern, re.IGNORECASE) for pattern in person_patterns] + + patterns_dict[person_id] = compiled_patterns + + return patterns_dict + + +def extract_person_name_with_regex(found_dict, process_comment, patterns_dict, people): + cleaned_text = process_comment + all_matches = [] + + for person_id, patterns in patterns_dict.items(): + person = next((p for p in people if p.get('id') == person_id), None) + if not person: + continue + + firstname_norm = normalize_text(person.get("firstname", "").strip()) if person.get("firstname") else "" + middle_name_norm = normalize_text(person.get("middle_name", "").strip()) if person.get("middle_name") else "" + surname_norm = normalize_text(person.get("surname", "").strip()) if person.get("surname") else "" + birthname_norm = normalize_text(person.get("birthname", "").strip()) if person.get("birthname") else "" + + text_norm = normalize_text(process_comment) + person_matches = [] + + for pattern in patterns: + for match in pattern.finditer(text_norm): + start, end = match.span() + matched_text = process_comment[start:end] + matched_text_norm = normalize_text(matched_text) + + is_valid_match = False + + # Strict validation: require both firstname AND surname/birthname + # No single-word matches allowed + if len(matched_text_norm.split()) <= 1: + # Single word matches are not allowed + is_valid_match = False + else: + # For multi-word matches, require firstname AND (surname OR birthname) + has_firstname = firstname_norm and firstname_norm in matched_text_norm + has_surname = surname_norm and surname_norm in matched_text_norm + has_birthname = birthname_norm and birthname_norm in matched_text_norm + + # Both firstname and surname/birthname must be present + if (has_firstname and has_surname) or (has_firstname and has_birthname): + is_valid_match = True + + if is_valid_match: + person_matches.append({ + 'matched_text': matched_text, + 'start': start, + 'end': end + }) + + if person_matches: + person_matches.sort(key=lambda x: len(x['matched_text']), reverse=True) + + non_overlapping_matches = [] + for match in person_matches: + overlaps = False + for existing_match in non_overlapping_matches: + if (match['start'] < existing_match['end'] and match['end'] > existing_match['start']): + overlaps = True + break + + if not overlaps: + non_overlapping_matches.append(match) + + if non_overlapping_matches: + found_dict["name_match"] = person + all_matches.extend([(match, person) for match in non_overlapping_matches]) + + if all_matches: + all_matches.sort(key=lambda x: x[0]['start'], reverse=True) + + for match, person in all_matches: + matched_text = match['matched_text'] + + matched_words = matched_text.split() + + for word in matched_words: + word_norm = normalize_text(word).strip() + + if not word_norm: + continue + + text_norm = normalize_text(cleaned_text) + for word_match in re.finditer(rf'\b{re.escape(word_norm)}\b', text_norm, re.IGNORECASE): + start, end = word_match.span() + cleaned_text = cleaned_text[:start] + ' ' * (end - start) + cleaned_text[end:] + + cleaned_text = re.sub(r'\s+', ' ', cleaned_text).strip() + + return found_dict, cleaned_text + + +def extract_build_parts_info(found_dict, process_comment): + + """ + Regex of parts such as : + 2 nolu daire + 9 NUMARALI DAI + daire 3 + 3 nolu dairenin + 11nolu daire + Daire No 12 + 2NOLU DAIRE + 12 No lu daire + D:10 + NO:11 + NO :3 + """ + # Initialize apartment number variable + apartment_number = None + cleaned_text = process_comment + + def clean_text_apartment_number(text, match): + clean_text = text.replace(match.group(0), '').strip() + clean_text = re.sub(r'\s+', ' ', clean_text).strip() + return clean_text + + # Pattern 1: X nolu daire (with space) + pattern1 = re.compile(r'(\d+)\s*nolu\s*daire', re.IGNORECASE) + match = pattern1.search(cleaned_text) + if match: + apartment_number = match.group(1) + found_dict['apartment_number'] = apartment_number + return found_dict, clean_text_apartment_number(cleaned_text, match) + + # Pattern 4: X nolu dairenin + pattern4 = re.compile(r'(\d+)\s*nolu\s*daire\w*', re.IGNORECASE) + match = pattern4.search(cleaned_text) + if match: + apartment_number = match.group(1) + found_dict['apartment_number'] = apartment_number + return found_dict, clean_text_apartment_number(cleaned_text, match) + + # Pattern 5: XNolu daire (without space) + pattern5 = re.compile(r'(\d+)nolu\s*daire', re.IGNORECASE) + match = pattern5.search(cleaned_text) + if match: + apartment_number = match.group(1) + found_dict['apartment_number'] = apartment_number + return found_dict, clean_text_apartment_number(cleaned_text, match) + + # Pattern 7: XNOLU DAIRE (all caps, no space) + pattern7 = re.compile(r'(\d+)nolu\s*daire', re.IGNORECASE) + match = pattern7.search(cleaned_text) + if match: + apartment_number = match.group(1) + found_dict['apartment_number'] = apartment_number + return found_dict, clean_text_apartment_number(cleaned_text, match) + + # Pattern 8: X No lu daire + pattern8 = re.compile(r'(\d+)\s*no\s*lu\s*daire', re.IGNORECASE) + match = pattern8.search(cleaned_text) + if match: + apartment_number = match.group(1) + found_dict['apartment_number'] = apartment_number + return found_dict, clean_text_apartment_number(cleaned_text, match) + + # Pattern 6: Daire No X + pattern6 = re.compile(r'daire\s*no\s*(\d+)', re.IGNORECASE) + match = pattern6.search(cleaned_text) + if match: + apartment_number = match.group(1) + found_dict['apartment_number'] = apartment_number + return found_dict, clean_text_apartment_number(cleaned_text, match) + + # Pattern 2: X NUMARALI DAI + pattern2 = re.compile(r'(\d+)\s*numarali\s*dai', re.IGNORECASE) + match = pattern2.search(cleaned_text) + if match: + apartment_number = match.group(1) + found_dict['apartment_number'] = apartment_number + return found_dict, clean_text_apartment_number(cleaned_text, match) + + # Pattern 3: daire X + pattern3 = re.compile(r'daire\s*(\d+)', re.IGNORECASE) + match = pattern3.search(cleaned_text) + if match: + apartment_number = match.group(1) + found_dict['apartment_number'] = apartment_number + return found_dict, clean_text_apartment_number(cleaned_text, match) + + # Pattern 9: D:X + pattern9 = re.compile(r'd\s*:\s*(\d+)', re.IGNORECASE) + match = pattern9.search(cleaned_text) + if match: + apartment_number = match.group(1) + found_dict['apartment_number'] = apartment_number + return found_dict, clean_text_apartment_number(cleaned_text, match) + + # Pattern 10: NO:X or NO :X + pattern10 = re.compile(r'no\s*:\s*(\d+)', re.IGNORECASE) + match = pattern10.search(cleaned_text) + if match: + apartment_number = match.group(1) + found_dict['apartment_number'] = apartment_number + return found_dict, clean_text_apartment_number(cleaned_text, match) + + return found_dict, cleaned_text + + +def extract_months(found_dict, process_comment): + """ + Extract Turkish month names and abbreviations from the process comment + """ + original_text = process_comment + # Updated dictionary with normalized keys for better matching + month_to_number_dict = { + "ocak": 1, "şubat": 2, "mart": 3, "nisan": 4, "mayıs": 5, "haziran": 6, + "temmuz": 7, "ağustos": 8, "eylül": 9, "ekim": 10, "kasım": 11, "aralık": 12, + # Add normalized versions without Turkish characters + "ocak": 1, "subat": 2, "mart": 3, "nisan": 4, "mayis": 5, "haziran": 6, + "temmuz": 7, "agustos": 8, "eylul": 9, "ekim": 10, "kasim": 11, "aralik": 12 + } + + def clean_text_month(text, match): + clean_text = text.replace(match.group(0), '').strip() + clean_text = re.sub(r'\s+', ' ', clean_text).strip() + return clean_text + + def normalize_turkish(text): + """Properly normalize Turkish text for case-insensitive comparison""" + text = text.lower() + text = text.replace('i̇', 'i') # Handle dotted i properly + text = text.replace('ı', 'i') # Convert dotless i to regular i for matching + text = unidecode(text) # Remove other diacritics + return text + + if 'months' not in found_dict: + found_dict['months'] = [] + + months_found, working_text = False, original_text + + for month in turkish_months: + pattern = re.compile(r'\b' + re.escape(month) + r'\b', re.IGNORECASE) + for match in pattern.finditer(original_text): + matched_text = match.group(0) + + normalized_month = normalize_turkish(month) + month_number = None + + if month.lower() in month_to_number_dict: + month_number = month_to_number_dict[month.lower()] + elif normalized_month in month_to_number_dict: + month_number = month_to_number_dict[normalized_month] + + month_info = {'name': month, 'number': month_number} + found_dict['months'].append(month_info) + months_found = True + working_text = working_text.replace(matched_text, '', 1) + + for abbr, full_month in turkish_months_abbr.items(): + pattern = re.compile(r'\b' + re.escape(abbr) + r'\b', re.IGNORECASE) + + for match in pattern.finditer(working_text): + matched_text = match.group(0) + normalized_month = normalize_turkish(full_month) + month_number = None + + if full_month.lower() in month_to_number_dict: + month_number = month_to_number_dict[full_month.lower()] + elif normalized_month in month_to_number_dict: + month_number = month_to_number_dict[normalized_month] + + month_info = {'name': full_month, 'number': month_number} + found_dict['months'].append(month_info) + months_found = True + working_text = working_text.replace(matched_text, '', 1) + + return found_dict, working_text + + +def extract_year(found_dict, process_comment): + """ + Extract years from the process comment + """ + original_text = process_comment + + if 'years' not in found_dict: + found_dict['years'] = [] + + working_text = original_text + + for year in range(start_year, current_year + 1): + pattern = re.compile(r'\b' + str(year) + r'\b', re.IGNORECASE) + for match in pattern.finditer(original_text): + matched_text = match.group(0) + if str(matched_text).isdigit(): + found_dict['years'].append(int(matched_text)) + working_text = working_text.replace(matched_text, '', 1) + + return found_dict, working_text + + +def extract_payment_type(found_dict, process_comment): + """ + Extract payment type from the process comment + aidat + AİD + aidatı + TADİLAT + YAKIT + yakıt + yakit + """ + original_text = process_comment + working_text = original_text + + if 'payment_types' not in found_dict: + found_dict['payment_types'] = [] + + payment_keywords = { + 'aidat': ['aidat', 'aİd', 'aid', 'aidatı', 'aidati'], + 'tadilat': ['tadilat', 'tadİlat', 'tadilatı'], + 'yakit': ['yakit', 'yakıt', 'yakıtı', 'yakiti'] + } + + for payment_type, keywords in payment_keywords.items(): + for keyword in keywords: + pattern = re.compile(r'\b' + keyword + r'\b', re.IGNORECASE) + for match in pattern.finditer(original_text): + matched_text = match.group(0) + if payment_type not in found_dict['payment_types']: + found_dict['payment_types'].append(payment_type) + working_text = working_text.replace(matched_text, '', 1) + + return found_dict, working_text + + +def main(session, account_records, people): + + list_of_regex_patterns = generate_dictonary_of_patterns(people=people) + dicts_found = dict() + dicts_not_found = dict() + count_extracted = 0 + for account_record in account_records: + account_record_id = str(account_record["id"]) + found_dict = {} + process_comment_iteration = clean_text(text=account_record["process_comment"]) + found_dict, cleaned_process_comment = extract_person_name_with_regex( + found_dict=found_dict, process_comment=process_comment_iteration, patterns_dict=list_of_regex_patterns, people=people + ) + + found_dict, cleaned_process_comment = extract_build_parts_info( + found_dict=found_dict, process_comment=cleaned_process_comment + ) + found_dict, cleaned_process_comment = extract_months( + found_dict=found_dict, process_comment=cleaned_process_comment + ) + found_dict, cleaned_process_comment = extract_year( + found_dict=found_dict, process_comment=cleaned_process_comment + ) + found_dict, cleaned_process_comment = extract_payment_type( + found_dict=found_dict, process_comment=cleaned_process_comment + ) + if found_dict: + dicts_found[str(account_record_id)] = found_dict + else: + dicts_not_found[str(account_record_id)] = account_record_id + + + for id_, item in dicts_found.items(): + AccountRecordsPredict.set_session(session) + AccountRecords.set_session(session) + + months_are_valid = bool(item.get("months", [])) + years_are_valid = bool(item.get("years", [])) + payment_types_are_valid = bool(item.get("payment_types", [])) + apartment_number_are_valid = bool(item.get("apartment_number", [])) + person_name_are_valid = bool(item.get("name_match", [])) + account_record_to_save = AccountRecords.query.filter_by(id=int(id_)).first() + save_dict = dict( + account_records_id=account_record_to_save.id, account_records_uu_id=str(account_record_to_save.uu_id), prediction_model="regex", treshold=1, is_first_prediction=False + ) + update_dict = dict(prediction_model="regex", treshold=1, is_first_prediction=False) + if any([months_are_valid, years_are_valid, payment_types_are_valid, apartment_number_are_valid, person_name_are_valid]): + count_extracted += 1 + if months_are_valid: + print(f"months: {item['months']}") + data_to_save = dumps({"data": item['months']}) + prediction_result = AccountRecordsPredict.query.filter_by(account_records_id=account_record_to_save.id, prediction_field="months", prediction_model="regex").first() + if not prediction_result: + created_account_prediction = AccountRecordsPredict.create(**save_dict, prediction_field="months", prediction_result=data_to_save) + created_account_prediction.save() + else: + prediction_result.update(**update_dict, prediction_result=data_to_save) + prediction_result.save() + if years_are_valid: + print(f"years: {item['years']}") + data_to_save = dumps({"data": item['years']}) + prediction_result = AccountRecordsPredict.query.filter_by(account_records_id=account_record_to_save.id, prediction_field="years", prediction_model="regex").first() + if not prediction_result: + created_account_prediction = AccountRecordsPredict.create(**save_dict, prediction_field="years", prediction_result=data_to_save) + created_account_prediction.save() + else: + prediction_result.update(**update_dict, prediction_result=data_to_save) + prediction_result.save() + if payment_types_are_valid: + print(f"payment_types: {item['payment_types']}") + data_to_save = dumps({"data": item['payment_types']}) + prediction_result = AccountRecordsPredict.query.filter_by(account_records_id=account_record_to_save.id, prediction_field="payment_types", prediction_model="regex").first() + if not prediction_result: + created_account_prediction = AccountRecordsPredict.create(**save_dict, prediction_field="payment_types", prediction_result=data_to_save) + created_account_prediction.save() + else: + prediction_result.update(**update_dict, prediction_result=data_to_save) + prediction_result.save() + if apartment_number_are_valid: + print(f"apartment_number: {item['apartment_number']}") + prediction_result = AccountRecordsPredict.query.filter_by(account_records_id=account_record_to_save.id, prediction_field="apartment_number", prediction_model="regex").first() + if not prediction_result: + created_account_prediction = AccountRecordsPredict.create(**save_dict, prediction_field="apartment_number", prediction_result=item['apartment_number']) + created_account_prediction.save() + else: + prediction_result.update(**update_dict, prediction_result=item['apartment_number']) + prediction_result.save() + if person_name_are_valid: + print(f"person_name: {item['name_match']}") + data_to_save = dumps({"data": item['name_match']}) + prediction_result = AccountRecordsPredict.query.filter_by(account_records_id=account_record_to_save.id, prediction_field="person_name", prediction_model="regex").first() + if not prediction_result: + created_account_prediction = AccountRecordsPredict.create(**save_dict, prediction_field="person_name", prediction_result=data_to_save) + created_account_prediction.save() + else: + prediction_result.update(**update_dict, prediction_result=data_to_save) + prediction_result.save() + + print("\n===== SUMMARY =====") + print(f"extracted data total : {count_extracted}") + print(f"not extracted data total : {len(account_records) - count_extracted}") + print(f"Total account records processed : {len(account_records)}") + + +if __name__ == "__main__": + + session_factory = get_session_factory() + session = session_factory() + + turkish_months = ["OCAK", "ŞUBAT", "MART", "NİSAN", "MAYIS", "HAZİRAN", "TEMMUZ", "AĞUSTOS", "EYLÜL", "EKİM", "KASIM", "ARALIK"] + turkish_months_abbr = { + "OCA": "OCAK", "SUB": "ŞUBAT", "ŞUB": "ŞUBAT", "MAR": "MART", "NIS": "NİSAN", "MAY": "MAYIS", "HAZ": "HAZİRAN", "HZR": "HAZİRAN", + "TEM": "TEMMUZ", "AGU": "AĞUSTOS", "AGT": "AĞUSTOS", "EYL": "EYLÜL", "EKI": "EKİM", "KAS": "KASIM", "ARA": "ARALIK", + } + start_year = 1950 + current_year = arrow.now().year + + people_query = sqlalchemy_text(""" + SELECT DISTINCT ON (p.id) p.firstname, p.middle_name, p.surname, p.birthname, bl.id + FROM public.people as p + INNER JOIN public.build_living_space as bl ON bl.person_id = p.id + INNER JOIN public.build_parts as bp ON bp.id = bl.build_parts_id + INNER JOIN public.build as b ON b.id = bp.build_id + WHERE b.id = 1 + ORDER BY p.id + """) + + people_raw = session.execute(people_query).all() + remove_duplicate = list() + clean_people_list = list() + for person in people_raw: + merged_name = f"{person[0]} {person[1]} {person[2]} {person[3]}" + if merged_name not in remove_duplicate: + clean_people_list.append(person) + remove_duplicate.append(merged_name) + + people = [{"firstname": p[0], "middle_name": p[1], "surname": p[2], "birthname": p[3], 'id': p[4]} for p in clean_people_list] + query_account_records = sqlalchemy_text(""" + SELECT a.id, a.iban, a.bank_date, a.process_comment FROM public.account_records as a where currency_value > 0 + """) # and bank_date::date >= '2020-01-01' + account_records = session.execute(query_account_records).all() + account_records = [{"id": ar[0], "iban": ar[1], "bank_date": ar[2], "process_comment": ar[3]} for ar in account_records] + + try: + main(session=session, account_records=account_records, people=people) + except Exception as e: + print(f"{e}") + + session.close() + session_factory.remove() diff --git a/ServicesBank/RoutineEmail/Dockerfile b/ServicesBank/RoutineEmail/Dockerfile new file mode 100644 index 0000000..c6ff012 --- /dev/null +++ b/ServicesBank/RoutineEmail/Dockerfile @@ -0,0 +1,26 @@ +FROM python:3.12-slim + +WORKDIR / + +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 /ServicesBank/RoutineEmail/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 +RUN apt-get update && apt-get install -y cron + +COPY /ServicesBank/RoutineEmail / + +RUN chmod +x /run_app.sh + +COPY /ServicesBank/RoutineEmail / +COPY /ServicesApi/Controllers /ServicesApi/Controllers +COPY /ServicesBank/Depends/config.py /ServicesBank/Depends/config.py +COPY /ServicesBank/Depends/template_accounts.html /template_accounts.html + +ENV PYTHONPATH=/ PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1 + +RUN touch /var/log/cron.log +RUN chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/ServicesBank/RoutineEmail/README.md b/ServicesBank/RoutineEmail/README.md new file mode 100644 index 0000000..1e72d3f --- /dev/null +++ b/ServicesBank/RoutineEmail/README.md @@ -0,0 +1,69 @@ +# Routine Email Service + +## Overview +This service sends automated email reports about account records at scheduled times using cron. It retrieves account records from a PostgreSQL database, formats them into an HTML email, and sends them to specified recipients. + +## Environment Setup +The service requires the following environment variables: + +### Email Configuration +- `EMAIL_HOST`: SMTP server address (e.g., "10.10.2.34") +- `EMAIL_USERNAME`: Email sender address (e.g., "example@domain.com") +- `EMAIL_PASSWORD`: Email password (sensitive) +- `EMAIL_PORT`: SMTP port (e.g., 587) +- `EMAIL_SEND`: Flag to enable/disable email sending (1 = enabled) + +### Database Configuration +- `DB_HOST`: PostgreSQL server address (e.g., "10.10.2.14") +- `DB_USER`: Database username (e.g., "postgres") +- `DB_PASSWORD`: Database password (sensitive) +- `DB_PORT`: Database port (e.g., 5432) +- `DB_NAME`: Database name (e.g., "postgres") + +## Cron Job Configuration +The service is configured to run daily at 11:00 Istanbul Time (08:00 UTC). This is set up in the entrypoint.sh script. + +## Docker Container Setup + +### Key Files + +1. **Dockerfile**: Defines the container image with Python and cron + +2. **entrypoint.sh**: Container entrypoint script that: + - Creates an environment file (/env.sh) with all configuration variables + - Sets up the crontab to run run_app.sh at the scheduled time + - Starts the cron service + - Tails the log file for monitoring + +3. **run_app.sh**: Script executed by cron that: + - Sources the environment file to get all configuration + - Exports variables to make them available to the Python script + - Runs the Python application + - Logs environment and execution results + +### Environment Variable Handling +Cron jobs run with a minimal environment that doesn't automatically include Docker container environment variables. Our solution: + +1. Captures all environment variables from Docker to a file at container startup +2. Has the run_app.sh script source this file before execution +3. Explicitly exports all variables to ensure they're available to the Python script + +## Logs +Logs are written to `/var/log/cron.log` and can be viewed with: +```bash +docker exec routine_email_service tail -f /var/log/cron.log +``` + +## Manual Execution +To run the service manually: +```bash +docker exec routine_email_service /run_app.sh +``` + +## Docker Compose Configuration +In the docker-compose.yml file, the service needs an explicit entrypoint configuration: +```yaml +entrypoint: ["/entrypoint.sh"] +``` + +This ensures the entrypoint script runs when the container starts. diff --git a/ServicesBank/RoutineEmail/app.py b/ServicesBank/RoutineEmail/app.py new file mode 100644 index 0000000..9aabf8f --- /dev/null +++ b/ServicesBank/RoutineEmail/app.py @@ -0,0 +1,157 @@ +import arrow +from typing import List, Any + +from Schemas import AccountRecords +from jinja2 import Environment, FileSystemLoader +from Controllers.Email.send_email import EmailSendModel, EmailService + + +def render_email_template( + headers: List[str], rows: List[List[Any]], balance_error: bool, bank_balance: str +) -> str: + """ + Render the HTML email template with the provided data. + + Args: + headers: List of column headers for the table + rows: List of data rows for the table + balance_error: Flag indicating if there's a balance discrepancy + bank_balance: Current bank balance formatted as string + + Returns: + Rendered HTML template as string + """ + try: + # Look for template in ServiceDepends directory + env = Environment(loader=FileSystemLoader("/")) + template = env.get_template("template_accounts.html") + + # Render template with variables + return template.render( + headers=headers, + rows=rows, + bank_balance=bank_balance, + balance_error=balance_error, + today=str(arrow.now().date()), + ) + except Exception as e: + print("Exception render template:", e) + raise + + +def send_email_to_given_address(send_to: str, html_template: str) -> bool: + """ + Send email with the rendered HTML template to the specified address. + + Args: + send_to: Email address of the recipient + html_template: Rendered HTML template content + + Returns: + Boolean indicating if the email was sent successfully + """ + today = arrow.now() + subject = f"{str(today.date())} Gunes Apt. Cari Durum Bilgilendirme Raporu" + + # Create email parameters using EmailSendModel + email_params = EmailSendModel( + subject=subject, + html=html_template, + receivers=[send_to], + text=f"Gunes Apt. Cari Durum Bilgilendirme Raporu - {today.date()}", + ) + + try: + # Use the context manager to handle connection errors + with EmailService.new_session() as email_session: + # Send email through the service + return EmailService.send_email(email_session, email_params) + except Exception as e: + print(f"Exception send email: {e}") + return False + + +def set_account_records_to_send_email() -> bool: + """ + Retrieve account records from the database, format them, and send an email report. + + Usage: + from app import set_account_records_to_send_email + + Returns: + Boolean indicating if the process completed successfully + """ + # Get database session and retrieve records + with AccountRecords.new_session() as db_session: + account_records_query = AccountRecords.filter_all(db=db_session).query + + # Get the 3 most recent records + account_records: List[AccountRecords] = ( + account_records_query.order_by( + AccountRecords.bank_date.desc(), + AccountRecords.iban.desc(), + ) + .limit(3) + .all() + ) + + # Check if we have enough records + if len(account_records) < 2: + print(f"Not enough records found: {len(account_records)}") + return False + + # Check for balance discrepancy + first_record, second_record = account_records[0], account_records[1] + expected_second_balance = ( + first_record.bank_balance - first_record.currency_value + ) + balance_error = expected_second_balance != second_record.bank_balance + + if balance_error: + print( + f"Balance error detected {expected_second_balance} != {second_record.bank_balance}" + ) + + # Format rows for the email template + list_of_rows = [] + for record in account_records: + list_of_rows.append( + [ + record.bank_date.strftime("%d/%m/%Y %H:%M"), + record.process_comment, + f"{record.currency_value:,.2f}", + f"{record.bank_balance:,.2f}", + ] + ) + # Get the most recent bank balance + last_bank_balance = sorted( + account_records, key=lambda x: x.bank_date, reverse=True + )[0].bank_balance + # Define headers for the table + headers = [ + "Ulaştığı Tarih", + "Banka Transaksiyonu Ek Bilgi", + "Aktarım Değeri", + "Banka Bakiyesi", + ] + + # Recipient email address + send_to = "karatay@mehmetkaratay.com.tr" + + # Render email template + html_template = render_email_template( + headers=headers, + rows=list_of_rows, + balance_error=balance_error, + bank_balance=f"{last_bank_balance:,.2f}", + ) + + # Send the email + return send_email_to_given_address(send_to=send_to, html_template=html_template) + + +if __name__ == "__main__": + success = set_account_records_to_send_email() + print("Email sent successfully" if success else "Failed to send email") + exit_code = 0 if success else 1 + exit(exit_code) diff --git a/ServicesBank/RoutineEmail/entrypoint.sh b/ServicesBank/RoutineEmail/entrypoint.sh new file mode 100644 index 0000000..d1bc6ca --- /dev/null +++ b/ServicesBank/RoutineEmail/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/RoutineEmail/pyproject.toml b/ServicesBank/RoutineEmail/pyproject.toml new file mode 100644 index 0000000..ddb7a04 --- /dev/null +++ b/ServicesBank/RoutineEmail/pyproject.toml @@ -0,0 +1,17 @@ +[project] +name = "routineemailservice" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "arrow>=1.3.0", + "redbox>=0.2.1", + "redis>=5.2.1", + "pydantic-settings>=2.8.1", + "sqlalchemy-mixins>=2.0.5", + "fastapi>=0.115.11", + "jinja2>=3.1.6", + "psycopg2-binary>=2.9.10", + "redmail>=0.6.0", +] diff --git a/ServicesBank/RoutineEmail/run_app.sh b/ServicesBank/RoutineEmail/run_app.sh new file mode 100644 index 0000000..4e54fd4 --- /dev/null +++ b/ServicesBank/RoutineEmail/run_app.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# Source the environment file directly +. /env.sh + +# Re-export all variables to ensure they're available to the Python script +export EMAIL_HOST +export EMAIL_USERNAME +export EMAIL_PASSWORD +export EMAIL_PORT +export EMAIL_SEND +export DB_HOST +export DB_USER +export DB_PASSWORD +export DB_PORT +export DB_NAME + +# Python environment variables +export PYTHONPATH +export PYTHONUNBUFFERED +export PYTHONDONTWRITEBYTECODE + +env >> /var/log/cron.log +/usr/local/bin/python /app.py \ No newline at end of file diff --git a/ServicesBank/RoutineEmail/templates/a.txt b/ServicesBank/RoutineEmail/templates/a.txt new file mode 100644 index 0000000..e69de29 diff --git a/bank-services-docker-compose copy.yml b/bank-services-docker-compose copy.yml new file mode 100644 index 0000000..5aade32 --- /dev/null +++ b/bank-services-docker-compose copy.yml @@ -0,0 +1,161 @@ +services: + email_service: + container_name: email_service + build: + context: . + dockerfile: ServicesBank/Email/Dockerfile + networks: + - bank-services-network + environment: + - MAILBOX=bilgilendirme@ileti.isbank.com.tr + - MAIN_MAIL=karatay.berkay@gmail.com + - INFO_MAIL=mehmet.karatay@hotmail.com + - EMAIL_HOST=10.10.2.34 + - EMAIL_USERNAME=isbank@mehmetkaratay.com.tr + - EMAIL_PASSWORD=system + - EMAIL_PORT=993 + - EMAIL_SEND_PORT=587 + - EMAIL_SLEEP=60 + - AUTHORIZE_IBAN=4245-0093333 + - REDIS_HOST=10.10.2.15 + - REDIS_PORT=6379 + - REDIS_PASSWORD=your_strong_password_here + restart: unless-stopped + volumes: + - tempory-email-service:/tmp + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + finder_build_extractor: + container_name: finder_build_extractor + env_file: + - api_env.env + build: + context: . + dockerfile: ServicesBank/Finder/BuildExtractor/Dockerfile + networks: + - bank-services-network + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + cpus: 0.25 + mem_limit: 512m + + finder_payment_service: + container_name: finder_payment_service + env_file: + - api_env.env + build: + context: . + dockerfile: ServicesBank/Finder/Payment/Dockerfile + networks: + - bank-services-network + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + cpus: 0.25 + mem_limit: 512m + + parser_service: + container_name: parser_service + build: + context: . + dockerfile: ServicesBank/Parser/Dockerfile + networks: + - bank-services-network + env_file: + - api_env.env + restart: unless-stopped + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # writer_service: + # container_name: writer_service + # build: + # context: . + # dockerfile: ServicesBank/WriterService/Dockerfile + # networks: + # - bank-services-network + # environment: + # - REDIS_HOST=10.10.2.15 + # - REDIS_PORT=6379 + # - REDIS_PASSWORD=your_strong_password_here + # - DB_HOST=10.10.2.14 + # - DB_PORT=5432 + # - DB_USER=postgres + # - DB_PASSWORD=password + # - DB_NAME=postgres + # restart: unless-stopped + # logging: + # driver: "json-file" + # options: + # max-size: "10m" + # max-file: "3" + + routine_email_service: + container_name: routine_email_service + build: + context: . + dockerfile: ServicesBank/RoutineEmailService/Dockerfile + entrypoint: ["/entrypoint.sh"] + networks: + - bank-services-network + environment: + - EMAIL_HOST=10.10.2.34 + - EMAIL_USERNAME=karatay@mehmetkaratay.com.tr + - EMAIL_PASSWORD=system + - EMAIL_PORT=587 + - EMAIL_SEND=1 + - DB_HOST=10.10.2.14 + - DB_PORT=5432 + - DB_USER=postgres + - DB_PASSWORD=password + - DB_NAME=postgres + restart: unless-stopped + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # sender_service: + # container_name: sender_service + # build: + # context: . + # dockerfile: ServicesBank/SenderService/Dockerfile + # networks: + # - bank-services-network + # environment: + # - EMAIL_HOST=10.10.2.34 + # - EMAIL_USERNAME=karatay@mehmetkaratay.com.tr + # - EMAIL_PASSWORD=system + # - EMAIL_PORT=587 + # - EMAIL_SEND=1 + # - DB_HOST=10.10.2.14 + # - DB_PORT=5432 + # - DB_USER=postgres + # - DB_PASSWORD=password + # - DB_NAME=postgres + # restart: unless-stopped + # logging: + # driver: "json-file" + # options: + # max-size: "10m" + # max-file: "3" + +networks: + bank-services-network: + driver: bridge + +volumes: + tempory-email-service: diff --git a/docker-compose.yml b/docker-compose.yml index e90d757..a8d3ff9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -189,76 +189,45 @@ services: - "8003:8003" # 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_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_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" + # zemberek-api: + # build: + # context: . + # dockerfile: ServicesBank/Zemberek/Dockerfile + # container_name: zemberek-api + # ports: + # - "8111:8111" + # restart: unless-stopped - zemberek-api: - build: - context: . - dockerfile: ServicesBank/Zemberek/Dockerfile - container_name: zemberek-api - ports: - - "8111:8111" - restart: unless-stopped - - 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 @@ -354,7 +323,7 @@ services: context: . dockerfile: ServicesApi/Builds/Initial/Dockerfile environment: - - SET_ALEMBIC=0 + - SET_ALEMBIC=1 networks: - wag-services env_file: diff --git a/ServicesBank/Finder/DecisionBook/.dockerignore b/trash/XBuildFromIban/.dockerignore similarity index 100% rename from ServicesBank/Finder/DecisionBook/.dockerignore rename to trash/XBuildFromIban/.dockerignore diff --git a/ServicesBank/Finder/BuildFromIban/Dockerfile b/trash/XBuildFromIban/Dockerfile similarity index 100% rename from ServicesBank/Finder/BuildFromIban/Dockerfile rename to trash/XBuildFromIban/Dockerfile diff --git a/ServicesBank/Finder/BuildFromIban/README.md b/trash/XBuildFromIban/README.md similarity index 100% rename from ServicesBank/Finder/BuildFromIban/README.md rename to trash/XBuildFromIban/README.md diff --git a/ServicesBank/Finder/BuildFromIban/entrypoint.sh b/trash/XBuildFromIban/entrypoint.sh similarity index 100% rename from ServicesBank/Finder/BuildFromIban/entrypoint.sh rename to trash/XBuildFromIban/entrypoint.sh diff --git a/trash/XBuildFromIban/run_app.sh b/trash/XBuildFromIban/run_app.sh new file mode 100644 index 0000000..83dcf19 --- /dev/null +++ b/trash/XBuildFromIban/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/trash/XBuildFromIban/runner.py similarity index 100% rename from ServicesBank/Finder/BuildFromIban/runner.py rename to trash/XBuildFromIban/runner.py diff --git a/trash/XBuildLivingSpace/.dockerignore b/trash/XBuildLivingSpace/.dockerignore new file mode 100644 index 0000000..1449c90 --- /dev/null +++ b/trash/XBuildLivingSpace/.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/trash/XBuildLivingSpace/Dockerfile similarity index 100% rename from ServicesBank/Finder/BuildLivingSpace/Dockerfile rename to trash/XBuildLivingSpace/Dockerfile diff --git a/ServicesBank/Finder/BuildLivingSpace/README.md b/trash/XBuildLivingSpace/README.md similarity index 100% rename from ServicesBank/Finder/BuildLivingSpace/README.md rename to trash/XBuildLivingSpace/README.md diff --git a/ServicesBank/Finder/BuildLivingSpace/configs.py b/trash/XBuildLivingSpace/configs.py similarity index 100% rename from ServicesBank/Finder/BuildLivingSpace/configs.py rename to trash/XBuildLivingSpace/configs.py diff --git a/trash/XBuildLivingSpace/entrypoint.sh b/trash/XBuildLivingSpace/entrypoint.sh new file mode 100644 index 0000000..73f4e4d --- /dev/null +++ b/trash/XBuildLivingSpace/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/trash/XBuildLivingSpace/parser.py similarity index 100% rename from ServicesBank/Finder/BuildLivingSpace/parser.py rename to trash/XBuildLivingSpace/parser.py diff --git a/ServicesBank/Finder/BuildLivingSpace/regex_func.py b/trash/XBuildLivingSpace/regex_func.py similarity index 100% rename from ServicesBank/Finder/BuildLivingSpace/regex_func.py rename to trash/XBuildLivingSpace/regex_func.py diff --git a/ServicesBank/Finder/BuildLivingSpace/run_app.sh b/trash/XBuildLivingSpace/run_app.sh similarity index 100% rename from ServicesBank/Finder/BuildLivingSpace/run_app.sh rename to trash/XBuildLivingSpace/run_app.sh diff --git a/ServicesBank/Finder/BuildLivingSpace/runner.py b/trash/XBuildLivingSpace/runner.py similarity index 100% rename from ServicesBank/Finder/BuildLivingSpace/runner.py rename to trash/XBuildLivingSpace/runner.py diff --git a/ServicesBank/Finder/BuildLivingSpace/validations.py b/trash/XBuildLivingSpace/validations.py similarity index 100% rename from ServicesBank/Finder/BuildLivingSpace/validations.py rename to trash/XBuildLivingSpace/validations.py diff --git a/trash/XDecisionBook/.dockerignore b/trash/XDecisionBook/.dockerignore new file mode 100644 index 0000000..1449c90 --- /dev/null +++ b/trash/XDecisionBook/.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/trash/XDecisionBook/Dockerfile similarity index 100% rename from ServicesBank/Finder/DecisionBook/Dockerfile rename to trash/XDecisionBook/Dockerfile diff --git a/ServicesBank/Finder/DecisionBook/README.md b/trash/XDecisionBook/README.md similarity index 100% rename from ServicesBank/Finder/DecisionBook/README.md rename to trash/XDecisionBook/README.md diff --git a/trash/XDecisionBook/entrypoint.sh b/trash/XDecisionBook/entrypoint.sh new file mode 100644 index 0000000..73f4e4d --- /dev/null +++ b/trash/XDecisionBook/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/trash/XDecisionBook/run_app.sh b/trash/XDecisionBook/run_app.sh new file mode 100644 index 0000000..83dcf19 --- /dev/null +++ b/trash/XDecisionBook/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/trash/XDecisionBook/runner.py similarity index 100% rename from ServicesBank/Finder/DecisionBook/runner.py rename to trash/XDecisionBook/runner.py