Compare commits
8 Commits
main
...
FixSchedul
| Author | SHA1 | Date |
|---|---|---|
|
|
1bc066eace | |
|
|
eb496a94fb | |
|
|
f55b303192 | |
|
|
ebee8687a8 | |
|
|
e620523022 | |
|
|
893dbea466 | |
|
|
61d9c3a256 | |
|
|
bb04667a5c |
|
|
@ -12,5 +12,3 @@ 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"]
|
|
||||||
|
|
@ -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"
|
sh "docker-compose up -d --force-recreate --build parse_saby && timeout 5m docker-compose logs -f || true"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
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')
|
||||||
48
app/main.py
48
app/main.py
|
|
@ -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
|
||||||
import write_error_to_log
|
from datetime import datetime
|
||||||
|
import sys
|
||||||
print("Запуск main.py")
|
|
||||||
|
|
||||||
def parse_data_in_list(dict_data: dict) -> list:
|
def parse_data_in_list(dict_data: dict) -> list:
|
||||||
"""
|
"""
|
||||||
|
|
@ -21,16 +21,34 @@ 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
|
||||||
|
|
||||||
print("Очиcтка лог файла")
|
def main():
|
||||||
write_error_to_log.clear_to_log()
|
start_time = datetime.now()
|
||||||
|
print("Очиcтка лог файла")
|
||||||
|
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()
|
||||||
print('2/5. Предварительный парсинг')
|
print('2/5. Предварительный парсинг')
|
||||||
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.SimpleDB().data_transfer_in_database(list_data_validated)
|
working_database.WorkingDB().data_transfer_in_database(list_data_validated)
|
||||||
print("5/5. Запуск планировщика")
|
print("5/5. Вывод записей из лог файла")
|
||||||
scheduler.launch_the_scheduler()
|
|
||||||
|
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()
|
||||||
|
else:
|
||||||
|
print("=== ОБЫЧНЫЙ ЗАПУСК ===")
|
||||||
|
main()
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
import requests
|
import requests
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
import re
|
import re
|
||||||
import write_error_to_log
|
import error_to_log
|
||||||
|
|
||||||
def parse_html(url: str):
|
def parse_html(url: str):
|
||||||
"""
|
"""
|
||||||
|
|
@ -86,7 +86,9 @@ 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:
|
||||||
print(f"Ошибка при обработке отчета {report_title}: ", e)
|
error_message = 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
|
||||||
|
|
||||||
|
|
@ -94,7 +96,7 @@ def print_report_data(dict_name:dict, name_title:str):
|
||||||
"""
|
"""
|
||||||
Вывод на стандартный поток вывода итоговых данных
|
Вывод на стандартный поток вывода итоговых данных
|
||||||
"""
|
"""
|
||||||
#Блок для красивого офорлмения файла
|
# Функция для красивого офорлмения файла
|
||||||
def center_text():
|
def center_text():
|
||||||
"""
|
"""
|
||||||
Функия парсит сколько нужно подчеркивания,
|
Функия парсит сколько нужно подчеркивания,
|
||||||
|
|
@ -113,9 +115,8 @@ 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')
|
||||||
for key, value in dict_name.items():
|
for key, value in dict_name.items():
|
||||||
str_k_v = f'{key}: {value}\n'
|
str_k_v = f'{key}: {value}\n'
|
||||||
|
|
@ -160,8 +161,10 @@ 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:
|
||||||
# Получаем имя тайтла через парсинг
|
# Получаем имя тайтла через парсинг
|
||||||
|
|
@ -173,7 +176,9 @@ 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):
|
||||||
print(f'Не удалось установить соедение c {url_title}')
|
error_message = 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)
|
||||||
|
|
@ -183,11 +188,13 @@ 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()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -2,34 +2,39 @@ 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 write_error_to_log
|
import error_to_log
|
||||||
|
|
||||||
init_oracle_client()
|
init_oracle_client()
|
||||||
|
|
||||||
class SimpleDB:
|
class WorkingDB:
|
||||||
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 с возвратом понятного сообщения.
|
||||||
"""
|
"""
|
||||||
print(message)
|
error_to_log.write_to_log(message)
|
||||||
|
ind = {' '*4}
|
||||||
|
|
||||||
if isinstance(e, hvac.exceptions.InvalidPath):
|
if isinstance(e, hvac.exceptions.InvalidPath):
|
||||||
raise hvac.exceptions.InvalidPath("Database configuration not found in Vault")
|
raise hvac.exceptions.InvalidPath(f"{ind}Database configuration not found in Vault")
|
||||||
elif isinstance(e, hvac.exceptions.Forbidden):
|
elif isinstance(e, hvac.exceptions.Forbidden):
|
||||||
raise hvac.exceptions.Forbidden("Permission denied to access Vault secrets")
|
raise hvac.exceptions.Forbidden(f"{ind}Permission denied to access Vault secrets")
|
||||||
elif isinstance(e, hvac.exceptions.Unauthorized):
|
elif isinstance(e, hvac.exceptions.Unauthorized):
|
||||||
raise hvac.exceptions.Unauthorized("Invalid Vault token")
|
raise hvac.exceptions.Unauthorized(f"{ind}Invalid Vault token")
|
||||||
elif isinstance(e, hvac.exceptions.VaultError):
|
elif isinstance(e, hvac.exceptions.VaultError):
|
||||||
raise hvac.exceptions.VaultError(f"Vault secret retrieval failed: {e}")
|
raise hvac.exceptions.VaultError(f"{ind}Vault secret retrieval failed: {e}")
|
||||||
elif isinstance(e, hvac.exceptions.InvalidRequest):
|
elif isinstance(e, hvac.exceptions.InvalidRequest):
|
||||||
raise hvac.exceptions.InvalidRequest(f"Missing database parameter in Vault response: {e}")
|
raise hvac.exceptions.InvalidRequest(f"{ind}Missing database parameter in Vault response: {e}")
|
||||||
elif isinstance(e, hvac.exceptions.VaultDown):
|
elif isinstance(e, hvac.exceptions.VaultDown):
|
||||||
raise hvac.exceptions.VaultDown(f"Database server not available: {e}")
|
raise hvac.exceptions.VaultDown(f"{ind}Database server not available: {e}")
|
||||||
else:
|
else:
|
||||||
raise Exception (f'Unexpected error reading from Vault: {e}')
|
raise Exception (f'{ind}Unexpected error reading from Vault: {e}')
|
||||||
|
|
||||||
def _create_db_pool_from_vault(self):
|
def _create_db_pool_from_vault(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -37,16 +42,19 @@ class SimpleDB:
|
||||||
"""
|
"""
|
||||||
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, "Ошибка при создание покдлючения c Vault")
|
self._handle_vault_exception(e, f"{' '*4}Ошибка при создание покдлючения c Vault")
|
||||||
|
|
||||||
# Проверка авторизации в Vault
|
# Проверка авторизации в Vault
|
||||||
|
print(f"{' '*4}Проверка авторизации")
|
||||||
if not client.is_authenticated():
|
if not client.is_authenticated():
|
||||||
raise Exception("Vault authentication failed")
|
raise Exception(f"{' '*6}Не удалось автозоваться в Vault")
|
||||||
|
else: print(f"{' '*6}Успешно")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Чтение секретов из Vault
|
# Чтение секретов из Vault
|
||||||
|
|
@ -55,7 +63,7 @@ class SimpleDB:
|
||||||
mount_point='kv'
|
mount_point='kv'
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._handle_vault_exception(e, "Ошибка чтение скретов из Vault")
|
self._handle_vault_exception(e, f"{' '*4}Ошибка чтение скретов из Vault")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Создаем пул соединений
|
# Создаем пул соединений
|
||||||
|
|
@ -68,7 +76,7 @@ class SimpleDB:
|
||||||
increment=1
|
increment=1
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._handle_vault_exception(e, "Ошибка при создание пула для подключение к Oracle")
|
self._handle_vault_exception(e, f"{' '*2}Ошибка при создание пула для подключение к Oracle")
|
||||||
|
|
||||||
def data_transfer_in_database(self, list_data: list):
|
def data_transfer_in_database(self, list_data: list):
|
||||||
"""
|
"""
|
||||||
|
|
@ -86,14 +94,18 @@ class SimpleDB:
|
||||||
"""
|
"""
|
||||||
# Данные для БД не могут быть пустыми
|
# Данные для БД не могут быть пустыми
|
||||||
if not list_data:
|
if not list_data:
|
||||||
raise ValueError(list_data, "No data to process")
|
raise ValueError(list_data, "Данные для БД не могут быть пустыми")
|
||||||
|
|
||||||
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)
|
# Просмотр отпавлямых арогументов
|
||||||
print("Типы данных: ", *map(type, dict_argument_bd.values()))
|
for key, value in dict_argument_bd.items():
|
||||||
|
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'],
|
||||||
|
|
@ -104,16 +116,16 @@ class SimpleDB:
|
||||||
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}"
|
||||||
# Запись логов
|
# Запись логов
|
||||||
write_error_to_log.write_to_log(error_message)
|
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}"
|
||||||
write_error_to_log.write_to_log(error_message)
|
error_to_log.write_to_log(error_message)
|
||||||
raise Error(f'Неожиданная ошибка: {e}')
|
raise Error(f'Неожиданная ошибка: {e}')
|
||||||
|
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
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')
|
|
||||||
|
|
@ -2,9 +2,8 @@ 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"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue