Files
clickstream-data-platform/dags/homework_check.py
T
ddadminandClaude Fable 5.1 d955f7d694 docs(labs): файлы ТЗ переименованы по схеме S-N-slug
- Зачем:
  - в docs/labs/ будет много заданий; номер ступени и номер задания в имени
    делают папку оглавлением программы и избавляют от поиска по README (#230).
- Что:
  - шесть ТЗ переименованы через git mv: 1-1…1-5 для ступени 1,
    2-1-superset-chart.md для ступени 2; факультатив superset-dashboard.md
    остался без номера.
  - обновлены ссылки в docs/labs/README.md, 2-1-superset-chart.md,
    docs/architecture/dds/session.md и пути в dags/homework_check.py.
  - правило имен для docs/labs/ в AGENTS.md переписано под S-N-slug.
- Проверка:
  - make lint; скрипт обхода относительных ссылок в docs/ и AGENTS.md —
    битых нет.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 09:09:14 -04:00

139 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Ручная самопроверка учебных витрин DM против их источников."""
from __future__ import annotations
import datetime
import logging
from pathlib import Path
from airflow.exceptions import AirflowException
from airflow.providers.clickhousedb.hooks.clickhouse import ClickHouseHook
from airflow.sdk import dag, task
START_DATE = datetime.datetime(2026, 1, 1, tzinfo=datetime.UTC)
SQL_ROOT = Path("/opt/airflow/sql/dq")
CLICKHOUSE_CONNECTION = "clickhouse_default"
DETAIL_LIMIT = 20
def _clickhouse_hook() -> ClickHouseHook:
"""Подключение к ClickHouse для чтения обеих сторон сверки."""
return ClickHouseHook(clickhouse_conn_id=CLICKHOUSE_CONNECTION)
def _mart_exists(hook: ClickHouseHook, mart: str) -> bool:
"""Проверить наличие публичной витрины на ноде подключения."""
database, name = mart.split(".", maxsplit=1)
rows = hook.get_records(
"""
SELECT count()
FROM system.tables
WHERE database = {database:String} AND name = {name:String}
""",
parameters={"database": database, "name": name},
)
return rows[0][0] == 1
@task
def check_mart(
mart: str,
lab_path: str,
sql_name: str,
scope_sql_name: str | None = None,
) -> None:
"""Сверить одну домашнюю витрину с независимым ответом из источника."""
hook = _clickhouse_hook()
if not _mart_exists(hook, mart):
raise AirflowException(f"витрина {mart} не найдена; выполните ТЗ {lab_path}")
try:
query = (SQL_ROOT / sql_name).read_text(encoding="utf-8")
scope_query = (
(SQL_ROOT / scope_sql_name).read_text(encoding="utf-8")
if scope_sql_name is not None
else None
)
except OSError as error:
raise AirflowException(
f"стенд не может прочитать запрос проверки {sql_name}: {error}; "
"сообщите наставнику"
) from error
try:
parameter_sets = [None]
if scope_query is not None:
parameter_sets = [{"day": day} for (day,) in hook.get_records(scope_query)]
summaries = [
summary
for parameters in parameter_sets
for summary in hook.get_records(query, parameters=parameters)
]
except Exception as error:
raise AirflowException(
f"не удалось проверить витрину {mart}: {error}; "
f"сверьте договор колонок и проверку в ТЗ {lab_path}"
) from error
failed_rows = sum(summary[3] for summary in summaries)
if failed_rows == 0:
return
diagnostics = [detail for summary in summaries for detail in summary[4]][
:DETAIL_LIMIT
]
for detail in diagnostics:
logging.error("%s: %s", mart, detail)
days = "; ".join(
f"{data_date}: ключей источника {reference_rows}, "
f"ключей витрины {actual_rows}, расходятся {day_failed_rows}"
for data_date, reference_rows, actual_rows, day_failed_rows, _ in summaries
if day_failed_rows > 0
)
compared_sides = diagnostics[0]
raise AirflowException(
f"витрина {mart} расходится с источником: {days}. "
f"Пример сравнения: {compared_sides}. Сверьтесь с ТЗ {lab_path}"
)
@dag(
dag_id="homework_check",
schedule=None,
start_date=START_DATE,
is_paused_upon_creation=False,
max_active_runs=1,
tags=["dm", "labs"],
)
def homework_check():
"""Дать отдельный результат самопроверки по каждой домашней витрине."""
check_mart.override(task_id="events_enriched")(
"dm.events_enriched_v",
"docs/labs/1-1-events-enriched.md",
"events_enriched_vs_dds.sql",
"events_enriched_scope.sql",
)
check_mart.override(task_id="top_pages_daily")(
"dm.top_pages_daily_v",
"docs/labs/1-2-top-pages-daily.md",
"top_pages_daily_vs_dds.sql",
)
check_mart.override(task_id="session_overview")(
"dm.session_overview_v",
"docs/labs/1-3-session-overview.md",
"session_overview_vs_dds.sql",
)
check_mart.override(task_id="dq_errors_daily")(
"dm.dq_errors_daily_v",
"docs/labs/1-4-dq-errors-daily.md",
"dq_errors_daily_vs_ods.sql",
)
check_mart.override(task_id="utm_effectiveness")(
"dm.utm_effectiveness_v",
"docs/labs/1-5-utm-effectiveness.md",
"utm_effectiveness_vs_dds.sql",
)
homework_check()