From 7ac2defcc058c6a2f67daa8fda20e3aa22c6eda6 Mon Sep 17 00:00:00 2001 From: "Andrey K. Choi" Date: Fri, 8 Aug 2025 22:51:21 +0900 Subject: [PATCH] api fixes. CHAT container NEEDS ATTENTION --- logs/api.log | 91 +++++++++++ scripts/fix_auth_response_model.sh | 32 ++++ scripts/fix_chat_uuid_schemas.sh | 117 +++++++++++++++ scripts/fix_future_imports.sh | 93 ++++++++++++ scripts/fix_match_response_models.sh | 41 +++++ scripts/patch.sh | 141 ++++++++++++++---- services/auth/src/app/schemas/__init__.py | 1 + services/auth/src/app/schemas/common.py | 1 + services/auth/src/app/schemas/user.py | 3 +- services/chat/src/app/schemas/__init__.py | 3 + services/chat/src/app/schemas/chat.py | 9 +- services/chat/src/app/schemas/common.py | 4 +- services/match/src/app/schemas/__init__.py | 1 + services/match/src/app/schemas/common.py | 2 + services/match/src/app/schemas/pair.py | 11 +- services/profiles/src/app/schemas/__init__.py | 1 + services/profiles/src/app/schemas/common.py | 1 + services/profiles/src/app/schemas/profile.py | 2 +- 18 files changed, 516 insertions(+), 38 deletions(-) create mode 100755 scripts/fix_auth_response_model.sh create mode 100755 scripts/fix_chat_uuid_schemas.sh create mode 100755 scripts/fix_future_imports.sh create mode 100755 scripts/fix_match_response_models.sh diff --git a/logs/api.log b/logs/api.log index 1e121d7..9ef43d2 100644 --- a/logs/api.log +++ b/logs/api.log @@ -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= +502 Bad Gateway + +

502 Bad Gateway

+
nginx/1.29.0
+ + + +2025-08-08 22:21:59 | ERROR | api_e2e | gateway/auth/health unexpected status 502, expected [200]; body= +502 Bad Gateway + +

502 Bad Gateway

+
nginx/1.29.0
+ + + +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= +502 Bad Gateway + +

502 Bad Gateway

+
nginx/1.29.0
+ + + +2025-08-08 22:22:04 | ERROR | api_e2e | gateway/auth/health unexpected status 502, expected [200]; body= +502 Bad Gateway + +

502 Bad Gateway

+
nginx/1.29.0
+ + + +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"} diff --git a/scripts/fix_auth_response_model.sh b/scripts/fix_auth_response_model.sh new file mode 100755 index 0000000..37fcb95 --- /dev/null +++ b/scripts/fix_auth_response_model.sh @@ -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 diff --git a/scripts/fix_chat_uuid_schemas.sh b/scripts/fix_chat_uuid_schemas.sh new file mode 100755 index 0000000..6fc188b --- /dev/null +++ b/scripts/fix_chat_uuid_schemas.sh @@ -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 diff --git a/scripts/fix_future_imports.sh b/scripts/fix_future_imports.sh new file mode 100755 index 0000000..a3131f5 --- /dev/null +++ b/scripts/fix_future_imports.sh @@ -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 diff --git a/scripts/fix_match_response_models.sh b/scripts/fix_match_response_models.sh new file mode 100755 index 0000000..7bb6904 --- /dev/null +++ b/scripts/fix_match_response_models.sh @@ -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 diff --git a/scripts/patch.sh b/scripts/patch.sh index e5404b1..31f69ce 100755 --- a/scripts/patch.sh +++ b/scripts/patch.sh @@ -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 - } - next - } - { print } - /\}/ { seen_auth=0 } -' "$CFG" > "$CFG.tmp" && mv "$CFG.tmp" "$CFG" +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 -echo "[gateway] restart..." -docker compose restart gateway -BASH -chmod +x scripts/patch_gateway_auth_header.sh -./scripts/patch_gateway_auth_header.sh diff --git a/services/auth/src/app/schemas/__init__.py b/services/auth/src/app/schemas/__init__.py index e69de29..9d48db4 100644 --- a/services/auth/src/app/schemas/__init__.py +++ b/services/auth/src/app/schemas/__init__.py @@ -0,0 +1 @@ +from __future__ import annotations diff --git a/services/auth/src/app/schemas/common.py b/services/auth/src/app/schemas/common.py index faa7726..c0867ef 100644 --- a/services/auth/src/app/schemas/common.py +++ b/services/auth/src/app/schemas/common.py @@ -1,3 +1,4 @@ +from __future__ import annotations from pydantic import BaseModel class Message(BaseModel): diff --git a/services/auth/src/app/schemas/user.py b/services/auth/src/app/schemas/user.py index 5e69bed..d3b6677 100644 --- a/services/auth/src/app/schemas/user.py +++ b/services/auth/src/app/schemas/user.py @@ -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 diff --git a/services/chat/src/app/schemas/__init__.py b/services/chat/src/app/schemas/__init__.py index e69de29..4f6e74c 100644 --- a/services/chat/src/app/schemas/__init__.py +++ b/services/chat/src/app/schemas/__init__.py @@ -0,0 +1,3 @@ +from __future__ import annotations +from pydantic import BaseModel, ConfigDict +from uuid import UUID diff --git a/services/chat/src/app/schemas/chat.py b/services/chat/src/app/schemas/chat.py index 48b35a2..9b79d1e 100644 --- a/services/chat/src/app/schemas/chat.py +++ b/services/chat/src/app/schemas/chat.py @@ -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) diff --git a/services/chat/src/app/schemas/common.py b/services/chat/src/app/schemas/common.py index faa7726..e379fd8 100644 --- a/services/chat/src/app/schemas/common.py +++ b/services/chat/src/app/schemas/common.py @@ -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 diff --git a/services/match/src/app/schemas/__init__.py b/services/match/src/app/schemas/__init__.py index e69de29..9d48db4 100644 --- a/services/match/src/app/schemas/__init__.py +++ b/services/match/src/app/schemas/__init__.py @@ -0,0 +1 @@ +from __future__ import annotations diff --git a/services/match/src/app/schemas/common.py b/services/match/src/app/schemas/common.py index faa7726..424302e 100644 --- a/services/match/src/app/schemas/common.py +++ b/services/match/src/app/schemas/common.py @@ -1,3 +1,5 @@ +from __future__ import annotations +from uuid import UUID from pydantic import BaseModel class Message(BaseModel): diff --git a/services/match/src/app/schemas/pair.py b/services/match/src/app/schemas/pair.py index 310798a..4c93398 100644 --- a/services/match/src/app/schemas/pair.py +++ b/services/match/src/app/schemas/pair.py @@ -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 diff --git a/services/profiles/src/app/schemas/__init__.py b/services/profiles/src/app/schemas/__init__.py index e69de29..9d48db4 100644 --- a/services/profiles/src/app/schemas/__init__.py +++ b/services/profiles/src/app/schemas/__init__.py @@ -0,0 +1 @@ +from __future__ import annotations diff --git a/services/profiles/src/app/schemas/common.py b/services/profiles/src/app/schemas/common.py index faa7726..c0867ef 100644 --- a/services/profiles/src/app/schemas/common.py +++ b/services/profiles/src/app/schemas/common.py @@ -1,3 +1,4 @@ +from __future__ import annotations from pydantic import BaseModel class Message(BaseModel): diff --git a/services/profiles/src/app/schemas/profile.py b/services/profiles/src/app/schemas/profile.py index 55d6123..77c7606 100644 --- a/services/profiles/src/app/schemas/profile.py +++ b/services/profiles/src/app/schemas/profile.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import List from uuid import UUID +from typing import List try: # Pydantic v2