Prefect 同步跳过缺失表并推送飞书提醒
This commit is contained in:
@@ -107,7 +107,7 @@ python run_project_bitable_pipeline.py \
|
|||||||
|
|
||||||
`--no-notify` 表示只同步数据,不发群通知。需要通知时,配置通知渠道并去掉该参数。
|
`--no-notify` 表示只同步数据,不发群通知。需要通知时,配置通知渠道并去掉该参数。
|
||||||
|
|
||||||
`--skip-missing-tables` 表示找不到对应表时记录日志并继续处理其他项目和日期,不会自动创建月份表。当前实现也会跳过匹配到多个表、映射指定表不存在等表匹配错误;应查看跳过日志确认哪些项目尚未同步。数据库连接或飞书写入失败等其他错误仍会中断运行。
|
`--skip-missing-tables` 表示找不到对应表时记录日志并继续处理其他项目和日期,不会自动创建月份表。匹配到多个表、映射指定表不存在等配置错误仍会中断运行,避免写入错误的表。
|
||||||
|
|
||||||
### 预览执行
|
### 预览执行
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ python run_project_bitable_pipeline.py \
|
|||||||
| `--start-date YYYY-MM-DD` | 开始日期,默认读取 `SYNC_START_DATE` |
|
| `--start-date YYYY-MM-DD` | 开始日期,默认读取 `SYNC_START_DATE` |
|
||||||
| `--end-date YYYY-MM-DD` | 结束日期,默认运行机器本地日期的昨天 |
|
| `--end-date YYYY-MM-DD` | 结束日期,默认运行机器本地日期的昨天 |
|
||||||
| `--no-notify` | 跳过飞书群通知,默认未开启 |
|
| `--no-notify` | 跳过飞书群通知,默认未开启 |
|
||||||
| `--skip-missing-tables` | 跳过表匹配错误,默认未开启 |
|
| `--skip-missing-tables` | 本地流水线跳过缺失月份表,默认未开启;Prefect 默认开启,并在完成后推送缺失表提醒 |
|
||||||
| `--dry-run` | 预览,不执行飞书写入和通知 |
|
| `--dry-run` | 预览,不执行飞书写入和通知 |
|
||||||
| `--mapping-file PATH` | 指定店铺映射 Excel 文件,覆盖环境变量配置 |
|
| `--mapping-file PATH` | 指定店铺映射 Excel 文件,覆盖环境变量配置 |
|
||||||
| `--table-map-file PATH` | 指定项目月份到飞书表名的 JSON 映射 |
|
| `--table-map-file PATH` | 指定项目月份到飞书表名的 JSON 映射 |
|
||||||
@@ -190,7 +190,7 @@ python prefect_gmv_scheduler.py \
|
|||||||
| `--mode notify` | 只发送通知 |
|
| `--mode notify` | 只发送通知 |
|
||||||
| `--mode all` | 先同步再通知,默认模式 |
|
| `--mode all` | 先同步再通知,默认模式 |
|
||||||
|
|
||||||
Prefect 同步未显式指定日期时,默认处理截至昨天的最近 5 天,并以 `SYNC_START_DATE` 为最早边界。通知未指定日期时,优先读取上次成功同步保存的日期范围。
|
Prefect 同步未显式指定日期时,默认处理截至昨天的最近 5 天,并以 `SYNC_START_DATE` 为最早边界。缺失月份表会自动跳过,其余表仍会写入;同步结束后,缺失的“项目 + 月份”会汇总为一条飞书提醒。通知未指定日期时,优先读取上次成功同步保存的日期范围。
|
||||||
|
|
||||||
`prefect.yaml` 定义了北京时间每天 08:00 同步、09:00 通知两条调度。部署前需准备可访问的 Prefect Server,并配置 `PREFECT_API_URL`。
|
`prefect.yaml` 定义了北京时间每天 08:00 同步、09:00 通知两条调度。部署前需准备可访问的 Prefect Server,并配置 `PREFECT_API_URL`。
|
||||||
|
|
||||||
@@ -199,7 +199,7 @@ docker compose up -d --build
|
|||||||
docker exec -it prefect-gmv-worker sh -lc 'cd /app && prefect deploy --all'
|
docker exec -it prefect-gmv-worker sh -lc 'cd /app && prefect deploy --all'
|
||||||
```
|
```
|
||||||
|
|
||||||
Compose 启动的是等待调度任务的 worker。定时任务需要跳过缺失表时,将 `prefect.yaml` 对应 deployment 的 `parameters.skip_missing_tables` 改为 `true`,再重新执行 `prefect deploy --all` 注册配置。
|
Compose 启动的是等待调度任务的 worker。当前 Prefect deployment 默认跳过缺失月份表,并在同步结束后推送一条汇总提醒。修改 `prefect.yaml` 后,执行 `prefect deploy --all` 注册新配置。
|
||||||
|
|
||||||
更多部署说明见项目中的 `DOCKER_PREFECT_USAGE.md`;其中示例的本机目录需要替换为实际项目目录。
|
更多部署说明见项目中的 `DOCKER_PREFECT_USAGE.md`;其中示例的本机目录需要替换为实际项目目录。
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -14,7 +14,7 @@ deployments:
|
|||||||
start_date: null
|
start_date: null
|
||||||
end_date: null
|
end_date: null
|
||||||
dry_run: false
|
dry_run: false
|
||||||
skip_missing_tables: false
|
skip_missing_tables: true
|
||||||
table_map_file: ""
|
table_map_file: ""
|
||||||
tags:
|
tags:
|
||||||
- gmv
|
- gmv
|
||||||
@@ -36,7 +36,7 @@ deployments:
|
|||||||
start_date: null
|
start_date: null
|
||||||
end_date: null
|
end_date: null
|
||||||
dry_run: false
|
dry_run: false
|
||||||
skip_missing_tables: false
|
skip_missing_tables: true
|
||||||
table_map_file: ""
|
table_map_file: ""
|
||||||
tags:
|
tags:
|
||||||
- gmv
|
- gmv
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ from project_bitable_common import (
|
|||||||
load_dotenv,
|
load_dotenv,
|
||||||
log_step,
|
log_step,
|
||||||
run_daily_sync_for_date,
|
run_daily_sync_for_date,
|
||||||
|
send_missing_table_notification,
|
||||||
send_sync_notification,
|
send_sync_notification,
|
||||||
validate_date,
|
validate_date,
|
||||||
)
|
)
|
||||||
@@ -72,7 +73,7 @@ def load_sync_config(
|
|||||||
start_date: str | None = None,
|
start_date: str | None = None,
|
||||||
end_date: str | None = None,
|
end_date: str | None = None,
|
||||||
dry_run: bool = False,
|
dry_run: bool = False,
|
||||||
skip_missing_tables: bool = False,
|
skip_missing_tables: bool = True,
|
||||||
table_map_file: str = "",
|
table_map_file: str = "",
|
||||||
) -> SyncConfig:
|
) -> SyncConfig:
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
@@ -174,7 +175,7 @@ def gmv_daily_data_sync_flow(
|
|||||||
start_date: str | None = None,
|
start_date: str | None = None,
|
||||||
end_date: str | None = None,
|
end_date: str | None = None,
|
||||||
dry_run: bool = False,
|
dry_run: bool = False,
|
||||||
skip_missing_tables: bool = False,
|
skip_missing_tables: bool = True,
|
||||||
table_map_file: str = "",
|
table_map_file: str = "",
|
||||||
) -> None:
|
) -> None:
|
||||||
config = load_sync_config(
|
config = load_sync_config(
|
||||||
@@ -205,6 +206,14 @@ def gmv_daily_data_sync_flow(
|
|||||||
run_daily_sync_for_date(context, stat_date)
|
run_daily_sync_for_date(context, stat_date)
|
||||||
if not config.dry_run:
|
if not config.dry_run:
|
||||||
mark_sync_window_completed(sync_window)
|
mark_sync_window_completed(sync_window)
|
||||||
|
if context.missing_tables:
|
||||||
|
log_step(f"Prefect 数据同步发现缺失表: {len(context.missing_tables)} 个")
|
||||||
|
send_missing_table_notification(
|
||||||
|
context,
|
||||||
|
context.missing_tables,
|
||||||
|
sync_window.start_date,
|
||||||
|
sync_window.end_date,
|
||||||
|
)
|
||||||
log_step("Prefect 数据同步完成")
|
log_step("Prefect 数据同步完成")
|
||||||
|
|
||||||
|
|
||||||
@@ -213,7 +222,7 @@ def gmv_daily_notify_flow(
|
|||||||
start_date: str | None = None,
|
start_date: str | None = None,
|
||||||
end_date: str | None = None,
|
end_date: str | None = None,
|
||||||
dry_run: bool = False,
|
dry_run: bool = False,
|
||||||
skip_missing_tables: bool = False,
|
skip_missing_tables: bool = True,
|
||||||
table_map_file: str = "",
|
table_map_file: str = "",
|
||||||
) -> None:
|
) -> None:
|
||||||
config = load_sync_config(
|
config = load_sync_config(
|
||||||
@@ -235,7 +244,12 @@ def parse_args() -> argparse.Namespace:
|
|||||||
parser.add_argument("--end-date", default="", help="结束日期,格式 YYYY-MM-DD,默认昨天")
|
parser.add_argument("--end-date", default="", help="结束日期,格式 YYYY-MM-DD,默认昨天")
|
||||||
parser.add_argument("--table-map-file", default="", help="可选,项目到表名的映射 JSON 文件路径")
|
parser.add_argument("--table-map-file", default="", help="可选,项目到表名的映射 JSON 文件路径")
|
||||||
parser.add_argument("--dry-run", action="store_true", help="只输出将要更新的内容,不调用飞书写接口")
|
parser.add_argument("--dry-run", action="store_true", help="只输出将要更新的内容,不调用飞书写接口")
|
||||||
parser.add_argument("--skip-missing-tables", action="store_true", help="找不到对应月份的飞书表时跳过")
|
parser.add_argument(
|
||||||
|
"--skip-missing-tables",
|
||||||
|
action=argparse.BooleanOptionalAction,
|
||||||
|
default=True,
|
||||||
|
help="找不到对应月份的飞书表时跳过并在同步结束后推送提醒(默认开启)",
|
||||||
|
)
|
||||||
parser.add_argument("--serve", action="store_true", help="启动 Prefect deployments,按 8 点同步、9 点通知 定时执行")
|
parser.add_argument("--serve", action="store_true", help="启动 Prefect deployments,按 8 点同步、9 点通知 定时执行")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--mode",
|
"--mode",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
from datetime import date, datetime, timedelta, timezone
|
from datetime import date, datetime, timedelta, timezone
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -380,6 +380,17 @@ class MetricSyncArtifacts:
|
|||||||
created_fields: set[str]
|
created_fields: set[str]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, order=True)
|
||||||
|
class MissingTable:
|
||||||
|
project: str
|
||||||
|
year: int
|
||||||
|
month: int
|
||||||
|
|
||||||
|
|
||||||
|
class TargetTableNotFoundError(ValueError):
|
||||||
|
"""Raised when a project's monthly Feishu table does not exist."""
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class DailySyncConfig:
|
class DailySyncConfig:
|
||||||
base_url: str
|
base_url: str
|
||||||
@@ -400,6 +411,7 @@ class DailySyncContext:
|
|||||||
mapping: dict[str, str]
|
mapping: dict[str, str]
|
||||||
table_overrides: dict[str, str]
|
table_overrides: dict[str, str]
|
||||||
config: DailySyncConfig
|
config: DailySyncConfig
|
||||||
|
missing_tables: set[MissingTable] = field(default_factory=set)
|
||||||
|
|
||||||
|
|
||||||
def _log_to_prefect(level_name: str, message: str) -> None:
|
def _log_to_prefect(level_name: str, message: str) -> None:
|
||||||
@@ -938,6 +950,7 @@ def send_feishu_sync_card(
|
|||||||
base_url: str,
|
base_url: str,
|
||||||
start_date: date,
|
start_date: date,
|
||||||
end_date: date,
|
end_date: date,
|
||||||
|
extra_content: str = "",
|
||||||
) -> None:
|
) -> None:
|
||||||
if not webhook_url:
|
if not webhook_url:
|
||||||
log_step("未配置飞书群机器人 webhook,跳过群消息推送")
|
log_step("未配置飞书群机器人 webhook,跳过群消息推送")
|
||||||
@@ -958,6 +971,7 @@ def send_feishu_sync_card(
|
|||||||
"content": (
|
"content": (
|
||||||
f"**同步范围**\n{start_date.isoformat()} 至 {end_date.isoformat()}\n\n"
|
f"**同步范围**\n{start_date.isoformat()} 至 {end_date.isoformat()}\n\n"
|
||||||
f"**多维表格**\n[点击打开多维表格]({base_url})"
|
f"**多维表格**\n[点击打开多维表格]({base_url})"
|
||||||
|
+ (f"\n\n{extra_content}" if extra_content else "")
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1005,6 +1019,7 @@ def send_feishu_app_card(
|
|||||||
end_date: date,
|
end_date: date,
|
||||||
image_path: str = "",
|
image_path: str = "",
|
||||||
token: str | None = None,
|
token: str | None = None,
|
||||||
|
extra_content: str = "",
|
||||||
) -> None:
|
) -> None:
|
||||||
if not chat_id:
|
if not chat_id:
|
||||||
log_step("未配置 FEISHU_CHAT_ID,跳过应用机器人卡片推送")
|
log_step("未配置 FEISHU_CHAT_ID,跳过应用机器人卡片推送")
|
||||||
@@ -1019,6 +1034,7 @@ def send_feishu_app_card(
|
|||||||
"content": (
|
"content": (
|
||||||
f"**同步范围**\n{start_date.isoformat()} 至 {end_date.isoformat()}\n\n"
|
f"**同步范围**\n{start_date.isoformat()} 至 {end_date.isoformat()}\n\n"
|
||||||
f"**多维表格**\n[{title}]({base_url})"
|
f"**多维表格**\n[{title}]({base_url})"
|
||||||
|
+ (f"\n\n{extra_content}" if extra_content else "")
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1118,7 +1134,7 @@ def find_target_table(
|
|||||||
return candidates[0]
|
return candidates[0]
|
||||||
if not candidates:
|
if not candidates:
|
||||||
all_table_names = [table.name for table in tables]
|
all_table_names = [table.name for table in tables]
|
||||||
raise ValueError(
|
raise TargetTableNotFoundError(
|
||||||
f"未找到匹配表: 项目={project}, 月份={year:04d}-{month:02d}, 项目相关表={project_related_tables or '[]'}, 全部表={all_table_names}"
|
f"未找到匹配表: 项目={project}, 月份={year:04d}-{month:02d}, 项目相关表={project_related_tables or '[]'}, 全部表={all_table_names}"
|
||||||
)
|
)
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@@ -1715,8 +1731,15 @@ def run_daily_sync_for_date(context: DailySyncContext, stat_date: date) -> None:
|
|||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
table = find_target_table(context.tables, project, stat_date.year, stat_date.month, context.table_overrides)
|
table = find_target_table(context.tables, project, stat_date.year, stat_date.month, context.table_overrides)
|
||||||
except ValueError as exc:
|
except TargetTableNotFoundError as exc:
|
||||||
if context.config.skip_missing_tables:
|
if context.config.skip_missing_tables:
|
||||||
|
missing_table = MissingTable(project=project, year=stat_date.year, month=stat_date.month)
|
||||||
|
if missing_table not in context.missing_tables:
|
||||||
|
context.missing_tables.add(missing_table)
|
||||||
|
log_step(
|
||||||
|
"记录缺失表: "
|
||||||
|
f"项目={project}, 月份={stat_date.year:04d}-{stat_date.month:02d}"
|
||||||
|
)
|
||||||
log_step(f"跳过: {exc}")
|
log_step(f"跳过: {exc}")
|
||||||
continue
|
continue
|
||||||
raise
|
raise
|
||||||
@@ -1815,3 +1838,43 @@ def send_sync_notification(
|
|||||||
start_date=start_date,
|
start_date=start_date,
|
||||||
end_date=end_date,
|
end_date=end_date,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def send_missing_table_notification(
|
||||||
|
context: DailySyncContext,
|
||||||
|
missing_tables: set[MissingTable],
|
||||||
|
start_date: date,
|
||||||
|
end_date: date,
|
||||||
|
) -> None:
|
||||||
|
"""Notify Feishu once after a sync run about tables skipped because they do not exist."""
|
||||||
|
if context.config.dry_run or not missing_tables:
|
||||||
|
return
|
||||||
|
|
||||||
|
table_lines = "\n".join(
|
||||||
|
f"- {item.project}:{item.year:04d}-{item.month:02d}" for item in sorted(missing_tables)
|
||||||
|
)
|
||||||
|
extra_content = (
|
||||||
|
"**以下月份表不存在,已跳过写入**\n"
|
||||||
|
f"{table_lines}\n\n"
|
||||||
|
"请创建对应月份表后重新执行补数。"
|
||||||
|
)
|
||||||
|
title = "GMV 数据同步:缺失表提醒"
|
||||||
|
if context.config.chat_id:
|
||||||
|
send_feishu_app_card(
|
||||||
|
chat_id=context.config.chat_id,
|
||||||
|
title=title,
|
||||||
|
base_url=context.config.base_url,
|
||||||
|
start_date=start_date,
|
||||||
|
end_date=end_date,
|
||||||
|
token=context.client.token,
|
||||||
|
extra_content=extra_content,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
send_feishu_sync_card(
|
||||||
|
webhook_url=context.config.group_webhook,
|
||||||
|
title=title,
|
||||||
|
base_url=context.config.base_url,
|
||||||
|
start_date=start_date,
|
||||||
|
end_date=end_date,
|
||||||
|
extra_content=extra_content,
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user