2026-04-29 14:06:41 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import argparse
|
2026-04-29 16:28:23 +08:00
|
|
|
import logging
|
2026-04-29 14:06:41 +08:00
|
|
|
import os
|
|
|
|
|
import sys
|
|
|
|
|
from pathlib import Path
|
2026-04-29 16:28:23 +08:00
|
|
|
import time
|
2026-04-29 14:06:41 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def bootstrap_prefect_env() -> None:
|
|
|
|
|
env_file = Path(__file__).resolve().parent / ".env"
|
|
|
|
|
if not env_file.exists():
|
|
|
|
|
return
|
|
|
|
|
for raw_line in env_file.read_text(encoding="utf-8").splitlines():
|
|
|
|
|
line = raw_line.strip()
|
|
|
|
|
if not line or line.startswith("#") or "=" not in line:
|
|
|
|
|
continue
|
|
|
|
|
key, value = line.split("=", 1)
|
|
|
|
|
key = key.strip()
|
|
|
|
|
if key.startswith("PREFECT_"):
|
|
|
|
|
os.environ.setdefault(key, value.strip())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bootstrap_prefect_env()
|
|
|
|
|
|
2026-04-29 16:28:23 +08:00
|
|
|
from prefect import flow, get_run_logger, task
|
2026-04-29 14:06:41 +08:00
|
|
|
|
2026-04-29 16:28:23 +08:00
|
|
|
from inventory_analytics.logging_utils import exception_fields
|
2026-04-29 14:06:41 +08:00
|
|
|
from inventory_analytics.channel_inventory_sync.entrypoint import main as channel_inventory_sync_main
|
|
|
|
|
from inventory_analytics.batch_inventory_report.cli import main as batch_inventory_report_main
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@task(name="run-channel-inventory-sync")
|
|
|
|
|
def run_channel_inventory_sync_task() -> None:
|
2026-04-29 16:28:23 +08:00
|
|
|
logger = get_run_logger()
|
|
|
|
|
started_at = time.monotonic()
|
|
|
|
|
logger.info("渠道库存统计开始,action=all,安全日志模式=enabled")
|
2026-04-29 14:06:41 +08:00
|
|
|
original_argv = sys.argv[:]
|
2026-04-29 16:28:23 +08:00
|
|
|
original_safe_logs = os.environ.get("INVENTORY_SAFE_LOGS")
|
|
|
|
|
root_logger = logging.getLogger()
|
|
|
|
|
original_handlers = root_logger.handlers[:]
|
|
|
|
|
original_level = root_logger.level
|
2026-04-29 14:06:41 +08:00
|
|
|
try:
|
2026-04-29 16:28:23 +08:00
|
|
|
os.environ["INVENTORY_SAFE_LOGS"] = "1"
|
2026-04-29 14:06:41 +08:00
|
|
|
sys.argv = ["sync_channel_inventory.py", "--action", "all"]
|
|
|
|
|
exit_code = channel_inventory_sync_main()
|
2026-04-29 16:28:23 +08:00
|
|
|
except Exception as exc: # noqa: BLE001
|
|
|
|
|
logger.error("渠道库存统计异常,%s", exception_fields(exc))
|
|
|
|
|
raise
|
2026-04-29 14:06:41 +08:00
|
|
|
finally:
|
|
|
|
|
sys.argv = original_argv
|
2026-04-29 16:28:23 +08:00
|
|
|
if original_safe_logs is None:
|
|
|
|
|
os.environ.pop("INVENTORY_SAFE_LOGS", None)
|
|
|
|
|
else:
|
|
|
|
|
os.environ["INVENTORY_SAFE_LOGS"] = original_safe_logs
|
|
|
|
|
root_logger.handlers = original_handlers
|
|
|
|
|
root_logger.setLevel(original_level)
|
2026-04-29 14:06:41 +08:00
|
|
|
if exit_code != 0:
|
2026-04-29 16:28:23 +08:00
|
|
|
logger.error("渠道库存统计失败,退出码=%s", exit_code)
|
2026-04-29 14:06:41 +08:00
|
|
|
raise RuntimeError(f"渠道库存统计执行失败,退出码: {exit_code}")
|
2026-04-29 16:28:23 +08:00
|
|
|
logger.info("渠道库存统计完成,耗时毫秒=%s", int((time.monotonic() - started_at) * 1000))
|
2026-04-29 14:06:41 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@task(name="run-batch-inventory-report")
|
2026-04-29 16:28:23 +08:00
|
|
|
def run_batch_inventory_report_task(
|
|
|
|
|
output: str = "",
|
|
|
|
|
feishu_table: str = "批次库存",
|
|
|
|
|
skip_feishu_sync: bool = False,
|
|
|
|
|
skip_report_output: bool = True,
|
|
|
|
|
) -> None:
|
|
|
|
|
logger = get_run_logger()
|
|
|
|
|
started_at = time.monotonic()
|
|
|
|
|
logger.info(
|
|
|
|
|
"批次库存统计开始,飞书同步=%s,本地报表输出=%s,安全日志模式=enabled",
|
|
|
|
|
"disabled" if skip_feishu_sync else "enabled",
|
|
|
|
|
"skipped" if skip_report_output else "enabled",
|
|
|
|
|
)
|
2026-04-29 14:06:41 +08:00
|
|
|
original_argv = sys.argv[:]
|
2026-04-29 16:28:23 +08:00
|
|
|
original_safe_logs = os.environ.get("INVENTORY_SAFE_LOGS")
|
|
|
|
|
root_logger = logging.getLogger()
|
|
|
|
|
original_handlers = root_logger.handlers[:]
|
|
|
|
|
original_level = root_logger.level
|
2026-04-29 14:06:41 +08:00
|
|
|
try:
|
2026-04-29 16:28:23 +08:00
|
|
|
os.environ["INVENTORY_SAFE_LOGS"] = "1"
|
2026-04-29 14:06:41 +08:00
|
|
|
sys.argv = ["generate_batch_inventory_report.py"]
|
|
|
|
|
if output:
|
|
|
|
|
sys.argv.extend(["--output", output])
|
|
|
|
|
if feishu_table:
|
|
|
|
|
sys.argv.extend(["--feishu-table", feishu_table])
|
|
|
|
|
if skip_feishu_sync:
|
|
|
|
|
sys.argv.append("--skip-feishu-sync")
|
2026-04-29 16:28:23 +08:00
|
|
|
if skip_report_output:
|
|
|
|
|
sys.argv.append("--skip-report-output")
|
|
|
|
|
sys.argv.append("--safe-logs")
|
2026-04-29 14:06:41 +08:00
|
|
|
exit_code = batch_inventory_report_main()
|
2026-04-29 16:28:23 +08:00
|
|
|
except Exception as exc: # noqa: BLE001
|
|
|
|
|
logger.error("批次库存统计异常,%s", exception_fields(exc))
|
|
|
|
|
raise
|
2026-04-29 14:06:41 +08:00
|
|
|
finally:
|
|
|
|
|
sys.argv = original_argv
|
2026-04-29 16:28:23 +08:00
|
|
|
if original_safe_logs is None:
|
|
|
|
|
os.environ.pop("INVENTORY_SAFE_LOGS", None)
|
|
|
|
|
else:
|
|
|
|
|
os.environ["INVENTORY_SAFE_LOGS"] = original_safe_logs
|
|
|
|
|
root_logger.handlers = original_handlers
|
|
|
|
|
root_logger.setLevel(original_level)
|
2026-04-29 14:06:41 +08:00
|
|
|
if exit_code != 0:
|
2026-04-29 16:28:23 +08:00
|
|
|
logger.error("批次库存统计失败,退出码=%s", exit_code)
|
2026-04-29 14:06:41 +08:00
|
|
|
raise RuntimeError(f"批次库存统计报表执行失败,退出码: {exit_code}")
|
2026-04-29 16:28:23 +08:00
|
|
|
logger.info("批次库存统计完成,耗时毫秒=%s", int((time.monotonic() - started_at) * 1000))
|
2026-04-29 14:06:41 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@flow(name="inventory-channel-inventory-sync")
|
|
|
|
|
def inventory_channel_inventory_sync_flow() -> None:
|
|
|
|
|
run_channel_inventory_sync_task()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@flow(name="inventory-batch-inventory-report")
|
|
|
|
|
def inventory_batch_inventory_report_flow(
|
|
|
|
|
output: str = "",
|
|
|
|
|
feishu_table: str = "批次库存",
|
|
|
|
|
skip_feishu_sync: bool = False,
|
2026-04-29 16:28:23 +08:00
|
|
|
skip_report_output: bool = True,
|
2026-04-29 14:06:41 +08:00
|
|
|
) -> None:
|
2026-04-29 16:28:23 +08:00
|
|
|
run_batch_inventory_report_task(
|
|
|
|
|
output=output,
|
|
|
|
|
feishu_table=feishu_table,
|
|
|
|
|
skip_feishu_sync=skip_feishu_sync,
|
|
|
|
|
skip_report_output=skip_report_output,
|
|
|
|
|
)
|
2026-04-29 14:06:41 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
2026-05-11 16:56:27 +08:00
|
|
|
parser = argparse.ArgumentParser(description="Prefect 调度:每天 09:15 和 19:15 执行渠道库存统计和批次库存统计写飞书。")
|
2026-04-29 14:06:41 +08:00
|
|
|
parser.add_argument("--serve-channel-inventory-sync", action="store_true", help="启动渠道库存统计 flow 的本地 serve 调度")
|
|
|
|
|
parser.add_argument("--serve-batch-inventory-report", action="store_true", help="启动批次库存统计 flow 的本地 serve 调度")
|
|
|
|
|
parser.add_argument("--run-channel-inventory-sync", action="store_true", help="立即执行一次渠道库存统计 flow")
|
|
|
|
|
parser.add_argument("--run-batch-inventory-report", action="store_true", help="立即执行一次批次库存统计 flow")
|
|
|
|
|
parser.add_argument("--output", default="", help="批次库存统计输出路径,可选")
|
|
|
|
|
parser.add_argument("--feishu-table", default="批次库存", help="批次库存统计同步目标飞书表名")
|
|
|
|
|
parser.add_argument("--skip-feishu-sync", action="store_true", help="批次库存统计只生成本地文件,不写飞书")
|
2026-04-29 16:28:23 +08:00
|
|
|
parser.add_argument("--write-report-output", action="store_true", help="Prefect 执行批次库存统计时同时生成本地报表文件")
|
2026-04-29 14:06:41 +08:00
|
|
|
return parser.parse_args()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> None:
|
|
|
|
|
args = parse_args()
|
|
|
|
|
flow_kwargs = {
|
|
|
|
|
"output": args.output,
|
|
|
|
|
"feishu_table": args.feishu_table,
|
|
|
|
|
"skip_feishu_sync": args.skip_feishu_sync,
|
2026-04-29 16:28:23 +08:00
|
|
|
"skip_report_output": not args.write_report_output,
|
2026-04-29 14:06:41 +08:00
|
|
|
}
|
|
|
|
|
if args.serve_channel_inventory_sync:
|
|
|
|
|
inventory_channel_inventory_sync_flow.serve(
|
2026-05-11 16:56:27 +08:00
|
|
|
name="channel-inventory-sync-0915-1915",
|
|
|
|
|
cron=("15 9 * * *", "15 19 * * *"),
|
2026-04-29 14:06:41 +08:00
|
|
|
tags=["inventory", "feishu", "channel-inventory-sync"],
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
if args.serve_batch_inventory_report:
|
|
|
|
|
inventory_batch_inventory_report_flow.serve(
|
2026-05-11 16:56:27 +08:00
|
|
|
name="batch-inventory-report-0915-1915",
|
|
|
|
|
cron=("15 9 * * *", "15 19 * * *"),
|
2026-04-29 14:06:41 +08:00
|
|
|
parameters=flow_kwargs,
|
|
|
|
|
tags=["inventory", "feishu", "batch-inventory-report"],
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
if args.run_channel_inventory_sync:
|
|
|
|
|
inventory_channel_inventory_sync_flow()
|
|
|
|
|
return
|
|
|
|
|
if args.run_batch_inventory_report:
|
|
|
|
|
inventory_batch_inventory_report_flow(**flow_kwargs)
|
|
|
|
|
return
|
|
|
|
|
inventory_channel_inventory_sync_flow()
|
|
|
|
|
inventory_batch_inventory_report_flow(**flow_kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|