Service Runner Finder and complete task chain completed

This commit is contained in:
2025-08-11 19:26:49 +03:00
parent 405ba2e95d
commit ca98adc338
18 changed files with 1205 additions and 257 deletions

View File

@@ -0,0 +1,14 @@
__pycache__/
*.pyc
*.pyo
*.pyd
*.db
*.sqlite3
*.log
*.env
venv/
.env.*
node_modules/
.prisma/
.prisma-cache/
ServicesRunnner/AccountRecordServices/Test/venv/

View File

@@ -0,0 +1,22 @@
FROM python:3.12-slim
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
ENV VIRTUAL_ENV=/opt/venv
ENV PRISMA_SCHEMA_PATH=/app/Depends/schema.prisma
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
ENV PYTHONPATH=/app
RUN apt-get update && apt-get install -y --no-install-recommends gcc curl && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY ServicesRunner/Depends/ /app/Depends/
COPY ServicesRunner/AccountRecordServices/Finder/Comment /app/
COPY ServicesRunner/requirements.txt /app/requirements.txt
COPY ServicesRunner/AccountRecordServices/Finder/Comment/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
CMD ["/entrypoint.sh"]

View File

@@ -0,0 +1,174 @@
import time
import arrow
import pprint
from json import dumps
from decimal import Decimal
from pydantic import BaseModel
from datetime import datetime
from typing import Optional
from Depends.prisma_client import PrismaService
from Depends.service_handler import ProcessCommentFinderService
from Depends.config import ConfigServices, MailSendModel, RedisMailSender, Status, RedisTaskObject, FinderComment
class BankReceive(BaseModel):
import_file_name: str
iban: str
bank_date: datetime
channel_branch: str
currency: Optional[str] = "TL"
currency_value: Decimal
bank_balance: Decimal
additional_balance: Decimal
process_name: str
process_type: str
process_comment: str
bank_reference_code: str
bank_date_w: int
bank_date_m: int
bank_date_d: int
bank_date_y: int
def check_task_belong_to_this_service(task: RedisTaskObject):
if not task.service == ConfigServices.SERVICE_PREFIX_FINDER_IBAN:
return False
if not task.completed:
return False
if task.is_completed:
return False
if not task.data:
return False
return True
def write_account_records_row_from_finder_comment(finder_comments: list[FinderComment], prisma_service: PrismaService, saved_list_of_account_records: dict):
finder_comments = list(finder_comments)
for finder_comment in finder_comments:
bank_date = arrow.get(finder_comment.bank_date).replace(tzinfo='GMT+3').datetime
bank_receive_record = BankReceive(
import_file_name=finder_comment.filename, iban=finder_comment.iban, bank_date=bank_date, channel_branch=finder_comment.channel_branch, currency="TL", currency_value=finder_comment.currency_value,
bank_balance=finder_comment.balance, additional_balance=finder_comment.additional_balance, process_name=finder_comment.process_name, process_type=finder_comment.process_type,
process_comment=finder_comment.process_comment, bank_reference_code=finder_comment.bank_reference_code, build_id=finder_comment.build_id, build_uu_id=finder_comment.build_uu_id,
decision_book_id=finder_comment.decision_book_id, decision_book_uu_id=finder_comment.decision_book_uu_id, bank_date_w=bank_date.weekday(), bank_date_m=bank_date.month,
bank_date_d=bank_date.day, bank_date_y=bank_date.year
)
account_record_found = prisma_service.find_first(table="account_records", query={"iban": bank_receive_record.iban, "bank_reference_code": bank_receive_record.bank_reference_code,
"bank_date": bank_receive_record.bank_date, "bank_balance": bank_receive_record.bank_balance, "currency_value": bank_receive_record.currency_value},
select={"id": True, "iban": True, "bank_reference_code": True, "bank_date": True, "bank_balance": True}
)
if not account_record_found:
created_account_record = prisma_service.create(table="account_records", data=bank_receive_record.dict(), select={"id": True, "iban": True, "bank_reference_code": True, "bank_date": True, "bank_balance": True} )
if created_account_record['build_id'] in saved_list_of_account_records.keys():
saved_list_of_account_records[created_account_record['build_id']] = [*saved_list_of_account_records[created_account_record['build_id']], created_account_record]
else:
saved_list_of_account_records[created_account_record['build_id']] = [created_account_record]
return saved_list_of_account_records
def enclose_task_and_send_mail_to_build_manager(prisma_service: PrismaService, saved_list_of_account_records: dict, process_comment_finder_service: ProcessCommentFinderService, task: RedisTaskObject):
"""
Enclose task and send mail to build manager
"""
if not saved_list_of_account_records:
return
today = arrow.now().to('GMT+3').datetime
for build_id, saved_list_of_account_record in saved_list_of_account_records.items():
build_manager_occupant_type = prisma_service.find_first(table="occupant_types", query={"occupant_code":"BU-MNG", "is_confirmed": True, "active": True})
living_space = prisma_service.find_first(
table="build_living_space", query={"build_id": build_id, "occupant_type_id": build_manager_occupant_type['id'], "expiry_starts": {"lte": today}, "expiry_ends": {"gte": today}})
build = prisma_service.find_first(table="builds", query={"id": build_id})
person = prisma_service.find_first(table="people", query={"id": living_space['person_id']})
user = prisma_service.find_first(table="users", query={"person_id": person['id']})
send_object = MailSendModel(
receivers=[user.email], data=saved_list_of_account_record, template_name=ConfigServices.TEMPLATE_ACCOUNT_RECORDS,
subject=f"{build['name']} Cari Durum Bilgilendirme Raporu - {today.strftime('%d/%m/%Y %H:%M')}",
)
set_mail_object = RedisMailSender(task=task, data=send_object, service=ConfigServices.SERVICE_PREFIX_MAIL_SENDER, status=Status.PENDING, completed=False, created_at=today.strftime('%Y-%m-%d %H:%M:%S'))
process_comment_finder_service.service_retriever.redis_client.set(ConfigServices.SERVICE_PREFIX_MAIL_SENDER, dumps(set_mail_object.dict()))
return
if __name__ == "__main__":
prisma_service = PrismaService()
process_comment_finder_service = ProcessCommentFinderService()
print("Process Comment service started")
try:
print("Process Comment service started sleeping for 5 seconds")
while True:
time.sleep(5)
saved_list_of_account_records = dict()
tasks = process_comment_finder_service.fetch_all_tasks()
for task in tasks:
if not check_task_belong_to_this_service(task):
continue
write_account_records_row_from_finder_comment(finder_comments=task.data.FinderComment, prisma_service=prisma_service, saved_list_of_account_records=saved_list_of_account_records)
process_comment_finder_service.update_task_status(task_uuid=task.task, is_completed=True, status=Status.COMPLETED)
process_comment_finder_service.delete_task(task_uuid=task.task)
except Exception as e:
raise
finally:
prisma_service.disconnect()
def fix_account_records_bank_date(prisma_service: PrismaService, bank_receive_record: BankReceive):
account_record_from_other_fields = prisma_service.find_first(
table="account_records",
query={
"iban": bank_receive_record.iban,
"bank_reference_code": bank_receive_record.bank_reference_code,
"bank_balance": bank_receive_record.bank_balance,
"currency_value": bank_receive_record.currency_value,
# "process_comment": {"contains": str(bank_receive_record.process_comment), "mode": "insensitive"},
},
select={
"id": True, "iban": True, "bank_reference_code": True, "bank_date": True,
"bank_balance": True, "currency_value": True, "process_comment": True
}
)
if account_record_from_other_fields:
prisma_service.update(
table="account_records", where={"id": account_record_from_other_fields['id']}, data={"bank_date": bank_receive_record.bank_date},
)
if not account_record_from_other_fields:
pprint.pprint({"not_found_bank_receive_record": bank_receive_record})
# prisma_service.update(
# table="account_records", where={"id": account_record_from_other_fields['id']}, data={"bank_date": bank_receive_record.bank_date},
# )
# from_database = arrow.get(account_record_from_other_fields['bank_date']).to('GMT+3').datetime
# print('old date', from_database, " - new date ", bank_receive_record.bank_date)
def commented_out_code():
account_record_found = None
old_bank_date=arrow.get(finder_comment.bank_date).datetime
if not account_record_found:
account_record_found_with_old_date = prisma_service.find_first(
table="account_records",
query={
"iban": bank_receive_record.iban, "bank_reference_code": bank_receive_record.bank_reference_code,
"bank_date": old_bank_date, "bank_balance": bank_receive_record.bank_balance,
},
)
if account_record_found_with_old_date:
prisma_service.update(
table="account_records", where={"id": account_record_found_with_old_date.id}, data={"bank_date": bank_receive_record.bank_date},
)
if account_record_found:
print('-' * 150)
pprint.pprint(
{
"account_record_found": dict(account_record_found),
"bank_receive_record": bank_receive_record.dict(),
"bank_receive_record.bank_date": bank_receive_record.bank_date,
"account_record_found.bank_date": account_record_found["bank_date"],
}
)
print('-' * 150)
return

View File

@@ -0,0 +1,19 @@
#!/bin/sh
VENV_PATH="/opt/venv"
REQUIREMENTS_PATH="/app/requirements.txt"
SCHEMA_PATH="/app/Depends/schema.prisma"
PRISMA_BINARY_PATH="/root/.cache/prisma-python/binaries"
if [ ! -x "$VENV_PATH/bin/python" ]; then
python -m venv "$VENV_PATH"
"$VENV_PATH/bin/pip" install pip --upgrade
"$VENV_PATH/bin/pip" install --no-cache-dir -r "$REQUIREMENTS_PATH"
"$VENV_PATH/bin/prisma" generate --schema "$SCHEMA_PATH"
fi
if ! find "$PRISMA_BINARY_PATH" -type f -name "prisma-query-engine-debian-openssl-3.0.x" | grep -q .; then
"$VENV_PATH/bin/prisma" py fetch
fi
exec "$VENV_PATH/bin/python" -u app.py

View File

@@ -1,74 +1,108 @@
import uvloop
import asyncio
import sys
import signal
import time
import arrow
from pydantic import BaseModel
from datetime import datetime
from Depends.prisma_client import prisma_client, disconnect_prisma
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
from Depends.prisma_client import PrismaService
from Depends.service_handler import IbanFinderService
from Depends.config import ConfigServices, Status, FinderIban, RedisTaskObject
# sys.stdout.reconfigure(line_buffering=True) # alternatif: python -u veya PYTHONUNBUFFERED=1
class IbanRecord(BaseModel):
id: int
uu_id: str
iban: str
build_id: int
build_uu_id: str
expiry_starts: datetime
expiry_ends: datetime
async def tick():
start = time.time()
print(f"[{datetime.now()}] Attempting database query...")
async with prisma_client() as db:
rows = await db.account_records.find_many(
take=5, skip=0, order=[{"bank_date": "desc"}]
)
print(f"[{datetime.now()}] Query completed in {time.time()-start:.2f}s")
for i, r in enumerate(rows):
# Dilersen burada formatı değiştir
print(f" Row: {i} | id={r.id} bank_date={r.bank_date} currency_value={r.currency_value}")
print("-" * 80)
class DecisionBookRecord(BaseModel):
id: int
uu_id: str
build_id: int
build_uu_id: str
expiry_starts: datetime
expiry_ends: datetime
async def service():
print(f"[{datetime.now()}] IBAN Finder service starting")
try:
iteration = 0
while True:
iteration += 1
print(f"\n[{datetime.now()}] Loop iteration {iteration}")
try:
await tick()
except Exception as e:
print(f"[{datetime.now()}] Error in service tick: {e}")
await asyncio.sleep(1) # bloklamayan bekleme
finally:
# Her durumda DB'yi temiz kapat
await disconnect_prisma()
print(f"[{datetime.now()}] Cleaning up database connection...")
def check_task_belong_to_this_service(task: RedisTaskObject):
if not task.service == ConfigServices.SERVICE_PREFIX_MAIL_PARSER:
return False
if not task.completed:
return False
if not task.data:
return False
return True
async def _graceful_shutdown(sig: signal.Signals):
print(f"\n[{datetime.now()}] Shutting down due to signal: {sig.name}")
# Burada istersen tüm pending task'leri iptal edebilirsin:
# for t in asyncio.all_tasks():
# if t is not asyncio.current_task():
# t.cancel()
await disconnect_prisma()
def extract_build_iban_from_task(task: RedisTaskObject, finder_iban: FinderIban, write_object: dict) -> tuple[bool, dict]:
bank_date = arrow.get(finder_iban.bank_date).datetime
iban_record_db = prisma_service.find_first(
table="build_ibans",
query={
"active": True, "deleted": False, "is_confirmed": True, "iban": finder_iban.iban,
"expiry_starts": {"lte": bank_date}, "expiry_ends": {"gte": bank_date},
},
select={"id": None, "uu_id": None, "iban": None, "build_id": None, "build_uu_id": None, "expiry_starts": None, "expiry_ends": None}
)
if iban_record_db:
iban_record = IbanRecord(**iban_record_db)
write_object["build_id"] = iban_record.build_id
write_object["build_uu_id"] = iban_record.build_uu_id
return True, write_object
return False, write_object
def _install_signal_handlers(loop: asyncio.AbstractEventLoop):
# Linux/Unix: SIGINT (Ctrl+C) ve SIGTERM (docker stop) için kibar kapanış
for s in (signal.SIGINT, signal.SIGTERM):
loop.add_signal_handler(s, lambda s=s: asyncio.create_task(_graceful_shutdown(s)))
def extract_decision_book_from_task(write_object: dict) -> tuple[bool, dict]:
bank_date = arrow.get(write_object["bank_date"]).datetime
decision_book_record_db = prisma_service.find_first(
table="build_decision_book",
query={
"active": True, "deleted": False, "is_confirmed": True, "build_id": write_object["build_id"],
"expiry_starts": {"lte": bank_date}, "expiry_ends": {"gte": bank_date},
},
select={"id": None, "uu_id": None, "build_id": None, "build_uu_id": None, "expiry_starts": None, "expiry_ends": None}
)
if decision_book_record_db:
decision_book_record = DecisionBookRecord(**decision_book_record_db)
write_object["build_decision_book_id"] = decision_book_record.id
write_object["build_decision_book_uu_id"] = decision_book_record.uu_id
return True, write_object
return False, write_object
async def main():
loop = asyncio.get_running_loop()
try:
_install_signal_handlers(loop)
except NotImplementedError:
# (Gerekirse Windows vs., ama sen Linux/Docker kullanıyorsun)
pass
await service()
if __name__ == "__main__":
# uvloop policy zaten yukarıda set edildi; burada normal asyncio.run kullanıyoruz
asyncio.run(main())
prisma_service = PrismaService()
iban_finder_service = IbanFinderService()
print("Find Build Iban service started")
try:
print("Find Build Iban service started sleeping for 5 seconds")
while True:
time.sleep(5)
tasks = iban_finder_service.fetch_all_tasks()
for task in tasks:
if not check_task_belong_to_this_service(task):
continue
if list(task.data.FinderIban):
finder_iban_list = []
for finder_iban in list(task.data.FinderIban):
write_object = finder_iban.dict()
is_build_found, is_decision_book_found = False, False
is_build_found, write_object = extract_build_iban_from_task(task, finder_iban, write_object)
if is_build_found:
is_decision_book_found, write_object = extract_decision_book_from_task(write_object)
if is_build_found or is_decision_book_found:
finder_iban_list.append(write_object)
if finder_iban_list:
iban_finder_service.update_service_data(task.task, ConfigServices.SERVICE_PREFIX_FINDER_COMMENT, finder_iban_list)
iban_finder_service.change_service(task.task, ConfigServices.SERVICE_PREFIX_FINDER_IBAN, Status.COMPLETED, True)
continue
iban_finder_service.change_service(task.task, ConfigServices.SERVICE_PREFIX_FINDER_IBAN, Status.FAILED, True)
except Exception as e:
raise
finally:
prisma_service.disconnect()

View File

@@ -7,6 +7,7 @@ PRISMA_BINARY_PATH="/root/.cache/prisma-python/binaries"
if [ ! -x "$VENV_PATH/bin/python" ]; then
python -m venv "$VENV_PATH"
"$VENV_PATH/bin/pip" install pip --upgrade
"$VENV_PATH/bin/pip" install --no-cache-dir -r "$REQUIREMENTS_PATH"
"$VENV_PATH/bin/prisma" generate --schema "$SCHEMA_PATH"
fi

View File

@@ -152,12 +152,11 @@ if __name__ == "__main__":
# Process each task
for active_task in all_tasks:
if active_task.service == ConfigServices.SERVICE_PREFIX_MAIL_PARSER and active_task.completed:
# logger.info(f"Task {active_task.task} is already processed.")
if active_task.service == ConfigServices.SERVICE_PREFIX_MAIL_READER and active_task.completed:
logger.info(f"Processing task {active_task.task}")
parser.process_task(active_task)
else:
continue
logger.info(f"Processing task {active_task.task}")
parser.process_task(active_task)
else:
logger.info("No tasks found to process")

View File

@@ -68,102 +68,69 @@ def initialize_service():
if __name__ == "__main__":
logger.info("Starting IsBank Email Service")
print(f"Starting Service Mail Reader.")
# Initialize service
runner = initialize_service()
# Configurable parameters
normal_sleep_time = 10 # seconds between normal operations
error_sleep_time = 30 # seconds to wait after an error before retrying
max_consecutive_errors = 5 # maximum number of consecutive errors before longer pause
extended_error_sleep = 120 # seconds to wait after hitting max consecutive errors
normal_sleep_time = 10
error_sleep_time = 30
max_consecutive_errors = 5
extended_error_sleep = 120
consecutive_errors = 0
# Main service loop
while True:
try:
# Main processing
print("Fetching and setting mails...")
runner.fetch_and_set_mails()
# Reset error counter on success
if consecutive_errors > 0:
logger.info(f"Service recovered after {consecutive_errors} consecutive errors")
consecutive_errors = 0
# Normal operation sleep
sleep(normal_sleep_time)
except MailReaderService.REDIS_EXCEPTIONS as e:
# Redis-specific errors
consecutive_errors += 1
logger.error(f"Redis error (attempt {consecutive_errors}): {str(e)}")
# Use centralized reconnection handler from RedisHandler
redis_handler, need_extended_sleep = MailReaderService.handle_reconnection(
consecutive_errors=consecutive_errors, max_consecutive_errors=max_consecutive_errors
)
if redis_handler:
# Update runner's redis handler with the new instance
runner.redis_handler = redis_handler
runner.redis_connected = False # Will trigger reconnection on next cycle
# Sleep based on error count
runner.redis_connected = False
if need_extended_sleep:
sleep(extended_error_sleep)
else:
sleep(error_sleep_time)
except socket.error as e:
# Email connection errors
consecutive_errors += 1
logger.error(f"Email connection error (attempt {consecutive_errors}): {str(e)}")
# Try to re-establish email connection
try:
logger.info("Attempting to re-establish email connection...")
# Create new email service directly
email_service = EmailReaderService(IsBankConfig())
email_service.login_and_connect()
# Create new runner with existing Redis handler and new email service
redis_handler = runner.redis_handler # Preserve existing Redis handler
redis_handler = runner.redis_handler
runner = EmailServiceRunner(redis_handler=redis_handler, email_service=email_service)
logger.info("Successfully re-established email connection")
except Exception as email_retry_error:
logger.error(f"Failed to re-establish email connection: {str(email_retry_error)}")
# Determine sleep time based on consecutive errors
if consecutive_errors >= max_consecutive_errors:
logger.warning(f"Hit {max_consecutive_errors} consecutive email errors, taking longer pause")
sleep(extended_error_sleep)
else:
sleep(error_sleep_time)
except Exception as e:
# Any other unexpected errors
consecutive_errors += 1
logger.error(f"Unexpected error (attempt {consecutive_errors}): {str(e)}")
# For any other error, try to reinitialize everything after some delay
if consecutive_errors >= max_consecutive_errors:
logger.warning(f"Hit {max_consecutive_errors} consecutive errors, reinitializing service")
try:
# Try to clean up existing connections
try:
runner.drop()
except Exception as cleanup_error:
logger.warning(f"Error during cleanup: {str(cleanup_error)}")
# Reinitialize the service directly
redis_handler = MailReaderService()
email_service = EmailReaderService(IsBankConfig())
email_service.login_and_connect()
runner = EmailServiceRunner(redis_handler=redis_handler, email_service=email_service)
if runner:
logger.info("Successfully reinitialized email service runner")
consecutive_errors = 0 # Reset counter after reinitialization
consecutive_errors = 0
else:
logger.error("Failed to reinitialize email service runner")
except Exception as reinit_error:
@@ -171,6 +138,5 @@ if __name__ == "__main__":
sleep(extended_error_sleep)
else:
# For fewer consecutive errors, just retry the current runner
print(f"Error: {str(e)}")
sleep(error_sleep_time)