Update WEHAGO comparison data and tools
This commit is contained in:
@@ -23,10 +23,12 @@ from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from openpyxl import load_workbook
|
||||
from sqlalchemy import bindparam, create_engine, event, text
|
||||
from sqlalchemy.exc import OperationalError
|
||||
from datasette.app import Datasette
|
||||
from wehago_compare import (
|
||||
enqueue_default_pair_recommend_precompute,
|
||||
get_erp_filtered_rows,
|
||||
get_compare_snapshot_status,
|
||||
get_individual_pair_recommendations,
|
||||
get_last_action_summary,
|
||||
get_status_field_suggestions,
|
||||
@@ -36,6 +38,7 @@ from wehago_compare import (
|
||||
get_wehago_filtered_rows,
|
||||
import_uploaded_erp_voucher_file,
|
||||
init_wehago_compare_db,
|
||||
request_compare_snapshot_rebuild,
|
||||
recommend_pair_matches,
|
||||
save_recommended_pair_matches,
|
||||
save_manual_pair_matches,
|
||||
@@ -54,8 +57,13 @@ _DB_BACKUP_STATE: dict[str, Any] = {
|
||||
"last_run_at": 0.0,
|
||||
}
|
||||
_DB_BACKUP_LOCK = threading.Lock()
|
||||
_DB_INIT_LOCK = threading.Lock()
|
||||
_DB_INIT_DONE = False
|
||||
_DB_ANALYZE_LOCK = threading.Lock()
|
||||
_DB_ANALYZE_LAST_ATTEMPT_AT = 0.0
|
||||
DB_BACKUP_MIN_INTERVAL_SECONDS = 900.0
|
||||
DB_BACKUP_KEEP_COUNT = 24
|
||||
DB_ANALYZE_MIN_INTERVAL_SECONDS = 6 * 60 * 60
|
||||
PROCESS_COST_CACHE_TTL_SECONDS = 120.0
|
||||
_PROCESS_COST_RUNTIME_CACHE_LOCK = threading.Lock()
|
||||
_PROCESS_COST_PROJECT_OPTIONS_CACHE: dict[tuple[Any, ...], dict[str, Any]] = {}
|
||||
@@ -395,8 +403,30 @@ DEFAULT_APP_KEYWORD_RULES = {
|
||||
|
||||
|
||||
def ensure_default_app_config(conn: Any) -> None:
|
||||
def _clean_text(value: Any) -> str:
|
||||
return "" if value is None else str(value).strip()
|
||||
|
||||
existing_option_rows = conn.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT group_key, item_key, label, value_text, sort_order
|
||||
FROM app_option_items
|
||||
"""
|
||||
)
|
||||
).mappings().all()
|
||||
existing_options = {
|
||||
(_clean_text(row.get("group_key")), _clean_text(row.get("item_key"))): (
|
||||
_clean_text(row.get("label")),
|
||||
_clean_text(row.get("value_text")),
|
||||
int(row.get("sort_order") or 0),
|
||||
)
|
||||
for row in existing_option_rows
|
||||
}
|
||||
for group_key, items in DEFAULT_APP_OPTION_ITEMS.items():
|
||||
for sort_order, (item_key, label, value_text) in enumerate(items):
|
||||
existing_value = existing_options.get((group_key, item_key))
|
||||
if existing_value == (label, value_text, sort_order):
|
||||
continue
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
@@ -420,8 +450,22 @@ def ensure_default_app_config(conn: Any) -> None:
|
||||
},
|
||||
)
|
||||
|
||||
existing_rule_rows = conn.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT rule_group, category_key, keyword, sort_order
|
||||
FROM app_keyword_rules
|
||||
"""
|
||||
)
|
||||
).mappings().all()
|
||||
existing_rules = {
|
||||
(_clean_text(row.get("rule_group")), _clean_text(row.get("category_key")), _clean_text(row.get("keyword"))): int(row.get("sort_order") or 0)
|
||||
for row in existing_rule_rows
|
||||
}
|
||||
for rule_group, items in DEFAULT_APP_KEYWORD_RULES.items():
|
||||
for sort_order, (category_key, keyword) in enumerate(items):
|
||||
if existing_rules.get((rule_group, category_key, keyword)) == sort_order:
|
||||
continue
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
@@ -445,12 +489,38 @@ def ensure_default_app_config(conn: Any) -> None:
|
||||
load_app_config.cache_clear()
|
||||
|
||||
|
||||
def _maybe_run_db_analyze() -> None:
|
||||
global _DB_ANALYZE_LAST_ATTEMPT_AT
|
||||
now = time.time()
|
||||
if now - _DB_ANALYZE_LAST_ATTEMPT_AT < DB_ANALYZE_MIN_INTERVAL_SECONDS:
|
||||
return
|
||||
with _DB_ANALYZE_LOCK:
|
||||
now = time.time()
|
||||
if now - _DB_ANALYZE_LAST_ATTEMPT_AT < DB_ANALYZE_MIN_INTERVAL_SECONDS:
|
||||
return
|
||||
_DB_ANALYZE_LAST_ATTEMPT_AT = now
|
||||
try:
|
||||
with engine.begin() as conn:
|
||||
conn.execute(text("ANALYZE"))
|
||||
except OperationalError as exc:
|
||||
logger.warning("ANALYZE skipped due to database lock: %s", exc)
|
||||
except Exception as exc:
|
||||
logger.warning("ANALYZE skipped due to unexpected error: %s", exc)
|
||||
|
||||
|
||||
def init_db() -> None:
|
||||
with engine.begin() as conn:
|
||||
global _DB_INIT_DONE
|
||||
if _DB_INIT_DONE:
|
||||
return
|
||||
with _DB_INIT_LOCK:
|
||||
if _DB_INIT_DONE:
|
||||
return
|
||||
conn = engine.connect()
|
||||
trans = conn.begin()
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS transactions (
|
||||
text(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS transactions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
approval_status TEXT,
|
||||
voucher_number TEXT,
|
||||
@@ -1333,8 +1403,11 @@ def init_db() -> None:
|
||||
migrate_project_status_entries(conn)
|
||||
migrate_project_basic_info(conn)
|
||||
ensure_default_app_config(conn)
|
||||
conn.execute(text("ANALYZE"))
|
||||
init_wehago_compare_db(engine)
|
||||
trans.commit()
|
||||
conn.close()
|
||||
init_wehago_compare_db(engine)
|
||||
_DB_INIT_DONE = True
|
||||
_maybe_run_db_analyze()
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
@@ -8459,11 +8532,36 @@ def render_wehago_compare_page(
|
||||
return templates.TemplateResponse(request, "wehago_compare.html", context)
|
||||
|
||||
|
||||
def build_wehago_compare_health_payload() -> dict[str, Any]:
|
||||
init_db()
|
||||
payload = get_wehago_compare_dashboard(
|
||||
engine,
|
||||
include_metric_counts=False,
|
||||
warm_caches=False,
|
||||
)
|
||||
return {
|
||||
"status": "ok",
|
||||
"selected_start_year": payload.get("selected_start_year"),
|
||||
"selected_end_year": payload.get("selected_end_year"),
|
||||
"available_year_count": len(payload.get("available_years") or []),
|
||||
"metric_section_count": len(payload.get("metric_sections") or []),
|
||||
}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health() -> dict[str, str]:
|
||||
return build_health_payload()
|
||||
|
||||
|
||||
@app.get("/health/wehago-compare")
|
||||
async def health_wehago_compare() -> JSONResponse:
|
||||
try:
|
||||
return JSONResponse(content=jsonable_encoder(build_wehago_compare_health_payload()))
|
||||
except Exception as exc:
|
||||
logger.exception("전표비교 readiness 에러: %s", exc)
|
||||
return JSONResponse(content={"status": "error", "error": str(exc)}, status_code=500)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def home(request: Request, edit_id: int | None = None, overview_year: int | None = None):
|
||||
try:
|
||||
@@ -9114,6 +9212,44 @@ async def wehago_compare_summary(
|
||||
return JSONResponse(content={"error": str(exc)}, status_code=500)
|
||||
|
||||
|
||||
@app.get("/wehago-compare/api/snapshot-status")
|
||||
async def wehago_compare_snapshot_status(
|
||||
start_year: int | None = None,
|
||||
end_year: int | None = None,
|
||||
):
|
||||
try:
|
||||
payload = get_compare_snapshot_status(
|
||||
engine,
|
||||
start_year=start_year,
|
||||
end_year=end_year,
|
||||
)
|
||||
return JSONResponse(content=jsonable_encoder(payload))
|
||||
except Exception as exc:
|
||||
logger.exception("전표비교 스냅샷 상태 조회 에러: %s", exc)
|
||||
return JSONResponse(content={"error": str(exc)}, status_code=500)
|
||||
|
||||
|
||||
@app.post("/wehago-compare/api/snapshot-rebuild")
|
||||
async def wehago_compare_snapshot_rebuild(request: Request):
|
||||
try:
|
||||
payload = await request.json()
|
||||
if not isinstance(payload, dict):
|
||||
payload = {}
|
||||
start_year = payload.get("start_year")
|
||||
end_year = payload.get("end_year")
|
||||
include_metric_counts = normalize_text(payload.get("include_metric_counts")) not in {"0", "false", "n", "no", "off"}
|
||||
response = request_compare_snapshot_rebuild(
|
||||
engine,
|
||||
start_year=int(start_year) if start_year is not None else None,
|
||||
end_year=int(end_year) if end_year is not None else None,
|
||||
include_metric_counts=include_metric_counts,
|
||||
)
|
||||
return JSONResponse(content=jsonable_encoder({"ok": True, **response}))
|
||||
except Exception as exc:
|
||||
logger.exception("전표비교 스냅샷 재생성 요청 에러: %s", exc)
|
||||
return JSONResponse(content={"error": str(exc)}, status_code=500)
|
||||
|
||||
|
||||
@app.get("/wehago-compare/api/status-suggestions")
|
||||
async def wehago_compare_status_suggestions(
|
||||
start_year: int | None = None,
|
||||
|
||||
Reference in New Issue
Block a user