feat(1c): add brief history and problematic company pages
This commit is contained in:
@@ -68,6 +68,36 @@ def load_latest_manager_brief() -> dict[str, Any]:
|
||||
return json.loads(latest_path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def load_brief_history_records(limit: int = 20) -> list[dict[str, Any]]:
|
||||
history_dir = manager_brief_state_dir() / "history"
|
||||
if not history_dir.exists():
|
||||
return []
|
||||
items: list[dict[str, Any]] = []
|
||||
for path in sorted(history_dir.glob("*.json"), reverse=True)[:limit]:
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
items.append(
|
||||
{
|
||||
"generated_at": payload.get("generated_at"),
|
||||
"render_mode": payload.get("render_mode"),
|
||||
"model": payload.get("model"),
|
||||
"headline": payload.get("brief", {}).get("headline", ""),
|
||||
"path": path.name,
|
||||
"brief": payload.get("brief", {}),
|
||||
}
|
||||
)
|
||||
return items
|
||||
|
||||
|
||||
def load_brief_history_record(name: str) -> dict[str, Any]:
|
||||
safe_name = Path(name).name
|
||||
if not safe_name.endswith(".json"):
|
||||
safe_name += ".json"
|
||||
path = manager_brief_state_dir() / "history" / safe_name
|
||||
if not path.exists():
|
||||
raise HTTPException(status_code=404, detail="manager brief history record not found")
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def grafana_company_dashboard_url() -> str:
|
||||
return os.getenv(
|
||||
"AW_1C_MANAGER_BRIEF_GRAFANA_URL",
|
||||
@@ -103,6 +133,11 @@ def company_detail_url(counterparty: str, infobase: str | None = None) -> str:
|
||||
return base
|
||||
|
||||
|
||||
def manager_brief_history_html_url(name: str) -> str:
|
||||
safe_name = Path(name).name
|
||||
return f"/manager/briefs/{quote(safe_name)}"
|
||||
|
||||
|
||||
def render_manager_brief_html(payload: dict[str, Any]) -> str:
|
||||
brief = payload.get("brief", {})
|
||||
context = payload.get("context", {})
|
||||
@@ -115,6 +150,9 @@ def render_manager_brief_html(payload: dict[str, Any]) -> str:
|
||||
render_mode = payload.get("render_mode", "unknown")
|
||||
generated_at = payload.get("generated_at", "")
|
||||
history_url = "/api/1/analytics-1c/manager/brief/history"
|
||||
history_html_url = "/manager/briefs"
|
||||
problematic_1d_url = "/manager/problematic?days=1"
|
||||
problematic_7d_url = "/manager/problematic?days=7"
|
||||
json_url = "/api/1/analytics-1c/manager/brief/latest"
|
||||
md_url = "/api/1/analytics-1c/manager/brief/latest.md"
|
||||
grafana_url = grafana_company_dashboard_url()
|
||||
@@ -409,7 +447,10 @@ def render_manager_brief_html(payload: dict[str, Any]) -> str:
|
||||
<nav class="hero-links">
|
||||
<a href="{html.escape(json_url)}">JSON</a>
|
||||
<a href="{html.escape(md_url)}">Markdown</a>
|
||||
<a href="{html.escape(history_url)}">History</a>
|
||||
<a href="{html.escape(history_html_url)}">История brief</a>
|
||||
<a href="{html.escape(problematic_1d_url)}">Проблемные 1д</a>
|
||||
<a href="{html.escape(problematic_7d_url)}">Проблемные 7д</a>
|
||||
<a href="{html.escape(history_url)}">History API</a>
|
||||
<a href="{html.escape(grafana_url)}">Grafana</a>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -494,6 +535,222 @@ def render_manager_brief_html(payload: dict[str, Any]) -> str:
|
||||
</html>"""
|
||||
|
||||
|
||||
def problematic_companies(days: int = 7, limit: int = 50) -> list[dict[str, Any]]:
|
||||
client = ch_client()
|
||||
sql = f"""
|
||||
WITH recent AS (
|
||||
SELECT
|
||||
infobase,
|
||||
counterparty,
|
||||
max(generated_at) AS latest_signal_at,
|
||||
max(score) AS max_score,
|
||||
sum(score) AS total_score,
|
||||
count() AS signals_total,
|
||||
countIf(severity = 'critical') AS critical_total,
|
||||
countIf(severity = 'high') AS high_total,
|
||||
argMax(severity, tuple(score, generated_at)) AS top_severity,
|
||||
argMax(signal_type, tuple(score, generated_at)) AS top_signal_type,
|
||||
argMax(summary, tuple(score, generated_at)) AS top_summary
|
||||
FROM analytics_1c.company_health_signals
|
||||
WHERE generated_at >= now() - INTERVAL {int(days)} DAY
|
||||
GROUP BY infobase, counterparty
|
||||
)
|
||||
SELECT
|
||||
p.infobase,
|
||||
p.counterparty,
|
||||
p.company_name,
|
||||
p.normalized_counterparty,
|
||||
p.registry_match_mode,
|
||||
p.registry_assignee_name,
|
||||
p.registry_status,
|
||||
p.signal_severity,
|
||||
p.signal_score,
|
||||
p.amount_30d,
|
||||
p.amount_forecast_30d,
|
||||
p.current_status,
|
||||
p.active_locks,
|
||||
p.open_cases_total,
|
||||
p.detections_total,
|
||||
r.latest_signal_at,
|
||||
r.max_score,
|
||||
r.total_score,
|
||||
r.signals_total,
|
||||
r.critical_total,
|
||||
r.high_total,
|
||||
r.top_severity,
|
||||
r.top_signal_type,
|
||||
r.top_summary
|
||||
FROM recent AS r
|
||||
INNER JOIN analytics_1c.v_company_portfolio_overview AS p
|
||||
ON p.infobase = r.infobase
|
||||
AND p.counterparty = r.counterparty
|
||||
ORDER BY r.max_score DESC, r.signals_total DESC, p.amount_30d DESC, p.counterparty
|
||||
LIMIT {int(limit)}
|
||||
"""
|
||||
return rows_to_dict(client.query(sql))
|
||||
|
||||
|
||||
def render_brief_history_html(items: list[dict[str, Any]]) -> str:
|
||||
rows = []
|
||||
for item in items:
|
||||
rows.append(
|
||||
"<tr>"
|
||||
f"<td><a class=\"inline-link\" href=\"{manager_brief_history_html_url(str(item.get('path', '')))}\">{html.escape(str(item.get('generated_at', '-')))}</a></td>"
|
||||
f"<td>{html.escape(str(item.get('render_mode', '-')))}</td>"
|
||||
f"<td>{html.escape(str(item.get('model') or '-'))}</td>"
|
||||
f"<td>{html.escape(str(item.get('headline') or '-'))}</td>"
|
||||
f"<td><a class=\"inline-link\" href=\"/api/1/analytics-1c/manager/brief/history/{quote(str(item.get('path', '')))}\">JSON</a></td>"
|
||||
"</tr>"
|
||||
)
|
||||
return f"""<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="refresh" content="300">
|
||||
<title>1C Brief History</title>
|
||||
<style>
|
||||
:root {{
|
||||
--bg: #f4f1ea; --paper: #fffdf8; --ink: #1c1a17; --muted: #6b655c; --line: #d8d0c4; --accent: #005f73; --shadow: 0 14px 40px rgba(28, 26, 23, 0.08);
|
||||
}}
|
||||
* {{ box-sizing: border-box; }}
|
||||
body {{ margin: 0; font-family: "IBM Plex Sans", "Segoe UI", system-ui, sans-serif; background: linear-gradient(180deg, #faf7f2 0%, var(--bg) 100%); color: var(--ink); }}
|
||||
.shell {{ max-width: 1380px; margin: 0 auto; padding: 28px 22px 60px; }}
|
||||
.hero {{ background: linear-gradient(135deg, rgba(0,95,115,0.94), rgba(10,77,104,0.92)); color: #f8fbfc; border-radius: 24px; padding: 28px 30px; box-shadow: var(--shadow); }}
|
||||
.hero h1 {{ margin: 0 0 10px; font-size: clamp(28px, 4vw, 42px); }}
|
||||
.hero-links {{ display: flex; gap: 10px; flex-wrap: wrap; }}
|
||||
.hero-links a {{ text-decoration: none; color: #f8fbfc; border: 1px solid rgba(248, 251, 252, 0.28); padding: 9px 12px; border-radius: 999px; font-size: 14px; }}
|
||||
.panel {{ margin-top: 22px; background: var(--paper); border: 1px solid var(--line); border-radius: 22px; padding: 22px; box-shadow: var(--shadow); }}
|
||||
table {{ width: 100%; border-collapse: collapse; font-size: 14px; }}
|
||||
th, td {{ text-align: left; padding: 10px 8px; border-bottom: 1px solid var(--line); vertical-align: top; }}
|
||||
th {{ color: var(--muted); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; }}
|
||||
.inline-link {{ color: var(--accent); text-decoration: none; font-weight: 600; }}
|
||||
.inline-link:hover {{ text-decoration: underline; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell">
|
||||
<section class="hero">
|
||||
<h1>История executive brief</h1>
|
||||
<div class="hero-links">
|
||||
<a href="/manager/brief">Текущий brief</a>
|
||||
<a href="/manager/problematic?days=1">Проблемные 1д</a>
|
||||
<a href="/manager/problematic?days=7">Проблемные 7д</a>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Сформировано</th>
|
||||
<th>Режим</th>
|
||||
<th>Модель</th>
|
||||
<th>Headline</th>
|
||||
<th>Raw</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{''.join(rows) or '<tr><td colspan="5">История пока пуста.</td></tr>'}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
def render_problematic_companies_html(items: list[dict[str, Any]], days: int) -> str:
|
||||
rows = []
|
||||
for item in items:
|
||||
rows.append(
|
||||
"<tr>"
|
||||
f"<td><a class=\"inline-link\" href=\"{company_detail_url(str(item.get('counterparty', '-')), str(item.get('infobase', '')) if item.get('infobase') else None)}\">{html.escape(str(item.get('counterparty', '-')))}</a></td>"
|
||||
f"<td>{html.escape(str(item.get('normalized_counterparty') or '-'))}</td>"
|
||||
f"<td>{severity_badge(str(item.get('top_severity') or item.get('signal_severity') or 'none'))}</td>"
|
||||
f"<td>{fmt_number(item.get('max_score'))}</td>"
|
||||
f"<td>{fmt_number(item.get('signals_total'))}</td>"
|
||||
f"<td>{fmt_number(item.get('critical_total'))}</td>"
|
||||
f"<td>{fmt_number(item.get('amount_30d'))}</td>"
|
||||
f"<td>{fmt_number(item.get('amount_forecast_30d'))}</td>"
|
||||
f"<td>{html.escape(str(item.get('top_signal_type') or '-'))}</td>"
|
||||
f"<td>{html.escape(str(item.get('top_summary') or '-'))}</td>"
|
||||
"</tr>"
|
||||
)
|
||||
return f"""<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="refresh" content="300">
|
||||
<title>1C Problem Companies</title>
|
||||
<style>
|
||||
:root {{
|
||||
--bg: #f4f1ea; --paper: #fffdf8; --ink: #1c1a17; --muted: #6b655c; --line: #d8d0c4; --accent: #005f73; --shadow: 0 14px 40px rgba(28, 26, 23, 0.08);
|
||||
--critical: #9b2226; --high: #bb3e03; --medium: #ca6702; --low: #4d7c0f; --none: #687076;
|
||||
}}
|
||||
* {{ box-sizing: border-box; }}
|
||||
body {{ margin: 0; font-family: "IBM Plex Sans", "Segoe UI", system-ui, sans-serif; background: linear-gradient(180deg, #faf7f2 0%, var(--bg) 100%); color: var(--ink); }}
|
||||
.shell {{ max-width: 1500px; margin: 0 auto; padding: 28px 22px 60px; }}
|
||||
.hero {{ background: linear-gradient(135deg, rgba(0,95,115,0.94), rgba(10,77,104,0.92)); color: #f8fbfc; border-radius: 24px; padding: 28px 30px; box-shadow: var(--shadow); }}
|
||||
.hero h1 {{ margin: 0 0 10px; font-size: clamp(28px, 4vw, 42px); }}
|
||||
.hero p {{ margin: 0 0 14px; line-height: 1.5; color: rgba(248,251,252,0.88); }}
|
||||
.hero-links {{ display: flex; gap: 10px; flex-wrap: wrap; }}
|
||||
.hero-links a {{ text-decoration: none; color: #f8fbfc; border: 1px solid rgba(248, 251, 252, 0.28); padding: 9px 12px; border-radius: 999px; font-size: 14px; }}
|
||||
.panel {{ margin-top: 22px; background: var(--paper); border: 1px solid var(--line); border-radius: 22px; padding: 22px; box-shadow: var(--shadow); }}
|
||||
table {{ width: 100%; border-collapse: collapse; font-size: 14px; }}
|
||||
th, td {{ text-align: left; padding: 10px 8px; border-bottom: 1px solid var(--line); vertical-align: top; }}
|
||||
th {{ color: var(--muted); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; }}
|
||||
.inline-link {{ color: var(--accent); text-decoration: none; font-weight: 600; }}
|
||||
.inline-link:hover {{ text-decoration: underline; }}
|
||||
.badge {{
|
||||
display: inline-flex; align-items: center; justify-content: center; padding: 6px 10px; border-radius: 999px;
|
||||
font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #fff;
|
||||
}}
|
||||
.badge-critical {{ background: var(--critical); }}
|
||||
.badge-high {{ background: var(--high); }}
|
||||
.badge-medium {{ background: var(--medium); }}
|
||||
.badge-low {{ background: var(--low); }}
|
||||
.badge-none {{ background: var(--none); }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell">
|
||||
<section class="hero">
|
||||
<h1>Проблемные компании за {days} {('день' if days == 1 else 'дней')}</h1>
|
||||
<p>Список собран по live company signals. В приоритете max score, плотность сигналов и общий вес проблемного контура.</p>
|
||||
<div class="hero-links">
|
||||
<a href="/manager/brief">Текущий brief</a>
|
||||
<a href="/manager/briefs">История brief</a>
|
||||
<a href="/manager/problematic?days=1">Срез 1д</a>
|
||||
<a href="/manager/problematic?days=7">Срез 7д</a>
|
||||
</div>
|
||||
</section>
|
||||
<section class="panel">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Компания</th>
|
||||
<th>Нормализация</th>
|
||||
<th>Severity</th>
|
||||
<th>Max score</th>
|
||||
<th>Signals</th>
|
||||
<th>Critical</th>
|
||||
<th>Активность 30д</th>
|
||||
<th>Прогноз 30д</th>
|
||||
<th>Top signal</th>
|
||||
<th>Комментарий</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{''.join(rows) or '<tr><td colspan="10">Нет сигналов за выбранный период.</td></tr>'}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
app = FastAPI(title="AW-rus 1C Company Intelligence API", version="1.0.0")
|
||||
|
||||
|
||||
@@ -695,29 +952,47 @@ def manager_brief_latest_markdown() -> str:
|
||||
|
||||
@app.get("/api/1/analytics-1c/manager/brief/history")
|
||||
def manager_brief_history(limit: int = Query(default=20, ge=1, le=200)) -> dict[str, Any]:
|
||||
history_dir = manager_brief_state_dir() / "history"
|
||||
if not history_dir.exists():
|
||||
return {"items": [], "count": 0}
|
||||
items: list[dict[str, Any]] = []
|
||||
for path in sorted(history_dir.glob("*.json"), reverse=True)[:limit]:
|
||||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||
items.append(
|
||||
{
|
||||
"generated_at": payload.get("generated_at"),
|
||||
"render_mode": payload.get("render_mode"),
|
||||
"model": payload.get("model"),
|
||||
"headline": payload.get("brief", {}).get("headline", ""),
|
||||
"path": path.name,
|
||||
}
|
||||
)
|
||||
items = load_brief_history_records(limit)
|
||||
return {"items": items, "count": len(items)}
|
||||
|
||||
|
||||
@app.get("/api/1/analytics-1c/manager/brief/history/{name}")
|
||||
def manager_brief_history_record(name: str) -> dict[str, Any]:
|
||||
return load_brief_history_record(name)
|
||||
|
||||
|
||||
@app.get("/api/1/analytics-1c/companies/problematic")
|
||||
def problematic_companies_api(
|
||||
days: int = Query(default=7, ge=1, le=30),
|
||||
limit: int = Query(default=50, ge=1, le=500),
|
||||
) -> dict[str, Any]:
|
||||
items = problematic_companies(days=days, limit=limit)
|
||||
return {"items": items, "count": len(items), "days": days}
|
||||
|
||||
|
||||
@app.get("/manager/brief", response_class=HTMLResponse)
|
||||
def manager_brief_view() -> str:
|
||||
return render_manager_brief_html(load_latest_manager_brief())
|
||||
|
||||
|
||||
@app.get("/manager/briefs", response_class=HTMLResponse)
|
||||
def manager_brief_history_view(limit: int = Query(default=40, ge=1, le=200)) -> str:
|
||||
return render_brief_history_html(load_brief_history_records(limit))
|
||||
|
||||
|
||||
@app.get("/manager/briefs/{name}", response_class=HTMLResponse)
|
||||
def manager_brief_history_detail_view(name: str) -> str:
|
||||
return render_manager_brief_html(load_brief_history_record(name))
|
||||
|
||||
|
||||
@app.get("/manager/problematic", response_class=HTMLResponse)
|
||||
def manager_problematic_companies_view(
|
||||
days: int = Query(default=7, ge=1, le=30),
|
||||
limit: int = Query(default=50, ge=1, le=500),
|
||||
) -> str:
|
||||
return render_problematic_companies_html(problematic_companies(days=days, limit=limit), days)
|
||||
|
||||
|
||||
def render_company_detail_html(summary_payload: dict[str, Any], infobase: str | None = None) -> str:
|
||||
card = summary_payload["card"]
|
||||
company_state = summary_payload.get("company_state") or {}
|
||||
@@ -908,6 +1183,8 @@ def render_company_detail_html(summary_payload: dict[str, Any], infobase: str |
|
||||
</div>
|
||||
<nav class="hero-links">
|
||||
<a href="/manager/brief">К портфелю</a>
|
||||
<a href="/manager/briefs">История brief</a>
|
||||
<a href="/manager/problematic?days=7">Проблемные компании</a>
|
||||
<a href="{html.escape(summary_url)}">JSON summary</a>
|
||||
<a href="{html.escape(forecast_url)}">JSON forecast</a>
|
||||
<a href="{html.escape(timeline_url)}">JSON timeline</a>
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
sys.modules.setdefault("clickhouse_connect", types.SimpleNamespace(get_client=lambda **_: None))
|
||||
sys.modules.setdefault("uvicorn", types.SimpleNamespace(run=lambda *args, **kwargs: None))
|
||||
|
||||
if "fastapi" not in sys.modules:
|
||||
fastapi_mod = types.ModuleType("fastapi")
|
||||
|
||||
class DummyFastAPI:
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
def decorator(fn):
|
||||
return fn
|
||||
|
||||
return decorator
|
||||
|
||||
class DummyHTTPException(Exception):
|
||||
def __init__(self, status_code: int, detail: str):
|
||||
super().__init__(detail)
|
||||
self.status_code = status_code
|
||||
self.detail = detail
|
||||
|
||||
def dummy_query(default=None, **kwargs):
|
||||
return default
|
||||
|
||||
fastapi_mod.FastAPI = DummyFastAPI
|
||||
fastapi_mod.HTTPException = DummyHTTPException
|
||||
fastapi_mod.Query = dummy_query
|
||||
sys.modules["fastapi"] = fastapi_mod
|
||||
|
||||
responses_mod = types.ModuleType("fastapi.responses")
|
||||
|
||||
class DummyResponse:
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
responses_mod.HTMLResponse = DummyResponse
|
||||
responses_mod.PlainTextResponse = DummyResponse
|
||||
responses_mod.Response = DummyResponse
|
||||
sys.modules["fastapi.responses"] = responses_mod
|
||||
|
||||
import company_intelligence_api as api
|
||||
|
||||
|
||||
class CompanyIntelligenceApiTests(unittest.TestCase):
|
||||
def test_render_problematic_companies_html(self) -> None:
|
||||
html_page = api.render_problematic_companies_html(
|
||||
[
|
||||
{
|
||||
"counterparty": "ФЕЛИЦТ ГРУПП 2026",
|
||||
"infobase": "ФЕЛИЦТ ГРУПП 2026",
|
||||
"normalized_counterparty": "ФЕЛИЦТ ГРУПП",
|
||||
"top_severity": "critical",
|
||||
"max_score": 95,
|
||||
"signals_total": 12,
|
||||
"critical_total": 5,
|
||||
"amount_30d": 270.68,
|
||||
"amount_forecast_30d": 8120.4,
|
||||
"top_signal_type": "open_cases",
|
||||
"top_summary": "Есть открытые кейсы",
|
||||
}
|
||||
],
|
||||
days=7,
|
||||
)
|
||||
self.assertIn("Проблемные компании за 7 дней", html_page)
|
||||
self.assertIn("ФЕЛИЦТ ГРУПП 2026", html_page)
|
||||
self.assertIn("/manager/company/", html_page)
|
||||
|
||||
def test_load_brief_history_records_and_detail(self) -> None:
|
||||
payload = {
|
||||
"generated_at": "2026-05-22T12:00:00+00:00",
|
||||
"render_mode": "codex",
|
||||
"model": "codex",
|
||||
"brief": {"headline": "Тестовый brief"},
|
||||
}
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
state_dir = Path(tmp)
|
||||
history_dir = state_dir / "history"
|
||||
history_dir.mkdir(parents=True, exist_ok=True)
|
||||
(history_dir / "20260522T120000Z.json").write_text(json.dumps(payload), encoding="utf-8")
|
||||
old = os.environ.get("AW_1C_MANAGER_BRIEF_STATE_DIR")
|
||||
os.environ["AW_1C_MANAGER_BRIEF_STATE_DIR"] = str(state_dir)
|
||||
try:
|
||||
items = api.load_brief_history_records(limit=5)
|
||||
self.assertEqual(len(items), 1)
|
||||
self.assertEqual(items[0]["headline"], "Тестовый brief")
|
||||
detail = api.load_brief_history_record("20260522T120000Z.json")
|
||||
self.assertEqual(detail["brief"]["headline"], "Тестовый brief")
|
||||
finally:
|
||||
if old is None:
|
||||
os.environ.pop("AW_1C_MANAGER_BRIEF_STATE_DIR", None)
|
||||
else:
|
||||
os.environ["AW_1C_MANAGER_BRIEF_STATE_DIR"] = old
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user