- Postgres в compose; пакет db (SQLAlchemy async): clients, windows, requests. - Все эндпоинты, кроме /health, — только с ключом в X-API-Key, иначе 401; без выпущенных ключей доступа нет. В базе — только sha256 ключа. - Задача и окно сессии привязаны к клиенту: чужой ключ их не видит. - Воркер пишет журнал: окно (открыто, последнее обращение, закрыто) и задачу (в очереди, начало, конец, итог, данные без секретов). Сбой записи не роняет задачу. - python -m db.cli create|list|revoke|stats; make key-create/key-list/key-revoke/stats. - smoke и loadtest с ключом. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9dc2c0b619
commit
1a4bd8d672
34 files changed
+857
-81
No files matched your search
@@ -0,0 +1,69 @@
|
||||
"""
|
||||
Таблицы:
|
||||
|
||||
* `clients` — клиенты API («Users» в #17): ключ (только sha256), время создания и последнего обращения.
|
||||
* `windows` — окна браузера: когда открыто и закрыто, последнее обращение, клиент, задача, открывшая окно.
|
||||
Из них считается время, которое браузер был активен.
|
||||
* `requests` — задачи: создание (постановка в очередь), начало и конец выполнения, итог, данные задачи без
|
||||
секретов (прокси, ключ 2captcha, вводимые значения и значения cookies не пишутся).
|
||||
"""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import JSON, DateTime, ForeignKey, Integer, String
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||
|
||||
|
||||
def utcnow() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
type_annotation_map = {datetime: DateTime(timezone=True)}
|
||||
|
||||
|
||||
class Client(Base):
|
||||
__tablename__ = "clients"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(200))
|
||||
# sha256 ключа: сам ключ показывается один раз при выпуске и нигде не хранится
|
||||
key_hash: Mapped[str] = mapped_column(String(64), unique=True, index=True)
|
||||
# начало ключа — чтобы отличать ключи в списке
|
||||
key_prefix: Mapped[str] = mapped_column(String(16))
|
||||
created_at: Mapped[datetime] = mapped_column(default=utcnow)
|
||||
last_access_at: Mapped[datetime | None]
|
||||
revoked_at: Mapped[datetime | None]
|
||||
|
||||
|
||||
class Window(Base):
|
||||
__tablename__ = "windows"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
client_id: Mapped[int | None] = mapped_column(ForeignKey("clients.id"), index=True)
|
||||
session_id: Mapped[str] = mapped_column(String(36), index=True)
|
||||
# задача, открывшая окно
|
||||
task_id: Mapped[str] = mapped_column(String(36))
|
||||
worker_id: Mapped[str] = mapped_column(String(100))
|
||||
started_at: Mapped[datetime] = mapped_column(default=utcnow)
|
||||
last_access_at: Mapped[datetime] = mapped_column(default=utcnow)
|
||||
closed_at: Mapped[datetime | None]
|
||||
|
||||
|
||||
class Request(Base):
|
||||
__tablename__ = "requests"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
task_id: Mapped[str] = mapped_column(String(36), unique=True)
|
||||
client_id: Mapped[int | None] = mapped_column(ForeignKey("clients.id"), index=True)
|
||||
session_id: Mapped[str] = mapped_column(String(36), index=True)
|
||||
window_id: Mapped[int | None] = mapped_column(ForeignKey("windows.id"))
|
||||
# api: /v1/solve или /v2/tasks
|
||||
api: Mapped[str] = mapped_column(String(8))
|
||||
created_at: Mapped[datetime | None]
|
||||
started_at: Mapped[datetime] = mapped_column(default=utcnow, index=True)
|
||||
finished_at: Mapped[datetime | None]
|
||||
duration_ms: Mapped[int | None]
|
||||
# ok / err / failed; code — как в ответе (challenge_failed, action_failed, ...)
|
||||
status: Mapped[str | None] = mapped_column(String(16))
|
||||
code: Mapped[str | None] = mapped_column(String(32))
|
||||
task_data: Mapped[dict] = mapped_column(JSON)
|
||||
Reference in new issue
Block a user