支持python 3.7及以上版本,避免问题
This commit is contained in:
1 parent
1edc97072c
commit
1e200715cb
18 files changed
+92
-205
No files matched your search
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from src.hooks import BaseHook, HookContext, HookErrorType
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,6 +2,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Provider 管理器:负责加载配置、构建运行时对象并暴露只读注册表接口。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import inspect
|
||||
|
||||
@@ -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)
|
||||
|
||||
Vendored
+3
-1
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,6 +2,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""用户仓储。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from ..utils.database import ConnectionFactory
|
||||
|
||||
@@ -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,6 +2,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""用户服务。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import threading
|
||||
from typing import Any, Dict, Iterable, List, Optional, Sequence
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import ssl
|
||||
import subprocess
|
||||
|
||||
Reference in new issue
Block a user