mirror of
https://github.com/furyhawk/agent_alpha.git
synced 2026-07-20 10:15:33 +00:00
feat: add user management API with CRUD operations
- Implemented SQLAlchemy ORM models for User with role-based access control. - Created API routes for user creation, retrieval, updating, and listing. - Added input and output schemas for user data handling. - Included session management for users.
This commit is contained in:
Vendored
+3
-1
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"chat.tools.terminal.autoApprove": {
|
||||
"cp": true
|
||||
"cp": true,
|
||||
"bun": true,
|
||||
"npx tsc": true
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -10,10 +10,11 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
from uvicorn.logging import DefaultFormatter
|
||||
|
||||
from backend.core.config import Settings
|
||||
from backend.core.database import close_engine, close_valkey
|
||||
from backend.core.database import close_engine, close_valkey, init_db
|
||||
from backend.core.dependencies import get_agent_service
|
||||
from backend.routes.chat import router as chat_router
|
||||
from backend.routes.health import router as health_router
|
||||
from backend.routes.users import router as users_router
|
||||
|
||||
logger = logging.getLogger("agent_alpha")
|
||||
|
||||
@@ -64,6 +65,7 @@ class AppBuilder:
|
||||
|
||||
app.include_router(health_router)
|
||||
app.include_router(chat_router)
|
||||
app.include_router(users_router)
|
||||
|
||||
return app
|
||||
|
||||
@@ -74,6 +76,9 @@ class AppBuilder:
|
||||
"""Startup / shutdown lifecycle."""
|
||||
logger.info("Agent Alpha backend starting")
|
||||
|
||||
# Create database tables if they don't exist.
|
||||
await init_db()
|
||||
|
||||
# Initialise the agent service on startup.
|
||||
agent_service = await get_agent_service()
|
||||
agent_service.initialize()
|
||||
|
||||
+172
-1
@@ -17,15 +17,26 @@ Chat persistence (Valkey)::
|
||||
|
||||
await save_message("session-1", "user", "Hello!")
|
||||
messages = await get_session_messages("session-1")
|
||||
|
||||
|
||||
User management::
|
||||
|
||||
from backend.core.database import create_user, get_user, list_users
|
||||
|
||||
user = await create_user("alice", "Alice", role="admin")
|
||||
user = await get_user(user.id)
|
||||
users = await list_users()
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from typing import AsyncIterator
|
||||
|
||||
from redis.asyncio import Redis
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import (
|
||||
AsyncSession,
|
||||
async_sessionmaker,
|
||||
@@ -33,6 +44,7 @@ from sqlalchemy.ext.asyncio import (
|
||||
)
|
||||
|
||||
from backend.core.config import settings
|
||||
from backend.core.models import Base, User
|
||||
|
||||
# ── PostgreSQL (SQLAlchemy async) ──────────────────────────────────────────
|
||||
|
||||
@@ -50,6 +62,12 @@ _session_factory = async_sessionmaker(
|
||||
)
|
||||
|
||||
|
||||
async def init_db() -> None:
|
||||
"""Create all tables if they don't exist (call on startup)."""
|
||||
async with _engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
|
||||
async def get_session() -> AsyncIterator[AsyncSession]:
|
||||
"""Yield an async SQLAlchemy session (for FastAPI dependency injection)."""
|
||||
async with _session_factory() as session:
|
||||
@@ -100,10 +118,116 @@ async def close_engine() -> None:
|
||||
await _engine.dispose()
|
||||
|
||||
|
||||
# ── User CRUD (PostgreSQL via SQLAlchemy) ─────────────────────────────────
|
||||
|
||||
|
||||
async def create_user(
|
||||
username: str,
|
||||
display_name: str,
|
||||
role: str = "user",
|
||||
team: str | None = None,
|
||||
session: AsyncSession | None = None,
|
||||
) -> User:
|
||||
"""Create a new user. Default role is ``user``.
|
||||
|
||||
Roles: ``admin``, ``user``, ``viewer``
|
||||
"""
|
||||
if session is None:
|
||||
async with _session_factory() as session:
|
||||
user = User(
|
||||
username=username,
|
||||
display_name=display_name,
|
||||
role=role,
|
||||
team=team,
|
||||
)
|
||||
session.add(user)
|
||||
await session.commit()
|
||||
await session.refresh(user)
|
||||
return user
|
||||
else:
|
||||
user = User(
|
||||
username=username,
|
||||
display_name=display_name,
|
||||
role=role,
|
||||
team=team,
|
||||
)
|
||||
session.add(user)
|
||||
await session.flush()
|
||||
await session.refresh(user)
|
||||
return user
|
||||
|
||||
|
||||
async def get_user(
|
||||
user_id: uuid.UUID,
|
||||
session: AsyncSession | None = None,
|
||||
) -> User | None:
|
||||
"""Return a user by their UUID, or ``None`` if not found."""
|
||||
if session is None:
|
||||
async with _session_factory() as session:
|
||||
return await session.get(User, user_id)
|
||||
return await session.get(User, user_id)
|
||||
|
||||
|
||||
async def get_user_by_username(
|
||||
username: str,
|
||||
session: AsyncSession | None = None,
|
||||
) -> User | None:
|
||||
"""Return a user by their username, or ``None`` if not found."""
|
||||
if session is None:
|
||||
async with _session_factory() as session:
|
||||
result = await session.execute(
|
||||
select(User).where(User.username == username)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
result = await session.execute(
|
||||
select(User).where(User.username == username)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def list_users(
|
||||
session: AsyncSession | None = None,
|
||||
) -> list[User]:
|
||||
"""Return all active users ordered by username."""
|
||||
if session is None:
|
||||
async with _session_factory() as session:
|
||||
result = await session.execute(
|
||||
select(User)
|
||||
.where(User.is_active.is_(True))
|
||||
.order_by(User.username)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
result = await session.execute(
|
||||
select(User)
|
||||
.where(User.is_active.is_(True))
|
||||
.order_by(User.username)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def update_user(
|
||||
user_id: uuid.UUID,
|
||||
session: AsyncSession,
|
||||
**kwargs: str | bool | None,
|
||||
) -> User | None:
|
||||
"""Update user fields. Pass ``display_name``, ``role``, ``is_active``, etc."""
|
||||
user = await session.get(User, user_id)
|
||||
if user is None:
|
||||
return None
|
||||
for key, value in kwargs.items():
|
||||
if value is not None and hasattr(user, key):
|
||||
setattr(user, key, value)
|
||||
await session.flush()
|
||||
await session.refresh(user)
|
||||
return user
|
||||
|
||||
|
||||
# ── Chat persistence (Valkey) ─────────────────────────────────────────────
|
||||
|
||||
_MESSAGES_KEY = "chat:{session_id}:messages"
|
||||
_SESSIONS_SET = "chat:sessions"
|
||||
_SESSION_USER_KEY = "chat:{session_id}:user_id"
|
||||
_USER_SESSIONS_KEY = "user:{user_id}:sessions"
|
||||
|
||||
|
||||
async def save_message(
|
||||
@@ -111,8 +235,12 @@ async def save_message(
|
||||
role: str,
|
||||
content: str,
|
||||
valkey: Redis | None = None,
|
||||
user_id: str | None = None,
|
||||
) -> None:
|
||||
"""Append a chat message to the session history in Valkey."""
|
||||
"""Append a chat message to the session history in Valkey.
|
||||
|
||||
If ``user_id`` is provided, the session is linked to that user.
|
||||
"""
|
||||
if valkey is None:
|
||||
valkey = await get_valkey()
|
||||
|
||||
@@ -125,6 +253,15 @@ async def save_message(
|
||||
async with valkey.pipeline(transaction=True) as pipe:
|
||||
pipe.rpush(key, json.dumps(msg))
|
||||
pipe.sadd(_SESSIONS_SET, session_id)
|
||||
if user_id is not None:
|
||||
pipe.set(
|
||||
_SESSION_USER_KEY.format(session_id=session_id),
|
||||
user_id,
|
||||
)
|
||||
pipe.sadd(
|
||||
_USER_SESSIONS_KEY.format(user_id=user_id),
|
||||
session_id,
|
||||
)
|
||||
await pipe.execute()
|
||||
|
||||
|
||||
@@ -145,6 +282,18 @@ async def get_session_messages(
|
||||
return [json.loads(item) for item in raw]
|
||||
|
||||
|
||||
async def get_session_user_id(
|
||||
session_id: str,
|
||||
valkey: Redis | None = None,
|
||||
) -> str | None:
|
||||
"""Return the user_id associated with a session, or ``None``."""
|
||||
if valkey is None:
|
||||
valkey = await get_valkey()
|
||||
|
||||
key = _SESSION_USER_KEY.format(session_id=session_id)
|
||||
return await valkey.get(key)
|
||||
|
||||
|
||||
async def list_sessions(
|
||||
valkey: Redis | None = None,
|
||||
) -> list[str]:
|
||||
@@ -155,6 +304,18 @@ async def list_sessions(
|
||||
return sorted(await valkey.smembers(_SESSIONS_SET))
|
||||
|
||||
|
||||
async def list_user_sessions(
|
||||
user_id: str,
|
||||
valkey: Redis | None = None,
|
||||
) -> list[str]:
|
||||
"""Return all session IDs for a given user."""
|
||||
if valkey is None:
|
||||
valkey = await get_valkey()
|
||||
|
||||
key = _USER_SESSIONS_KEY.format(user_id=user_id)
|
||||
return sorted(await valkey.smembers(key))
|
||||
|
||||
|
||||
async def delete_session(
|
||||
session_id: str,
|
||||
valkey: Redis | None = None,
|
||||
@@ -163,8 +324,18 @@ async def delete_session(
|
||||
if valkey is None:
|
||||
valkey = await get_valkey()
|
||||
|
||||
# Remove from user's session set if linked.
|
||||
user_id = await get_session_user_id(session_id, valkey=valkey)
|
||||
user_sessions_key = (
|
||||
_USER_SESSIONS_KEY.format(user_id=user_id) if user_id else None
|
||||
)
|
||||
|
||||
key = _MESSAGES_KEY.format(session_id=session_id)
|
||||
session_user_key = _SESSION_USER_KEY.format(session_id=session_id)
|
||||
async with valkey.pipeline(transaction=True) as pipe:
|
||||
pipe.delete(key)
|
||||
pipe.delete(session_user_key)
|
||||
pipe.srem(_SESSIONS_SET, session_id)
|
||||
if user_sessions_key is not None:
|
||||
pipe.srem(user_sessions_key, session_id)
|
||||
await pipe.execute()
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, AsyncIterator
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from backend.core.agent import AgentService
|
||||
from backend.core.database import get_session as _get_db_session
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from backend.core.config import Settings
|
||||
@@ -23,3 +26,9 @@ async def get_agent_service() -> AgentService:
|
||||
from backend.core.agent import get_service
|
||||
|
||||
return get_service()
|
||||
|
||||
|
||||
async def get_db_session() -> AsyncIterator[AsyncSession]:
|
||||
"""Provide an async SQLAlchemy session for route dependencies."""
|
||||
async for session in _get_db_session():
|
||||
yield session
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
"""SQLAlchemy ORM models for Agent Alpha."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, String
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
"""Declarative base for all ORM models."""
|
||||
|
||||
|
||||
class User(Base):
|
||||
"""Application user with role-based access control.
|
||||
|
||||
Roles: ``admin``, ``user``, ``viewer``
|
||||
"""
|
||||
|
||||
__tablename__ = "users"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
primary_key=True,
|
||||
default=uuid.uuid4,
|
||||
)
|
||||
username: Mapped[str] = mapped_column(
|
||||
String(100), unique=True, nullable=False
|
||||
)
|
||||
display_name: Mapped[str] = mapped_column(String(200), nullable=False)
|
||||
role: Mapped[str] = mapped_column(
|
||||
String(20), nullable=False, default="user"
|
||||
)
|
||||
team: Mapped[str | None] = mapped_column(
|
||||
String(100), nullable=True, default=None
|
||||
)
|
||||
is_active: Mapped[bool] = mapped_column(
|
||||
Boolean, nullable=False, default=True
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
nullable=False,
|
||||
default=lambda: datetime.now(timezone.utc),
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
nullable=False,
|
||||
default=lambda: datetime.now(timezone.utc),
|
||||
onupdate=lambda: datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<User {self.username!r} role={self.role!r}>"
|
||||
+34
-11
@@ -1,18 +1,20 @@
|
||||
"""Chat API endpoint — sends prompts to the agent and returns replies.
|
||||
|
||||
Messages are persisted in Valkey so chat history survives server restarts.
|
||||
Sessions can optionally be linked to a user for session/role management.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
|
||||
from backend.core.agent import AgentService
|
||||
from backend.core.database import (
|
||||
get_session_messages,
|
||||
get_session_user_id,
|
||||
list_sessions,
|
||||
save_message,
|
||||
)
|
||||
@@ -24,6 +26,7 @@ router = APIRouter(prefix="/api/chat", tags=["chat"])
|
||||
class ChatRequest(BaseModel):
|
||||
message: str
|
||||
session_id: str | None = None
|
||||
user_id: str | None = None
|
||||
|
||||
|
||||
class ChatResponse(BaseModel):
|
||||
@@ -40,6 +43,7 @@ class MessageOut(BaseModel):
|
||||
class SessionOut(BaseModel):
|
||||
session_id: str
|
||||
message_count: int
|
||||
user_id: str | None = None
|
||||
|
||||
|
||||
# ── POST /api/chat ────────────────────────────────────────────────────────
|
||||
@@ -54,12 +58,18 @@ async def chat_endpoint(
|
||||
|
||||
Saves both the user message and the assistant reply to Valkey.
|
||||
Assigns a new UUID ``session_id`` if none was provided.
|
||||
If ``user_id`` is provided, the session is linked to that user.
|
||||
"""
|
||||
session_id = body.session_id or uuid.uuid4().hex
|
||||
|
||||
try:
|
||||
# Persist the user message.
|
||||
await save_message(session_id, "user", body.message)
|
||||
# Persist the user message (linked to user if provided).
|
||||
await save_message(
|
||||
session_id,
|
||||
"user",
|
||||
body.message,
|
||||
user_id=body.user_id,
|
||||
)
|
||||
|
||||
# Ask the agent.
|
||||
output = await agent.ask(body.message, session_id=session_id)
|
||||
@@ -75,10 +85,6 @@ async def chat_endpoint(
|
||||
# ── GET /api/chat/history ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
class HistoryParams(BaseModel):
|
||||
session_id: str
|
||||
|
||||
|
||||
@router.get("/history", response_model=list[MessageOut])
|
||||
async def get_history(session_id: str) -> list[MessageOut]:
|
||||
"""Return all messages for a given session."""
|
||||
@@ -90,11 +96,28 @@ async def get_history(session_id: str) -> list[MessageOut]:
|
||||
|
||||
|
||||
@router.get("/sessions", response_model=list[SessionOut])
|
||||
async def sessions_list() -> list[SessionOut]:
|
||||
"""Return all known session IDs with their message counts."""
|
||||
ids = await list_sessions()
|
||||
async def sessions_list(
|
||||
user_id: str | None = Query(None, description="Filter by user ID"),
|
||||
) -> list[SessionOut]:
|
||||
"""Return all known session IDs with their message counts.
|
||||
|
||||
If ``user_id`` is provided, only sessions belonging to that user
|
||||
are returned.
|
||||
"""
|
||||
if user_id is not None:
|
||||
from backend.core.database import list_user_sessions
|
||||
|
||||
ids = await list_user_sessions(user_id)
|
||||
else:
|
||||
ids = await list_sessions()
|
||||
|
||||
result: list[SessionOut] = []
|
||||
for sid in ids:
|
||||
msgs = await get_session_messages(sid)
|
||||
result.append(SessionOut(session_id=sid, message_count=len(msgs)))
|
||||
uid = await get_session_user_id(sid)
|
||||
result.append(
|
||||
SessionOut(
|
||||
session_id=sid, message_count=len(msgs), user_id=uid
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
"""User management API routes — CRUD for application users.
|
||||
|
||||
Each user has a ``role``: ``admin``, ``user``, or ``viewer``.
|
||||
Sessions are associated with users for role-based access.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from backend.core.database import (
|
||||
create_user,
|
||||
get_user,
|
||||
get_user_by_username,
|
||||
list_user_sessions,
|
||||
list_users,
|
||||
update_user,
|
||||
)
|
||||
from backend.core.dependencies import get_db_session
|
||||
|
||||
router = APIRouter(prefix="/api/users", tags=["users"])
|
||||
|
||||
|
||||
# ── Schemas ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class UserCreate(BaseModel):
|
||||
username: str
|
||||
display_name: str
|
||||
role: str = "user"
|
||||
team: str | None = None
|
||||
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
display_name: str | None = None
|
||||
role: str | None = None
|
||||
team: str | None = None
|
||||
is_active: bool | None = None
|
||||
|
||||
|
||||
class UserOut(BaseModel):
|
||||
id: str
|
||||
username: str
|
||||
display_name: str
|
||||
role: str
|
||||
team: str | None = None
|
||||
is_active: bool
|
||||
created_at: str
|
||||
updated_at: str
|
||||
|
||||
|
||||
class UserSessionOut(BaseModel):
|
||||
session_id: str
|
||||
|
||||
|
||||
# ── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _user_to_out(user: object) -> UserOut:
|
||||
"""Convert a User ORM instance to a UserOut schema."""
|
||||
return UserOut(
|
||||
id=str(user.id),
|
||||
username=user.username,
|
||||
display_name=user.display_name,
|
||||
role=user.role,
|
||||
team=user.team,
|
||||
is_active=user.is_active,
|
||||
created_at=user.created_at.isoformat(),
|
||||
updated_at=user.updated_at.isoformat(),
|
||||
)
|
||||
|
||||
|
||||
# ── Routes ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.post("", response_model=UserOut, status_code=201)
|
||||
async def create_user_endpoint(
|
||||
body: UserCreate,
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> UserOut:
|
||||
"""Create a new user."""
|
||||
# Check for duplicate username.
|
||||
existing = await get_user_by_username(body.username, session=session)
|
||||
if existing is not None:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail=f"User '{body.username}' already exists",
|
||||
)
|
||||
|
||||
user = await create_user(
|
||||
username=body.username,
|
||||
display_name=body.display_name,
|
||||
role=body.role,
|
||||
team=body.team,
|
||||
session=session,
|
||||
)
|
||||
return _user_to_out(user)
|
||||
|
||||
|
||||
@router.get("", response_model=list[UserOut])
|
||||
async def list_users_endpoint(
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> list[UserOut]:
|
||||
"""List all active users."""
|
||||
users = await list_users(session=session)
|
||||
return [_user_to_out(u) for u in users]
|
||||
|
||||
|
||||
@router.get("/{user_id}", response_model=UserOut)
|
||||
async def get_user_endpoint(
|
||||
user_id: uuid.UUID,
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> UserOut:
|
||||
"""Get a user by their UUID."""
|
||||
user = await get_user(user_id, session=session)
|
||||
if user is None:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return _user_to_out(user)
|
||||
|
||||
|
||||
@router.patch("/{user_id}", response_model=UserOut)
|
||||
async def update_user_endpoint(
|
||||
user_id: uuid.UUID,
|
||||
body: UserUpdate,
|
||||
session: AsyncSession = Depends(get_db_session),
|
||||
) -> UserOut:
|
||||
"""Update a user's display_name, role, or is_active."""
|
||||
kwargs = body.model_dump(exclude_none=True)
|
||||
if not kwargs:
|
||||
raise HTTPException(status_code=400, detail="No fields to update")
|
||||
|
||||
user = await update_user(user_id, session=session, **kwargs)
|
||||
if user is None:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return _user_to_out(user)
|
||||
|
||||
|
||||
@router.get("/{user_id}/sessions", response_model=list[UserSessionOut])
|
||||
async def user_sessions_endpoint(
|
||||
user_id: uuid.UUID,
|
||||
) -> list[UserSessionOut]:
|
||||
"""List all session IDs associated with a user."""
|
||||
session_ids = await list_user_sessions(str(user_id))
|
||||
return [UserSessionOut(session_id=sid) for sid in session_ids]
|
||||
+372
-15
@@ -1,5 +1,14 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { sendMessage, getHistory, type MessageData } from "./api";
|
||||
import {
|
||||
sendMessage,
|
||||
getHistory,
|
||||
listUsers,
|
||||
createUser,
|
||||
getUserSessions,
|
||||
type MessageData,
|
||||
type UserData,
|
||||
type UserSessionData,
|
||||
} from "./api";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Types */
|
||||
@@ -10,7 +19,14 @@ interface Message {
|
||||
content: string;
|
||||
}
|
||||
|
||||
const STORAGE_KEY = "agent_alpha_session_id";
|
||||
const STORAGE_SESSION_KEY = "agent_alpha_session_id";
|
||||
const STORAGE_USER_KEY = "agent_alpha_user_id";
|
||||
|
||||
const ROLE_BADGES: Record<string, string> = {
|
||||
admin: "bg-purple-600",
|
||||
user: "bg-indigo-600",
|
||||
viewer: "bg-gray-600",
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Component */
|
||||
@@ -21,13 +37,47 @@ export default function App() {
|
||||
const [input, setInput] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [sessionId, setSessionId] = useState<string>(() => {
|
||||
return localStorage.getItem(STORAGE_KEY) ?? "";
|
||||
return localStorage.getItem(STORAGE_SESSION_KEY) ?? "";
|
||||
});
|
||||
|
||||
/* ── User state ──────────────────────────────────────────────────── */
|
||||
const [users, setUsers] = useState<UserData[]>([]);
|
||||
const [currentUser, setCurrentUser] = useState<UserData | null>(null);
|
||||
const [showUserPanel, setShowUserPanel] = useState(false);
|
||||
const [showCreateUser, setShowCreateUser] = useState(false);
|
||||
|
||||
/* ── Create user form state ──────────────────────────────────────── */
|
||||
const [newUsername, setNewUsername] = useState("");
|
||||
const [newDisplayName, setNewDisplayName] = useState("");
|
||||
const [newRole, setNewRole] = useState("user");
|
||||
const [newTeam, setNewTeam] = useState("");
|
||||
|
||||
/* ── Session sidebar state ───────────────────────────────────────── */
|
||||
const [userSessions, setUserSessions] = useState<UserSessionData[]>([]);
|
||||
const [showSessions, setShowSessions] = useState(false);
|
||||
|
||||
const bottomRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
/* Restore chat history on mount */
|
||||
/* Load users on mount and restore selected user */
|
||||
useEffect(() => {
|
||||
const sid = localStorage.getItem(STORAGE_KEY);
|
||||
const storedUserId = localStorage.getItem(STORAGE_USER_KEY);
|
||||
|
||||
listUsers()
|
||||
.then((allUsers) => {
|
||||
setUsers(allUsers);
|
||||
if (storedUserId) {
|
||||
const found = allUsers.find((u) => u.id === storedUserId);
|
||||
if (found) setCurrentUser(found);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Users table may not exist yet — that's OK.
|
||||
});
|
||||
}, []);
|
||||
|
||||
/* Restore chat history on mount or session change */
|
||||
useEffect(() => {
|
||||
const sid = localStorage.getItem(STORAGE_SESSION_KEY);
|
||||
if (sid) {
|
||||
getHistory(sid)
|
||||
.then((history: MessageData[]) => {
|
||||
@@ -69,6 +119,80 @@ export default function App() {
|
||||
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, [messages]);
|
||||
|
||||
/* ── User handlers ────────────────────────────────────────────────── */
|
||||
|
||||
const handleSelectUser = (user: UserData) => {
|
||||
setCurrentUser(user);
|
||||
localStorage.setItem(STORAGE_USER_KEY, user.id);
|
||||
setShowUserPanel(false);
|
||||
};
|
||||
|
||||
const handleCreateUser = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!newUsername.trim() || !newDisplayName.trim()) return;
|
||||
|
||||
try {
|
||||
const user = await createUser({
|
||||
username: newUsername.trim(),
|
||||
display_name: newDisplayName.trim(),
|
||||
role: newRole,
|
||||
team: newTeam.trim() || null,
|
||||
});
|
||||
setUsers((prev) => [...prev, user]);
|
||||
setCurrentUser(user);
|
||||
localStorage.setItem(STORAGE_USER_KEY, user.id);
|
||||
setNewUsername("");
|
||||
setNewDisplayName("");
|
||||
setNewRole("user");
|
||||
setNewTeam("");
|
||||
setShowCreateUser(false);
|
||||
setShowUserPanel(false);
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : "Unknown error";
|
||||
alert(`Failed to create user: ${msg}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewSessions = async () => {
|
||||
if (!currentUser) return;
|
||||
try {
|
||||
const sessions = await getUserSessions(currentUser.id);
|
||||
setUserSessions(sessions);
|
||||
setShowSessions(true);
|
||||
} catch {
|
||||
setUserSessions([]);
|
||||
setShowSessions(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectSession = (sid: string) => {
|
||||
localStorage.setItem(STORAGE_SESSION_KEY, sid);
|
||||
setSessionId(sid);
|
||||
setShowSessions(false);
|
||||
// Reload messages for this session.
|
||||
getHistory(sid)
|
||||
.then((history: MessageData[]) => {
|
||||
if (history.length > 0) {
|
||||
setMessages(
|
||||
history.map((m) => ({ role: m.role, content: m.content })),
|
||||
);
|
||||
} else {
|
||||
setMessages([
|
||||
{
|
||||
role: "assistant",
|
||||
content:
|
||||
"Hello! I'm **Agent Alpha**. How can I help you today?",
|
||||
},
|
||||
]);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setMessages([]);
|
||||
});
|
||||
};
|
||||
|
||||
/* ── Chat handler ────────────────────────────────────────────────── */
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const text = input.trim();
|
||||
@@ -79,9 +203,12 @@ export default function App() {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const data = await sendMessage(text, sessionId || undefined);
|
||||
// Persist session_id so future refreshes restore history.
|
||||
localStorage.setItem(STORAGE_KEY, data.session_id);
|
||||
const data = await sendMessage(
|
||||
text,
|
||||
sessionId || undefined,
|
||||
currentUser?.id,
|
||||
);
|
||||
localStorage.setItem(STORAGE_SESSION_KEY, data.session_id);
|
||||
setSessionId(data.session_id);
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
@@ -98,6 +225,8 @@ export default function App() {
|
||||
}
|
||||
};
|
||||
|
||||
/* ── Render ──────────────────────────────────────────────────────── */
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex h-dvh max-w-4xl flex-col">
|
||||
{/* ---- Header ---- */}
|
||||
@@ -105,14 +234,240 @@ export default function App() {
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-gradient-to-br from-indigo-500 to-purple-600 text-sm font-bold text-white shadow-lg shadow-indigo-500/25">
|
||||
α
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex-1">
|
||||
<h1 className="text-lg font-semibold leading-tight tracking-tight">
|
||||
Agent Alpha
|
||||
</h1>
|
||||
<p className="text-xs text-gray-500">powered by pydantic-ai</p>
|
||||
</div>
|
||||
|
||||
{/* User badge */}
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowUserPanel(!showUserPanel)}
|
||||
className="flex items-center gap-2 rounded-lg border border-gray-700 px-3 py-1.5 text-sm transition hover:border-indigo-500"
|
||||
>
|
||||
{currentUser ? (
|
||||
<>
|
||||
<span
|
||||
className={`h-2 w-2 rounded-full ${ROLE_BADGES[currentUser.role] ?? "bg-gray-500"}`}
|
||||
/>
|
||||
<span className="text-gray-200">{currentUser.display_name}</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
{currentUser.team ? `${currentUser.team} · ` : ""}
|
||||
{currentUser.role}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-gray-400">Select user</span>
|
||||
)}
|
||||
<svg
|
||||
className={`h-4 w-4 text-gray-400 transition ${showUserPanel ? "rotate-180" : ""}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* User dropdown panel */}
|
||||
{showUserPanel && (
|
||||
<div className="absolute right-0 top-full z-50 mt-2 w-64 rounded-xl border border-gray-700 bg-gray-900 p-3 shadow-2xl">
|
||||
<p className="mb-2 text-xs font-semibold uppercase tracking-wider text-gray-500">
|
||||
Users
|
||||
</p>
|
||||
{users.length === 0 && (
|
||||
<p className="py-2 text-sm text-gray-500">
|
||||
No users yet. Create one below.
|
||||
</p>
|
||||
)}
|
||||
<div className="mb-2 max-h-48 space-y-1 overflow-y-auto">
|
||||
{users.map((user) => (
|
||||
<button
|
||||
key={user.id}
|
||||
onClick={() => handleSelectUser(user)}
|
||||
className={`flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm transition hover:bg-gray-800 ${
|
||||
currentUser?.id === user.id
|
||||
? "bg-gray-800 ring-1 ring-indigo-500"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`h-2 w-2 shrink-0 rounded-full ${ROLE_BADGES[user.role] ?? "bg-gray-500"}`}
|
||||
/>
|
||||
<span className="flex-1 truncate text-gray-200">
|
||||
{user.display_name}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
{user.team ?? user.role}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowUserPanel(false);
|
||||
setShowCreateUser(true);
|
||||
}}
|
||||
className="w-full rounded-lg border border-dashed border-gray-600 px-3 py-2 text-sm text-gray-400 transition hover:border-indigo-500 hover:text-indigo-400"
|
||||
>
|
||||
+ New user
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sessions button */}
|
||||
{currentUser && (
|
||||
<button
|
||||
onClick={handleViewSessions}
|
||||
className="rounded-lg border border-gray-700 px-3 py-1.5 text-sm text-gray-400 transition hover:border-indigo-500 hover:text-indigo-400"
|
||||
title="View my sessions"
|
||||
>
|
||||
Sessions
|
||||
</button>
|
||||
)}
|
||||
</header>
|
||||
|
||||
{/* ---- Create User Modal ---- */}
|
||||
{showCreateUser && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
|
||||
<form
|
||||
onSubmit={handleCreateUser}
|
||||
className="w-full max-w-sm rounded-2xl border border-gray-700 bg-gray-900 p-6 shadow-2xl"
|
||||
>
|
||||
<h2 className="mb-4 text-lg font-semibold text-gray-100">
|
||||
Create User
|
||||
</h2>
|
||||
|
||||
<label className="mb-1 block text-xs font-medium text-gray-400">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newUsername}
|
||||
onChange={(e) => setNewUsername(e.target.value)}
|
||||
className="mb-3 w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-100 outline-none focus:border-indigo-500"
|
||||
placeholder="alice"
|
||||
required
|
||||
/>
|
||||
|
||||
<label className="mb-1 block text-xs font-medium text-gray-400">
|
||||
Display Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newDisplayName}
|
||||
onChange={(e) => setNewDisplayName(e.target.value)}
|
||||
className="mb-3 w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-100 outline-none focus:border-indigo-500"
|
||||
placeholder="Alice"
|
||||
required
|
||||
/>
|
||||
|
||||
<label className="mb-1 block text-xs font-medium text-gray-400">
|
||||
Team
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newTeam}
|
||||
onChange={(e) => setNewTeam(e.target.value)}
|
||||
className="mb-3 w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-100 outline-none focus:border-indigo-500"
|
||||
placeholder="Engineering (optional)"
|
||||
/>
|
||||
|
||||
<label className="mb-1 block text-xs font-medium text-gray-400">
|
||||
Role
|
||||
</label>
|
||||
<select
|
||||
value={newRole}
|
||||
onChange={(e) => setNewRole(e.target.value)}
|
||||
className="mb-4 w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-100 outline-none focus:border-indigo-500"
|
||||
>
|
||||
<option value="user">User</option>
|
||||
<option value="admin">Admin</option>
|
||||
<option value="viewer">Viewer</option>
|
||||
</select>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowCreateUser(false)}
|
||||
className="flex-1 rounded-lg border border-gray-700 px-4 py-2 text-sm text-gray-400 transition hover:bg-gray-800"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex-1 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-indigo-500"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ---- Sessions Sidebar ---- */}
|
||||
{showSessions && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
|
||||
<div className="w-full max-w-md rounded-2xl border border-gray-700 bg-gray-900 p-6 shadow-2xl">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-gray-100">
|
||||
Sessions — {currentUser?.display_name}
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setShowSessions(false)}
|
||||
className="text-gray-500 hover:text-gray-300"
|
||||
>
|
||||
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{userSessions.length === 0 ? (
|
||||
<p className="py-8 text-center text-sm text-gray-500">
|
||||
No sessions yet. Start a chat to create one.
|
||||
</p>
|
||||
) : (
|
||||
<div className="max-h-64 space-y-2 overflow-y-auto">
|
||||
{userSessions.map((s) => (
|
||||
<button
|
||||
key={s.session_id}
|
||||
onClick={() => handleSelectSession(s.session_id)}
|
||||
className={`flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left transition hover:bg-gray-800 ${
|
||||
sessionId === s.session_id
|
||||
? "bg-gray-800 ring-1 ring-indigo-500"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-gray-800 text-xs text-gray-400">
|
||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1 truncate">
|
||||
<p className="text-sm text-gray-200">
|
||||
{s.session_id.slice(0, 16)}…
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500">
|
||||
{s.session_id.slice(0, 8)}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setShowSessions(false)}
|
||||
className="mt-4 w-full rounded-lg border border-gray-700 px-4 py-2 text-sm text-gray-400 transition hover:bg-gray-800"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ---- Messages ---- */}
|
||||
<div className="flex-1 overflow-y-auto px-6 py-4 scrollbar-thin">
|
||||
<div className="space-y-4">
|
||||
@@ -181,15 +536,17 @@ export default function App() {
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
function Markdown({ content }: { content: string }) {
|
||||
/* Bold */
|
||||
const rendered = content
|
||||
.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
|
||||
.replace(/(?<!\*)__(.+?)__(?!\*)/g, "<strong>$1</strong>")
|
||||
/* Code blocks */
|
||||
.replace(/```(\w*)\n([\s\S]*?)```/g, "<pre class='my-2 overflow-x-auto rounded-lg bg-gray-900 p-3 text-xs'><code>$2</code></pre>")
|
||||
/* Inline code */
|
||||
.replace(/`([^`]+)`/g, "<code class='rounded bg-gray-900 px-1 py-0.5 text-xs text-indigo-300'>$1</code>")
|
||||
/* Line breaks */
|
||||
.replace(
|
||||
/```(\w*)\n([\s\S]*?)```/g,
|
||||
"<pre class='my-2 overflow-x-auto rounded-lg bg-gray-900 p-3 text-xs'><code>$2</code></pre>",
|
||||
)
|
||||
.replace(
|
||||
/`([^`]+)`/g,
|
||||
"<code class='rounded bg-gray-900 px-1 py-0.5 text-xs text-indigo-300'>$1</code>",
|
||||
)
|
||||
.replace(/\n/g, "<br />");
|
||||
|
||||
return <span dangerouslySetInnerHTML={{ __html: rendered }} />;
|
||||
|
||||
+74
-2
@@ -1,6 +1,6 @@
|
||||
const API_BASE = "/api";
|
||||
|
||||
/* ── Types ──────────────────────────────────────────────────────── */
|
||||
/* ── Chat Types ────────────────────────────────────────────────── */
|
||||
|
||||
export interface ChatResponse {
|
||||
reply: string;
|
||||
@@ -13,16 +13,51 @@ export interface MessageData {
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export interface SessionData {
|
||||
session_id: string;
|
||||
message_count: number;
|
||||
user_id: string | null;
|
||||
}
|
||||
|
||||
/* ── User Types ────────────────────────────────────────────────── */
|
||||
|
||||
export interface UserData {
|
||||
id: string;
|
||||
username: string;
|
||||
display_name: string;
|
||||
role: "admin" | "user" | "viewer";
|
||||
team: string | null;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface UserCreateData {
|
||||
username: string;
|
||||
display_name: string;
|
||||
role?: string;
|
||||
team?: string | null;
|
||||
}
|
||||
|
||||
export interface UserSessionData {
|
||||
session_id: string;
|
||||
}
|
||||
|
||||
/* ── Send a message ────────────────────────────────────────────── */
|
||||
|
||||
export async function sendMessage(
|
||||
message: string,
|
||||
sessionId?: string,
|
||||
userId?: string,
|
||||
): Promise<ChatResponse> {
|
||||
const res = await fetch(`${API_BASE}/chat`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ message, session_id: sessionId ?? null }),
|
||||
body: JSON.stringify({
|
||||
message,
|
||||
session_id: sessionId ?? null,
|
||||
user_id: userId ?? null,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => null);
|
||||
@@ -43,3 +78,40 @@ export async function getHistory(sessionId: string): Promise<MessageData[]> {
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/* ── User API ──────────────────────────────────────────────────── */
|
||||
|
||||
export async function createUser(
|
||||
data: UserCreateData,
|
||||
): Promise<UserData> {
|
||||
const res = await fetch(`${API_BASE}/users`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => null);
|
||||
throw new Error(body?.detail ?? `HTTP ${res.status}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function listUsers(): Promise<UserData[]> {
|
||||
const res = await fetch(`${API_BASE}/users`);
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => null);
|
||||
throw new Error(body?.detail ?? `HTTP ${res.status}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function getUserSessions(
|
||||
userId: string,
|
||||
): Promise<UserSessionData[]> {
|
||||
const res = await fetch(`${API_BASE}/users/${userId}/sessions`);
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => null);
|
||||
throw new Error(body?.detail ?? `HTTP ${res.status}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ name = "agent-alpha"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "asyncpg" },
|
||||
{ name = "fastapi", extra = ["standard"] },
|
||||
{ name = "logfire", extra = ["asyncpg", "fastapi", "httpx", "sqlite3"] },
|
||||
{ name = "pydantic-ai-backend" },
|
||||
@@ -22,11 +23,14 @@ dependencies = [
|
||||
{ name = "pydantic-ai-todo" },
|
||||
{ name = "pydantic-deep" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "redis", extra = ["hiredis"] },
|
||||
{ name = "sqlalchemy", extra = ["asyncio"] },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "asyncpg", specifier = ">=0.30.0" },
|
||||
{ name = "fastapi", extras = ["standard"], specifier = ">=0.136.3" },
|
||||
{ name = "logfire", extras = ["asyncpg", "fastapi", "httpx", "sqlite3"], specifier = ">=4.36.0" },
|
||||
{ name = "pydantic-ai-backend", specifier = ">=0.2.11" },
|
||||
@@ -37,6 +41,8 @@ requires-dist = [
|
||||
{ name = "pydantic-ai-todo", specifier = ">=0.2.4" },
|
||||
{ name = "pydantic-deep", specifier = ">=0.3.28" },
|
||||
{ name = "pydantic-settings", specifier = ">=2.7.0" },
|
||||
{ name = "redis", extras = ["hiredis"], specifier = ">=5.2.0" },
|
||||
{ name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.36" },
|
||||
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.34.0" },
|
||||
]
|
||||
|
||||
@@ -798,6 +804,73 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "greenlet"
|
||||
version = "3.5.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6d/6e/802acd792aebb2256fbbee8cacf2727faaeb6f240ac11008f09eae4414bc/greenlet-3.5.1.tar.gz", hash = "sha256:5a56aeb7d5d9cc4b3a735efb5095bd4b4f6f0e4f93e5ca876d0e2315137b7829", size = 197356, upload-time = "2026-05-20T15:05:03.917Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/37/4549f149c9797c21b32c2683c33522af22522099de128b2406672526d005/greenlet-3.5.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:fa4f98af3a528f0c3fd592a26df7f376f93329c8f4d987f6bb979057af8bf5e2", size = 286220, upload-time = "2026-05-20T13:07:28.463Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/ff/a4f436709716965eaab9f36ea7b906c8a927fbe32fb1372a2071d964f6b1/greenlet-3.5.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffea73584b216150eab159b6d12348fb253e68757974de1e2c40d8a318ac89ed", size = 601585, upload-time = "2026-05-20T14:00:06.141Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/ad/54bc3fcee3ad368a61b19b67d88117f7a8c29727bf71fffdeda81fbd946e/greenlet-3.5.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1072b4f9edcc1e192d9283a66a3e68d6b84c561de33a83d7858beb9ba1effe10", size = 614215, upload-time = "2026-05-20T14:05:42.675Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/6c/de5b1b388cd2d9fbdfeab324863daba37d54e6e233ddbefd70b385a8c591/greenlet-3.5.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:89101bfd5011e069be974903cb3a4e4523845e4ece2d62dcd8d358933c0ef249", size = 620094, upload-time = "2026-05-20T14:09:09.18Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/69/b91cda0647df839483201545913514c2827ebea5e5ccdf931842763bc127/greenlet-3.5.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:add5217d68b31130f0beca584d7fef4878327d2e31642b66618a14eef312b63b", size = 611358, upload-time = "2026-05-20T13:14:26.37Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/43/1204baffab8a6476464795a7ccf394a3248d4f22c9f87173a15b36b6d971/greenlet-3.5.1-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:e6cd99ea59dd5d89f0c956606571d79bfe6f68c9eb7f4a4083a41a7f1587edee", size = 422782, upload-time = "2026-05-20T14:01:39.597Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/90/3cf77e080350cd02fa307bb2abf05df48f4482c240275bbd2c203ba8bb1c/greenlet-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a5ea42a752d47a145eae922b605cd1634665ac3d5ec1e72402d5048e8d60d207", size = 1570475, upload-time = "2026-05-20T14:02:25.29Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/2c/18cece62045e74598c3c393f70dce4a63f56222015ba29a5d4eeb04f764c/greenlet-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5551170cf4f5ff5623e9af81323751979fee2c731e2287b61f73cd27257b823", size = 1635625, upload-time = "2026-05-20T13:14:34.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/f5/310d104ddf41eb5a70f4c268d22508dfb0c3c8e86fec152be34d0d2ed819/greenlet-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c8bb982ad117d29478ef8f5533e97df21f1e2befd17a299257b0c96d1371c0b", size = 238791, upload-time = "2026-05-20T13:10:39.018Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/90/ceca11f504cd23a8047a3dea31919adc48df9b626dd0c13f0d858734fdfd/greenlet-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:80eb4b04dadc4e67df3fae179a32c4706a3f495bc7f22fc8a81115d5f5512188", size = 235580, upload-time = "2026-05-20T13:08:45.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/69/7f7e5372d998b81001899b1c0823c957aa413ba0f2662e65821611cc31e4/greenlet-3.5.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:51518ff74664078fc51bffcc6fc529b0df5ae58da192691cee765d45ce944a2b", size = 285060, upload-time = "2026-05-20T13:08:51.899Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/bf/387f9b6b865fd2ae0d0be09e0004827295a01b71be76ed350dd1e28a91a4/greenlet-3.5.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ffdb3c0bb002c99cd8f298957e046c3dbf6006b5b7cdf11a4e19194624a0a0a", size = 604370, upload-time = "2026-05-20T14:00:07.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/f5/169ce3d4e4c67291bd18f8cbe0299c9f3e45102c7f1fb3c14780c93e4532/greenlet-3.5.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7715a5a2c3378ba602c3a440558261e13a820bb53a82693aacd7b7f6d964e283", size = 616987, upload-time = "2026-05-20T14:05:44.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/ba/c24110c55dffa55aa6e1d98b45310da33801aeba7686ff0190fe5d46fd32/greenlet-3.5.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d40a890035c0058cadbdc4af7569800fd28a0e527a0fdbb7b5f9418f176846ce", size = 622911, upload-time = "2026-05-20T14:09:10.598Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/e5/7f2e41d5273be07e77560d61ea4e56485b4d6c316d2a84518c62d1364061/greenlet-3.5.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc71ff466927a201b08305acac451ebe1aedfcea002f62f1f2f2ac2ac1e6a135", size = 613911, upload-time = "2026-05-20T13:14:27.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/7b/d20db2e8a5ad6c038702f3179b136f93f0a3d1a21a0c0777f3e470cdf4b2/greenlet-3.5.1-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:67821bb03e4e98664490edb787ff6af501194c29bbee0f5c1dfdcf1dc3d9d436", size = 425228, upload-time = "2026-05-20T14:01:40.837Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/a4/fbdc67579b73615a1f91615e814303cc71e06128f7baaba87be79b8fb90c/greenlet-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cd443683db272ebaaca03af98c0b063ab30db70ea8a31a1559f35e3f7b744ccd", size = 1570689, upload-time = "2026-05-20T14:02:27.225Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/b4/77abbe35078be39718a46cd49caf16bceb35662f97a34101dca28aa98e47/greenlet-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:089fff7a6ce8d9316d1f65ebc00273a56be258c1725b32b94de90a3a979557e1", size = 1635602, upload-time = "2026-05-20T13:14:36.344Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/f7/129f27ca700845b8ee8ca88ce7f43435a1239c2eddb7677fc938822762cf/greenlet-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:110a1ca7b49b014b097f6078272c3f4ed31af45b254de5228b79adba879f6af9", size = 238683, upload-time = "2026-05-20T13:11:50.57Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/5c/a485a36e87df8d8fd0632ee01511244f5156a20ed3746cc6599340326395/greenlet-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f16ba1efc0715b680a18b8123d90dad887c6112ae3555b4b5c32c149540c6b4e", size = 235499, upload-time = "2026-05-20T13:12:42.028Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/cb/c62454606daf5640369c94d8a9dd540599b1bfc090e2d2180cb77f4038d2/greenlet-3.5.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8ab31c9de8651a2facdd5c5bb0011f2380dd1a7af78ce2adf4b56095294fc07", size = 285579, upload-time = "2026-05-20T13:08:56.396Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/71/c4270398c2eba968a6071af1dfbdcaeee6ec1c24bc8b435b8cc452700da6/greenlet-3.5.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e300185139abc337ade480c327183adf42a875ac7181bfe66d7d4efea31fbea", size = 651106, upload-time = "2026-05-20T14:00:09.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/ab/71e34b78a44ec271fb5f550c17bc46d301ddc5953890d935f270b0dcdb5a/greenlet-3.5.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7ffdb990dcaa0234cf9845aead5df2e3c3a8b6507d409274dd87e0d5ab05ffc2", size = 663478, upload-time = "2026-05-20T14:05:45.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/2d/2d80842910da44f78c286532d084b8a5c3717c844ae80ceb3858738ae89a/greenlet-3.5.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c09df69dc1712d131332054a858a3e5cca400967fa3a672e2324fbb0971448c", size = 667767, upload-time = "2026-05-20T14:09:12.15Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/96/4efd6fa5c62c85426a0c19077a586258ebc3a2a146ff2493e4312a697a22/greenlet-3.5.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f82b3597e9d83b63408affed0b48fd0f54935edac4302237b9a837be0dae33c", size = 660800, upload-time = "2026-05-20T13:14:29.129Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/d3/dad2eecedfbb1ed7050a20dcfae40c1442b74bc7423608be2c7e03ee7133/greenlet-3.5.1-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:a4764e0bfc6a4d114c865b32520805c16a990ef5f286a514413b05d5ecd6a23d", size = 470786, upload-time = "2026-05-20T14:01:42.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/e0/6c71401a25cac7000261304e866a2f2cc04dc74810d40e2f118aa4799495/greenlet-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c0141e37414c10164e702b8fb1473304221ad98f71600850c6ef7ff4880feba0", size = 1617518, upload-time = "2026-05-20T14:02:28.662Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/26/c5c06643e8c0af9e7bf18e16cb51d0ab7625155f0392e1c9015d66d556cd/greenlet-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:50ae25a67bea74ea41fb14b960bc532df73eb713417b2d61892dced82fe8d3bc", size = 1681593, upload-time = "2026-05-20T13:14:39.417Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/bd/e11a108317485075e68af9d23039619b86b28130c3b50d227d42edece64b/greenlet-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:8a17c42330e261299766b75ac1ea32caa437a9453c8f65d16a13140db378ecd3", size = 239800, upload-time = "2026-05-20T13:09:30.128Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/f8/8e8e8417b7bf28639a5a56356ef934d0375e1d0c70a57e04d7701e870ffe/greenlet-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:7b5f5fae05b8ac6d176a61b60c394a8cbdc2b5b91b81793066e68745cf165e54", size = 236862, upload-time = "2026-05-20T13:09:10.498Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/12/41bf27fde4d3605d3773ae57751eda182b8be2f5398011c041173b1d9534/greenlet-3.5.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:ea8da1e900d758d078810d4255d8c6aa572181896a31ec79d779eb79c3adc9ad", size = 293637, upload-time = "2026-05-20T13:12:35.529Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/44/ba14b23e9757707050c2f397d305bbcae62e5d7cad122f8b6baec5ae4a1f/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a19570c52a21420dcbc94e661994bc325c0b5b11304540fed514586da5dc8f2e", size = 650840, upload-time = "2026-05-20T14:00:11.079Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/37/5ddc2b686a6844f91abecef43411842426da2e1573f60b49ecf2547f4ae1/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3d955c89b75eeca4723d7cc14135f393cd47c32e2a6cb4a8e4c6e760a26b0986", size = 656416, upload-time = "2026-05-20T14:05:47.118Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/46/5987dcd1a2570ba84f3b187536b2ca3ae97613387e57f5cfa99df068fe5e/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea37d5a157eb9493820d3792ac4ece28619a394391d2b9f2f78057d396ff0f0f", size = 656607, upload-time = "2026-05-20T14:09:13.949Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/f0/d17510297c35a2992712f0bf84de3779749999f7d3d63aa1f09db7c62dbe/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2daaaebd1a5aa88c49045b6baf9310b3263796bd88db713edf37cf53e7bb4e", size = 654397, upload-time = "2026-05-20T13:14:30.696Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/c1/6da0a9ddcc29d7e51ef14883fa3dc1e53b3f4ffba00582106c7bf55da1d8/greenlet-3.5.1-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:8d8a23250ea3ec7b36de8fa4b541e9e2db3ee82915cc060ab0631609ad8b28de", size = 488287, upload-time = "2026-05-20T14:01:43.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/eb/147387705bb89092645b012586e7273cb5ed3c90ef7eaf3a69173eaf0209/greenlet-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bfbd69cc349e43bf3a8ae1c85548ff0718efc887615c2db16c3833d7b0b072d", size = 1614469, upload-time = "2026-05-20T14:02:30.192Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/4e/37ee0da7732b7aa9896f17e15579a9df34b9fcb9dd494f0adfa749af6623/greenlet-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4378720dd888136c27215a0214d32a4d37c3852765d45bc37aad0623423cfd78", size = 1675115, upload-time = "2026-05-20T13:14:40.972Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/f3/97dfcf4a6eb5077f8a672234216fb5923eb89f2cab7081cb10b2cf75b605/greenlet-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:45718441607f9325d948db98cbc691276059316d0358c188c246da4e1d4d23d2", size = 245246, upload-time = "2026-05-20T13:12:22.646Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/73/d7f72e34b582f694f4a9b248162db7b09cc458a259ba8f0c0bfa1a34ea7d/greenlet-3.5.1-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:2baee5ca02031757ffe8cc3d69f0cc0aec7065ce362622da74f32d3bcab1c541", size = 285575, upload-time = "2026-05-20T13:12:07.043Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/59/fa9c6e87dc8ad27a95dabe2f29f372b733d05a8a67470f6c901ed9975655/greenlet-3.5.1-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b1ec3274918a81d3ea778b9e75b56b72b33f300edb6cf7f3a7fe1dae56683de", size = 656428, upload-time = "2026-05-20T14:00:12.556Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/f9/e753408871eaa61dfe35e619cfc67512b036fde99893685d50eea9e07146/greenlet-3.5.1-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:111e2390ffffc47d5840b01711dd7fac07d4c09283d0283e7f3264b14e284c64", size = 667064, upload-time = "2026-05-20T14:05:48.662Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/74/807a047255bf1e09303627c46dc043dca596b6958a354d904f32ab382005/greenlet-3.5.1-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:10a9a1c0bfbc93d41156ffcb90c75fbc05544054faf15dcc1fdf9765f8b607f0", size = 672962, upload-time = "2026-05-20T14:09:15.532Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/27/5565b5b40389f1c7753003a07e21892fda8660926787036d5bc0308b8113/greenlet-3.5.1-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e630136e905fe5ff43e86945ae41220b6d1470956a39220e708110ac48d01ea5", size = 665697, upload-time = "2026-05-20T13:14:32.943Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/32/19d4e13225193c29b13e308015223f7d75fd3d8623d49dd19040d2ce8ec1/greenlet-3.5.1-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:ef08c1567c78074b22d1a200183d52d04a14df447bf70bcbb6a3507a48e776fc", size = 476047, upload-time = "2026-05-20T14:01:44.39Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/82/e7de4178c0c2d1c9a5a3be3cc0b33e46a85b3ee4a77c071bf7ad8600e079/greenlet-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:975eac34b44a7077ca4d421348455b94f0f518246a7f14bc6d2fdcfe5b584368", size = 1621256, upload-time = "2026-05-20T14:02:31.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/10/f2dddcf7dacac17dfc68691809589adad06135eb28930429cf58a6467a2f/greenlet-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9ab3c3a0b2ae6198e67c898dad5215a49f9ae0d0081b3c3ec59f333e39eeca26", size = 1685956, upload-time = "2026-05-20T13:14:42.55Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/17/4a232b32133230ada52f70e9d7f5b65b0caef8772f01849bd8d149e7e4ca/greenlet-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:cbfc69be86e10dcfef5b1e6269d1d6926552aa89ee39e1de3353360c1b6989ab", size = 239802, upload-time = "2026-05-20T13:13:15.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/ae/4e623a7e6d4d2a5f4cb8e4c82de4169fc637942caae68d6e676b8a128ac5/greenlet-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:92fd6d44ac5e5a887c8a5dc4a8ba0ba908527c31c12f78c6bc7dcfe8aab279f6", size = 236853, upload-time = "2026-05-20T13:15:37.301Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/57/816d9cff29119da3505b3d6a5e14a8af89006ac36f47f891ff293ee05af1/greenlet-3.5.1-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:a6fdf2433a5441ef9a95464f7c3e674775da1c8c1177fff311cee1acad4626ed", size = 293877, upload-time = "2026-05-20T13:10:19.078Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/a1/59b0a7c7d140ff1a75626680b9a9899b79a9176cab298b394968fb023295/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7546556f0d649f99f6a361098a55f761181bb2ea12ff150bb16d26092ad88244", size = 655333, upload-time = "2026-05-20T14:00:14.758Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/1b/5efe127597625042218939d01855109f352779050768b670b52edcc16a6c/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5ee3ea898009fa898f85f9982255d35278c477bebe185beca249cab42d4526c", size = 659443, upload-time = "2026-05-20T14:05:50.159Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/9d/1dcdf7b95ab3cf8c7b6d7277c18a5e167312f2b362ddfcc5d5e6d8d84b43/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a57b0d05a0448eed231d59c0ceb287dde984551e54cbc51ac2d4865712838e9c", size = 659998, upload-time = "2026-05-20T14:09:16.912Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/6d/c404246ea4d22d097a7426d0efb5b781bd7eb67715f09e79001bd552ab18/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5c81f74d204d3edd136ebfd50dce53acbb776995d721a0fe801626cfc93b8cd", size = 658356, upload-time = "2026-05-20T13:14:35.091Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/7e/c4959664fc231d587d66d8e81f2095e98056ba1954beafdcbe635e251052/greenlet-3.5.1-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:b0703c2cef53e01baec47f7a3868009913ad71ec678bbecb42a6f40895e4ce62", size = 494470, upload-time = "2026-05-20T14:01:45.611Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/02/f8ee37fb6d2219329f350af241c27fcf12df57e723d11f6fc6d3bacdadaa/greenlet-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:2c18ef16bf6d4dd410e4dd52996888ea1497be26892fe5bbc73580aba4287b8e", size = 1619216, upload-time = "2026-05-20T14:02:33.403Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/c5/3dc9475ace2c7a3680da12372cddd7f1ac874eb410a1ac48d3e9dab83782/greenlet-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:17d86354f0ae6b61bf9be5148d0dd34e06c3cb7c602c671f79f29ac3b150e659", size = 1678427, upload-time = "2026-05-20T13:14:43.71Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/4e/750c15c317a41ffb36f0bf40b933e3d744a7dede61889f74443ea69690cf/greenlet-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:e7516cf6ae6b8a582c2770a0caed47b8a48373ed732c33d69a72913ae6ac923e", size = 245225, upload-time = "2026-05-20T13:13:59.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/fd/d3baea2eeb7b617efd47e87ca06e2ec2c6118d303aa9e918e0ce16eadc10/greenlet-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:5028648bf2253ec4745add746129d3904121fa7fe871a76bed23c5720573ce0a", size = 239590, upload-time = "2026-05-20T13:13:37.382Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "griffelib"
|
||||
version = "2.0.2"
|
||||
@@ -829,6 +902,70 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hiredis"
|
||||
version = "3.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1f/e2/1654d65851f39fd94e91a77a5655d09d4b64901fdc594020d8348db697b2/hiredis-3.4.0.tar.gz", hash = "sha256:da19331354433af6a2c54c21f2d70ba084933c0d7d2c43578ec5c5b446674ad5", size = 137169, upload-time = "2026-06-03T16:23:46.226Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/84/f74deb132d238a0d5a3eb1618bf7558c65230b279421f909a9753231c516/hiredis-3.4.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:9e88048a66dfffec7a3f578f2a2a0fd907c75b5bd85b3c9184f76f0149ea399f", size = 138679, upload-time = "2026-06-03T16:22:17.598Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/13/399fe51d399b8d4f5717aa68cb1dafcb8c244b19b1b9b0afaaa526c1be94/hiredis-3.4.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:8b3f1d03046765c0a83558bf1756811101e3947649c7ca22a71d9dc3c92929d1", size = 74657, upload-time = "2026-06-03T16:22:18.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/cf/6a0bcf454b1642997c4dd007bd89beada43f38b22781afdf475060e427ac/hiredis-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:24751054bb11353016d242d09a4a902ecf8f25e3b56fe396cccb6f056fdda016", size = 70115, upload-time = "2026-06-03T16:22:19.649Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/99/62340215f80e59680c79ae5080c5422311da105870c57bbefc5d87487025/hiredis-3.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:258f820cdd6ee6be39ae6a8ea94a76b8856d34113de6604f63bc81327ef06240", size = 306481, upload-time = "2026-06-03T16:22:20.608Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/be/97f349e5bb0dcab0ef28b15523443d9bbe81f8ccbd3dadff56594dfa82fe/hiredis-3.4.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3774461209688790734b5db8934400a4456493fc1a172fb5298cc5d72201aceb", size = 339560, upload-time = "2026-06-03T16:22:21.861Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/3f/eb6a9632bcc13a3fbefce5de90090052fb1ae1cd3d57faf687f20149d592/hiredis-3.4.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccdb63363c82ea9cea2d48126bc8e9241437b8b3b36413e967647a17add59643", size = 351549, upload-time = "2026-06-03T16:22:22.969Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/8c/440369f727dcb856f3eeda238d6e67781b180feaa831bd28997d8af10c3b/hiredis-3.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:452cff764acb30c106d1e33f1bdf03fa9d4a9b0a9c995d722d4d39c998b40582", size = 313066, upload-time = "2026-06-03T16:22:23.987Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/d1/3d76c4d5c46cd2e7b38641f7c8b325e0cab7d49d565ea573256eb3837d0c/hiredis-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fb0a139cd52535f3e5a532816b5c36b3aea95817410fbf28ca4a676026347a5", size = 300827, upload-time = "2026-06-03T16:22:25.287Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/bc/d112dd9704ae47243a515fb021ec4d0b5a1b8d83a7a3eff3284c0248412d/hiredis-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:163d8c43e2706d23490532ea0de8736fc1493cfa52f0ee65f85b0f074f2fe017", size = 331284, upload-time = "2026-06-03T16:22:26.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/7b/8a4dc0a15e4658c81a9e79b2c167fbfbf750e0c1c7ef13e00e69d4273ced/hiredis-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4b8f52844cd260d7805eca55c834e3e06b4c0d5b53a4178143b92242c2517c0d", size = 332962, upload-time = "2026-06-03T16:22:27.392Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/52/d3d0bb234de8deb4cbd432cdc63d001a6cad1f9c05fe07d2fa652f8cf412/hiredis-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03374d663b0e025e4039757ef5fad02e3ff714f7a01e5b34c88de2a9c91359dc", size = 311698, upload-time = "2026-06-03T16:22:28.442Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/5b/54a052eccaf901703b57d7c28509e74341fa0da08d770f485345397ea1e5/hiredis-3.4.0-cp312-cp312-win32.whl", hash = "sha256:696e0a2118e1df5ccacf8ecf8abe528cf0c4f1f1d867f64c34579bef77778cdb", size = 38921, upload-time = "2026-06-03T16:22:29.39Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/64/6508236eda66765fbe873d1d0a0722e38059302e96dc9915b162ff17b35a/hiredis-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:ee6b4beb79a71df67af15a8451366babc2687fcac674d5c6eacec4197e4ce8c1", size = 40090, upload-time = "2026-06-03T16:22:30.204Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/1c/7333aba1b4b7cef2591b244140aec0f1aad903397bbaa31c1858722b2fe4/hiredis-3.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:14524fdc751e3960d78d848872576b5442b40baae3cac14fbab1ba7ac523891f", size = 36875, upload-time = "2026-06-03T16:22:31.087Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/e5/9e47dda8f1d55e77293c6cdf4169182b7f2f55b56913d1fb16a0ddf63a3d/hiredis-3.4.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:4f0e3536eea76c03435d411099d165850bc3c9d873efe62843b995027135a763", size = 138688, upload-time = "2026-06-03T16:22:31.825Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/07/039bcf7ce8262ed66db736349c121486874826248ccd70c98c2f830ec9da/hiredis-3.4.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:82860f050aabd08c046f304eb57c105bb3d5a7370f79a4a0b74d2b771767cc13", size = 74666, upload-time = "2026-06-03T16:22:32.758Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/6d/692c50d846a0a36578e9ef0c62c6193ce01a48f353f6961de9de88a30b37/hiredis-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:74bcfb26189939daba2a0eb4bad05a6a30773bb2461f3d9967b8ced224bd0de9", size = 70119, upload-time = "2026-06-03T16:22:33.692Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/5d/c8b9ca711b4d6b7637eae744d6b45ea47f6bded61bac0232bb42ed8c583e/hiredis-3.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d95b602ab022f3505288ce51feaa48c072a62e57da55d6a7a38ecb8c5ad67d81", size = 306364, upload-time = "2026-06-03T16:22:34.62Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/7e/e940eea3c2ee1aa5947f2e6224f03a1dfd38a5813307259a25f580411820/hiredis-3.4.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de3e2297a182253dfa4400883a9a4fb46d44946aed3157ea2da873b93e2525c4", size = 339454, upload-time = "2026-06-03T16:22:35.87Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/ea/b8147da5c270a2a5b85090c97d0ff7e2fae6e7c5f7749f8c3c2decadd3ac/hiredis-3.4.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:454236d2a5bd917daf38914ce363e71aeef41240e6800f4799e04ee82689bfd2", size = 351457, upload-time = "2026-06-03T16:22:36.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/b5/ff8fe4f812348f09d2943b109cb64c5301af4f601e1cf026518e93a72fff/hiredis-3.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:35ab3653569b9867b8d8a3b4c0684a20dc769fe45d4666bedfe9a3391a61b30b", size = 312970, upload-time = "2026-06-03T16:22:38.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/2a/c90dff527cb2521ee1687e9e30bdf1156f2f4acfd47833b44dc52fec3ec6/hiredis-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:afff0876dafad6d3bb446c907da2836954876243f6bb9d5e44915d175e424aa4", size = 300850, upload-time = "2026-06-03T16:22:39.146Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/0b/c48e93a1e524198b10ccc26d770368547c0c29d126a992fd4b4aa533f1ac/hiredis-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d5c33eb2da5c9ccd281c396e1c618cfe6a91eb841e957f17d2fa520383b3111d", size = 331430, upload-time = "2026-06-03T16:22:40.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/12/ed5bdc482d5c98930ffa264dd707dfb04b83118b2f7f760760c5dfbe6782/hiredis-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:04e54fc3bcecf8c7cb2846947b84baf7ce1507caba641bd23590c52fefade865", size = 333021, upload-time = "2026-06-03T16:22:41.363Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/42/d4a2e7be82f2b2db7b67ec622806ba099d8fe09d218568f71197922cbe79/hiredis-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5f1ddfe6429f9adc0a8d705afbcd40530fddeafa919873ffbb11f59eda44dbb9", size = 311747, upload-time = "2026-06-03T16:22:42.374Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/33/b5ac3420bd803ca9affd68a4a2a6111812bd26bfb9d6b41a721e009d79d9/hiredis-3.4.0-cp313-cp313-win32.whl", hash = "sha256:165e6405b48f9bd66ddb4ad52ce28b0c0041a0308654d7a0cb4357a1939134dc", size = 38921, upload-time = "2026-06-03T16:22:43.513Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/e7/76e68122b1cf680b93b951a82953fff5b5883dc08ec93f63677eb3653591/hiredis-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:306aae11a52e495aaf0a14e3efcd7b51029e632c74b847bc03159e1e1f6db591", size = 40095, upload-time = "2026-06-03T16:22:44.296Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/05/9313dc27ed159512dc22b4ecf8a62a84d0aa5fbd500ffdad955b361cb2a8/hiredis-3.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:975a8e75a10425442037dd9c7abbaae31941c34328d9f01b1ca42d9db44ac31d", size = 36884, upload-time = "2026-06-03T16:22:45.134Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/ea/cbc922aeaa5af11f1c1235d8b2b04ff8cdf6e3e95c785a500521f32d8d70/hiredis-3.4.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:d3a12ae5685e9621a988af07b5af0ad685c7d19d6a7246ac852e35060178cff4", size = 138762, upload-time = "2026-06-03T16:22:45.927Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/e9/e004067ffad9f707174cde04d117c985d5f22dd4d9409f0983892738cb44/hiredis-3.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0a70df45cf167b5af99b9fe3e2044716919e30580a869dfa766f2a6467c0c320", size = 74696, upload-time = "2026-06-03T16:22:46.924Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/d1/5fe5b6d05e59116d78f9d228d9cc0022efbb84d234333c5fbe6a0c6e13fe/hiredis-3.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0a68b0e48509e6e66f4c212e53d98f29178addf83b0701a71bf0fce792954419", size = 70163, upload-time = "2026-06-03T16:22:47.798Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/93/c86f0a7ae2cd10b72e30476f87aafd1af22992e080feb4b5d2ec1cbdf4e4/hiredis-3.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a45822bc8487da8151fe67c788de74b834582b1d510c67b888fcda64bf6ba4bb", size = 306631, upload-time = "2026-06-03T16:22:48.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/10/3746b028d9c43fab1fa4126fe69c6967df89ab9819140092930322b0550c/hiredis-3.4.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0b82cab9ad7a1574ab273a78942f780c1b1496101eb342b630c46c3e918ca21b", size = 339758, upload-time = "2026-06-03T16:22:49.662Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/f3/c6fb383854237891039a4d94d3e66dc5eec8a2993fed6020c983d63c5393/hiredis-3.4.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db13f8039ad8229f77f0e242be14e53bd67e8f3aadeb16f3af30944287cca092", size = 351360, upload-time = "2026-06-03T16:22:50.779Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/b7/32110aa458690722a1069c7349b8ebe374a6ba0bdf9ef8925a9f37a74978/hiredis-3.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54b6267918c66d8ba4a3cf519db1235a4bd56d2a0969ca5b2ae3c6b6b7d9ed79", size = 313070, upload-time = "2026-06-03T16:22:51.966Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/23/bccfa0fb7b1b529cff35c8725cfd99a2d18fa4123f52f52bf03e84210855/hiredis-3.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:88396e6a24b80c86f4dc180964d9cc467ba3aa3c886af6532fe077c5a5dc0c3c", size = 300927, upload-time = "2026-06-03T16:22:53.085Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/0f/e1e2295ee863efc7ce8c88ec10bcc4b1504352373998cb493f10e900dbe5/hiredis-3.4.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:73dd607b47863633d8070f1eb3bab1b3b097ee747783fe69c0dd0f93ec673d8b", size = 331764, upload-time = "2026-06-03T16:22:54.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/df/11b1de2ac85dfd7a8713d72a6ed7ac0f1a6e28d906bd362e0df3a27f5c86/hiredis-3.4.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:e6e8d5fa63ec2a0738d188488e828818cbe4cb4d37c0c706836cf3888d82c53d", size = 333144, upload-time = "2026-06-03T16:22:55.277Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/10/4b104565c936d51b4b02597352ec068937c9d6a73a3c4c9609c08ae3923e/hiredis-3.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d77901d058923a09ed25063ea6fb2842c153bbe75060a46e3949e73ad12ce352", size = 311593, upload-time = "2026-06-03T16:22:56.573Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/ae/c9eda3c116bef50fcf0dc7e44379e3577f3627caca4ffd7af04675b02d98/hiredis-3.4.0-cp314-cp314-win32.whl", hash = "sha256:05384fcfe5851b5af868bf24265c14ab86f38562679f9c6f712895b67a98163c", size = 39662, upload-time = "2026-06-03T16:22:57.683Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/c8/cedb336a0386a97271761ace460a362cb2433c6cdf1d1ba760ad99225734/hiredis-3.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:53233656e4fecf9f8ec654f1f4c5d445bf1c2957d7f63ffdedbba2682c9d1584", size = 40682, upload-time = "2026-06-03T16:22:58.526Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/ea/3a05247ce4e2afe56f59d24b73ba38e37f2b324dba8290beba56fbd9fd1f/hiredis-3.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3348ba4e101f3a96c927447ff2edcb3e0026dc6df375ba117485a43edcbb6980", size = 37541, upload-time = "2026-06-03T16:22:59.307Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/14/caeaa1be1205ebdc1cf6760c5f6882afbdb3b82a6bdf0559d01205b1c857/hiredis-3.4.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:3159c54fe560aa30bf1ab76e65c4c23dc45ad79d7cf4aecc25ec9942f5ea4cea", size = 139787, upload-time = "2026-06-03T16:23:00.139Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/85/8f52b485b9d835e0f8da063a635290d916a6f5ab60c18db5411ecea344d1/hiredis-3.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:be4a41496a0a48c3abf57ef1bbeb11980060ce9c7a1dd8b92caa028a813a9c59", size = 75136, upload-time = "2026-06-03T16:23:01.705Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/09/ee568562f36f481395d5cea3ab75fd9350cd77d98d55ee5f9b395f3fc358/hiredis-3.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a2f9a9a591b3eaade523f3e778dfcd8684965ee6e954ae25cd2fd6d8c75e881d", size = 70772, upload-time = "2026-06-03T16:23:02.765Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/0d/3cb03fbbe72f86541f42ee49dba95ff428c87908815152970fbf24bdcf4c/hiredis-3.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c2852eaa26c0a73be4a30118cd5ad6a77c095d224ccb5ac38e40cb865747d22", size = 315571, upload-time = "2026-06-03T16:23:03.826Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/fc/c8667282e41153bc20930aeba8ba0dff989cbaa9eb7594f8bcac02558dea/hiredis-3.4.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:18ff3d9b23ebe6c8248c3debca2402ad209d60c48495e7ed76407c2fe54cb9b4", size = 348131, upload-time = "2026-06-03T16:23:05.077Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/13/5431ace8330904b2b9d9ce5425c13b7a8fa2b443ff272a92f248c07e6400/hiredis-3.4.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:94f83352295bf3d332678689ecd4ce190a4d233a20ad2f432724efd3ce03e49a", size = 359915, upload-time = "2026-06-03T16:23:06.293Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/57/30dab05cf2a70905e5d2807edd4afa30a4747599070faf80f18e61375e11/hiredis-3.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:393d5e7c8c67cdddf7109a8e925d885e788f3f43e5b1043f84390df40c59944b", size = 321426, upload-time = "2026-06-03T16:23:07.447Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/6f/0a6e030d96d927000735b39aa8b8fef03b43fafdf4a79c80755be351a0f5/hiredis-3.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7e7ab4c1c8c4d365b02d9e82cdf25b01a065edf2ededd7b5acb043201ff80203", size = 309862, upload-time = "2026-06-03T16:23:08.672Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/48/26b2771d2b2403124c1f97c2a6d45df0ba3fa59f0c2d4d244e90543722fb/hiredis-3.4.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:cfe23f8dcf2c0f4e03d107ff68a9ee9707f9d76abeddbe59633e5de1564a650c", size = 339568, upload-time = "2026-06-03T16:23:09.755Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/b1/01c18f676d5dea65e894c01ffae8da2f15df1fceed1c69b16877ba57be60/hiredis-3.4.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a7e76904148c229549db7240a4f9963deb8bb328c0c0844fc9f2320aca05b530", size = 341424, upload-time = "2026-06-03T16:23:10.964Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/58/ab3a5672e506f282e1dd6dfb1c0c3f7e17f02398280c2a2994f8d7b478ba/hiredis-3.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:92b570225f6097430615a82543c3eb7974ca354738a6cef38053138f7d983151", size = 320386, upload-time = "2026-06-03T16:23:12.174Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/af/3f26324cca720f56ace408883c1c7311ce71b571e82e6434515f7ba4eb59/hiredis-3.4.0-cp314-cp314t-win32.whl", hash = "sha256:decc176d86127c620b5d280b3fe5f97a788be58ca945971f3852c3bf54f4d5ad", size = 40516, upload-time = "2026-06-03T16:23:13.179Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/18/e011a424a9608ff152ebeb7bbae2be3163e5716e92cf75baddcb5a8fc312/hiredis-3.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:05c852c58fec65d4c9fb861372dd7391d8b2ce96c960ba8714145f8cd85cd0ec", size = 41453, upload-time = "2026-06-03T16:23:14.091Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/5f/829287555ce7286be8d6c87c69f93aa1f38fe67c46740806416142231cf3/hiredis-3.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:7ff29c9f5d3c91fda948c2fde58f457b3244550781d3bc0891b1b9d93c10f47f", size = 37968, upload-time = "2026-06-03T16:23:14.948Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hpack"
|
||||
version = "4.1.0"
|
||||
@@ -2162,6 +2299,11 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/27/e3/b519734372d305bd547534a9f32e4ce9f98552af753dce72cf3483a0ff0b/redis-8.0.0-py3-none-any.whl", hash = "sha256:c938c18338585009f0bc310f4c7e4e4b4d37639356c4ac072cedf3af570c8dc7", size = 499870, upload-time = "2026-05-28T12:45:11.697Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
hiredis = [
|
||||
{ name = "hiredis" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "referencing"
|
||||
version = "0.37.0"
|
||||
@@ -2555,6 +2697,52 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "2.0.50"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/57/da/6fbf010c8ebb347679d0d100b22fe9ba5e13fd04046c5df7280d2f0bf706/sqlalchemy-2.0.50.tar.gz", hash = "sha256:af5607d11ef90fd6a5c0549fe0045dce1663d427426bcfb506dcb5346a85a3b9", size = 9907424, upload-time = "2026-05-24T19:20:04.018Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/be/b0/a9d19b43f38f878b1278bca5b00b909f7540d41494396dd2561f9ad0956d/sqlalchemy-2.0.50-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23ae23d8b9d344d30d0a92f06d45825024a5790f1c1dd4cf452636a50d3e58cb", size = 2159807, upload-time = "2026-05-24T19:27:53.086Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/2c/191dd58a248fd2cfd4780fa82c375c505e4ad98c8b522fa69ec492130d77/sqlalchemy-2.0.50-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47b71b933e7b4ebad407c8fdfd70d2c4f08b78b3238bb30eebdd6eb32ca51b89", size = 3343358, upload-time = "2026-05-24T20:09:29.279Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/2b/514fce8a7df81cf5bad7ff7865de7ac0c5776a38cc043475c4703eb7fe8b/sqlalchemy-2.0.50-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:110fdac56ace278949f00de805edacbd6141e382d992f9ba28238b3a0827a600", size = 3357994, upload-time = "2026-05-24T20:17:13.495Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/a6/a0e283f5494f92b0d77e319ff77e437b1ffe4a051ba67c81d53234825475/sqlalchemy-2.0.50-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5e4ac70e9e757f6b3e87c0491ff034442ecd8dfd36d041a50564c322dafc0e", size = 3289399, upload-time = "2026-05-24T20:09:32.239Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/96/1b07325ba71752d6a028b77d07bed1483ad545f794e8b1dc89b3ba3b3c68/sqlalchemy-2.0.50-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:724f3dcbe53dd0151e3cb5e7ec4ba4c620bede579caacd16275dc35ce06e8615", size = 3321216, upload-time = "2026-05-24T20:17:15.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/8e/bad6ed253e8a99edfc99af02f7173ec48a1d3ed1b9b35a1b8bc1700900cc/sqlalchemy-2.0.50-cp312-cp312-win32.whl", hash = "sha256:1208050441471d003b7c8cb4054fb084f185cf35ac3f0ea270803865bca9939a", size = 2119194, upload-time = "2026-05-24T19:50:04.943Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/2d/314a6690dda4b9cfc571eab1a63cf6fe6e1470aa3759ccda6aa016ee0f5a/sqlalchemy-2.0.50-cp312-cp312-win_amd64.whl", hash = "sha256:9d1af51558029a156a70986b7df88f042b3d158d7c8d8fb5072912d4b32d89c7", size = 2146186, upload-time = "2026-05-24T19:50:06.74Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/c4/c42356b527296e9862f67990efce31ef78b4cf69cd3f80873a528a060320/sqlalchemy-2.0.50-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:06a9210bdc5f4298cff0781087e2ff45683922252dacc452846373a58761f093", size = 2156697, upload-time = "2026-05-24T19:27:54.764Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/a1/b1a70e3c4365ac7fe9e347f3710f19b562c866fb96d45e3c891588789a7b/sqlalchemy-2.0.50-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b53784972ade4f8174b9aa661f31a06f8a936d2cfdd602913ff3c6dd40ae873", size = 3284260, upload-time = "2026-05-24T20:09:34.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/4a/f3ac3caa19f263d57b0a47f8c91bbf56583dc2d3fc63acfbf644abb24fe0/sqlalchemy-2.0.50-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31648fa14460537e768a7303b078e4344d208e0d23e06867c1f376a227ed82db", size = 3302280, upload-time = "2026-05-24T20:17:17.825Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/55/ccada3e3d62254587819749a0bc69f41173eb48a6e385d10e66d32a9c88e/sqlalchemy-2.0.50-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:03f4323c980ad0e918cc9e5369b015f759f4e534db5bbaf4dc36832c10d05064", size = 3231580, upload-time = "2026-05-24T20:09:36.406Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/f6/6809349130a2de0e109e7f00fd7d431da9565b9b2868b32ee684754f672b/sqlalchemy-2.0.50-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2b9dcc43afef8ac157cd92fce96985d6b8b0cfbd3df4d666f66b4d55a75d202f", size = 3269375, upload-time = "2026-05-24T20:17:20.34Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/84/278a811ef4e07be9c89dc5cdd7be833268509a66a68c4897cf585e67428f/sqlalchemy-2.0.50-cp313-cp313-win32.whl", hash = "sha256:60922d6599065ddca2c6f376b9aa2f41a6b85a271725e0909490bbc50b1998a5", size = 2117229, upload-time = "2026-05-24T19:50:08.215Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/1c/067cc6187ed32d2ec222fe6d2643acc1659a6d0659f8a7cbc5ad3ae83280/sqlalchemy-2.0.50-cp313-cp313-win_amd64.whl", hash = "sha256:287086e67275a212c4582d166a6fb03a65ccc5551d80866270ce0dd9f34eccd3", size = 2143126, upload-time = "2026-05-24T19:50:09.691Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/32/10ac51b4be7cdecd7e93d069251c86dfbf70b7adbd7c67b48ccea6c49e1c/sqlalchemy-2.0.50-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c966932507a4d7d0a37314927dbfcd89720e3f37d2a1e3352e7ae7939fa8e8a0", size = 2158519, upload-time = "2026-05-24T19:27:56.472Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/76/e703d2f7681d7d66c4c891af3f07c7ccf4c76ad7f18351de035b5eda007a/sqlalchemy-2.0.50-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:faffef4bcc20a1892e65e155293d99d60855bbbc79250ab712819cfd56a8e6bb", size = 3282063, upload-time = "2026-05-24T20:09:38.57Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/26/ef168b184a25701f9995e8fb7e503fafd7a99c1c77cda1bc1a26ea2ed486/sqlalchemy-2.0.50-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c206aec519a2e7bd08abbfb33436e325fd22c632d9c21a9047e376ce241646e", size = 3287069, upload-time = "2026-05-24T20:17:21.942Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/15/765acc2bc693bccc43ca4a95d5b69750da8aaf6db1b5c616536e087f8920/sqlalchemy-2.0.50-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bef4ac756363227ef6402a75fee025a4bc690f92328e825868939b3b3a446a6d", size = 3230453, upload-time = "2026-05-24T20:09:40.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/61/08e03c3adbf5db0087a0b6816746fec8f3032fb2f7fc899a9bb9b2a48ce4/sqlalchemy-2.0.50-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96fbee6b19c19cd1556c8bf9419447cf2ec149ffcab7ab64348c23e54ef8547f", size = 3252413, upload-time = "2026-05-24T20:17:24.067Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/0c/370a1f2db38436c615e10134c8a37de3688e74084792380695f3f5083860/sqlalchemy-2.0.50-cp314-cp314-win32.whl", hash = "sha256:8f00e3eb43ba30eb1b238ee03a8a62309486d1321eda3328bb611e0340033ad8", size = 2120063, upload-time = "2026-05-24T19:50:11.08Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/a0/fe92bb9817863bc13ba093bda931979a26cc2ca69f8e8f26d07add3d7c6f/sqlalchemy-2.0.50-cp314-cp314-win_amd64.whl", hash = "sha256:15708c613cd5005b7dffe1f66ee6a63ee8f5e46799f71c70ebad74178c676a39", size = 2145830, upload-time = "2026-05-24T19:50:12.452Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/ff/e5640a98a0b2f491eb8fde10fb6c773621a2e44340de231fafcc9370f4a9/sqlalchemy-2.0.50-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3699dac4be410e97049a1658e9480da9cde956594aa0f3aebc60b88f21c5ba70", size = 2178435, upload-time = "2026-05-24T19:42:58.889Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/85/337116e186f1236375b5fb70c21cfac98e8e8ab0d3a47be838dc47a59e08/sqlalchemy-2.0.50-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f96233858e3df43932ac11589e22520da6e8aeb624b03fedfeebb0e8ea213086", size = 3566059, upload-time = "2026-05-24T20:01:20.848Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/34/bb0e190e161c3c2c24314a65add57218be14a4a9486886b7f5047c1ff7c8/sqlalchemy-2.0.50-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c4e70c46fad30c3bcc6a4708bc0130a3173e11a5b25f0ea4a9d8911b450f1f52", size = 3535366, upload-time = "2026-05-24T20:03:56.768Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/5a/a7f759f97e4fd499c5d4e4488c760d5a7fbecf3028b465a04274fcd52384/sqlalchemy-2.0.50-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1918a3cf564d16d95bca7301005f41ab2ad50b07cd3b9da50d3ed986db148d6a", size = 3474879, upload-time = "2026-05-24T20:01:23.058Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/d9/2907ea38eb60687d297bf9c39e5ee58053c87b57fe8a9cae97090cecbf10/sqlalchemy-2.0.50-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b00098cdbdbd38c7be3d568b0c9c3122b8c0ec62b911b57cd5e6e0254d60a76d", size = 3486117, upload-time = "2026-05-24T20:03:59.052Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/e3/5aa06f167559f8c0bdae487e297d23ba548150ab016a3418265d617a4985/sqlalchemy-2.0.50-cp314-cp314t-win32.whl", hash = "sha256:1fbd55a969d7ac44a98e3dec75016074f809fa08f871585ace58dde110d1bf3e", size = 2150823, upload-time = "2026-05-24T20:08:58.644Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/9b/112fb8f977582d7489d036e409e3723948bcf5320b3ac465f3c481bbe8f9/sqlalchemy-2.0.50-cp314-cp314t-win_amd64.whl", hash = "sha256:c5c3cdb753a9004183e1ccb634b41611654c989e61bc68617ce878e46d6f1e51", size = 2185794, upload-time = "2026-05-24T20:09:00.319Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/10/f7220e9b784d295d241c86ed99aeb537f92afcd469a64861f2717e9bb077/sqlalchemy-2.0.50-py3-none-any.whl", hash = "sha256:92064363517a3ff8212b5a93b8c62876579d8dfd1ca5b561335f30152d884fa9", size = 1943861, upload-time = "2026-05-24T19:59:01.119Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
asyncio = [
|
||||
{ name = "greenlet" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sse-starlette"
|
||||
version = "3.4.4"
|
||||
|
||||
Reference in New Issue
Block a user