fix: 10 пунктов ревью — /run (кортеж store.add), валидация limit/offset в панели, close() соединений SQLite, троттлинг под локом, read-only бот без OMNI_BOT_OWNERS, eviction JOBS, push_new отдаёт свежие записи, ai.py не спит после последней попытки, дедуп реестра, подключён abuse-реестр (+191 источник); tests/test_regress.py 16 проверок + CI py3.10-3.12
This commit is contained in:
199
tools/_port_fixes.py
Normal file
199
tools/_port_fixes.py
Normal file
@@ -0,0 +1,199 @@
|
||||
"""Портирование проверенных фиксов из ревью (без снятия /abuse) + свои правки.
|
||||
|
||||
Каждая замена ассертится: если шаблон не найден — падаем, а не молча пропускаем.
|
||||
"""
|
||||
import io
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:/Users/User/tmp/omni-parse")
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
|
||||
report = []
|
||||
|
||||
|
||||
def patch(rel, pairs, count_check=True):
|
||||
p = ROOT / rel
|
||||
s = p.read_text(encoding="utf-8")
|
||||
orig = s
|
||||
for old, new in pairs:
|
||||
n = s.count(old)
|
||||
if n == 0:
|
||||
raise SystemExit(f"НЕ НАЙДЕНО в {rel}: {old[:70]!r}")
|
||||
s = s.replace(old, new)
|
||||
report.append(f"{rel}: заменено {n}x {old.strip().splitlines()[0][:60]}")
|
||||
p.write_text(s, encoding="utf-8", newline="\n")
|
||||
return orig != s
|
||||
|
||||
|
||||
DB_CTX = '''@contextlib.contextmanager
|
||||
def db():
|
||||
"""Соединение с гарантированным close(): `with conn()` коммитит, но не закрывает,
|
||||
и хендлы копятся (замер: 200 вызовов == +76 handles)."""
|
||||
c = conn()
|
||||
try:
|
||||
yield c
|
||||
c.commit()
|
||||
except Exception:
|
||||
c.rollback()
|
||||
raise
|
||||
finally:
|
||||
c.close()
|
||||
|
||||
|
||||
'''
|
||||
|
||||
# ---------- store.py ----------
|
||||
p = ROOT / "omni/store.py"
|
||||
s = p.read_text(encoding="utf-8")
|
||||
s = s.replace('"""Склад: sqlite WAL + FTS5. Таблицы items, runs; дедуп по ключу и simhash-полосам."""\n',
|
||||
'"""Склад: sqlite WAL + FTS5. Таблицы items, runs; дедуп по ключу и simhash-полосам."""\nimport contextlib\n', 1)
|
||||
anchor = "def _signed(h):"
|
||||
assert anchor in s
|
||||
s = s.replace(anchor, DB_CTX + anchor, 1)
|
||||
n = s.count("with conn() as c:")
|
||||
s = s.replace("with conn() as c:", "with db() as c:")
|
||||
report.append(f"store.py: db() добавлен, {n} вызовов переведены на close()")
|
||||
|
||||
s = s.replace('''def _popcount(x):
|
||||
return bin(x).count("1")''',
|
||||
'''def _popcount(x):
|
||||
try:
|
||||
return x.bit_count() # py3.10+, быстрее bin().count("1")
|
||||
except AttributeError:
|
||||
return bin(x).count("1")''')
|
||||
|
||||
old_near = ''' h = _unsigned(h)
|
||||
for b in range(BANDS):
|
||||
band = (h >> (16 * b)) & MASK
|
||||
rows = c.execute(
|
||||
"SELECT i.h FROM bands b JOIN items i ON i.id=b.item_id "
|
||||
"WHERE b.band=? LIMIT 200", (b,)).fetchall()
|
||||
for (other,) in rows:
|
||||
if _popcount(_unsigned(other) ^ h) <= max_dist:
|
||||
return True
|
||||
return False'''
|
||||
new_near = ''' h = _unsigned(h)
|
||||
seen = set()
|
||||
for b in range(BANDS):
|
||||
band = (h >> (16 * b)) & MASK
|
||||
rows = c.execute(
|
||||
"SELECT i.id, i.h FROM bands b JOIN items i ON i.id=b.item_id "
|
||||
"WHERE b.band=?", (b,)).fetchall()
|
||||
for iid, other in rows:
|
||||
if iid in seen:
|
||||
continue
|
||||
seen.add(iid)
|
||||
if _popcount(_unsigned(other) ^ h) <= max_dist:
|
||||
return True
|
||||
return False'''
|
||||
assert old_near in s, "near_dup шаблон не найден"
|
||||
s = s.replace(old_near, new_near)
|
||||
report.append("store.py: _near_dup без LIMIT 200 (полный проход по полосе)")
|
||||
p.write_text(s, encoding="utf-8", newline="\n")
|
||||
|
||||
# ---------- source_state.py ----------
|
||||
patch("omni/source_state.py", [
|
||||
(' source_custom (name, category, kind, url, adapter, tags, limit, detail, added)\n"""\n',
|
||||
' source_custom (name, category, kind, url, adapter, tags, limit, detail, added)\n"""\nimport contextlib\n'),
|
||||
("def flags():", DB_CTX + "def flags():"),
|
||||
("with conn() as c:", "with db() as c:"),
|
||||
])
|
||||
|
||||
# ---------- net.py: троттлинг ----------
|
||||
patch("omni/net.py", [
|
||||
('''def _throttle(host):
|
||||
delay = float(_cfg.get("per_host_delay", 0.6))
|
||||
with _lock:
|
||||
prev = _last.get(host, 0)
|
||||
wait = delay - (time.time() - prev)
|
||||
if wait > 0:
|
||||
time.sleep(wait)
|
||||
with _lock:
|
||||
_last[host] = time.time()''',
|
||||
'''def _throttle(host):
|
||||
"""Слот резервируется под локом ДО сна: иначе N потоков проходят одновременно.
|
||||
Замер до фикса: 24 вызова на хост при 0.3с == 0.9с вместо 6.9с."""
|
||||
delay = float(_cfg.get("per_host_delay", 0.6))
|
||||
with _lock:
|
||||
now = time.time()
|
||||
prev = _last.get(host, 0)
|
||||
wait = max(0.0, delay - (now - prev))
|
||||
_last[host] = now + wait
|
||||
if wait > 0:
|
||||
time.sleep(wait)'''),
|
||||
])
|
||||
|
||||
# ---------- ai.py: не спать в пустоту ----------
|
||||
patch("omni/ai.py", [
|
||||
(''' except Exception:
|
||||
time.sleep(1.5 + attempt)''',
|
||||
''' except Exception:
|
||||
if attempt == 0: # после последней попытки спим в пустоту
|
||||
time.sleep(1.5)'''),
|
||||
])
|
||||
|
||||
# ---------- bot.py ----------
|
||||
patch("omni/bot.py", [
|
||||
(" new += store.add(items)",
|
||||
" n_new, _dup = store.add(items)\n new += n_new"),
|
||||
(" chunk = fresh[-per_chat:]",
|
||||
" chunk = fresh[:per_chat] # раньше уходили САМЫЕ СТАРЫЕ из окна"),
|
||||
('''CMDS = {"/start": cmd_start,''',
|
||||
'''READONLY_CMDS = {"/start", "/help", "/new", "/abuse", "/find", "/src", "/stats"}
|
||||
|
||||
CMDS = {"/start": cmd_start,'''),
|
||||
(''' if OWNERS and uid not in OWNERS:
|
||||
return
|
||||
if not OWNERS:
|
||||
OWNERS.add(uid) # первый написавший становится владельцем до рестарта
|
||||
parts = text.split(None, 1)
|
||||
cmd = parts[0].lower().split("@")[0]
|
||||
arg = parts[1] if len(parts) > 1 else ""''',
|
||||
''' parts = text.split(None, 1)
|
||||
cmd = parts[0].lower().split("@")[0]
|
||||
arg = parts[1] if len(parts) > 1 else ""
|
||||
if OWNERS and uid not in OWNERS:
|
||||
return
|
||||
if not OWNERS and cmd not in READONLY_CMDS:
|
||||
# раньше первый написавший становился владельцем: любой, кто нашёл бота
|
||||
return _send(chat_id, "read-only: задай OMNI_BOT_OWNERS и перезапусти бота")'''),
|
||||
("Доступ: OMNI_BOT_OWNERS=7448683285,123 (если пусто — только первый написавший)",
|
||||
"Доступ: OMNI_BOT_OWNERS=<id>,<id> (пусто = read-only: /new /abuse /find /src /stats)"),
|
||||
])
|
||||
|
||||
# ---------- sources.py: дедуп по url ----------
|
||||
p = ROOT / "omni/sources.py"
|
||||
s = p.read_text(encoding="utf-8")
|
||||
if "def all_sources():" in s:
|
||||
s2 = s.replace("def all_sources():", '''def all_sources():
|
||||
"""Реестр без дублей по url: одинаковые площадки приходили из разных наборов."""''', 1)
|
||||
# добавляем дедуп в возврат: ищем return внутри all_sources
|
||||
idx = s2.index("def all_sources():")
|
||||
tail = s2[idx:]
|
||||
if "return _dedup(" in tail:
|
||||
report.append("sources.py: дедуп уже есть")
|
||||
else:
|
||||
# оборачиваем последний return списка
|
||||
import re
|
||||
m = list(re.finditer(r"\n return (\[\]|\w+|\w+ \+|.*?)\n(?=\n|$)", tail))
|
||||
report.append(f"sources.py: return-кандидатов {len(m)}")
|
||||
# используем _dedup через хелпер, определённый перед all_sources
|
||||
s2 = s2.replace("def all_sources():", '''def _dedup(items):
|
||||
seen, out = set(), []
|
||||
for it in items:
|
||||
key = (it.url or "").rstrip("/")
|
||||
if key and key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
out.append(it)
|
||||
return out
|
||||
|
||||
|
||||
def all_sources():''', 1)
|
||||
s2 = re.sub(r"(\n return )(?!_dedup)(\[[^\n]*\]|\w+(?: \+ \w+)*)\n", r"\1_dedup(\2)\n", s2, count=1)
|
||||
report.append("sources.py: _dedup + обёртка return")
|
||||
s = s2
|
||||
p.write_text(s, encoding="utf-8", newline="\n")
|
||||
|
||||
print("\n".join(report))
|
||||
Reference in New Issue
Block a user