운영 지표와 지원 요청 저장소 추가
This commit is contained in:
parent
50fa4ad432
commit
e7ebb38177
20 changed files with 3038 additions and 39 deletions
157
scripts/export-phase3-kpi.py
Normal file
157
scripts/export-phase3-kpi.py
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
API_ROOT = REPO_ROOT / "apps" / "api"
|
||||
sys.path.insert(0, str(API_ROOT))
|
||||
|
||||
import asyncpg # noqa: E402
|
||||
|
||||
from app.services.phase3_kpi_export import ( # noqa: E402
|
||||
KPI_REPORT_PATH,
|
||||
PREPOST_CSV_PATH,
|
||||
ParticipantKeys,
|
||||
build_kpi_report,
|
||||
build_prepost_csv_rows,
|
||||
write_kpi_report,
|
||||
write_prepost_csv,
|
||||
)
|
||||
|
||||
|
||||
PREPOST_QUERY = """
|
||||
WITH ranked AS (
|
||||
SELECT
|
||||
learner_id::text AS learner_id,
|
||||
pilot_id,
|
||||
measure_name,
|
||||
timepoint,
|
||||
raw_score,
|
||||
min_score,
|
||||
max_score,
|
||||
instrument_version,
|
||||
item_count,
|
||||
collected_at,
|
||||
updated_at,
|
||||
row_number() OVER (
|
||||
PARTITION BY learner_id, measure_name, timepoint
|
||||
ORDER BY updated_at DESC, collected_at DESC, instrument_version DESC
|
||||
) AS rn
|
||||
FROM app.learner_prepost_measure
|
||||
WHERE pilot_id = $1
|
||||
AND ($2::timestamptz IS NULL OR collected_at >= $2::timestamptz)
|
||||
AND ($3::timestamptz IS NULL OR collected_at < $3::timestamptz)
|
||||
)
|
||||
SELECT
|
||||
learner_id,
|
||||
pilot_id,
|
||||
measure_name,
|
||||
timepoint,
|
||||
raw_score,
|
||||
min_score,
|
||||
max_score,
|
||||
instrument_version,
|
||||
item_count,
|
||||
collected_at,
|
||||
updated_at
|
||||
FROM ranked
|
||||
WHERE rn = 1
|
||||
ORDER BY learner_id, measure_name, timepoint
|
||||
"""
|
||||
|
||||
|
||||
def parse_time(value: str | None) -> datetime | None:
|
||||
if not value:
|
||||
return None
|
||||
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
if parsed.tzinfo is None:
|
||||
parsed = parsed.replace(tzinfo=UTC)
|
||||
return parsed
|
||||
|
||||
|
||||
def now_utc() -> str:
|
||||
return datetime.now(UTC).isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
async def fetch_rows(args: argparse.Namespace) -> list[dict[str, Any]]:
|
||||
conn = await asyncpg.connect(args.database_url)
|
||||
try:
|
||||
records = await conn.fetch(
|
||||
PREPOST_QUERY,
|
||||
args.pilot_id,
|
||||
parse_time(args.started_at),
|
||||
parse_time(args.ended_at),
|
||||
)
|
||||
finally:
|
||||
await conn.close()
|
||||
return [dict(record) for record in records]
|
||||
|
||||
|
||||
async def run(args: argparse.Namespace) -> int:
|
||||
rows = await fetch_rows(args)
|
||||
keys = ParticipantKeys()
|
||||
csv_rows = build_prepost_csv_rows(rows, participant_keys=keys)
|
||||
generated_at = args.generated_at or now_utc()
|
||||
source_window = None
|
||||
if args.started_at or args.ended_at:
|
||||
source_window = {
|
||||
"started_at": args.started_at or "",
|
||||
"ended_at": args.ended_at or "",
|
||||
}
|
||||
report = build_kpi_report(
|
||||
rows,
|
||||
pilot_id=args.pilot_id,
|
||||
generated_at=generated_at,
|
||||
source_window=source_window,
|
||||
review_operator=args.operator,
|
||||
)
|
||||
output_root = Path(args.output_root)
|
||||
prepost_path = output_root / PREPOST_CSV_PATH
|
||||
report_path = output_root / KPI_REPORT_PATH
|
||||
write_prepost_csv(csv_rows, prepost_path)
|
||||
write_kpi_report(report, report_path)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"prepost_csv": str(prepost_path),
|
||||
"kpi_report": str(report_path),
|
||||
"rows": len(csv_rows),
|
||||
"participants": len(keys),
|
||||
"pilot_id": args.pilot_id,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Export Phase 3 pre/post KPI evidence CSV and kpi_report.json from app.learner_prepost_measure."
|
||||
)
|
||||
parser.add_argument("--database-url", default=os.environ.get("DATABASE_URL"), required=os.environ.get("DATABASE_URL") is None)
|
||||
parser.add_argument("--output-root", default=str(REPO_ROOT / "data" / "phase3-dry-run"))
|
||||
parser.add_argument("--pilot-id", default="phase3-pilot-draft")
|
||||
parser.add_argument("--started-at")
|
||||
parser.add_argument("--ended-at")
|
||||
parser.add_argument("--generated-at")
|
||||
parser.add_argument("--operator", default="")
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args()
|
||||
return asyncio.run(run(args))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue