Результат прогона линтера
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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("✅ Дубликаты не обнаружены")
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user