codex-lb 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. app/__init__.py +5 -0
  2. app/cli.py +24 -0
  3. app/core/__init__.py +0 -0
  4. app/core/auth/__init__.py +96 -0
  5. app/core/auth/models.py +49 -0
  6. app/core/auth/refresh.py +144 -0
  7. app/core/balancer/__init__.py +19 -0
  8. app/core/balancer/logic.py +140 -0
  9. app/core/balancer/types.py +9 -0
  10. app/core/clients/__init__.py +0 -0
  11. app/core/clients/http.py +39 -0
  12. app/core/clients/oauth.py +340 -0
  13. app/core/clients/proxy.py +265 -0
  14. app/core/clients/usage.py +143 -0
  15. app/core/config/__init__.py +0 -0
  16. app/core/config/settings.py +69 -0
  17. app/core/crypto.py +37 -0
  18. app/core/errors.py +73 -0
  19. app/core/openai/__init__.py +0 -0
  20. app/core/openai/models.py +122 -0
  21. app/core/openai/parsing.py +55 -0
  22. app/core/openai/requests.py +59 -0
  23. app/core/types.py +4 -0
  24. app/core/usage/__init__.py +185 -0
  25. app/core/usage/logs.py +57 -0
  26. app/core/usage/models.py +35 -0
  27. app/core/usage/pricing.py +172 -0
  28. app/core/usage/types.py +95 -0
  29. app/core/utils/__init__.py +0 -0
  30. app/core/utils/request_id.py +30 -0
  31. app/core/utils/retry.py +16 -0
  32. app/core/utils/sse.py +13 -0
  33. app/core/utils/time.py +19 -0
  34. app/db/__init__.py +0 -0
  35. app/db/models.py +82 -0
  36. app/db/session.py +44 -0
  37. app/dependencies.py +123 -0
  38. app/main.py +124 -0
  39. app/modules/__init__.py +0 -0
  40. app/modules/accounts/__init__.py +0 -0
  41. app/modules/accounts/api.py +81 -0
  42. app/modules/accounts/repository.py +80 -0
  43. app/modules/accounts/schemas.py +66 -0
  44. app/modules/accounts/service.py +211 -0
  45. app/modules/health/__init__.py +0 -0
  46. app/modules/health/api.py +10 -0
  47. app/modules/oauth/__init__.py +0 -0
  48. app/modules/oauth/api.py +57 -0
  49. app/modules/oauth/schemas.py +32 -0
  50. app/modules/oauth/service.py +356 -0
  51. app/modules/oauth/templates/oauth_success.html +122 -0
  52. app/modules/proxy/__init__.py +0 -0
  53. app/modules/proxy/api.py +76 -0
  54. app/modules/proxy/auth_manager.py +51 -0
  55. app/modules/proxy/load_balancer.py +208 -0
  56. app/modules/proxy/schemas.py +85 -0
  57. app/modules/proxy/service.py +707 -0
  58. app/modules/proxy/types.py +37 -0
  59. app/modules/proxy/usage_updater.py +147 -0
  60. app/modules/request_logs/__init__.py +0 -0
  61. app/modules/request_logs/api.py +31 -0
  62. app/modules/request_logs/repository.py +86 -0
  63. app/modules/request_logs/schemas.py +25 -0
  64. app/modules/request_logs/service.py +77 -0
  65. app/modules/shared/__init__.py +0 -0
  66. app/modules/shared/schemas.py +8 -0
  67. app/modules/usage/__init__.py +0 -0
  68. app/modules/usage/api.py +31 -0
  69. app/modules/usage/repository.py +113 -0
  70. app/modules/usage/schemas.py +62 -0
  71. app/modules/usage/service.py +246 -0
  72. app/static/7.css +1336 -0
  73. app/static/index.css +543 -0
  74. app/static/index.html +457 -0
  75. app/static/index.js +1898 -0
  76. codex_lb-0.1.2.dist-info/METADATA +108 -0
  77. codex_lb-0.1.2.dist-info/RECORD +80 -0
  78. codex_lb-0.1.2.dist-info/WHEEL +4 -0
  79. codex_lb-0.1.2.dist-info/entry_points.txt +2 -0
  80. codex_lb-0.1.2.dist-info/licenses/LICENSE +21 -0
app/db/models.py ADDED
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from enum import Enum
5
+
6
+ from sqlalchemy import Boolean, DateTime, Float, ForeignKey, Index, Integer, LargeBinary, String, Text, func
7
+ from sqlalchemy import Enum as SqlEnum
8
+ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
9
+
10
+
11
+ class Base(DeclarativeBase):
12
+ pass
13
+
14
+
15
+ class AccountStatus(str, Enum):
16
+ ACTIVE = "active"
17
+ RATE_LIMITED = "rate_limited"
18
+ QUOTA_EXCEEDED = "quota_exceeded"
19
+ PAUSED = "paused"
20
+ DEACTIVATED = "deactivated"
21
+
22
+
23
+ class Account(Base):
24
+ __tablename__ = "accounts"
25
+
26
+ id: Mapped[str] = mapped_column(String, primary_key=True)
27
+ email: Mapped[str] = mapped_column(String, unique=True, nullable=False)
28
+ plan_type: Mapped[str] = mapped_column(String, nullable=False)
29
+
30
+ access_token_encrypted: Mapped[bytes] = mapped_column(LargeBinary, nullable=False)
31
+ refresh_token_encrypted: Mapped[bytes] = mapped_column(LargeBinary, nullable=False)
32
+ id_token_encrypted: Mapped[bytes] = mapped_column(LargeBinary, nullable=False)
33
+
34
+ last_refresh: Mapped[datetime] = mapped_column(DateTime, nullable=False)
35
+ created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), nullable=False)
36
+
37
+ status: Mapped[AccountStatus] = mapped_column(
38
+ SqlEnum(AccountStatus, name="account_status", validate_strings=True),
39
+ default=AccountStatus.ACTIVE,
40
+ nullable=False,
41
+ )
42
+ deactivation_reason: Mapped[str | None] = mapped_column(Text, nullable=True)
43
+
44
+
45
+ class UsageHistory(Base):
46
+ __tablename__ = "usage_history"
47
+
48
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
49
+ account_id: Mapped[str] = mapped_column(String, ForeignKey("accounts.id"), nullable=False)
50
+ recorded_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), nullable=False)
51
+ window: Mapped[str | None] = mapped_column(String, nullable=True)
52
+ used_percent: Mapped[float] = mapped_column(Float, nullable=False)
53
+ input_tokens: Mapped[int | None] = mapped_column(Integer, nullable=True)
54
+ output_tokens: Mapped[int | None] = mapped_column(Integer, nullable=True)
55
+ reset_at: Mapped[int | None] = mapped_column(Integer, nullable=True)
56
+ window_minutes: Mapped[int | None] = mapped_column(Integer, nullable=True)
57
+ credits_has: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
58
+ credits_unlimited: Mapped[bool | None] = mapped_column(Boolean, nullable=True)
59
+ credits_balance: Mapped[float | None] = mapped_column(Float, nullable=True)
60
+
61
+
62
+ class RequestLog(Base):
63
+ __tablename__ = "request_logs"
64
+
65
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
66
+ account_id: Mapped[str] = mapped_column(String, ForeignKey("accounts.id"), nullable=False)
67
+ request_id: Mapped[str] = mapped_column(String, nullable=False)
68
+ requested_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), nullable=False)
69
+ model: Mapped[str] = mapped_column(String, nullable=False)
70
+ input_tokens: Mapped[int | None] = mapped_column(Integer, nullable=True)
71
+ output_tokens: Mapped[int | None] = mapped_column(Integer, nullable=True)
72
+ cached_input_tokens: Mapped[int | None] = mapped_column(Integer, nullable=True)
73
+ reasoning_tokens: Mapped[int | None] = mapped_column(Integer, nullable=True)
74
+ latency_ms: Mapped[int | None] = mapped_column(Integer, nullable=True)
75
+ status: Mapped[str] = mapped_column(String, nullable=False)
76
+ error_code: Mapped[str | None] = mapped_column(String, nullable=True)
77
+ error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
78
+
79
+
80
+ Index("idx_usage_recorded_at", UsageHistory.recorded_at)
81
+ Index("idx_usage_account_time", UsageHistory.account_id, UsageHistory.recorded_at)
82
+ Index("idx_logs_account_time", RequestLog.account_id, RequestLog.requested_at)
app/db/session.py ADDED
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import AsyncIterator
5
+
6
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
7
+
8
+ from app.core.config.settings import get_settings
9
+
10
+ DATABASE_URL = get_settings().database_url
11
+
12
+ engine = create_async_engine(DATABASE_URL, echo=False)
13
+ SessionLocal = async_sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)
14
+
15
+
16
+ def _ensure_sqlite_dir(url: str) -> None:
17
+ prefix = "sqlite+aiosqlite:///"
18
+ if not url.startswith(prefix):
19
+ return
20
+ path = url[len(prefix) :]
21
+ if path == ":memory:":
22
+ return
23
+ Path(path).expanduser().parent.mkdir(parents=True, exist_ok=True)
24
+
25
+
26
+ async def get_session() -> AsyncIterator[AsyncSession]:
27
+ async with SessionLocal() as session:
28
+ try:
29
+ yield session
30
+ except Exception:
31
+ await session.rollback()
32
+ raise
33
+ finally:
34
+ if session.in_transaction():
35
+ await session.rollback()
36
+
37
+
38
+ async def init_db() -> None:
39
+ from app.db.models import Base
40
+
41
+ _ensure_sqlite_dir(DATABASE_URL)
42
+
43
+ async with engine.begin() as conn:
44
+ await conn.run_sync(Base.metadata.create_all)
app/dependencies.py ADDED
@@ -0,0 +1,123 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import AsyncIterator
4
+ from contextlib import asynccontextmanager
5
+ from dataclasses import dataclass
6
+
7
+ from fastapi import Depends
8
+ from sqlalchemy.ext.asyncio import AsyncSession
9
+
10
+ from app.db.session import SessionLocal, get_session
11
+ from app.modules.accounts.repository import AccountsRepository
12
+ from app.modules.accounts.service import AccountsService
13
+ from app.modules.oauth.service import OauthService
14
+ from app.modules.proxy.service import ProxyService
15
+ from app.modules.request_logs.repository import RequestLogsRepository
16
+ from app.modules.request_logs.service import RequestLogsService
17
+ from app.modules.usage.repository import UsageRepository
18
+ from app.modules.usage.service import UsageService
19
+
20
+
21
+ @dataclass(slots=True)
22
+ class AccountsContext:
23
+ session: AsyncSession
24
+ repository: AccountsRepository
25
+ usage_repository: UsageRepository
26
+ request_logs_repository: RequestLogsRepository
27
+ service: AccountsService
28
+
29
+
30
+ @dataclass(slots=True)
31
+ class UsageContext:
32
+ session: AsyncSession
33
+ usage_repository: UsageRepository
34
+ request_logs_repository: RequestLogsRepository
35
+ accounts_repository: AccountsRepository
36
+ service: UsageService
37
+
38
+
39
+ @dataclass(slots=True)
40
+ class OauthContext:
41
+ service: OauthService
42
+
43
+
44
+ @dataclass(slots=True)
45
+ class ProxyContext:
46
+ service: ProxyService
47
+
48
+
49
+ @dataclass(slots=True)
50
+ class RequestLogsContext:
51
+ session: AsyncSession
52
+ repository: RequestLogsRepository
53
+ service: RequestLogsService
54
+
55
+
56
+ def get_accounts_context(
57
+ session: AsyncSession = Depends(get_session),
58
+ ) -> AccountsContext:
59
+ repository = AccountsRepository(session)
60
+ usage_repository = UsageRepository(session)
61
+ request_logs_repository = RequestLogsRepository(session)
62
+ service = AccountsService(repository, usage_repository, request_logs_repository)
63
+ return AccountsContext(
64
+ session=session,
65
+ repository=repository,
66
+ usage_repository=usage_repository,
67
+ request_logs_repository=request_logs_repository,
68
+ service=service,
69
+ )
70
+
71
+
72
+ def get_usage_context(
73
+ session: AsyncSession = Depends(get_session),
74
+ ) -> UsageContext:
75
+ usage_repository = UsageRepository(session)
76
+ request_logs_repository = RequestLogsRepository(session)
77
+ accounts_repository = AccountsRepository(session)
78
+ service = UsageService(usage_repository, request_logs_repository, accounts_repository)
79
+ return UsageContext(
80
+ session=session,
81
+ usage_repository=usage_repository,
82
+ request_logs_repository=request_logs_repository,
83
+ accounts_repository=accounts_repository,
84
+ service=service,
85
+ )
86
+
87
+
88
+ @asynccontextmanager
89
+ async def _accounts_repo_context() -> AsyncIterator[AccountsRepository]:
90
+ async with SessionLocal() as session:
91
+ try:
92
+ yield AccountsRepository(session)
93
+ except Exception:
94
+ await session.rollback()
95
+ raise
96
+ finally:
97
+ if session.in_transaction():
98
+ await session.rollback()
99
+
100
+
101
+ def get_oauth_context(
102
+ session: AsyncSession = Depends(get_session),
103
+ ) -> OauthContext:
104
+ accounts_repository = AccountsRepository(session)
105
+ return OauthContext(service=OauthService(accounts_repository, repo_factory=_accounts_repo_context))
106
+
107
+
108
+ def get_proxy_context(
109
+ session: AsyncSession = Depends(get_session),
110
+ ) -> ProxyContext:
111
+ accounts_repository = AccountsRepository(session)
112
+ usage_repository = UsageRepository(session)
113
+ request_logs_repository = RequestLogsRepository(session)
114
+ service = ProxyService(accounts_repository, usage_repository, request_logs_repository)
115
+ return ProxyContext(service=service)
116
+
117
+
118
+ def get_request_logs_context(
119
+ session: AsyncSession = Depends(get_session),
120
+ ) -> RequestLogsContext:
121
+ repository = RequestLogsRepository(session)
122
+ service = RequestLogsService(repository)
123
+ return RequestLogsContext(session=session, repository=repository, service=service)
app/main.py ADDED
@@ -0,0 +1,124 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from contextlib import asynccontextmanager
5
+ from pathlib import Path
6
+ from uuid import uuid4
7
+
8
+ from fastapi import FastAPI, Request
9
+ from fastapi.exception_handlers import (
10
+ http_exception_handler,
11
+ request_validation_exception_handler,
12
+ )
13
+ from fastapi.exceptions import RequestValidationError
14
+ from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
15
+ from fastapi.staticfiles import StaticFiles
16
+ from starlette.exceptions import HTTPException as StarletteHTTPException
17
+
18
+ from app.core.clients.http import close_http_client, init_http_client
19
+ from app.core.errors import dashboard_error
20
+ from app.core.utils.request_id import get_request_id, reset_request_id, set_request_id
21
+ from app.db.session import init_db
22
+ from app.modules.accounts import api as accounts_api
23
+ from app.modules.health import api as health_api
24
+ from app.modules.oauth import api as oauth_api
25
+ from app.modules.proxy import api as proxy_api
26
+ from app.modules.request_logs import api as request_logs_api
27
+ from app.modules.usage import api as usage_api
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ @asynccontextmanager
33
+ async def lifespan(_: FastAPI):
34
+ await init_db()
35
+ await init_http_client()
36
+
37
+ try:
38
+ yield
39
+ finally:
40
+ await close_http_client()
41
+
42
+
43
+ def create_app() -> FastAPI:
44
+ app = FastAPI(title="codex-lb", version="0.1.0", lifespan=lifespan)
45
+
46
+ @app.middleware("http")
47
+ async def request_id_middleware(request: Request, call_next) -> JSONResponse:
48
+ inbound_request_id = request.headers.get("x-request-id") or request.headers.get("request-id")
49
+ request_id = inbound_request_id or str(uuid4())
50
+ token = set_request_id(request_id)
51
+ try:
52
+ response = await call_next(request)
53
+ except Exception:
54
+ reset_request_id(token)
55
+ raise
56
+ response.headers.setdefault("x-request-id", request_id)
57
+ return response
58
+
59
+ @app.middleware("http")
60
+ async def api_unhandled_error_middleware(request: Request, call_next) -> JSONResponse:
61
+ try:
62
+ return await call_next(request)
63
+ except Exception:
64
+ if request.url.path.startswith("/api/"):
65
+ logger.exception(
66
+ "Unhandled API error request_id=%s",
67
+ get_request_id(),
68
+ )
69
+ return JSONResponse(
70
+ status_code=500,
71
+ content=dashboard_error("internal_error", "Unexpected error"),
72
+ )
73
+ raise
74
+
75
+ @app.exception_handler(RequestValidationError)
76
+ async def _validation_error_handler(
77
+ request: Request,
78
+ exc: RequestValidationError,
79
+ ) -> JSONResponse:
80
+ if request.url.path.startswith("/api/"):
81
+ return JSONResponse(
82
+ status_code=422,
83
+ content=dashboard_error("validation_error", "Invalid request payload"),
84
+ )
85
+ return await request_validation_exception_handler(request, exc)
86
+
87
+ @app.exception_handler(StarletteHTTPException)
88
+ async def _http_error_handler(
89
+ request: Request,
90
+ exc: StarletteHTTPException,
91
+ ) -> JSONResponse:
92
+ if request.url.path.startswith("/api/"):
93
+ detail = exc.detail if isinstance(exc.detail, str) else "Request failed"
94
+ return JSONResponse(
95
+ status_code=exc.status_code,
96
+ content=dashboard_error(f"http_{exc.status_code}", detail),
97
+ )
98
+ return await http_exception_handler(request, exc)
99
+
100
+ app.include_router(proxy_api.router)
101
+ app.include_router(proxy_api.usage_router)
102
+ app.include_router(accounts_api.router)
103
+ app.include_router(usage_api.router)
104
+ app.include_router(request_logs_api.router)
105
+ app.include_router(oauth_api.router)
106
+ app.include_router(health_api.router)
107
+
108
+ static_dir = Path(__file__).parent / "static"
109
+ index_html = static_dir / "index.html"
110
+
111
+ @app.get("/", include_in_schema=False)
112
+ async def root_redirect():
113
+ return RedirectResponse(url="/dashboard", status_code=302)
114
+
115
+ @app.get("/accounts", include_in_schema=False)
116
+ async def spa_accounts():
117
+ return FileResponse(index_html, media_type="text/html")
118
+
119
+ app.mount("/dashboard", StaticFiles(directory=static_dir, html=True), name="dashboard")
120
+
121
+ return app
122
+
123
+
124
+ app = create_app()
File without changes
File without changes
@@ -0,0 +1,81 @@
1
+ from __future__ import annotations
2
+
3
+ from fastapi import APIRouter, Depends, File, UploadFile
4
+ from fastapi.responses import JSONResponse
5
+
6
+ from app.core.errors import dashboard_error
7
+ from app.dependencies import AccountsContext, get_accounts_context
8
+ from app.modules.accounts.schemas import (
9
+ AccountDeleteResponse,
10
+ AccountImportResponse,
11
+ AccountPauseResponse,
12
+ AccountReactivateResponse,
13
+ AccountsResponse,
14
+ )
15
+
16
+ router = APIRouter(prefix="/api/accounts", tags=["dashboard"])
17
+
18
+
19
+ @router.get("", response_model=AccountsResponse)
20
+ async def list_accounts(
21
+ context: AccountsContext = Depends(get_accounts_context),
22
+ ) -> AccountsResponse:
23
+ accounts = await context.service.list_accounts()
24
+ return AccountsResponse(accounts=accounts)
25
+
26
+
27
+ @router.post("/import", response_model=AccountImportResponse)
28
+ async def import_account(
29
+ auth_json: UploadFile = File(...),
30
+ context: AccountsContext = Depends(get_accounts_context),
31
+ ) -> AccountImportResponse | JSONResponse:
32
+ raw = await auth_json.read()
33
+ try:
34
+ return await context.service.import_account(raw)
35
+ except Exception:
36
+ return JSONResponse(
37
+ status_code=400,
38
+ content=dashboard_error("invalid_auth_json", "Invalid auth.json payload"),
39
+ )
40
+
41
+
42
+ @router.post("/{account_id}/reactivate", response_model=AccountReactivateResponse)
43
+ async def reactivate_account(
44
+ account_id: str,
45
+ context: AccountsContext = Depends(get_accounts_context),
46
+ ) -> AccountReactivateResponse | JSONResponse:
47
+ success = await context.service.reactivate_account(account_id)
48
+ if not success:
49
+ return JSONResponse(
50
+ status_code=404,
51
+ content=dashboard_error("account_not_found", "Account not found"),
52
+ )
53
+ return AccountReactivateResponse(status="reactivated")
54
+
55
+
56
+ @router.post("/{account_id}/pause", response_model=AccountPauseResponse)
57
+ async def pause_account(
58
+ account_id: str,
59
+ context: AccountsContext = Depends(get_accounts_context),
60
+ ) -> AccountPauseResponse | JSONResponse:
61
+ success = await context.service.pause_account(account_id)
62
+ if not success:
63
+ return JSONResponse(
64
+ status_code=404,
65
+ content=dashboard_error("account_not_found", "Account not found"),
66
+ )
67
+ return AccountPauseResponse(status="paused")
68
+
69
+
70
+ @router.delete("/{account_id}", response_model=AccountDeleteResponse)
71
+ async def delete_account(
72
+ account_id: str,
73
+ context: AccountsContext = Depends(get_accounts_context),
74
+ ) -> AccountDeleteResponse | JSONResponse:
75
+ success = await context.service.delete_account(account_id)
76
+ if not success:
77
+ return JSONResponse(
78
+ status_code=404,
79
+ content=dashboard_error("account_not_found", "Account not found"),
80
+ )
81
+ return AccountDeleteResponse(status="deleted")
@@ -0,0 +1,80 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+
5
+ from sqlalchemy import delete, select, update
6
+ from sqlalchemy.ext.asyncio import AsyncSession
7
+
8
+ from app.db.models import Account, AccountStatus
9
+
10
+
11
+ class AccountsRepository:
12
+ def __init__(self, session: AsyncSession) -> None:
13
+ self._session = session
14
+
15
+ async def list_accounts(self) -> list[Account]:
16
+ result = await self._session.execute(select(Account).order_by(Account.email))
17
+ return list(result.scalars().all())
18
+
19
+ async def upsert(self, account: Account) -> Account:
20
+ existing = await self._session.get(Account, account.id)
21
+ if existing:
22
+ existing.email = account.email
23
+ existing.plan_type = account.plan_type
24
+ existing.access_token_encrypted = account.access_token_encrypted
25
+ existing.refresh_token_encrypted = account.refresh_token_encrypted
26
+ existing.id_token_encrypted = account.id_token_encrypted
27
+ existing.last_refresh = account.last_refresh
28
+ existing.status = account.status
29
+ existing.deactivation_reason = account.deactivation_reason
30
+ await self._session.commit()
31
+ await self._session.refresh(existing)
32
+ return existing
33
+
34
+ self._session.add(account)
35
+ await self._session.commit()
36
+ await self._session.refresh(account)
37
+ return account
38
+
39
+ async def update_status(
40
+ self,
41
+ account_id: str,
42
+ status: AccountStatus,
43
+ deactivation_reason: str | None = None,
44
+ ) -> bool:
45
+ result = await self._session.execute(
46
+ update(Account)
47
+ .where(Account.id == account_id)
48
+ .values(status=status, deactivation_reason=deactivation_reason)
49
+ )
50
+ await self._session.commit()
51
+ return bool(result.rowcount)
52
+
53
+ async def delete(self, account_id: str) -> bool:
54
+ result = await self._session.execute(delete(Account).where(Account.id == account_id))
55
+ await self._session.commit()
56
+ return bool(result.rowcount)
57
+
58
+ async def update_tokens(
59
+ self,
60
+ account_id: str,
61
+ access_token_encrypted: bytes,
62
+ refresh_token_encrypted: bytes,
63
+ id_token_encrypted: bytes,
64
+ last_refresh: datetime,
65
+ plan_type: str | None = None,
66
+ email: str | None = None,
67
+ ) -> bool:
68
+ values = {
69
+ "access_token_encrypted": access_token_encrypted,
70
+ "refresh_token_encrypted": refresh_token_encrypted,
71
+ "id_token_encrypted": id_token_encrypted,
72
+ "last_refresh": last_refresh,
73
+ }
74
+ if plan_type is not None:
75
+ values["plan_type"] = plan_type
76
+ if email is not None:
77
+ values["email"] = email
78
+ result = await self._session.execute(update(Account).where(Account.id == account_id).values(**values))
79
+ await self._session.commit()
80
+ return bool(result.rowcount)
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from typing import List
5
+
6
+ from pydantic import Field
7
+
8
+ from app.modules.shared.schemas import DashboardModel
9
+
10
+
11
+ class AccountUsage(DashboardModel):
12
+ primary_remaining_percent: float | None = None
13
+ secondary_remaining_percent: float | None = None
14
+
15
+
16
+ class AccountTokenStatus(DashboardModel):
17
+ expires_at: datetime | None = None
18
+ state: str | None = None
19
+
20
+
21
+ class AccountAuthStatus(DashboardModel):
22
+ access: AccountTokenStatus | None = None
23
+ refresh: AccountTokenStatus | None = None
24
+ id_token: AccountTokenStatus | None = None
25
+
26
+
27
+ class AccountSummary(DashboardModel):
28
+ account_id: str
29
+ email: str
30
+ display_name: str
31
+ plan_type: str
32
+ status: str
33
+ usage: AccountUsage | None = None
34
+ reset_at_primary: datetime | None = None
35
+ reset_at_secondary: datetime | None = None
36
+ last_refresh_at: datetime | None = None
37
+ capacity_credits_primary: float | None = None
38
+ remaining_credits_primary: float | None = None
39
+ capacity_credits_secondary: float | None = None
40
+ remaining_credits_secondary: float | None = None
41
+ cost_usd_24h: float | None = None
42
+ deactivation_reason: str | None = None
43
+ auth: AccountAuthStatus | None = None
44
+
45
+
46
+ class AccountsResponse(DashboardModel):
47
+ accounts: List[AccountSummary] = Field(default_factory=list)
48
+
49
+
50
+ class AccountImportResponse(DashboardModel):
51
+ account_id: str
52
+ email: str
53
+ plan_type: str
54
+ status: str
55
+
56
+
57
+ class AccountPauseResponse(DashboardModel):
58
+ status: str
59
+
60
+
61
+ class AccountReactivateResponse(DashboardModel):
62
+ status: str
63
+
64
+
65
+ class AccountDeleteResponse(DashboardModel):
66
+ status: str