Результат прогона линтера

This commit is contained in:
2025-10-18 23:56:37 +03:00
parent b2f40e4dd2
commit f1e01274de
5 changed files with 91 additions and 72 deletions
+23 -16
View File
@@ -8,10 +8,11 @@ from pathlib import Path
from typing import List from typing import List
import pandas as pd import pandas as pd
from airflow import DAG
from airflow.operators.python import PythonOperator from airflow.operators.python import PythonOperator
from helpers.greenplum import get_gp_conn from helpers.greenplum import get_gp_conn
from airflow import DAG
CSV_DIR = Path(os.getenv("CSV_DIR", "/opt/airflow/data")) CSV_DIR = Path(os.getenv("CSV_DIR", "/opt/airflow/data"))
CSV_ROWS = int(os.getenv("CSV_ROWS", "1000")) CSV_ROWS = int(os.getenv("CSV_ROWS", "1000"))
@@ -43,29 +44,27 @@ def _generate_csv(rows: int, csv_dir: Path) -> str:
base_order_id = int(datetime.utcnow().timestamp() * 1_000) base_order_id = int(datetime.utcnow().timestamp() * 1_000)
# Создаём DataFrame с использованием pandas методов # Создаём DataFrame с использованием pandas методов
df = pd.DataFrame({ df = pd.DataFrame(
{
# Уникальные order_id начиная с базового значения # Уникальные order_id начиная с базового значения
"order_id": pd.Series(range(base_order_id, base_order_id + rows), dtype="int64"), "order_id": pd.Series(
range(base_order_id, base_order_id + rows), dtype="int64"
),
# Временные метки с интервалом в 1 секунду в обратном порядке # Временные метки с интервалом в 1 секунду в обратном порядке
"order_ts": pd.date_range( "order_ts": pd.date_range(
end=datetime.utcnow(), end=datetime.utcnow(), periods=rows, freq="1S"
periods=rows,
freq="1S"
).sort_values(ascending=False), ).sort_values(ascending=False),
# Случайные customer_id от 1 до 1000 # Случайные customer_id от 1 до 1000
"customer_id": pd.Series( "customer_id": pd.Series(
random.choices(range(1, 1001), k=rows), random.choices(range(1, 1001), k=rows), dtype="int64"
dtype="int64"
), ),
# Случайные суммы от 10 до 500 с округлением до 2 знаков # Случайные суммы от 10 до 500 с округлением до 2 знаков
"amount": pd.Series( "amount": pd.Series(
[round(random.uniform(10, 500), 2) for _ in range(rows)], [round(random.uniform(10, 500), 2) for _ in range(rows)],
dtype="float64" dtype="float64",
),
}
) )
})
# Сохраняем CSV без индекса # Сохраняем CSV без индекса
df.to_csv(csv_path, index=False) df.to_csv(csv_path, index=False)
@@ -77,7 +76,9 @@ def _preview_csv(csv_path: str, sample_rows: int = 5) -> None:
"""Отображает предпросмотр CSV через pandas (head и describe).""" """Отображает предпросмотр CSV через pandas (head и describe)."""
df = pd.read_csv(csv_path) df = pd.read_csv(csv_path)
df["order_ts"] = pd.to_datetime(df["order_ts"], errors="coerce") df["order_ts"] = pd.to_datetime(df["order_ts"], errors="coerce")
logging.info("Первые %s строк:\n%s", sample_rows, df.head(sample_rows).to_string(index=False)) logging.info(
"Первые %s строк:\n%s", sample_rows, df.head(sample_rows).to_string(index=False)
)
numeric_summary = df.describe(include="number") numeric_summary = df.describe(include="number")
logging.info("Числовая статистика:\n%s", numeric_summary.to_string()) logging.info("Числовая статистика:\n%s", numeric_summary.to_string())
if df["order_ts"].notna().any(): if df["order_ts"].notna().any():
@@ -94,8 +95,14 @@ def _load_csv(csv_path: str) -> None:
if not csv_file.exists(): if not csv_file.exists():
raise FileNotFoundError(f"CSV не найден: {csv_file}") raise FileNotFoundError(f"CSV не найден: {csv_file}")
with get_gp_conn() as conn, conn.cursor() as cur, csv_file.open("r", encoding="utf-8") as f: with (
cur.execute("CREATE TEMP TABLE tmp_orders (LIKE public.orders INCLUDING DEFAULTS) ON COMMIT DROP;") get_gp_conn() as conn,
conn.cursor() as cur,
csv_file.open("r", encoding="utf-8") as f,
):
cur.execute(
"CREATE TEMP TABLE tmp_orders (LIKE public.orders INCLUDING DEFAULTS) ON COMMIT DROP;"
)
cur.copy_expert( cur.copy_expert(
"COPY tmp_orders (order_id, order_ts, customer_id, amount) FROM STDIN WITH CSV HEADER", "COPY tmp_orders (order_id, order_ts, customer_id, amount) FROM STDIN WITH CSV HEADER",
f, f,
+4 -7
View File
@@ -3,16 +3,13 @@ from __future__ import annotations
import logging import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from airflow import DAG
from airflow.operators.python import PythonOperator from airflow.operators.python import PythonOperator
from helpers.greenplum import (assert_orders_have_rows,
from helpers.greenplum import (
assert_orders_have_rows,
assert_orders_no_duplicates, assert_orders_no_duplicates,
assert_orders_schema, assert_orders_schema,
assert_orders_table_exists, assert_orders_table_exists, get_gp_conn)
get_gp_conn,
) from airflow import DAG
def _run_check(check_callable): def _run_check(check_callable):
+20 -6
View File
@@ -9,7 +9,11 @@ import psycopg2
# Настройки для подключения к Greenplum. По умолчанию используем Airflow Connection, # Настройки для подключения к Greenplum. По умолчанию используем Airflow Connection,
# но при проблемах можно переключиться на ENV-подключение, установив GP_USE_AIRFLOW_CONN=false. # но при проблемах можно переключиться на ENV-подключение, установив GP_USE_AIRFLOW_CONN=false.
GP_CONN_ID = os.getenv("GP_CONN_ID", "greenplum_conn") GP_CONN_ID = os.getenv("GP_CONN_ID", "greenplum_conn")
GP_USE_AIRFLOW_CONN = os.getenv("GP_USE_AIRFLOW_CONN", "true").lower() in ("1", "true", "yes") GP_USE_AIRFLOW_CONN = os.getenv("GP_USE_AIRFLOW_CONN", "true").lower() in (
"1",
"true",
"yes",
)
# Ожидаемая схема таблицы orders для проверки качества данных # Ожидаемая схема таблицы orders для проверки качества данных
EXPECTED_ORDERS_SCHEMA: List[Tuple[str, str]] = [ EXPECTED_ORDERS_SCHEMA: List[Tuple[str, str]] = [
@@ -52,7 +56,9 @@ def get_gp_conn():
"host": os.getenv("GP_HOST", "greenplum"), "host": os.getenv("GP_HOST", "greenplum"),
"port": int(os.getenv("GP_PORT", "5432")), "port": int(os.getenv("GP_PORT", "5432")),
} }
logging.info("🔗 Подключение к Greenplum: %s:%s", conn_params["host"], conn_params["port"]) logging.info(
"🔗 Подключение к Greenplum: %s:%s", conn_params["host"], conn_params["port"]
)
return psycopg2.connect(**conn_params) return psycopg2.connect(**conn_params)
@@ -76,7 +82,9 @@ def assert_orders_table_exists(conn) -> None:
""" """
) )
if cur.fetchone() is None: if cur.fetchone() is None:
raise ValueError("❌ Таблица public.orders не найдена; запусти DAG csv_to_greenplum.") raise ValueError(
"❌ Таблица public.orders не найдена; запусти DAG csv_to_greenplum."
)
logging.info("✅ Таблица public.orders существует") logging.info("✅ Таблица public.orders существует")
@@ -118,7 +126,9 @@ def assert_orders_schema(conn) -> None:
logging.info("📊 Ожидаемая схема: %s", EXPECTED_ORDERS_SCHEMA) logging.info("📊 Ожидаемая схема: %s", EXPECTED_ORDERS_SCHEMA)
if list(schema) != EXPECTED_ORDERS_SCHEMA: if list(schema) != EXPECTED_ORDERS_SCHEMA:
raise ValueError(f"❌ Неожиданная схема orders: {schema}. Ожидали {EXPECTED_ORDERS_SCHEMA}.") raise ValueError(
f"❌ Неожиданная схема orders: {schema}. Ожидали {EXPECTED_ORDERS_SCHEMA}."
)
logging.info("✅ Схема таблицы orders соответствует ожиданиям") logging.info("✅ Схема таблицы orders соответствует ожиданиям")
@@ -152,7 +162,9 @@ def assert_orders_have_rows(conn) -> None:
logging.info("📈 Количество строк в orders: %s", row_count) logging.info("📈 Количество строк в orders: %s", row_count)
if row_count <= 0: if row_count <= 0:
raise ValueError("❌ Таблица public.orders пустая — запусти DAG csv_to_greenplum перед проверкой.") raise ValueError(
"❌ Таблица public.orders пустая — запусти DAG csv_to_greenplum перед проверкой."
)
logging.info("✅ Таблица orders содержит данные (%s строк)", row_count) logging.info("✅ Таблица orders содержит данные (%s строк)", row_count)
@@ -195,5 +207,7 @@ def assert_orders_no_duplicates(conn) -> None:
logging.info("📊 Найдено дубликатов: %s", duplicates) logging.info("📊 Найдено дубликатов: %s", duplicates)
if duplicates: if duplicates:
raise ValueError(f"❌ Обнаружены дубли по order_id ({duplicates} шт.) — проверь загрузку данных.") raise ValueError(
f"❌ Обнаружены дубли по order_id ({duplicates} шт.) — проверь загрузку данных."
)
logging.info("✅ Дубликаты не обнаружены") logging.info("✅ Дубликаты не обнаружены")
-1
View File
@@ -6,7 +6,6 @@ from pathlib import Path
from types import ModuleType from types import ModuleType
from typing import Type from typing import Type
PROJECT_ROOT = Path(__file__).resolve().parents[1] PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path: if str(PROJECT_ROOT) not in sys.path:
sys.path.append(str(PROJECT_ROOT)) sys.path.append(str(PROJECT_ROOT))
+3 -1
View File
@@ -14,7 +14,9 @@ def _airflow_available() -> bool:
return hasattr(af, "DAG") return hasattr(af, "DAG")
pytestmark = pytest.mark.skipif(not _airflow_available(), reason="Airflow is not installed for DAG smoke tests") pytestmark = pytest.mark.skipif(
not _airflow_available(), reason="Airflow is not installed for DAG smoke tests"
)
def _load_dag(module_name: str): def _load_dag(module_name: str):