Compare commits

..

No commits in common. "FixScheduler" and "main" have entirely different histories.

9 changed files with 71 additions and 114 deletions

View File

@ -12,3 +12,5 @@ COPY ./app /code/app
RUN python3 -m pip install --upgrade pip RUN python3 -m pip install --upgrade pip
RUN echo Y | python3 -m pip install --no-cache-dir --upgrade -r /code/requirements.txt RUN echo Y | python3 -m pip install --no-cache-dir --upgrade -r /code/requirements.txt
CMD ["python", "main.py"]

2
Jenkinsfile vendored
View File

@ -4,7 +4,7 @@ pipeline {
stages { stages {
stage('recreate > build > run container') { stage('recreate > build > run container') {
steps { steps {
sh "docker-compose up -d --force-recreate --build parse_saby && timeout 5m docker-compose logs -f || true" sh "docker-compose up -d --force-recreate --build parse_saby"
} }
} }
} }

View File

@ -1,28 +0,0 @@
import datetime
LOG_FILE = "errors.log"
def current_datatime():
return datetime.datetime.now().strftime("[%d-%m-%Y %H:%M:%S]")
def write_to_log(error_message:str, log_file=LOG_FILE):
"""
Записывает ошибку в лог-файл с временной меткой
"""
print(f"Ошибка записана в {log_file} {error_message}")
with open(log_file, 'w', encoding='utf8') as f:
f.write(f"[{current_datatime()}] {error_message}\n")
def read_to_log(log_file=LOG_FILE):
try:
with open(log_file, 'r', encoding='utf8') as f:
for line in f.read():
print(line, end='')
except FileNotFoundError: print(f"{LOG_FILE} не существует")
def clear_to_log(log_file=LOG_FILE):
"""
Очищает лог-файл и записывает новую шапку
"""
with open(log_file, 'w+', encoding='utf-8') as f:
f.write(f'=== Лог ошибок создан в {current_datatime()} ===\n\n')

View File

@ -1,10 +1,10 @@
import error_to_log
import working_database import working_database
import parse_saby import parse_saby
from validation import DataValid from validation import DataValid
import scheduler import scheduler
from datetime import datetime import write_error_to_log
import sys
print("Запуск main.py")
def parse_data_in_list(dict_data: dict) -> list: def parse_data_in_list(dict_data: dict) -> list:
""" """
@ -21,10 +21,8 @@ def parse_data_in_list(dict_data: dict) -> list:
result.append([key_id, *value_data]) result.append([key_id, *value_data])
return result return result
def main():
start_time = datetime.now()
print("Очиcтка лог файла") print("Очиcтка лог файла")
error_to_log.clear_to_log() write_error_to_log.clear_to_log()
print("1/5. Запуск парсинга сайта") print("1/5. Запуск парсинга сайта")
result_dict_data = parse_saby.process_reports_parse() result_dict_data = parse_saby.process_reports_parse()
@ -32,23 +30,7 @@ def main():
list_data = parse_data_in_list(result_dict_data) list_data = parse_data_in_list(result_dict_data)
print("3/5. Запуск валидации") print("3/5. Запуск валидации")
list_data_validated = DataValid.validate_data(list_data) list_data_validated = DataValid.validate_data(list_data)
print("4/5. Отправка данных в БД") print("4/5 Отправка данных в БД")
working_database.WorkingDB().data_transfer_in_database(list_data_validated) working_database.SimpleDB().data_transfer_in_database(list_data_validated)
print("5/5. Вывод записей из лог файла") print("5/5. Запуск планировщика")
diff = datetime.now() - start_time
print(str(diff).split('.')[0])
if __name__ == "__main__":
print("Запуск main.py")
if sys.argv[1] == "--scheduler-run":
print("=== ЗАПУСК ПЛАНИРОВЩИКОМ ===")
main()
elif sys.argv[1] == "--first-run":
print("=== ПЕРВЫЙ ЗАПУСК ===")
main()
print("=== АКТИВАЦИЯ ПЛАНИРОВЩИКА ===")
scheduler.launch_the_scheduler() scheduler.launch_the_scheduler()
else:
print("=== ОБЫЧНЫЙ ЗАПУСК ===")
main()

View File

@ -5,7 +5,7 @@
import requests import requests
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import re import re
import error_to_log import write_error_to_log
def parse_html(url: str): def parse_html(url: str):
""" """
@ -86,9 +86,7 @@ def parse_reports(soup:BeautifulSoup, # HTML объект
# Добавление всех данных в итоговый словарь # Добавление всех данных в итоговый словарь
result_dict_data.update({id: (name_title, span.text, from_date, to_date, version)}) result_dict_data.update({id: (name_title, span.text, from_date, to_date, version)})
except Exception as e: except Exception as e:
error_message = f"Ошибка при обработке отчета {report_title}: {e}" print(f"Ошибка при обработке отчета {report_title}: ", e)
print(error_message)
error_to_log.write_to_log(error_message)
continue continue
return result_dict_data return result_dict_data
@ -96,7 +94,7 @@ def print_report_data(dict_name:dict, name_title:str):
""" """
Вывод на стандартный поток вывода итоговых данных Вывод на стандартный поток вывода итоговых данных
""" """
# Функция для красивого офорлмения файла #Блок для красивого офорлмения файла
def center_text(): def center_text():
""" """
Функия парсит сколько нужно подчеркивания, Функия парсит сколько нужно подчеркивания,
@ -115,6 +113,7 @@ def print_report_data(dict_name:dict, name_title:str):
list_result.append('_' * left + text + '_' * right) list_result.append('_' * left + text + '_' * right)
return list_result[:2] return list_result[:2]
dash_start, dash_end = center_text() dash_start, dash_end = center_text()
#Конец блока
#Вывод с красивым офрмление в виде нижнего подчеркивания #Вывод с красивым офрмление в виде нижнего подчеркивания
print(f'\n{dash_start}\n') print(f'\n{dash_start}\n')
@ -161,10 +160,8 @@ def process_reports_parse(url_formats = 'https://formats.saby.ru'):
url_formats - используется для создание полных URL url_formats - используется для создание полных URL
""" """
# Лист имеет вид: ['/report/fns', '/report/sfr'...] # Лист имеет вид: ['/report/fns', '/report/sfr'...]
print(f'{' '*2}Получаем url список всех форматов отчетности в госорганы')
set_title = search_title() set_title = search_title()
dict_result = {} dict_result = {}
print(f"{' '*2}Обработка полученых фрматов")
for report_title in set_title: for report_title in set_title:
try: try:
# Получаем имя тайтла через парсинг # Получаем имя тайтла через парсинг
@ -176,9 +173,7 @@ def process_reports_parse(url_formats = 'https://formats.saby.ru'):
# Объект HTML, одного(конкретного) title # Объект HTML, одного(конкретного) title
soup = parse_html(url_title) soup = parse_html(url_title)
if isinstance(soup, int): if isinstance(soup, int):
error_message = f"Не удалось установить соедение c {url_title}" print(f'Не удалось установить соедение c {url_title}')
print({' '*4}, error_message)
error_to_log.write_to_log(error_message)
continue continue
# Словарь с нужными данными # Словарь с нужными данными
dict_result_title = parse_reports(soup, report_title, url_formats, name_title) dict_result_title = parse_reports(soup, report_title, url_formats, name_title)
@ -188,13 +183,11 @@ def process_reports_parse(url_formats = 'https://formats.saby.ru'):
except Exception as e: except Exception as e:
print(f"Ошибка при обработке отчета {report_title}: {str(e)}") print(f"Ошибка при обработке отчета {report_title}: {str(e)}")
error_message = f"ERROR-PARSE_SABY: {e} DATA: {report_title}" error_message = f"ERROR-PARSE_SABY: {e} DATA: {report_title}"
error_to_log.write_to_log(error_message)
continue continue
return dict_result return dict_result
if __name__ == '__main__': if __name__ == '__main__':
# Можно запустить отдельно от всего проекта # Можно запустить отдельно от всего проекта
print("Запущен parse_saby.py")
process_reports_parse() process_reports_parse()

View File

@ -43,7 +43,7 @@ class DataValid(BaseModel):
""" """
Функция принимает список списков, выполняет парсинг и валидацию Функция принимает список списков, выполняет парсинг и валидацию
с пмошью класса DataValid из файла validation.py. с пмошью класса DataValid из файла validation.py.
Возвращает распаршеный валидный список словарей для БД. Возвращает распршеный валидный список словарей для БД.
""" """
result = [] result = []
for intem_list in list_data: for intem_list in list_data:

View File

@ -2,39 +2,34 @@ from os import environ
import hvac import hvac
import hvac.exceptions import hvac.exceptions
from oracledb import Error, create_pool, init_oracle_client from oracledb import Error, create_pool, init_oracle_client
import error_to_log import write_error_to_log
init_oracle_client() init_oracle_client()
class WorkingDB: class SimpleDB:
def __init__(self): def __init__(self):
print('-'*60)
print("Инициализация WorkingDB")
self._create_db_pool_from_vault() self._create_db_pool_from_vault()
self.pool self.pool
print('-'*60)
def _handle_vault_exception(self, e: Exception, message: str): def _handle_vault_exception(self, e: Exception, message: str):
""" """
Обработка исключений Vault с возвратом понятного сообщения. Обработка исключений Vault с возвратом понятного сообщения.
""" """
error_to_log.write_to_log(message) print(message)
ind = {' '*4}
if isinstance(e, hvac.exceptions.InvalidPath): if isinstance(e, hvac.exceptions.InvalidPath):
raise hvac.exceptions.InvalidPath(f"{ind}Database configuration not found in Vault") raise hvac.exceptions.InvalidPath("Database configuration not found in Vault")
elif isinstance(e, hvac.exceptions.Forbidden): elif isinstance(e, hvac.exceptions.Forbidden):
raise hvac.exceptions.Forbidden(f"{ind}Permission denied to access Vault secrets") raise hvac.exceptions.Forbidden("Permission denied to access Vault secrets")
elif isinstance(e, hvac.exceptions.Unauthorized): elif isinstance(e, hvac.exceptions.Unauthorized):
raise hvac.exceptions.Unauthorized(f"{ind}Invalid Vault token") raise hvac.exceptions.Unauthorized("Invalid Vault token")
elif isinstance(e, hvac.exceptions.VaultError): elif isinstance(e, hvac.exceptions.VaultError):
raise hvac.exceptions.VaultError(f"{ind}Vault secret retrieval failed: {e}") raise hvac.exceptions.VaultError(f"Vault secret retrieval failed: {e}")
elif isinstance(e, hvac.exceptions.InvalidRequest): elif isinstance(e, hvac.exceptions.InvalidRequest):
raise hvac.exceptions.InvalidRequest(f"{ind}Missing database parameter in Vault response: {e}") raise hvac.exceptions.InvalidRequest(f"Missing database parameter in Vault response: {e}")
elif isinstance(e, hvac.exceptions.VaultDown): elif isinstance(e, hvac.exceptions.VaultDown):
raise hvac.exceptions.VaultDown(f"{ind}Database server not available: {e}") raise hvac.exceptions.VaultDown(f"Database server not available: {e}")
else: else:
raise Exception (f'{ind}Unexpected error reading from Vault: {e}') raise Exception (f'Unexpected error reading from Vault: {e}')
def _create_db_pool_from_vault(self): def _create_db_pool_from_vault(self):
""" """
@ -42,19 +37,16 @@ class WorkingDB:
""" """
try: try:
# Подключение к Vault # Подключение к Vault
print(f"{' '*2}Подключение к Vault")
client = hvac.Client( client = hvac.Client(
url='https://vlt.dataekb.ru:8222', url='https://vlt.dataekb.ru:8222',
token=environ.get('VAULT_TOKEN'), token=environ.get('VAULT_TOKEN'),
) )
except Exception as e: except Exception as e:
self._handle_vault_exception(e, f"{' '*4}Ошибка при создание покдлючения c Vault") self._handle_vault_exception(e, "Ошибка при создание покдлючения c Vault")
# Проверка авторизации в Vault # Проверка авторизации в Vault
print(f"{' '*4}Проверка авторизации")
if not client.is_authenticated(): if not client.is_authenticated():
raise Exception(f"{' '*6}Не удалось автозоваться в Vault") raise Exception("Vault authentication failed")
else: print(f"{' '*6}Успешно")
try: try:
# Чтение секретов из Vault # Чтение секретов из Vault
@ -63,7 +55,7 @@ class WorkingDB:
mount_point='kv' mount_point='kv'
) )
except Exception as e: except Exception as e:
self._handle_vault_exception(e, f"{' '*4}Ошибка чтение скретов из Vault") self._handle_vault_exception(e, "Ошибка чтение скретов из Vault")
try: try:
# Создаем пул соединений # Создаем пул соединений
@ -76,7 +68,7 @@ class WorkingDB:
increment=1 increment=1
) )
except Exception as e: except Exception as e:
self._handle_vault_exception(e, f"{' '*2}Ошибка при создание пула для подключение к Oracle") self._handle_vault_exception(e, "Ошибка при создание пула для подключение к Oracle")
def data_transfer_in_database(self, list_data: list): def data_transfer_in_database(self, list_data: list):
""" """
@ -94,18 +86,14 @@ class WorkingDB:
""" """
# Данные для БД не могут быть пустыми # Данные для БД не могут быть пустыми
if not list_data: if not list_data:
raise ValueError(list_data, "Данные для БД не могут быть пустыми") raise ValueError(list_data, "No data to process")
try: try:
print(f"{' '*2}Отправка данных в бд")
with self.pool.acquire() as connection: with self.pool.acquire() as connection:
with connection.cursor() as cursor: with connection.cursor() as cursor:
for dict_argument_bd in list_data: for dict_argument_bd in list_data:
# Просмотр отпавлямых арогументов print("Отправляемые аргрументы: ", dict_argument_bd)
for key, value in dict_argument_bd.items(): print("Типы данных: ", *map(type, dict_argument_bd.values()))
print('-'*40)
print(f"{' '*4}Отправляемые аргументы - {key}: {value} -> {type(value)}")
try: try:
cursor.callproc('P_RK_GOVERNMENT_REPORTS_INSERS', [ cursor.callproc('P_RK_GOVERNMENT_REPORTS_INSERS', [
dict_argument_bd['id'], dict_argument_bd['id'],
@ -116,16 +104,16 @@ class WorkingDB:
dict_argument_bd['ver'], dict_argument_bd['ver'],
]) ])
except Error as e: except Error as e:
print(e) # нужно убрать, проверка работат ли.
# Проверка является ли запись дублирующей # Проверка является ли запись дублирующей
if 'ORA-00001' in str(e): continue if 'ORA-00001' in str(e): continue
# В остальных случаях запись ошибки и пропуск данных. # В остальных случаях запись ошибки и пропуск данных.
else: else:
error_message = f"ERROR_DB-WRITE: {e} DATA: {dict_argument_bd}" error_message = f"ERROR_DB-WRITE: {e} DATA: {dict_argument_bd}"
# Запись логов # Запись логов
error_to_log.write_to_log(error_message) write_error_to_log.write_to_log(error_message)
continue continue
except Exception as e: except Exception as e:
error_message = f"ERROR_DB-GLOBAL: {e}" error_message = f"ERROR_DB-GLOBAL: {e}"
error_to_log.write_to_log(error_message) write_error_to_log.write_to_log(error_message)
raise Error(f'Неожиданная ошибка: {e}') raise Error(f'Неожиданная ошибка: {e}')

View File

@ -0,0 +1,19 @@
import datetime
TIMESTAMP = datetime.datetime.now().strftime("%d-%m-%Y %H:%M:%S")
LOG_FILE = "error_log.txt"
def write_to_log(error_message, log_file=LOG_FILE):
"""
Записывает ошибку в лог-файл с временной меткой
"""
print("Сообщение ошибки перед записю в файл: ", error_message) # Отладочная информация
with open(log_file, 'a', encoding='utf-8') as f:
f.write(f"[{TIMESTAMP}] {error_message}\n")
def clear_to_log(log_file=LOG_FILE):
"""
Очищает лог-файл и записывает новую шапку
"""
with open(log_file, 'w', encoding='utf-8') as f:
f.write(f'=== Лог ошибок создан в {TIMESTAMP} ===\n\n')

View File

@ -2,8 +2,9 @@ version: "3.8"
services: services:
parse_saby: parse_saby:
# image: git.dataekb.ru/sadikov/parse_saby/parse_saby_main:latest
container_name: parse_saby
build: . build: .
command: python ./main.py --first-run
volumes: volumes:
- "/etc/timezone:/etc/timezone:ro" - "/etc/timezone:/etc/timezone:ro"
- "/etc/localtime:/etc/localtime:ro" - "/etc/localtime:/etc/localtime:ro"