支持python 3.7及以上版本,避免问题

This commit is contained in:
Von committed 2026-04-03 21:34:38 +08:00
1 parent 1edc97072c
commit 1e200715cb
18 files changed
+92 -205

No files matched your search

+2
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
from typing import Any
from src.hooks import BaseHook, HookContext, HookErrorType
+3 -1
View File
@@ -4,7 +4,9 @@
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING, Protocol
from typing import TYPE_CHECKING
from ..utils.compat import Protocol
if TYPE_CHECKING:
from ..config import ConfigManager
+1 -1
View File
@@ -4,7 +4,6 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from email.utils import parsedate_to_datetime
from typing import Any, Dict, Mapping, Optional, Sequence
@@ -12,6 +11,7 @@ from typing import Any, Dict, Mapping, Optional, Sequence
from ..application.app_context import AppContext, Logger
from ..hooks import HookErrorType
from ..repositories import AuthGroupRepository
from ..utils.compat import dataclass
from ..utils.local_time import format_local_datetime, now_local_datetime, parse_local_datetime
from .provider_config import (
AuthEntrySchema,
+1 -1
View File
@@ -4,10 +4,10 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence
from urllib.parse import urlparse
from ..utils.compat import dataclass
from ..utils.net import normalize_proxy_url
DEFAULT_PROVIDER_TIMEOUT_SECONDS = 1200
+2
View File
@@ -2,6 +2,8 @@
# -*- coding: utf-8 -*-
"""Provider 管理器:负责加载配置、构建运行时对象并暴露只读注册表接口。"""
from __future__ import annotations
import hashlib
import importlib.util
import inspect
+2 -2
View File
@@ -4,10 +4,10 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Dict, Optional, Protocol
from typing import Any, Dict, Optional
from ..external import LLMProvider
from ..utils.compat import Protocol, dataclass
@dataclass(frozen=True, slots=True)
+3 -1
View File
@@ -2,10 +2,12 @@
# -*- coding: utf-8 -*-
"""LLM provider 运行时对象。"""
from dataclasses import dataclass
from __future__ import annotations
from typing import Any, Dict, Optional
from ..hooks import HookContext, HookModule
from ..utils.compat import dataclass
@dataclass(frozen=True, slots=True)
+2 -2
View File
@@ -3,11 +3,11 @@
"""Hook type contracts."""
from dataclasses import dataclass
from enum import StrEnum
from pathlib import Path
from typing import Any, Dict, Optional, Protocol
from typing import Any, Dict, Optional
from ..application.app_context import Logger
from ..utils.compat import Protocol, StrEnum
class HookAbortError(Exception):
+3 -2
View File
@@ -4,8 +4,9 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Dict, Literal, Optional
from typing import Any, Dict, Optional
from ..utils.compat import Literal, dataclass
@dataclass(frozen=True, slots=True)
+2
View File
@@ -2,6 +2,8 @@
# -*- coding: utf-8 -*-
"""用户仓储。"""
from __future__ import annotations
from typing import Any, Dict, List, Optional
from ..utils.database import ConnectionFactory
+2 -1
View File
@@ -5,7 +5,7 @@
from __future__ import annotations
import json
from dataclasses import dataclass, replace
from dataclasses import replace
from typing import Any, Callable, Dict, Iterator, Optional, Tuple
import requests
@@ -26,6 +26,7 @@ from ..proxy_core import (
should_emit_terminal_chunk,
)
from ..translators import Translator, build_default_translator_registry
from ..utils.compat import dataclass
from ..utils.net import build_requests_proxies
+2
View File
@@ -2,6 +2,8 @@
# -*- coding: utf-8 -*-
"""用户服务。"""
from __future__ import annotations
import json
import threading
from typing import Any, Dict, Iterable, List, Optional, Sequence
+6 -3
View File
@@ -6,10 +6,10 @@ from __future__ import annotations
import json
import time
from dataclasses import dataclass
from typing import Any, Dict, Optional, Protocol
from typing import Any, Dict, Optional
from ..proxy_core.contracts import DownstreamChunk, StreamEvent
from ..utils.compat import Protocol, dataclass
from .claude_bridge import (
convert_claude_request_to_openai_chat_request as _convert_claude_request_to_openai_chat_request,
convert_openai_chat_response_to_claude as _convert_openai_chat_response_to_claude,
@@ -175,8 +175,11 @@ class OpenAIResponsesTranslator:
if isinstance(item, dict) and str(item.get("type") or "").strip().lower() == "function_call":
item_id = str(item.get("id") or payload.get("item_id") or "").strip()
if item_id:
call_id = str(item.get("call_id") or "").strip()
if not call_id:
call_id = item_id[3:] if item_id.startswith("fc_") else item_id
items[item_id] = {
"id": str(item.get("call_id") or item_id.removeprefix("fc_") or item_id),
"id": call_id,
"name": str(item.get("name") or ""),
"index": int(payload.get("output_index") or 0),
"delta_emitted": False,
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Compatibility helpers for supported Python runtimes."""
import sys
from dataclasses import dataclass as _dataclass
from enum import Enum
from typing import Any
try:
from enum import StrEnum
except ImportError:
class StrEnum(str, Enum):
"""Fallback StrEnum for Python versions before 3.11."""
pass
try:
from typing import Literal
except ImportError:
try:
from typing_extensions import Literal # type: ignore
except ImportError:
class _LiteralCompat(object):
def __getitem__(self, _item):
return Any
Literal = _LiteralCompat()
try:
from typing import Protocol
except ImportError:
try:
from typing_extensions import Protocol # type: ignore
except ImportError:
class Protocol(object):
"""Fallback Protocol base when typing_extensions is unavailable."""
pass
def dataclass(_cls=None, **kwargs):
"""Mirror dataclasses.dataclass while skipping unsupported keywords."""
if sys.version_info < (3, 10):
kwargs.pop("slots", None)
if _cls is None:
return _dataclass(**kwargs)
return _dataclass(_cls, **kwargs)
__all__ = ["dataclass", "Literal", "Protocol", "StrEnum"]
+5 -1
View File
@@ -29,8 +29,12 @@ def is_valid_ip(ip_value: Optional[str]) -> bool:
if not ip_value:
return False
value = ip_value.strip()
if value.startswith('::ffff:'):
value = value[7:]
try:
ipaddress.ip_address(ip_value.strip().removeprefix('::ffff:'))
ipaddress.ip_address(value)
return True
except ValueError:
return False
-190
View File
@@ -1,190 +0,0 @@
import sys
import tempfile
import unittest
from pathlib import Path
from types import SimpleNamespace
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from src.repositories import LogRepository, UserRepository
from src.services import LogService, UserService
from src.utils.database import create_connection_factory
from src.utils.local_time import parse_local_datetime
class DummyLogger:
def info(self, *args, **kwargs):
return None
def error(self, *args, **kwargs):
return None
def warning(self, *args, **kwargs):
return None
def debug(self, *args, **kwargs):
return None
class LocalTimeRepositoryTests(unittest.TestCase):
def setUp(self) -> None:
self._tempdir = tempfile.TemporaryDirectory()
self._db_path = Path(self._tempdir.name) / "requests.db"
self._get_connection = create_connection_factory(self._db_path)
def tearDown(self) -> None:
self._tempdir.cleanup()
def _table_sql(self, table_name: str) -> str:
with self._get_connection() as conn:
row = conn.execute(
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?",
(table_name,),
).fetchone()
self.assertIsNotNone(row)
return row["sql"]
def test_user_table_schema_removes_timestamp_defaults(self) -> None:
UserRepository(self._get_connection)
sql = self._table_sql("users")
self.assertIn("created_at TEXT NOT NULL", sql)
self.assertIn("updated_at TEXT NOT NULL", sql)
self.assertNotIn("CURRENT_TIMESTAMP", sql)
def test_log_tables_schema_remove_timestamp_defaults(self) -> None:
LogRepository(self._get_connection)
request_logs_sql = self._table_sql("request_logs")
daily_stats_sql = self._table_sql("daily_request_stats")
self.assertIn("start_time TEXT NOT NULL", request_logs_sql)
self.assertIn("created_at TEXT NOT NULL", request_logs_sql)
self.assertNotIn("CURRENT_TIMESTAMP", request_logs_sql)
self.assertIn("created_at TEXT NOT NULL", daily_stats_sql)
self.assertIn("updated_at TEXT NOT NULL", daily_stats_sql)
self.assertNotIn("CURRENT_TIMESTAMP", daily_stats_sql)
def test_user_repository_create_writes_local_time_text(self) -> None:
repository = UserRepository(self._get_connection)
user_id = repository.create("alice", "127.0.0.1")
user = repository.get_by_id(user_id)
self.assertIsNotNone(user)
self.assertEqual(user["created_at"], user["updated_at"])
self.assertIn(".", user["created_at"])
self.assertIsNotNone(parse_local_datetime(user["created_at"]))
def test_log_repository_insert_writes_local_times_and_stat_date(self) -> None:
repository = LogRepository(self._get_connection)
log_id = repository.insert(
request_model="demo-model",
response_model="demo-response",
total_tokens=12,
prompt_tokens=5,
completion_tokens=7,
start_time="2026-03-30 09:10:11.123456",
end_time="2026-03-30 09:10:12.654321",
ip_address="127.0.0.1",
)
with self._get_connection() as conn:
log_row = conn.execute(
"SELECT start_time, end_time, created_at FROM request_logs WHERE id = ?",
(log_id,),
).fetchone()
stat_row = conn.execute(
"""
SELECT stat_date, created_at, updated_at
FROM daily_request_stats
WHERE ip_address = ? AND request_model = ?
""",
("127.0.0.1", "demo-model"),
).fetchone()
self.assertEqual("2026-03-30 09:10:11.123456", log_row["start_time"])
self.assertEqual("2026-03-30 09:10:12.654321", log_row["end_time"])
self.assertIn(".", log_row["created_at"])
self.assertEqual("2026-03-30", stat_row["stat_date"])
self.assertIn(".", stat_row["created_at"])
self.assertIn(".", stat_row["updated_at"])
class LocalTimeServiceTests(unittest.TestCase):
def setUp(self) -> None:
self._tempdir = tempfile.TemporaryDirectory()
self._db_path = Path(self._tempdir.name) / "requests.db"
self._get_connection = create_connection_factory(self._db_path)
self._ctx = SimpleNamespace(logger=DummyLogger())
def tearDown(self) -> None:
self._tempdir.cleanup()
def test_user_service_returns_normalized_local_time_strings(self) -> None:
repository = UserRepository(self._get_connection)
user_id = repository.create("alice", "127.0.0.1")
with self._get_connection() as conn:
conn.execute(
"UPDATE users SET created_at = ?, updated_at = ? WHERE id = ?",
("2026-03-30 08:09:10", "2026-03-30 08:09:11", user_id),
)
service = UserService(self._ctx, repository)
user = service.get_user_by_id(user_id)
self.assertEqual("2026-03-30 08:09:10.000000", user["created_at"])
self.assertEqual("2026-03-30 08:09:11.000000", user["updated_at"])
def test_log_service_returns_normalized_local_time_strings(self) -> None:
UserRepository(self._get_connection)
repository = LogRepository(self._get_connection)
log_id = repository.insert(
request_model="demo-model",
response_model="demo-response",
total_tokens=4,
start_time="2026-03-30 08:09:10",
end_time="2026-03-30 08:09:11",
ip_address="127.0.0.1",
)
with self._get_connection() as conn:
conn.execute(
"UPDATE request_logs SET created_at = ? WHERE id = ?",
("2026-03-30 08:09:12", log_id),
)
service = LogService(self._ctx, repository)
payload = service.get_request_logs(page=1, page_size=10)
log = payload["logs"][0]
self.assertEqual("2026-03-30 08:09:10.000000", log["start_time"])
self.assertEqual("2026-03-30 08:09:11.000000", log["end_time"])
self.assertEqual("2026-03-30 08:09:12.000000", log["created_at"])
class LocalTimeSqlScriptTests(unittest.TestCase):
def test_manual_sql_scripts_exist(self) -> None:
root = Path(__file__).resolve().parents[1]
migrate_sql = (root / "scripts" / "sql" / "001_migrate_created_at_to_localtime.sql").read_text(
encoding="utf-8"
)
rebuild_sql = (root / "scripts" / "sql" / "002_rebuild_tables_remove_time_defaults.sql").read_text(
encoding="utf-8"
)
self.assertIn("datetime(created_at, 'localtime') || '.000000'", migrate_sql)
self.assertIn("ALTER TABLE users RENAME TO users_old;", rebuild_sql)
self.assertIn("DROP INDEX IF EXISTS idx_users_ip;", rebuild_sql)
self.assertIn("DROP INDEX IF EXISTS idx_start_time;", rebuild_sql)
self.assertIn("DROP INDEX IF EXISTS idx_daily_stats_date;", rebuild_sql)
self.assertIn("CREATE TABLE request_logs (", rebuild_sql)
self.assertIn("created_at TEXT NOT NULL", rebuild_sql)
self.assertNotIn("CURRENT_TIMESTAMP", rebuild_sql)
if __name__ == "__main__":
unittest.main()
+2
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
import sys
import unittest
from pathlib import Path
+2
View File
@@ -1,3 +1,5 @@
from __future__ import annotations
import json
import ssl
import subprocess