api fixes. CHAT container NEEDS ATTENTION
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
2025-08-08 22:51:21 +09:00
parent 32f7b5276f
commit 7ac2defcc0
18 changed files with 516 additions and 38 deletions

View File

@@ -191,3 +191,94 @@
2025-08-08 21:56:21 | DEBUG | api_e2e | HTTP GET http://localhost:8080/profiles/v1/profiles/me | headers={Authorization: Bearer eyJhbGciOiJI...} | body={}
2025-08-08 21:56:21 | DEBUG | api_e2e | ← 403 in 2 ms | body={"detail":"Not authenticated"}
2025-08-08 21:56:21 | ERROR | api_e2e | profiles/me unexpected status 403, expected [200, 404]; body={"detail":"Not authenticated"}
2025-08-08 22:21:56 | INFO | api_e2e | === API E2E START ===
2025-08-08 22:21:56 | INFO | api_e2e | BASE_URL=http://localhost:8080 clients=2 domain=agency.dev
2025-08-08 22:21:56 | INFO | api_e2e | Waiting gateway/auth health: http://localhost:8080/auth/health
2025-08-08 22:21:56 | DEBUG | api_e2e | HTTP GET http://localhost:8080/auth/health | headers={Authorization: Bearer } | body={}
2025-08-08 22:21:59 | DEBUG | api_e2e | ← 502 in 3056 ms | body=<html>
<head><title>502 Bad Gateway</title></head>
<body>
<center><h1>502 Bad Gateway</h1></center>
<hr><center>nginx/1.29.0</center>
</body>
</html>
2025-08-08 22:21:59 | ERROR | api_e2e | gateway/auth/health unexpected status 502, expected [200]; body=<html>
<head><title>502 Bad Gateway</title></head>
<body>
<center><h1>502 Bad Gateway</h1></center>
<hr><center>nginx/1.29.0</center>
</body>
</html>
2025-08-08 22:22:00 | DEBUG | api_e2e | HTTP GET http://localhost:8080/auth/health | headers={Authorization: Bearer } | body={}
2025-08-08 22:22:04 | DEBUG | api_e2e | ← 502 in 3095 ms | body=<html>
<head><title>502 Bad Gateway</title></head>
<body>
<center><h1>502 Bad Gateway</h1></center>
<hr><center>nginx/1.29.0</center>
</body>
</html>
2025-08-08 22:22:04 | ERROR | api_e2e | gateway/auth/health unexpected status 502, expected [200]; body=<html>
<head><title>502 Bad Gateway</title></head>
<body>
<center><h1>502 Bad Gateway</h1></center>
<hr><center>nginx/1.29.0</center>
</body>
</html>
2025-08-08 22:22:05 | DEBUG | api_e2e | HTTP GET http://localhost:8080/auth/health | headers={Authorization: Bearer } | body={}
2025-08-08 22:31:43 | INFO | api_e2e | === API E2E START ===
2025-08-08 22:31:43 | INFO | api_e2e | BASE_URL=http://localhost:8080 clients=2 domain=agency.dev
2025-08-08 22:31:43 | INFO | api_e2e | Waiting gateway/auth health: http://localhost:8080/auth/health
2025-08-08 22:31:43 | DEBUG | api_e2e | HTTP GET http://localhost:8080/auth/health | headers={Authorization: Bearer } | body={}
2025-08-08 22:31:43 | DEBUG | api_e2e | ← 200 in 3 ms | body={"status":"ok","service":"auth"}
2025-08-08 22:31:43 | INFO | api_e2e | gateway/auth is healthy
2025-08-08 22:31:43 | INFO | api_e2e | Waiting profiles health: http://localhost:8080/profiles/health
2025-08-08 22:31:43 | DEBUG | api_e2e | HTTP GET http://localhost:8080/profiles/health | headers={Authorization: Bearer } | body={}
2025-08-08 22:31:43 | DEBUG | api_e2e | ← 200 in 3 ms | body={"status":"ok","service":"profiles"}
2025-08-08 22:31:43 | INFO | api_e2e | profiles is healthy
2025-08-08 22:31:43 | INFO | api_e2e | Waiting match health: http://localhost:8080/match/health
2025-08-08 22:31:43 | DEBUG | api_e2e | HTTP GET http://localhost:8080/match/health | headers={Authorization: Bearer } | body={}
2025-08-08 22:31:43 | DEBUG | api_e2e | ← 200 in 2 ms | body={"status":"ok","service":"match"}
2025-08-08 22:31:43 | INFO | api_e2e | match is healthy
2025-08-08 22:31:43 | INFO | api_e2e | Waiting chat health: http://localhost:8080/chat/health
2025-08-08 22:31:43 | DEBUG | api_e2e | HTTP GET http://localhost:8080/chat/health | headers={Authorization: Bearer } | body={}
2025-08-08 22:31:43 | DEBUG | api_e2e | ← 200 in 2 ms | body={"status":"ok","service":"chat"}
2025-08-08 22:31:43 | INFO | api_e2e | chat is healthy
2025-08-08 22:31:43 | INFO | api_e2e | Waiting payments health: http://localhost:8080/payments/health
2025-08-08 22:31:43 | DEBUG | api_e2e | HTTP GET http://localhost:8080/payments/health | headers={Authorization: Bearer } | body={}
2025-08-08 22:31:43 | DEBUG | api_e2e | ← 200 in 2 ms | body={"status":"ok","service":"payments"}
2025-08-08 22:31:43 | INFO | api_e2e | payments is healthy
2025-08-08 22:31:43 | DEBUG | api_e2e | HTTP POST http://localhost:8080/auth/v1/token | headers={Authorization: Bearer } | body={'email': 'admin+1754659903.xaji0y@agency.dev', 'password': '***hidden***'}
2025-08-08 22:31:43 | DEBUG | api_e2e | ← 401 in 4 ms | body={"detail":"Invalid credentials"}
2025-08-08 22:31:43 | ERROR | api_e2e | login unexpected status 401, expected [200]; body={"detail":"Invalid credentials"}
2025-08-08 22:31:43 | INFO | api_e2e | Login failed for admin+1754659903.xaji0y@agency.dev: login unexpected status 401, expected [200]; body={"detail":"Invalid credentials"}; will try register
2025-08-08 22:31:43 | DEBUG | api_e2e | HTTP POST http://localhost:8080/auth/v1/register | headers={Authorization: Bearer } | body={'email': 'admin+1754659903.xaji0y@agency.dev', 'password': '***hidden***', 'full_name': 'Kimberly Banks', 'role': 'ADMIN'}
2025-08-08 22:31:44 | DEBUG | api_e2e | ← 201 in 227 ms | body={"id":"caf24a32-42bd-491e-9ff3-31e2eb0211ed","email":"admin+1754659903.xaji0y@agency.dev","full_name":"Kimberly Banks","role":"ADMIN","is_active":true}
2025-08-08 22:31:44 | DEBUG | api_e2e | HTTP POST http://localhost:8080/auth/v1/token | headers={Authorization: Bearer } | body={'email': 'admin+1754659903.xaji0y@agency.dev', 'password': '***hidden***'}
2025-08-08 22:31:44 | DEBUG | api_e2e | ← 200 in 212 ms | body={"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjYWYyNGEzMi00MmJkLTQ5MWUtOWZmMy0zMWUyZWIwMjExZWQiLCJlbWFpbCI6ImFkbWluKzE3NTQ2NTk5MDMueGFqaTB5QGFnZW5jeS5kZXYiLCJyb2xlIjoiQURNSU4iLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzU0NjYwODA0fQ.TBg4_Xu2js-yD6aIjx-BAt3n8MJtM4K5Ck1Yc-ZG8sg","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjYWYyNGEzMi00MmJkLTQ5MWUtOWZmMy0zMWUyZWIwMjExZWQiLCJlbWFpbCI6ImFkbWluKzE3NTQ2NTk5MDMueGFqaTB5QGFnZW5jeS5kZXYiLCJyb2xlIjoiQURNSU4iLCJ0eXBlIjoicmVmcmVzaCIsImV4cCI6MTc1NzI1MTkwNH0.W0AsJT8t9QQTZhKKTiKk0Q-pZtpnCxp_Kw75h1f7yJA","token_type":"bearer"}
2025-08-08 22:31:44 | INFO | api_e2e | Registered+Login OK: admin+1754659903.xaji0y@agency.dev -> caf24a32-42bd-491e-9ff3-31e2eb0211ed
2025-08-08 22:31:44 | DEBUG | api_e2e | HTTP POST http://localhost:8080/auth/v1/token | headers={Authorization: Bearer } | body={'email': 'user1+1754659904.6dpbhs@agency.dev', 'password': '***hidden***'}
2025-08-08 22:31:44 | DEBUG | api_e2e | ← 401 in 4 ms | body={"detail":"Invalid credentials"}
2025-08-08 22:31:44 | ERROR | api_e2e | login unexpected status 401, expected [200]; body={"detail":"Invalid credentials"}
2025-08-08 22:31:44 | INFO | api_e2e | Login failed for user1+1754659904.6dpbhs@agency.dev: login unexpected status 401, expected [200]; body={"detail":"Invalid credentials"}; will try register
2025-08-08 22:31:44 | DEBUG | api_e2e | HTTP POST http://localhost:8080/auth/v1/register | headers={Authorization: Bearer } | body={'email': 'user1+1754659904.6dpbhs@agency.dev', 'password': '***hidden***', 'full_name': 'Jose Meyer', 'role': 'CLIENT'}
2025-08-08 22:31:44 | DEBUG | api_e2e | ← 201 in 217 ms | body={"id":"f7f05b43-00f6-436d-ab66-6e81c5ccbc33","email":"user1+1754659904.6dpbhs@agency.dev","full_name":"Jose Meyer","role":"CLIENT","is_active":true}
2025-08-08 22:31:44 | DEBUG | api_e2e | HTTP POST http://localhost:8080/auth/v1/token | headers={Authorization: Bearer } | body={'email': 'user1+1754659904.6dpbhs@agency.dev', 'password': '***hidden***'}
2025-08-08 22:31:44 | DEBUG | api_e2e | ← 200 in 212 ms | body={"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmN2YwNWI0My0wMGY2LTQzNmQtYWI2Ni02ZTgxYzVjY2JjMzMiLCJlbWFpbCI6InVzZXIxKzE3NTQ2NTk5MDQuNmRwYmhzQGFnZW5jeS5kZXYiLCJyb2xlIjoiQ0xJRU5UIiwidHlwZSI6ImFjY2VzcyIsImV4cCI6MTc1NDY2MDgwNH0.CJA6gG8WJdEPMSQSKLAPBIRY3JMA34TGjveNICF_d-I","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmN2YwNWI0My0wMGY2LTQzNmQtYWI2Ni02ZTgxYzVjY2JjMzMiLCJlbWFpbCI6InVzZXIxKzE3NTQ2NTk5MDQuNmRwYmhzQGFnZW5jeS5kZXYiLCJyb2xlIjoiQ0xJRU5UIiwidHlwZSI6InJlZnJlc2giLCJleHAiOjE3NTcyNTE5MDR9.aYdrjuY5smBsgrF_6EtP83P2d_700yVIdlWDbPXM5DU","token_type":"bearer"}
2025-08-08 22:31:44 | INFO | api_e2e | Registered+Login OK: user1+1754659904.6dpbhs@agency.dev -> f7f05b43-00f6-436d-ab66-6e81c5ccbc33
2025-08-08 22:31:44 | DEBUG | api_e2e | HTTP POST http://localhost:8080/auth/v1/token | headers={Authorization: Bearer } | body={'email': 'user2+1754659904.ahxthv@agency.dev', 'password': '***hidden***'}
2025-08-08 22:31:44 | DEBUG | api_e2e | ← 401 in 3 ms | body={"detail":"Invalid credentials"}
2025-08-08 22:31:44 | ERROR | api_e2e | login unexpected status 401, expected [200]; body={"detail":"Invalid credentials"}
2025-08-08 22:31:44 | INFO | api_e2e | Login failed for user2+1754659904.ahxthv@agency.dev: login unexpected status 401, expected [200]; body={"detail":"Invalid credentials"}; will try register
2025-08-08 22:31:44 | DEBUG | api_e2e | HTTP POST http://localhost:8080/auth/v1/register | headers={Authorization: Bearer } | body={'email': 'user2+1754659904.ahxthv@agency.dev', 'password': '***hidden***', 'full_name': 'Teresa Mckenzie', 'role': 'CLIENT'}
2025-08-08 22:31:45 | DEBUG | api_e2e | ← 201 in 225 ms | body={"id":"540546d6-563c-452c-b4bc-b0a0ce977b50","email":"user2+1754659904.ahxthv@agency.dev","full_name":"Teresa Mckenzie","role":"CLIENT","is_active":true}
2025-08-08 22:31:45 | DEBUG | api_e2e | HTTP POST http://localhost:8080/auth/v1/token | headers={Authorization: Bearer } | body={'email': 'user2+1754659904.ahxthv@agency.dev', 'password': '***hidden***'}
2025-08-08 22:31:45 | DEBUG | api_e2e | ← 200 in 212 ms | body={"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1NDA1NDZkNi01NjNjLTQ1MmMtYjRiYy1iMGEwY2U5NzdiNTAiLCJlbWFpbCI6InVzZXIyKzE3NTQ2NTk5MDQuYWh4dGh2QGFnZW5jeS5kZXYiLCJyb2xlIjoiQ0xJRU5UIiwidHlwZSI6ImFjY2VzcyIsImV4cCI6MTc1NDY2MDgwNX0.wYH9Tf_Q1oz0u_hxzyLYrOmAur_RxJyySxkJgxXOIqY","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1NDA1NDZkNi01NjNjLTQ1MmMtYjRiYy1iMGEwY2U5NzdiNTAiLCJlbWFpbCI6InVzZXIyKzE3NTQ2NTk5MDQuYWh4dGh2QGFnZW5jeS5kZXYiLCJyb2xlIjoiQ0xJRU5UIiwidHlwZSI6InJlZnJlc2giLCJleHAiOjE3NTcyNTE5MDV9.TsULoLRT4s40aOD0IUE6uamDyKAvSHiw9AW9EfacVeo","token_type":"bearer"}
2025-08-08 22:31:45 | INFO | api_e2e | Registered+Login OK: user2+1754659904.ahxthv@agency.dev -> 540546d6-563c-452c-b4bc-b0a0ce977b50
2025-08-08 22:31:45 | INFO | api_e2e | [1/3] Ensure profile for admin+1754659903.xaji0y@agency.dev (role=ADMIN)
2025-08-08 22:31:45 | DEBUG | api_e2e | HTTP GET http://localhost:8080/profiles/v1/profiles/me | headers={Authorization: Bearer eyJhbGciOiJI...} | body={}
2025-08-08 22:31:45 | DEBUG | api_e2e | ← 403 in 2 ms | body={"detail":"Not authenticated"}
2025-08-08 22:31:45 | ERROR | api_e2e | profiles/me unexpected status 403, expected [200, 404]; body={"detail":"Not authenticated"}

View File

@@ -0,0 +1,32 @@
set -euo pipefail
FILE="services/auth/src/app/schemas/user.py"
[ -f "$FILE" ] || { echo "Not found: $FILE"; exit 1; }
python3 - "$FILE" <<'PY'
from pathlib import Path
import re, sys
p = Path(sys.argv[1]); s = p.read_text()
if "from uuid import UUID" not in s:
s = "from uuid import UUID\n" + s
s = re.sub(r'(\bid\s*:\s*)str', r'\1UUID', s)
s = re.sub(r'(\buser_id\s*:\s*)str', r'\1UUID', s)
# Включаем from_attributes (pydantic v2) или orm_mode (v1) для моделей ответа
if "model_config" not in s and "orm_mode" not in s:
s = re.sub(
r"class\s+\w+Out\s*\((?:.|\n)*?\):",
lambda m: m.group(0) +
"\n try:\n from pydantic import ConfigDict\n model_config = ConfigDict(from_attributes=True)\n"
" except Exception:\n class Config:\n orm_mode = True\n",
s
)
p.write_text(s)
print("[auth] Patched:", p)
PY
echo "[auth] Rebuild & restart…"
docker compose build auth
docker compose restart auth

117
scripts/fix_chat_uuid_schemas.sh Executable file
View File

@@ -0,0 +1,117 @@
set -euo pipefail
svc_dir="services/chat/src/app/schemas"
test -d "$svc_dir" || { echo "Not found: $svc_dir"; exit 1; }
python3 - <<'PY'
import re
from pathlib import Path
root = Path("services/chat/src/app/schemas")
def ensure_future_and_uuid(text: str) -> str:
lines = text.splitlines(keepends=True)
changed = False
# Удалим дубли future-импорта
fut_pat = re.compile(r'^\s*from\s+__future__\s+import\s+annotations\s*(#.*)?$\n?', re.M)
if fut_pat.search(text):
lines = [ln for ln in lines if not fut_pat.match(ln)]
changed = True
# Вставим future после докстринга (если он есть)
i = 0
while i < len(lines) and (lines[i].strip()=="" or lines[i].lstrip().startswith("#!") or re.match(r'^\s*#.*coding[:=]', lines[i])):
i += 1
insert_at = i
if i < len(lines) and re.match(r'^\s*[rubfRUBF]*[\'"]{3}', lines[i]): # докстринг
q = '"""' if '"""' in lines[i] else "'''"
j = i
while j < len(lines):
if q in lines[j] and j != i:
j += 1
break
j += 1
insert_at = min(j, len(lines))
if not any("from __future__ import annotations" in ln for ln in lines):
lines.insert(insert_at, "from __future__ import annotations\n")
changed = True
txt = "".join(lines)
if "from uuid import UUID" not in txt:
# вставим сразу после future
# найдём позицию future
pos = next((k for k,ln in enumerate(lines) if "from __future__ import annotations" in ln), 0)
lines.insert(pos+1, "from uuid import UUID\n")
changed = True
return "".join(lines), changed
def set_config(text: str) -> str:
# Pydantic v2: добавим ConfigDict и model_config, если их нет
if "ConfigDict" not in text:
text = re.sub(r'from pydantic import ([^\n]+)',
lambda m: m.group(0).replace(m.group(1), (m.group(1) + ", ConfigDict").replace(", ConfigDict,"," , ConfigDict,")),
text, count=1) if "from pydantic import" in text else ("from pydantic import BaseModel, ConfigDict\n" + text)
# В каждую модель, наследующую BaseModel, которая заканчивается на Read/Out, добавим from_attributes
def inject_cfg(block: str) -> str:
# если уже есть model_config/orm_mode — не трогаем
if "model_config" in block or "class Config" in block:
return block
# аккуратно добавим model_config сверху класса
header, body = block.split(":", 1)
return header + ":\n model_config = ConfigDict(from_attributes=True)\n" + body
# Разобьём по классам
parts = re.split(r'(\nclass\s+[A-Za-z0-9_]+\s*\([^\)]*BaseModel[^\)]*\)\s*:)', text)
if len(parts) > 1:
out = [parts[0]]
for i in range(1, len(parts), 2):
head = parts[i]
body = parts[i+1]
m = re.search(r'class\s+([A-Za-z0-9_]+)\s*\(', head)
name = m.group(1) if m else ""
if name.endswith(("Read","Out")):
out.append(inject_cfg(head + body))
else:
out.append(head + body)
text = "".join(out)
return text
def fix_uuid_fields(text: str) -> str:
# Преобразуем только в классах Read/Out
def repl_in_class(cls_text: str) -> str:
# заменяем аннотации полей
repl = {
r'(^\s*)(id)\s*:\s*str(\b)': r'\1\2: UUID\3',
r'(^\s*)(pair_id)\s*:\s*str(\b)': r'\1\2: UUID\3',
r'(^\s*)(room_id)\s*:\s*str(\b)': r'\1\2: UUID\3',
r'(^\s*)(sender_id)\s*:\s*str(\b)': r'\1\2: UUID\3',
r'(^\s*)(user_id)\s*:\s*str(\b)': r'\1\2: UUID\3',
}
for pat, sub in repl.items():
cls_text = re.sub(pat, sub, cls_text, flags=re.M)
return cls_text
patt = re.compile(r'(\nclass\s+[A-Za-z0-9_]+(Read|Out)\s*\([^\)]*BaseModel[^\)]*\)\s*:\n(?:\s+.*\n)+)', re.M)
return patt.sub(lambda m: repl_in_class(m.group(1)), text)
changed_any = False
for p in root.glob("**/*.py"):
txt = p.read_text()
new, c1 = ensure_future_and_uuid(txt)
new = set_config(new)
new2 = fix_uuid_fields(new)
if new2 != txt:
p.write_text(new2)
print("fixed:", p)
changed_any = True
print("DONE" if changed_any else "NO-CHANGES")
PY
echo "[docker] rebuild & restart chat…"
docker compose build --no-cache chat >/dev/null
docker compose restart chat

93
scripts/fix_future_imports.sh Executable file
View File

@@ -0,0 +1,93 @@
set -euo pipefail
python3 - <<'PY'
import re
from pathlib import Path
def fix_file(p: Path) -> bool:
s = p.read_text()
lines = s.splitlines(keepends=True)
changed = False
# собрать все строки future-импортов и убрать их
fut_pat = re.compile(r'^\s*from\s+__future__\s+import\s+annotations\s*(#.*)?$\n?', re.M)
if fut_pat.search(s):
new_lines = []
for ln in lines:
if not fut_pat.match(ln):
new_lines.append(ln)
lines = new_lines
changed = True
# определить позицию вставки future (сразу после верхнего докстринга, если он есть)
i = 0
# пропускаем пустые строки и комментарии/кодировки/шейбанг
while i < len(lines) and (
lines[i].strip() == "" or
lines[i].lstrip().startswith("#!") or
re.match(r'^\s*#.*coding[:=]', lines[i])
):
i += 1
insert_at = i
# если на вершине модульный докстринг ("""...""" или '''...''')
if i < len(lines) and re.match(r'^\s*[rubfRUBF]*[\'"]{3}', lines[i]):
quote = '"""' if '"""' in lines[i] else "'''"
j = i
# ищем закрывающую кавычку
while j < len(lines):
if quote in lines[j] and j != i:
j += 1
break
j += 1
insert_at = min(j, len(lines))
# вставляем future-импорт
future_line = "from __future__ import annotations\n"
if not any(l.strip().startswith("from __future__ import annotations") for l in lines):
lines.insert(insert_at, future_line)
changed = True
# обеспечить корректное положение "from uuid import UUID":
txt = "".join(lines)
uuid_pat = re.compile(r'^\s*from\s+uuid\s+import\s+UUID\s*(#.*)?$\n?', re.M)
has_uuid = uuid_pat.search(txt) is not None
if has_uuid:
# удалим все вхождения, потом вставим одно — сразу после future
new_lines = []
removed = False
for ln in lines:
if uuid_pat.match(ln):
removed = True
continue
new_lines.append(ln)
lines = new_lines
if removed:
lines.insert(insert_at + 1, "from uuid import UUID\n")
changed = True
else:
# импорт отсутствует — добавлять имеет смысл только если в файле встречается "UUID" как тип
if re.search(r'\bUUID\b', "".join(lines)):
lines.insert(insert_at + 1, "from uuid import UUID\n")
changed = True
if changed:
p.write_text("".join(lines))
return changed
changed_any = False
for sub in ("services/auth/src/app/schemas", "services/match/src/app/schemas", "services/profiles/src/app/schemas"):
d = Path(sub)
if not d.exists():
continue
for p in d.rglob("*.py"):
if fix_file(p):
print("fixed:", p)
changed_any = True
print("DONE" if changed_any else "NO-CHANGES")
PY
echo "[docker] rebuild & restart auth/match/profiles…"
docker compose build auth match profiles >/dev/null
docker compose restart auth match profiles

View File

@@ -0,0 +1,41 @@
set -euo pipefail
DIR="services/match/src/app/schemas"
[ -d "$DIR" ] || { echo "Not found: $DIR"; exit 1; }
python3 - "$DIR" <<'PY'
from pathlib import Path
import re, sys
d = Path(sys.argv[1])
for p in d.glob("*.py"):
s = p.read_text(); orig = s
if "class " not in s:
continue
if "from uuid import UUID" not in s:
s = "from uuid import UUID\n" + s
# самые частые поля
s = re.sub(r'(\bid\s*:\s*)str', r'\1UUID', s)
s = re.sub(r'(\buser_id_a\s*:\s*)str', r'\1UUID', s)
s = re.sub(r'(\buser_id_b\s*:\s*)str', r'\1UUID', s)
s = re.sub(r'(\buser_id\s*:\s*)str', r'\1UUID', s)
# включаем from_attributes / orm_mode для Out/Response моделей
def patch_block(m):
block = m.group(0)
if "model_config" in block or "orm_mode" in block:
return block
return (block +
"\n try:\n from pydantic import ConfigDict\n model_config = ConfigDict(from_attributes=True)\n"
" except Exception:\n class Config:\n orm_mode = True\n")
s = re.sub(r"class\s+\w+(Out|Response)\s*\([^\)]*\)\s*:(?:\n\s+.+)+", patch_block, s)
if s != orig:
p.write_text(s)
print("[match] Patched:", p)
PY
echo "[match] Rebuild & restart…"
docker compose build match
docker compose restart match

View File

@@ -1,31 +1,120 @@
# scripts/patch_gateway_auth_header.sh
cat > scripts/patch_gateway_auth_header.sh <<'BASH'
#!/usr/bin/env bash
cat > scripts/fix_chat_uuid_schemas.sh <<'SH'
set -euo pipefail
CFG="infra/gateway/nginx.conf"
[ -f "$CFG" ] || { echo "Not found: $CFG"; exit 1; }
svc_dir="services/chat/src/app/schemas"
test -d "$svc_dir" || { echo "Not found: $svc_dir"; exit 1; }
# Грубая, но надёжная вставка proxy_set_header Authorization во все блоки location к сервисам
awk '
/location[[:space:]]+\/(auth|profiles|match|chat|payments)\//,/\}/ {
print
if ($0 ~ /proxy_pass/ && !seen_auth) {
print " proxy_set_header Authorization $http_authorization;"
print " proxy_set_header X-Forwarded-Proto $scheme;"
print " proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;"
print " proxy_set_header Host $host;"
seen_auth=1
python3 - <<'PY'
import re
from pathlib import Path
root = Path("services/chat/src/app/schemas")
def ensure_future_and_uuid(text: str) -> str:
lines = text.splitlines(keepends=True)
changed = False
# Удалим дубли future-импорта
fut_pat = re.compile(r'^\s*from\s+__future__\s+import\s+annotations\s*(#.*)?$\n?', re.M)
if fut_pat.search(text):
lines = [ln for ln in lines if not fut_pat.match(ln)]
changed = True
# Вставим future после докстринга (если он есть)
i = 0
while i < len(lines) and (lines[i].strip()=="" or lines[i].lstrip().startswith("#!") or re.match(r'^\s*#.*coding[:=]', lines[i])):
i += 1
insert_at = i
if i < len(lines) and re.match(r'^\s*[rubfRUBF]*[\'"]{3}', lines[i]): # докстринг
q = '"""' if '"""' in lines[i] else "'''"
j = i
while j < len(lines):
if q in lines[j] and j != i:
j += 1
break
j += 1
insert_at = min(j, len(lines))
if not any("from __future__ import annotations" in ln for ln in lines):
lines.insert(insert_at, "from __future__ import annotations\n")
changed = True
txt = "".join(lines)
if "from uuid import UUID" not in txt:
# вставим сразу после future
# найдём позицию future
pos = next((k for k,ln in enumerate(lines) if "from __future__ import annotations" in ln), 0)
lines.insert(pos+1, "from uuid import UUID\n")
changed = True
return "".join(lines), changed
def set_config(text: str) -> str:
# Pydantic v2: добавим ConfigDict и model_config, если их нет
if "ConfigDict" not in text:
text = re.sub(r'from pydantic import ([^\n]+)',
lambda m: m.group(0).replace(m.group(1), (m.group(1) + ", ConfigDict").replace(", ConfigDict,"," , ConfigDict,")),
text, count=1) if "from pydantic import" in text else ("from pydantic import BaseModel, ConfigDict\n" + text)
# В каждую модель, наследующую BaseModel, которая заканчивается на Read/Out, добавим from_attributes
def inject_cfg(block: str) -> str:
# если уже есть model_config/orm_mode — не трогаем
if "model_config" in block or "class Config" in block:
return block
# аккуратно добавим model_config сверху класса
header, body = block.split(":", 1)
return header + ":\n model_config = ConfigDict(from_attributes=True)\n" + body
# Разобьём по классам
parts = re.split(r'(\nclass\s+[A-Za-z0-9_]+\s*\([^\)]*BaseModel[^\)]*\)\s*:)', text)
if len(parts) > 1:
out = [parts[0]]
for i in range(1, len(parts), 2):
head = parts[i]
body = parts[i+1]
m = re.search(r'class\s+([A-Za-z0-9_]+)\s*\(', head)
name = m.group(1) if m else ""
if name.endswith(("Read","Out")):
out.append(inject_cfg(head + body))
else:
out.append(head + body)
text = "".join(out)
return text
def fix_uuid_fields(text: str) -> str:
# Преобразуем только в классах Read/Out
def repl_in_class(cls_text: str) -> str:
# заменяем аннотации полей
repl = {
r'(^\s*)(id)\s*:\s*str(\b)': r'\1\2: UUID\3',
r'(^\s*)(pair_id)\s*:\s*str(\b)': r'\1\2: UUID\3',
r'(^\s*)(room_id)\s*:\s*str(\b)': r'\1\2: UUID\3',
r'(^\s*)(sender_id)\s*:\s*str(\b)': r'\1\2: UUID\3',
r'(^\s*)(user_id)\s*:\s*str(\b)': r'\1\2: UUID\3',
}
next
}
{ print }
/\}/ { seen_auth=0 }
' "$CFG" > "$CFG.tmp" && mv "$CFG.tmp" "$CFG"
for pat, sub in repl.items():
cls_text = re.sub(pat, sub, cls_text, flags=re.M)
return cls_text
patt = re.compile(r'(\nclass\s+[A-Za-z0-9_]+(Read|Out)\s*\([^\)]*BaseModel[^\)]*\)\s*:\n(?:\s+.*\n)+)', re.M)
return patt.sub(lambda m: repl_in_class(m.group(1)), text)
changed_any = False
for p in root.glob("**/*.py"):
txt = p.read_text()
new, c1 = ensure_future_and_uuid(txt)
new = set_config(new)
new2 = fix_uuid_fields(new)
if new2 != txt:
p.write_text(new2)
print("fixed:", p)
changed_any = True
print("DONE" if changed_any else "NO-CHANGES")
PY
echo "[docker] rebuild & restart chat…"
docker compose build --no-cache chat >/dev/null
docker compose restart chat
echo "[gateway] restart..."
docker compose restart gateway
BASH
chmod +x scripts/patch_gateway_auth_header.sh
./scripts/patch_gateway_auth_header.sh

View File

@@ -0,0 +1 @@
from __future__ import annotations

View File

@@ -1,3 +1,4 @@
from __future__ import annotations
from pydantic import BaseModel
class Message(BaseModel):

View File

@@ -1,4 +1,5 @@
from __future__ import annotations
from uuid import UUID
from typing import Optional
from pydantic import BaseModel, EmailStr, ConfigDict
@@ -21,7 +22,7 @@ class UserUpdate(BaseModel):
password: Optional[str] = None
class UserRead(BaseModel):
id: str
id: UUID
email: EmailStr
full_name: Optional[str] = None
role: str

View File

@@ -0,0 +1,3 @@
from __future__ import annotations
from pydantic import BaseModel, ConfigDict
from uuid import UUID

View File

@@ -1,4 +1,5 @@
from __future__ import annotations
from uuid import UUID
from pydantic import BaseModel, ConfigDict
from typing import Optional
@@ -7,7 +8,7 @@ class RoomCreate(BaseModel):
participants: list[str] # user IDs
class RoomRead(BaseModel):
id: str
id: UUID
title: Optional[str] = None
model_config = ConfigDict(from_attributes=True)
@@ -15,8 +16,8 @@ class MessageCreate(BaseModel):
content: str
class MessageRead(BaseModel):
id: str
room_id: str
sender_id: str
id: UUID
room_id: UUID
sender_id: UUID
content: str
model_config = ConfigDict(from_attributes=True)

View File

@@ -1,4 +1,6 @@
from pydantic import BaseModel
from __future__ import annotations
from uuid import UUID
from pydantic import BaseModel, ConfigDict
class Message(BaseModel):
message: str

View File

@@ -0,0 +1 @@
from __future__ import annotations

View File

@@ -1,3 +1,5 @@
from __future__ import annotations
from uuid import UUID
from pydantic import BaseModel
class Message(BaseModel):

View File

@@ -1,10 +1,11 @@
from __future__ import annotations
from uuid import UUID
from typing import Optional
from pydantic import BaseModel, ConfigDict
class PairCreate(BaseModel):
user_id_a: str
user_id_b: str
user_id_a: UUID
user_id_b: UUID
score: Optional[float] = None
notes: Optional[str] = None
@@ -13,9 +14,9 @@ class PairUpdate(BaseModel):
notes: Optional[str] = None
class PairRead(BaseModel):
id: str
user_id_a: str
user_id_b: str
id: UUID
user_id_a: UUID
user_id_b: UUID
status: str
score: Optional[float] = None
notes: Optional[str] = None

View File

@@ -0,0 +1 @@
from __future__ import annotations

View File

@@ -1,3 +1,4 @@
from __future__ import annotations
from pydantic import BaseModel
class Message(BaseModel):

View File

@@ -1,6 +1,6 @@
from __future__ import annotations
from typing import List
from uuid import UUID
from typing import List
try:
# Pydantic v2