164 lines
6.8 KiB
Python
164 lines
6.8 KiB
Python
"""
|
||
actions.py — API-обёртки для симулятора.
|
||
|
||
Все запросы к contracts.kube5s.ru с таймаутами.
|
||
Эмулирует действия пользователя через UI (загрузка, classify, groups, compare).
|
||
"""
|
||
|
||
import requests, json, time, sys, os, uuid
|
||
|
||
BASE = "https://contracts.kube5s.ru"
|
||
TIMEOUT = 120
|
||
|
||
# Убрать warnings
|
||
import urllib3
|
||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||
|
||
def _post(url, **kw):
|
||
return requests.post(url, timeout=TIMEOUT, verify=False, **kw)
|
||
|
||
def _get(url, **kw):
|
||
return requests.get(url, timeout=TIMEOUT, verify=False, **kw)
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# Действия
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
class Session:
|
||
"""Состояние сессии: batch_id, contract_id, files."""
|
||
def __init__(self):
|
||
self.batch_id = str(uuid.uuid4())
|
||
self.contract_id = None
|
||
self.files = [] # {name, doc_id, status}
|
||
|
||
def upload_file(self, path):
|
||
"""Загрузить один файл. Возвращает ответ API."""
|
||
fname = os.path.basename(path)
|
||
with open(path, 'rb') as f:
|
||
data = {'batch_id': self.batch_id}
|
||
if self.contract_id:
|
||
data['contract_id'] = self.contract_id
|
||
r = _post(f"{BASE}/upload", files={'files': (fname, f)}, data=data)
|
||
result = r.json()
|
||
if result.get('ok'):
|
||
doc_id = result.get('doc_id')
|
||
parsed = result.get('parsed', {})
|
||
self.files.append({
|
||
'name': fname, 'doc_id': doc_id,
|
||
'status': parsed.get('status', 'uploaded'),
|
||
'element_count': parsed.get('element_count', 0)
|
||
})
|
||
if result.get('contract_id'):
|
||
self.contract_id = result['contract_id']
|
||
return result
|
||
|
||
def upload_files(self, paths, delay=0.2):
|
||
"""Загрузить несколько файлов последовательно."""
|
||
for p in paths:
|
||
r = self.upload_file(p)
|
||
status = r.get('parsed', {}).get('status', r.get('ok', '?'))
|
||
print(f" ↑ {os.path.basename(p)}: {status}")
|
||
time.sleep(delay)
|
||
|
||
def classify(self):
|
||
"""Запустить классификацию, ждать завершения."""
|
||
r = _post(f"{BASE}/api/classify-batch", json={'batch_id': self.batch_id})
|
||
result = r.json()
|
||
if not result.get('ok'):
|
||
print(f" ✗ classify failed: {result}")
|
||
return result
|
||
|
||
# Поллить прогресс
|
||
total = result.get('total', 0)
|
||
print(f" ⏳ classify {total} файлов...")
|
||
for _ in range(300): # макс 10 минут (300 × 2с)
|
||
time.sleep(2)
|
||
pr = _get(f"{BASE}/api/batch-progress?batch={self.batch_id}").json()
|
||
done = pr.get('counts', {}).get('classified', 0) + pr.get('counts', {}).get('failed', 0)
|
||
if done >= total and total > 0:
|
||
print(f" ✓ classify done: {pr.get('counts')}")
|
||
return pr
|
||
return {"ok": False, "error": "timeout"}
|
||
|
||
def get_groups(self):
|
||
"""Получить группы после классификации."""
|
||
r = _get(f"{BASE}/api/groups?batch={self.batch_id}")
|
||
data = r.json()
|
||
groups = data.get('groups', [])
|
||
real = [g for g in groups if g.get('contract_number') != '__unresolved__']
|
||
unres = [g for g in groups if g.get('contract_number') == '__unresolved__']
|
||
print(f" 📋 группы: {len(real)} (+ {len(unres)} нераспознано)")
|
||
for g in real:
|
||
print(f" №{g.get('contract_number')} — {g.get('counterparty','?')} ({len(g.get('documents',[]))} док.)")
|
||
return data
|
||
|
||
def apply_and_compare(self, group_index=0):
|
||
"""Применить группу и запустить сравнение через SSE."""
|
||
r = _get(f"{BASE}/api/groups?batch={self.batch_id}")
|
||
groups = r.json().get('groups', [])
|
||
real = [g for g in groups if g.get('contract_number') != '__unresolved__']
|
||
if not real:
|
||
print(" ✗ нет групп для сравнения")
|
||
return None
|
||
|
||
group = real[group_index]
|
||
print(f" ⚖ compare группы №{group.get('contract_number')}...")
|
||
|
||
# Применить группу
|
||
ar = _post(f"{BASE}/api/apply-groups", json={'batch_id': self.batch_id, 'groups': [group]})
|
||
ad = ar.json()
|
||
if not ad.get('ok') or not ad.get('contract_ids'):
|
||
print(f" ✗ apply failed: {ad}")
|
||
return None
|
||
|
||
cid = ad['contract_ids'][0]
|
||
|
||
# SSE compare
|
||
import sseclient # pip install sseclient-py
|
||
url = f"{BASE}/process-v2?contract_id={cid}"
|
||
response = requests.get(url, stream=True, timeout=300, verify=False)
|
||
client = sseclient.SSEClient(response)
|
||
|
||
sections = {}
|
||
total_ops = 0
|
||
for event in client.events():
|
||
d = json.loads(event.data)
|
||
t = d.get('type')
|
||
|
||
if t == 'extract_start':
|
||
sections[d['supplement_id']] = d['filename']
|
||
elif t == 'llm_done':
|
||
print(f" ✓ {sections.get(d['supplement_id'],'?')}: {d.get('ops_count',0)} оп. {d.get('mode','?')} ({d.get('time_s',0)}с)")
|
||
elif t == 'applied':
|
||
ops = d.get('ops', [])
|
||
total_ops += len(ops)
|
||
s = d.get('summary', {})
|
||
print(f" 📊 +{s.get('added',0)} ~{s.get('updated',0)} -{s.get('deleted',0)}")
|
||
elif t == 'done':
|
||
print(f" ✓ compare done: {d.get('total_time_s',0)}с, всего {total_ops} оп.")
|
||
return d
|
||
elif t == 'error':
|
||
print(f" ✗ compare error: {d.get('message','?')}")
|
||
return None
|
||
|
||
return None
|
||
|
||
def cleanup(self):
|
||
"""Очистить БД после теста."""
|
||
try:
|
||
_post(f"{BASE}/api/cleanup")
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def upload_files_from_dir(session, dir_path, pattern="*", delay=0.2):
|
||
"""Загрузить все файлы из папки."""
|
||
import glob
|
||
files = sorted(glob.glob(os.path.join(dir_path, pattern)))
|
||
files = [f for f in files if f.endswith(('.docx', '.doc', '.pdf', '.zip'))]
|
||
if not files:
|
||
print(f" (нет файлов в {dir_path})")
|
||
return
|
||
print(f" Загрузка {len(files)} файлов из {dir_path}...")
|
||
session.upload_files(files, delay)
|