refactor(manual): упрощение архитектуры учебного пайплайна и документации
- Зачем: - сделать примеры более доступными для начинающих, убрав лишнюю архитектурную сложность (хелперы, внешние SQL файлы). - сфокусировать обучение на самом Airflow, а не на структуре Python-проекта. - Что: - консолидирована логика: DDL и вспомогательные функции перенесены из helpers/ и sql/ напрямую в csv_to_postgres.py и csv_to_postgres_dq.py. - удалена избыточная инфраструктура: папки sql/, tests/, helpers/ и файл requirements.txt больше не содержат специфичных для этого пайплайна файлов. - упрощена работа с БД: использование PostgresHook напрямую в задачах вместо кастомных оберток. - исправлен порт PostgreSQL для метаданных Airflow (5433 → 5434) в README.md и educational-setup-plan.md. - изменен путь генерации CSV на /opt/airflow/data/output в csv_to_postgres.py. - добавлен шаблон data/*.csv в .gitignore. - обновлен комментарий в sql_basic_dag.py о создании соединения. - исправлено описание практического задания в educational-tasks.md. - добавлены переводы строк в конце файлов csv_to_postgres.py и educational-setup-plan.md. - Проверка: - запуск DAG-ов csv_to_postgres и csv_to_postgres_dq в Airflow UI.
This commit is contained in:
@@ -4,94 +4,135 @@ import logging
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from airflow.operators.python import PythonOperator
|
||||
from helpers.postgres import (
|
||||
assert_orders_have_rows,
|
||||
assert_orders_no_duplicates,
|
||||
assert_orders_schema,
|
||||
assert_orders_table_exists,
|
||||
get_postgres_conn,
|
||||
)
|
||||
from airflow.providers.postgres.hooks.postgres import PostgresHook
|
||||
|
||||
from airflow import DAG
|
||||
|
||||
POSTGRES_CONN_ID = "postgres_training"
|
||||
|
||||
def _run_check(check_callable):
|
||||
"""
|
||||
Оборачивает проверку качества данных в контекст подключения к Postgres.
|
||||
EXPECTED_ORDERS_SCHEMA = [
|
||||
("order_id", "bigint"),
|
||||
("order_ts", "timestamp without time zone"),
|
||||
("customer_id", "bigint"),
|
||||
("amount", "numeric"),
|
||||
]
|
||||
|
||||
Этот DAG предназначен для автоматической проверки качества данных
|
||||
после CSV-пайплайна в таблице public.orders:
|
||||
1. Проверяет существование таблицы
|
||||
2. Проверяет соответствие схемы
|
||||
3. Проверяет наличие данных
|
||||
4. Проверяет отсутствие дубликатов
|
||||
|
||||
Args:
|
||||
check_callable: Функция проверки, принимающая подключение к БД
|
||||
"""
|
||||
# Получаем имя функции для логов
|
||||
check_name = check_callable.__name__.replace("assert_", "")
|
||||
logging.info("🚀 Запуск проверки: %s", check_name)
|
||||
def _get_conn():
|
||||
return PostgresHook(postgres_conn_id=POSTGRES_CONN_ID).get_conn()
|
||||
|
||||
with get_postgres_conn() as conn:
|
||||
check_callable(conn)
|
||||
|
||||
logging.info("✅ Проверка пройдена: %s", check_name)
|
||||
def _check_table_exists():
|
||||
"""Проверяет наличие таблицы public.orders."""
|
||||
conn = _get_conn()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("""
|
||||
SELECT 1 FROM pg_catalog.pg_tables
|
||||
WHERE schemaname = 'public' AND tablename = 'orders'
|
||||
""")
|
||||
if cur.fetchone() is None:
|
||||
raise ValueError("Таблица public.orders не найдена")
|
||||
logging.info("Таблица public.orders существует")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _check_schema():
|
||||
"""Проверяет соответствие схемы таблицы public.orders ожидаемой."""
|
||||
conn = _get_conn()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("""
|
||||
SELECT column_name, data_type
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public' AND table_name = 'orders'
|
||||
ORDER BY ordinal_position
|
||||
""")
|
||||
actual = cur.fetchall()
|
||||
if actual != EXPECTED_ORDERS_SCHEMA:
|
||||
raise ValueError(
|
||||
f"Схема не совпадает. Ожидалось: {EXPECTED_ORDERS_SCHEMA}, "
|
||||
f"получено: {actual}"
|
||||
)
|
||||
logging.info("Схема таблицы public.orders соответствует ожидаемой")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _check_has_rows():
|
||||
"""Проверяет, что в таблице public.orders есть данные."""
|
||||
conn = _get_conn()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT COUNT(*) FROM public.orders")
|
||||
count = cur.fetchone()[0]
|
||||
if count == 0:
|
||||
raise ValueError("Таблица public.orders пуста")
|
||||
logging.info("Таблица public.orders содержит %s строк", count)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _check_no_duplicates():
|
||||
"""Проверяет отсутствие дубликатов order_id в public.orders."""
|
||||
conn = _get_conn()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("""
|
||||
SELECT order_id, COUNT(*) AS cnt
|
||||
FROM public.orders
|
||||
GROUP BY order_id
|
||||
HAVING COUNT(*) > 1
|
||||
""")
|
||||
duplicates = cur.fetchall()
|
||||
if duplicates:
|
||||
raise ValueError(f"Обнаружены дубликаты order_id: {duplicates}")
|
||||
logging.info("Дубликатов order_id не обнаружено")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _log_dq_summary():
|
||||
"""
|
||||
Логирует итоговую сводку по качеству данных.
|
||||
Эта задача выполняется после всех проверок и показывает общий результат.
|
||||
"""
|
||||
logging.info("🎉 Все проверки качества данных пройдены успешно!")
|
||||
logging.info("📊 Качество данных в таблице orders соответствует требованиям.")
|
||||
"""Логирует итоговую сводку по качеству данных."""
|
||||
logging.info("Все проверки качества данных пройдены успешно!")
|
||||
logging.info("Качество данных в таблице orders соответствует требованиям.")
|
||||
|
||||
|
||||
default_args = {"owner": "airflow", "retries": 1, "retry_delay": timedelta(seconds=30)}
|
||||
|
||||
with DAG(
|
||||
dag_id="csv_to_postgres_dq",
|
||||
start_date=datetime(2017, 1, 1),
|
||||
start_date=datetime(2023, 1, 1),
|
||||
schedule=None,
|
||||
catchup=False,
|
||||
default_args=default_args,
|
||||
tags=["demo", "postgres", "quality", "csv", "dq"],
|
||||
description="Проверки качества данных после CSV → public.orders в Postgres",
|
||||
description="Проверки качества данных после CSV -> public.orders в Postgres",
|
||||
) as dag:
|
||||
# Задача 1: Проверка существования таблицы
|
||||
check_exists = PythonOperator(
|
||||
task_id="check_orders_table_exists",
|
||||
python_callable=_run_check,
|
||||
op_args=[assert_orders_table_exists],
|
||||
python_callable=_check_table_exists,
|
||||
)
|
||||
|
||||
# Задача 2: Проверка соответствия схемы таблицы
|
||||
check_schema = PythonOperator(
|
||||
task_id="check_orders_schema",
|
||||
python_callable=_run_check,
|
||||
op_args=[assert_orders_schema],
|
||||
python_callable=_check_schema,
|
||||
)
|
||||
|
||||
# Задача 3: Проверка наличия данных
|
||||
check_has_rows = PythonOperator(
|
||||
task_id="check_orders_has_rows",
|
||||
python_callable=_run_check,
|
||||
op_args=[assert_orders_have_rows],
|
||||
python_callable=_check_has_rows,
|
||||
)
|
||||
|
||||
# Задача 4: Проверка отсутствия дубликатов
|
||||
check_no_duplicates = PythonOperator(
|
||||
task_id="check_order_duplicates",
|
||||
python_callable=_run_check,
|
||||
op_args=[assert_orders_no_duplicates],
|
||||
python_callable=_check_no_duplicates,
|
||||
)
|
||||
|
||||
# Задача 5: Итоговая сводка
|
||||
dq_summary = PythonOperator(
|
||||
task_id="data_quality_summary",
|
||||
python_callable=_log_dq_summary,
|
||||
)
|
||||
|
||||
# Определяем последовательность выполнения задач
|
||||
check_exists >> check_schema >> check_has_rows >> check_no_duplicates >> dq_summary
|
||||
check_exists >> check_schema >> check_has_rows >> check_no_duplicates >> dq_summary
|
||||
|
||||
Reference in New Issue
Block a user