/
ivanstrike
/
tasker
Обзор
Документация
Войти
/
ivanstrike
/
tasker
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
python_Sklyarenko/Lab1/app/idempotency.py
113 строк
4 KB
ivanstrike
Lab3
13 дек 2025, 11:15
13 дек 2025, 11:15
15c0fa5
Код
Авторство
О чём код?
# app/idempotency.py from __future__ import annotations import json import hashlib from datetime import datetime from typing import Any, Optional from fastapi import HTTPException, status, Header, Depends from sqlalchemy import Column, Integer, String, DateTime from sqlalchemy.orm import Session from .database import Base, get_db from . import models # --- ORM-модель хранения идемпотентных записей ----------------------------- class IdempotencyRecord(Base): __tablename__ = "idempotency_records" id = Column(Integer, primary_key=True, index=True) key = Column(String(255), unique=True, index=True, nullable=False) request_hash = Column(String(64), nullable=False) # sha256 resource_type = Column(String(50), nullable=False) # "project" | "task" | ... resource_id = Column(Integer, nullable=False) created_at = Column(DateTime, default=datetime.utcnow, nullable=False) # --- Вспомогательные функции ----------------------------------------------- def _stable_dump(obj: Any) -> str: if hasattr(obj, "model_dump"): data = obj.model_dump() elif hasattr(obj, "dict"): data = obj.dict() else: data = obj return json.dumps(data, sort_keys=True, ensure_ascii=False, separators=(",", ":")) def _hash_body(body: Any) -> str: return hashlib.sha256(_stable_dump(body).encode("utf-8")).hexdigest() # --- «Старые» имена (для routers/v1/projects.py) --------------------------- def check_idempotency(db: Session, key: str, request_body: Any) -> Any | None: rec: Optional[IdempotencyRecord] = db.query(IdempotencyRecord).filter_by(key=key).first() if not rec: return None if rec.request_hash != _hash_body(request_body): raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Idempotency-Key already used with different request body", ) # вернуть уже созданный ресурс if rec.resource_type == "project": obj = db.query(models.Project).get(rec.resource_id) elif rec.resource_type == "task": obj = db.query(models.Task).get(rec.resource_id) else: obj = None if obj is not None: return obj # ресурс удалён/недоступен — не создаём дубликат raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Resource no longer available") def save_idempotent_response( db: Session, key: str, request_body: Any, created_obj: Any, *, resource_type: str ) -> None: rec = IdempotencyRecord( key=key, request_hash=_hash_body(request_body), resource_type=resource_type, resource_id=getattr(created_obj, "id"), ) db.add(rec) db.commit() # --- «Новые» имена (ожидаются в routers/v1/tasks.py) ----------------------- # 1) Извлечение ключа из заголовка (можно подцепить как Depends) def get_idempotency_key( idempotency_key: Optional[str] = Header(default=None, alias="Idempotency-Key"), ) -> Optional[str]: return idempotency_key # 2) Утилита хеша тела (совместимое имя) def body_hash(body: Any) -> str: return _hash_body(body) # 3) Проверка идемпотентности (совместимое имя) def enforce_idempotency( db: Session = Depends(get_db), *, key: Optional[str], request_body: Any, ) -> Any | None: if not key: return None return check_idempotency(db, key, request_body) # 4) Сохранение записи (совместимое имя) def save_idempotent_record( db: Session, *, key: Optional[str], request_body: Any, created_obj: Any, resource_type: str, ) -> None: if not key: return save_idempotent_response(db, key, request_body, created_obj, resource_type=resource_type)