refactor(backend): extract shared helper modules per architecture rules

- Add backend/ai/utils.py — parse_classification, parse_suggestions, strip_code_fences
  shared by all AI providers; removes duplicated private functions from
  anthropic_provider.py and openai_provider.py
- Add backend/deps/utils.py — get_client_ip, parse_uuid request-parsing helpers;
  removes local _ip() variants from admin.py, auth.py, shares.py, folders.py
- Add backend/storage/exceptions.py — canonical CloudConnectionError definition;
  all routers and backends import from here instead of redefining
- Move validate_password_strength to backend/services/auth.py; removes duplicated
  _validate_password_strength from admin.py and auth.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-06-02 16:10:35 +02:00
parent 89f8d5a654
commit a548266461
14 changed files with 171 additions and 232 deletions
+3 -37
View File
@@ -1,7 +1,6 @@
import json
import re
import anthropic
from ai.base import AIProvider, ClassificationResult
from ai.utils import parse_classification, parse_suggestions
MAX_AI_CHARS = 8_000
@@ -33,7 +32,7 @@ class AnthropicProvider(AIProvider):
messages=[{"role": "user", "content": user_msg}],
)
raw = response.content[0].text
return _parse_classification(raw)
return parse_classification(raw)
async def suggest_topics(
self,
@@ -53,7 +52,7 @@ class AnthropicProvider(AIProvider):
messages=[{"role": "user", "content": user_msg}],
)
raw = response.content[0].text
return _parse_suggestions(raw)
return parse_suggestions(raw)
async def health_check(self) -> bool:
try:
@@ -68,36 +67,3 @@ class AnthropicProvider(AIProvider):
return False
def _strip_code_fences(text: str) -> str:
text = re.sub(r"```(?:json)?\s*", "", text)
text = re.sub(r"```", "", text)
return text.strip()
def _parse_classification(raw: str) -> ClassificationResult:
raw = _strip_code_fences(raw)
# Try to find JSON object
match = re.search(r"\{.*\}", raw, re.DOTALL)
if match:
try:
data = json.loads(match.group())
return ClassificationResult(
topics=data.get("assigned_topics", []),
suggested_new_topics=data.get("new_topic_suggestions", []),
reasoning=data.get("reasoning", ""),
)
except json.JSONDecodeError:
pass
return ClassificationResult()
def _parse_suggestions(raw: str) -> list[str]:
raw = _strip_code_fences(raw)
match = re.search(r"\{.*\}", raw, re.DOTALL)
if match:
try:
data = json.loads(match.group())
return data.get("suggested_topics", [])
except json.JSONDecodeError:
pass
return []
+3 -36
View File
@@ -1,7 +1,6 @@
import json
import re
from openai import AsyncOpenAI
from ai.base import AIProvider, ClassificationResult
from ai.utils import parse_classification, parse_suggestions
MAX_AI_CHARS = 8_000
@@ -35,7 +34,7 @@ class OpenAIProvider(AIProvider):
],
)
raw = response.choices[0].message.content or ""
return _parse_classification(raw)
return parse_classification(raw)
async def suggest_topics(
self,
@@ -56,7 +55,7 @@ class OpenAIProvider(AIProvider):
],
)
raw = response.choices[0].message.content or ""
return _parse_suggestions(raw)
return parse_suggestions(raw)
async def health_check(self) -> bool:
try:
@@ -70,35 +69,3 @@ class OpenAIProvider(AIProvider):
return False
def _strip_code_fences(text: str) -> str:
text = re.sub(r"```(?:json)?\s*", "", text)
text = re.sub(r"```", "", text)
return text.strip()
def _parse_classification(raw: str) -> ClassificationResult:
raw = _strip_code_fences(raw)
match = re.search(r"\{.*\}", raw, re.DOTALL)
if match:
try:
data = json.loads(match.group())
return ClassificationResult(
topics=data.get("assigned_topics", []),
suggested_new_topics=data.get("new_topic_suggestions", []),
reasoning=data.get("reasoning", ""),
)
except json.JSONDecodeError:
pass
return ClassificationResult()
def _parse_suggestions(raw: str) -> list[str]:
raw = _strip_code_fences(raw)
match = re.search(r"\{.*\}", raw, re.DOTALL)
if match:
try:
data = json.loads(match.group())
return data.get("suggested_topics", [])
except json.JSONDecodeError:
pass
return []
+51
View File
@@ -0,0 +1,51 @@
"""Shared AI response parsing utilities — used by all provider implementations."""
from __future__ import annotations
import json
import re
from ai.base import ClassificationResult
def strip_code_fences(text: str) -> str:
"""Remove markdown code fences (```json ... ```) from *text*."""
text = re.sub(r"```(?:json)?\s*", "", text)
text = re.sub(r"```", "", text)
return text.strip()
def parse_classification(raw: str) -> ClassificationResult:
"""Parse a classification JSON response into a ClassificationResult.
Tolerates markdown code fences and extracts the first JSON object found.
Returns an empty ClassificationResult on any parse failure.
"""
raw = strip_code_fences(raw)
match = re.search(r"\{.*\}", raw, re.DOTALL)
if match:
try:
data = json.loads(match.group())
return ClassificationResult(
topics=data.get("assigned_topics", []),
suggested_new_topics=data.get("new_topic_suggestions", []),
reasoning=data.get("reasoning", ""),
)
except json.JSONDecodeError:
pass
return ClassificationResult()
def parse_suggestions(raw: str) -> list[str]:
"""Parse a topic-suggestion JSON response into a list of topic name strings.
Tolerates markdown code fences. Returns an empty list on parse failure.
"""
raw = strip_code_fences(raw)
match = re.search(r"\{.*\}", raw, re.DOTALL)
if match:
try:
data = json.loads(match.group())
return data.get("suggested_topics", [])
except json.JSONDecodeError:
pass
return []