feat(errors): наполнить каталог кодов ошибок
96 кодов по доменам common/auth/user/exam/proctoring/result/ notification/billing/bot/storage/journal с HTTP-статусом и severity. Переименовать ERROR_CODE в ERROR_META по ELEXAM_CORE.md §5.
This commit is contained in:
parent
6fe2daf4a3
commit
fd4af8e103
281
src/elexam_core/errors.py
Normal file
281
src/elexam_core/errors.py
Normal file
@ -0,0 +1,281 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ErrorCode(str, Enum):
|
||||
|
||||
# --- common ---
|
||||
# Универсальные коды для случаев, когда доменный код избыточен
|
||||
COMMON_INTERNAL_ERROR = "common.internal_error"
|
||||
COMMON_SERVICE_UNAVAILABLE = "common.service_unavailable"
|
||||
COMMON_VALIDATION_FAILED = "common.validation_failed"
|
||||
COMMON_NOT_FOUND = "common.not_found"
|
||||
COMMON_FORBIDDEN = "common.forbidden"
|
||||
COMMON_UNAUTHORIZED = "common.unauthorized"
|
||||
COMMON_RATE_LIMITED = "common.rate_limited"
|
||||
COMMON_CONFLICT = "common.conflict"
|
||||
COMMON_METHOD_NOT_ALLOWED = "common.method_not_allowed"
|
||||
COMMON_PAYLOAD_TOO_LARGE = "common.payload_too_large"
|
||||
|
||||
# --- auth ---
|
||||
AUTH_TOKEN_EXPIRED = "auth.token.expired"
|
||||
AUTH_TOKEN_INVALID = "auth.token.invalid"
|
||||
AUTH_TOKEN_REVOKED = "auth.token.revoked"
|
||||
AUTH_CREDENTIALS_INVALID = "auth.credentials.invalid"
|
||||
AUTH_ACCOUNT_LOCKED = "auth.account.locked"
|
||||
AUTH_ACCOUNT_DEPRECATED = "auth.account.deprecated"
|
||||
AUTH_DEVICE_UNTRUSTED = "auth.device.untrusted"
|
||||
AUTH_DEVICE_NOT_FOUND = "auth.device.not_found"
|
||||
AUTH_REFRESH_EXPIRED = "auth.refresh.expired"
|
||||
AUTH_REFRESH_REVOKED = "auth.refresh.revoked"
|
||||
# Применяется, когда организация заморожена gateway'ем по событию billing
|
||||
AUTH_ORG_SUSPENDED = "auth.org.suspended"
|
||||
# Порог брутфорса достигнут — фиксируется журналом + rate-limit-ом
|
||||
AUTH_BRUTE_FORCE = "auth.brute_force"
|
||||
|
||||
# --- user ---
|
||||
USER_NOT_FOUND = "user.not_found"
|
||||
USER_EMAIL_TAKEN = "user.email.taken"
|
||||
USER_DEPRECATED = "user.deprecated"
|
||||
USER_ORG_NOT_FOUND = "user.org.not_found"
|
||||
USER_ORG_DEPRECATED = "user.org.deprecated"
|
||||
USER_BRANCH_NOT_FOUND = "user.branch.not_found"
|
||||
USER_MEMBERSHIP_EXISTS = "user.membership.exists"
|
||||
USER_MEMBERSHIP_NOT_FOUND = "user.membership.not_found"
|
||||
USER_ROLE_NOT_FOUND = "user.role.not_found"
|
||||
USER_PERMISSION_NOT_FOUND = "user.permission.not_found"
|
||||
|
||||
# --- exam ---
|
||||
EXAM_NOT_FOUND = "exam.not_found"
|
||||
EXAM_ALREADY_PUBLISHED = "exam.already_published"
|
||||
EXAM_CLOSED = "exam.closed"
|
||||
# Нельзя опубликовать черновик без единого вопроса
|
||||
EXAM_DRAFT_EMPTY = "exam.draft.empty"
|
||||
EXAM_VERSION_NOT_FOUND = "exam.version.not_found"
|
||||
EXAM_QUESTION_NOT_FOUND = "exam.question.not_found"
|
||||
EXAM_QUESTION_DUPLICATE = "exam.question.duplicate"
|
||||
EXAM_ATTEMPT_NOT_FOUND = "exam.attempt.not_found"
|
||||
# Попытка уже запущена — двойной старт невозможен
|
||||
EXAM_ATTEMPT_ALREADY_STARTED = "exam.attempt.already_started"
|
||||
# Студент исчерпал максимум попыток (exam.settings.attempts.max)
|
||||
EXAM_ATTEMPT_MAX_EXCEEDED = "exam.attempt.max_exceeded"
|
||||
# Серверный дедлайн истёк до сабмита
|
||||
EXAM_ATTEMPT_TIMED_OUT = "exam.attempt.timed_out"
|
||||
# Попытка аннулирована по вердикту прокторинга
|
||||
EXAM_ATTEMPT_VOIDED = "exam.attempt.voided"
|
||||
# Попытка уже закрыта (submitted) — нельзя добавлять ответы
|
||||
EXAM_ATTEMPT_SUBMITTED = "exam.attempt.submitted"
|
||||
# Старт заблокирован: нет факта согласия (proctoring.consent_given)
|
||||
EXAM_ATTEMPT_CONSENT_REQUIRED = "exam.attempt.consent_required"
|
||||
# Повторная попытка раньше cooldown_seconds
|
||||
EXAM_ATTEMPT_COOLDOWN_ACTIVE = "exam.attempt.cooldown_active"
|
||||
|
||||
# --- proctoring ---
|
||||
PROCTORING_CONSENT_ALREADY_GIVEN = "proctoring.consent.already_given"
|
||||
PROCTORING_CONSENT_NOT_FOUND = "proctoring.consent.not_found"
|
||||
PROCTORING_SESSION_NOT_FOUND = "proctoring.session.not_found"
|
||||
PROCTORING_SESSION_ALREADY_ACTIVE = "proctoring.session.already_active"
|
||||
# Сессия уже закрыта (closed) — новые сигналы не принимаются
|
||||
PROCTORING_SESSION_CLOSED = "proctoring.session.closed"
|
||||
# Сигнал не прошёл валидацию схемы (неизвестный type, невалидный payload)
|
||||
PROCTORING_SIGNAL_REJECTED = "proctoring.signal.rejected"
|
||||
PROCTORING_INCIDENT_NOT_FOUND = "proctoring.incident.not_found"
|
||||
# Вердикт по этой сессии уже вынесен (один вердикт на сессию — аудит)
|
||||
PROCTORING_VERDICT_ALREADY_ISSUED = "proctoring.verdict.already_issued"
|
||||
PROCTORING_VERDICT_NOT_FOUND = "proctoring.verdict.not_found"
|
||||
# Снимок для ручной идентификации не загружен при открытии сессии
|
||||
PROCTORING_IDENTITY_SHOT_MISSING = "proctoring.identity_shot.missing"
|
||||
# Клиент пытается слать данные по каналу (напр. mic), на который не давал согласия
|
||||
PROCTORING_CHANNEL_NOT_CONSENTED = "proctoring.channel.not_consented"
|
||||
|
||||
# --- result ---
|
||||
RESULT_NOT_FOUND = "result.not_found"
|
||||
# Результат аннулирован — изменения невозможны
|
||||
RESULT_VOIDED = "result.voided"
|
||||
# Итог уже финализирован — повторная финализация запрещена
|
||||
RESULT_ALREADY_FINALIZED = "result.already_finalized"
|
||||
# Финализация невозможна: остались непроверенные grading-записи
|
||||
RESULT_PENDING_GRADING = "result.pending_grading"
|
||||
RESULT_GRADING_NOT_FOUND = "result.grading.not_found"
|
||||
# Вопрос уже оценён — повторная оценка без апелляции запрещена
|
||||
RESULT_GRADING_ALREADY_DONE = "result.grading.already_done"
|
||||
# Апелляции отключены в настройках экзамена (appeals.enabled = false)
|
||||
RESULT_APPEAL_NOT_ALLOWED = "result.appeal.not_allowed"
|
||||
RESULT_APPEAL_NOT_FOUND = "result.appeal.not_found"
|
||||
# По этому результату уже открыта апелляция (одна активная)
|
||||
RESULT_APPEAL_ALREADY_OPEN = "result.appeal.already_open"
|
||||
# Апелляция уже закрыта (accepted/rejected)
|
||||
RESULT_APPEAL_CLOSED = "result.appeal.closed"
|
||||
|
||||
# --- notification ---
|
||||
NOTIFICATION_NOT_FOUND = "notification.not_found"
|
||||
NOTIFICATION_TEMPLATE_NOT_FOUND = "notification.template.not_found"
|
||||
# Канал (email/telegram) отключён настройками пользователя
|
||||
NOTIFICATION_CHANNEL_DISABLED = "notification.channel.disabled"
|
||||
# Внешний провайдер (SMTP / Telegram API) вернул ошибку
|
||||
NOTIFICATION_DELIVERY_FAILED = "notification.delivery.failed"
|
||||
# Уведомление по этому event_id уже существует (идемпотентный дедуп)
|
||||
NOTIFICATION_DUPLICATE = "notification.duplicate"
|
||||
NOTIFICATION_PREF_NOT_FOUND = "notification.pref.not_found"
|
||||
|
||||
# --- billing ---
|
||||
BILLING_PLAN_NOT_FOUND = "billing.plan.not_found"
|
||||
BILLING_SUBSCRIPTION_NOT_FOUND = "billing.subscription.not_found"
|
||||
BILLING_SUBSCRIPTION_ALREADY_EXISTS = "billing.subscription.already_exists"
|
||||
# Подписка истекла и не продлена (период закончился)
|
||||
BILLING_SUBSCRIPTION_EXPIRED = "billing.subscription.expired"
|
||||
# Организация заморожена (suspended) — операции недоступны
|
||||
BILLING_SUBSCRIPTION_SUSPENDED = "billing.subscription.suspended"
|
||||
# Лимит тарифа исчерпан (напр. max_concurrent_attempts)
|
||||
BILLING_LIMIT_EXCEEDED = "billing.limit.exceeded"
|
||||
BILLING_INVOICE_NOT_FOUND = "billing.invoice.not_found"
|
||||
# Платёжный провайдер отклонил транзакцию
|
||||
BILLING_PAYMENT_FAILED = "billing.payment.failed"
|
||||
|
||||
# --- bot ---
|
||||
BOT_LINK_NOT_FOUND = "bot.link.not_found"
|
||||
# Пользователь уже привязан к Telegram-чату
|
||||
BOT_LINK_ALREADY_EXISTS = "bot.link.already_exists"
|
||||
BOT_LINK_CODE_NOT_FOUND = "bot.link_code.not_found"
|
||||
# Одноразовый код привязки истёк (expires_at пройден)
|
||||
BOT_LINK_CODE_EXPIRED = "bot.link_code.expired"
|
||||
# Код уже был активирован — повторное использование невозможно
|
||||
BOT_LINK_CODE_USED = "bot.link_code.used"
|
||||
# Telegram Bot API вернул ошибку при отправке
|
||||
BOT_DELIVERY_FAILED = "bot.delivery.failed"
|
||||
|
||||
# --- storage ---
|
||||
STORAGE_FILE_NOT_FOUND = "storage.file.not_found"
|
||||
STORAGE_UPLOAD_TOO_LARGE = "storage.upload.too_large"
|
||||
STORAGE_TYPE_NOT_ALLOWED = "storage.type.not_allowed"
|
||||
# Квота хранилища организации исчерпана (billing.limits.storage_gb)
|
||||
STORAGE_QUOTA_EXCEEDED = "storage.quota.exceeded"
|
||||
# Хранилище недоступно или вернуло ошибку при записи
|
||||
STORAGE_UPLOAD_FAILED = "storage.upload.failed"
|
||||
|
||||
# --- journal ---
|
||||
JOURNAL_AUDIT_NOT_FOUND = "journal.audit.not_found"
|
||||
JOURNAL_STATS_NOT_FOUND = "journal.stats.not_found"
|
||||
# Запрошенный экспорт слишком велик для синхронной выдачи
|
||||
JOURNAL_EXPORT_TOO_LARGE = "journal.export.too_large"
|
||||
|
||||
|
||||
ERROR_META: dict[ErrorCode, dict] = {
|
||||
|
||||
# --- common ---
|
||||
ErrorCode.COMMON_INTERNAL_ERROR: {"http": 500, "severity": "error"},
|
||||
ErrorCode.COMMON_SERVICE_UNAVAILABLE: {"http": 503, "severity": "error"},
|
||||
ErrorCode.COMMON_VALIDATION_FAILED: {"http": 422, "severity": "warning"},
|
||||
ErrorCode.COMMON_NOT_FOUND: {"http": 404, "severity": "warning"},
|
||||
ErrorCode.COMMON_FORBIDDEN: {"http": 403, "severity": "warning"},
|
||||
ErrorCode.COMMON_UNAUTHORIZED: {"http": 401, "severity": "warning"},
|
||||
ErrorCode.COMMON_RATE_LIMITED: {"http": 429, "severity": "warning"},
|
||||
ErrorCode.COMMON_CONFLICT: {"http": 409, "severity": "warning"},
|
||||
ErrorCode.COMMON_METHOD_NOT_ALLOWED: {"http": 405, "severity": "warning"},
|
||||
ErrorCode.COMMON_PAYLOAD_TOO_LARGE: {"http": 413, "severity": "warning"},
|
||||
|
||||
# --- auth ---
|
||||
ErrorCode.AUTH_TOKEN_EXPIRED: {"http": 401, "severity": "info"},
|
||||
ErrorCode.AUTH_TOKEN_INVALID: {"http": 401, "severity": "warning"},
|
||||
ErrorCode.AUTH_TOKEN_REVOKED: {"http": 401, "severity": "info"},
|
||||
ErrorCode.AUTH_CREDENTIALS_INVALID: {"http": 401, "severity": "warning"},
|
||||
ErrorCode.AUTH_ACCOUNT_LOCKED: {"http": 403, "severity": "warning"},
|
||||
ErrorCode.AUTH_ACCOUNT_DEPRECATED: {"http": 403, "severity": "info"},
|
||||
ErrorCode.AUTH_DEVICE_UNTRUSTED: {"http": 403, "severity": "info"},
|
||||
ErrorCode.AUTH_DEVICE_NOT_FOUND: {"http": 404, "severity": "info"},
|
||||
ErrorCode.AUTH_REFRESH_EXPIRED: {"http": 401, "severity": "info"},
|
||||
ErrorCode.AUTH_REFRESH_REVOKED: {"http": 401, "severity": "info"},
|
||||
ErrorCode.AUTH_ORG_SUSPENDED: {"http": 403, "severity": "warning"},
|
||||
ErrorCode.AUTH_BRUTE_FORCE: {"http": 429, "severity": "error"},
|
||||
|
||||
# --- user ---
|
||||
ErrorCode.USER_NOT_FOUND: {"http": 404, "severity": "warning"},
|
||||
ErrorCode.USER_EMAIL_TAKEN: {"http": 409, "severity": "info"},
|
||||
ErrorCode.USER_DEPRECATED: {"http": 403, "severity": "info"},
|
||||
ErrorCode.USER_ORG_NOT_FOUND: {"http": 404, "severity": "warning"},
|
||||
ErrorCode.USER_ORG_DEPRECATED: {"http": 403, "severity": "warning"},
|
||||
ErrorCode.USER_BRANCH_NOT_FOUND: {"http": 404, "severity": "warning"},
|
||||
ErrorCode.USER_MEMBERSHIP_EXISTS: {"http": 409, "severity": "info"},
|
||||
ErrorCode.USER_MEMBERSHIP_NOT_FOUND: {"http": 404, "severity": "warning"},
|
||||
ErrorCode.USER_ROLE_NOT_FOUND: {"http": 404, "severity": "warning"},
|
||||
ErrorCode.USER_PERMISSION_NOT_FOUND: {"http": 404, "severity": "warning"},
|
||||
|
||||
# --- exam ---
|
||||
ErrorCode.EXAM_NOT_FOUND: {"http": 404, "severity": "warning"},
|
||||
ErrorCode.EXAM_ALREADY_PUBLISHED: {"http": 409, "severity": "info"},
|
||||
ErrorCode.EXAM_CLOSED: {"http": 409, "severity": "warning"},
|
||||
ErrorCode.EXAM_DRAFT_EMPTY: {"http": 422, "severity": "warning"},
|
||||
ErrorCode.EXAM_VERSION_NOT_FOUND: {"http": 404, "severity": "warning"},
|
||||
ErrorCode.EXAM_QUESTION_NOT_FOUND: {"http": 404, "severity": "warning"},
|
||||
ErrorCode.EXAM_QUESTION_DUPLICATE: {"http": 409, "severity": "info"},
|
||||
ErrorCode.EXAM_ATTEMPT_NOT_FOUND: {"http": 404, "severity": "warning"},
|
||||
ErrorCode.EXAM_ATTEMPT_ALREADY_STARTED: {"http": 409, "severity": "info"},
|
||||
ErrorCode.EXAM_ATTEMPT_MAX_EXCEEDED: {"http": 409, "severity": "warning"},
|
||||
ErrorCode.EXAM_ATTEMPT_TIMED_OUT: {"http": 409, "severity": "warning"},
|
||||
ErrorCode.EXAM_ATTEMPT_VOIDED: {"http": 409, "severity": "warning"},
|
||||
ErrorCode.EXAM_ATTEMPT_SUBMITTED: {"http": 409, "severity": "info"},
|
||||
ErrorCode.EXAM_ATTEMPT_CONSENT_REQUIRED: {"http": 403, "severity": "warning"},
|
||||
ErrorCode.EXAM_ATTEMPT_COOLDOWN_ACTIVE: {"http": 429, "severity": "info"},
|
||||
|
||||
# --- proctoring ---
|
||||
ErrorCode.PROCTORING_CONSENT_ALREADY_GIVEN: {"http": 409, "severity": "info"},
|
||||
ErrorCode.PROCTORING_CONSENT_NOT_FOUND: {"http": 404, "severity": "warning"},
|
||||
ErrorCode.PROCTORING_SESSION_NOT_FOUND: {"http": 404, "severity": "warning"},
|
||||
ErrorCode.PROCTORING_SESSION_ALREADY_ACTIVE: {"http": 409, "severity": "info"},
|
||||
ErrorCode.PROCTORING_SESSION_CLOSED: {"http": 409, "severity": "warning"},
|
||||
ErrorCode.PROCTORING_SIGNAL_REJECTED: {"http": 422, "severity": "info"},
|
||||
ErrorCode.PROCTORING_INCIDENT_NOT_FOUND: {"http": 404, "severity": "warning"},
|
||||
ErrorCode.PROCTORING_VERDICT_ALREADY_ISSUED: {"http": 409, "severity": "info"},
|
||||
ErrorCode.PROCTORING_VERDICT_NOT_FOUND: {"http": 404, "severity": "warning"},
|
||||
ErrorCode.PROCTORING_IDENTITY_SHOT_MISSING: {"http": 422, "severity": "warning"},
|
||||
ErrorCode.PROCTORING_CHANNEL_NOT_CONSENTED: {"http": 403, "severity": "info"},
|
||||
|
||||
# --- result ---
|
||||
ErrorCode.RESULT_NOT_FOUND: {"http": 404, "severity": "warning"},
|
||||
ErrorCode.RESULT_VOIDED: {"http": 409, "severity": "warning"},
|
||||
ErrorCode.RESULT_ALREADY_FINALIZED: {"http": 409, "severity": "info"},
|
||||
ErrorCode.RESULT_PENDING_GRADING: {"http": 409, "severity": "info"},
|
||||
ErrorCode.RESULT_GRADING_NOT_FOUND: {"http": 404, "severity": "warning"},
|
||||
ErrorCode.RESULT_GRADING_ALREADY_DONE: {"http": 409, "severity": "info"},
|
||||
ErrorCode.RESULT_APPEAL_NOT_ALLOWED: {"http": 403, "severity": "info"},
|
||||
ErrorCode.RESULT_APPEAL_NOT_FOUND: {"http": 404, "severity": "warning"},
|
||||
ErrorCode.RESULT_APPEAL_ALREADY_OPEN: {"http": 409, "severity": "info"},
|
||||
ErrorCode.RESULT_APPEAL_CLOSED: {"http": 409, "severity": "info"},
|
||||
|
||||
# --- notification ---
|
||||
ErrorCode.NOTIFICATION_NOT_FOUND: {"http": 404, "severity": "warning"},
|
||||
ErrorCode.NOTIFICATION_TEMPLATE_NOT_FOUND: {"http": 404, "severity": "warning"},
|
||||
ErrorCode.NOTIFICATION_CHANNEL_DISABLED: {"http": 422, "severity": "info"},
|
||||
ErrorCode.NOTIFICATION_DELIVERY_FAILED: {"http": 502, "severity": "error"},
|
||||
ErrorCode.NOTIFICATION_DUPLICATE: {"http": 409, "severity": "info"},
|
||||
ErrorCode.NOTIFICATION_PREF_NOT_FOUND: {"http": 404, "severity": "info"},
|
||||
|
||||
# --- billing ---
|
||||
ErrorCode.BILLING_PLAN_NOT_FOUND: {"http": 404, "severity": "warning"},
|
||||
ErrorCode.BILLING_SUBSCRIPTION_NOT_FOUND: {"http": 404, "severity": "warning"},
|
||||
ErrorCode.BILLING_SUBSCRIPTION_ALREADY_EXISTS: {"http": 409, "severity": "info"},
|
||||
ErrorCode.BILLING_SUBSCRIPTION_EXPIRED: {"http": 403, "severity": "warning"},
|
||||
ErrorCode.BILLING_SUBSCRIPTION_SUSPENDED: {"http": 403, "severity": "warning"},
|
||||
ErrorCode.BILLING_LIMIT_EXCEEDED: {"http": 429, "severity": "warning"},
|
||||
ErrorCode.BILLING_INVOICE_NOT_FOUND: {"http": 404, "severity": "warning"},
|
||||
ErrorCode.BILLING_PAYMENT_FAILED: {"http": 402, "severity": "error"},
|
||||
|
||||
# --- bot ---
|
||||
ErrorCode.BOT_LINK_NOT_FOUND: {"http": 404, "severity": "warning"},
|
||||
ErrorCode.BOT_LINK_ALREADY_EXISTS: {"http": 409, "severity": "info"},
|
||||
ErrorCode.BOT_LINK_CODE_NOT_FOUND: {"http": 404, "severity": "warning"},
|
||||
ErrorCode.BOT_LINK_CODE_EXPIRED: {"http": 410, "severity": "info"},
|
||||
ErrorCode.BOT_LINK_CODE_USED: {"http": 409, "severity": "info"},
|
||||
ErrorCode.BOT_DELIVERY_FAILED: {"http": 502, "severity": "error"},
|
||||
|
||||
# --- storage ---
|
||||
ErrorCode.STORAGE_FILE_NOT_FOUND: {"http": 404, "severity": "warning"},
|
||||
ErrorCode.STORAGE_UPLOAD_TOO_LARGE: {"http": 413, "severity": "warning"},
|
||||
ErrorCode.STORAGE_TYPE_NOT_ALLOWED: {"http": 422, "severity": "warning"},
|
||||
ErrorCode.STORAGE_QUOTA_EXCEEDED: {"http": 429, "severity": "warning"},
|
||||
ErrorCode.STORAGE_UPLOAD_FAILED: {"http": 502, "severity": "error"},
|
||||
|
||||
# --- journal ---
|
||||
ErrorCode.JOURNAL_AUDIT_NOT_FOUND: {"http": 404, "severity": "info"},
|
||||
ErrorCode.JOURNAL_STATS_NOT_FOUND: {"http": 404, "severity": "info"},
|
||||
ErrorCode.JOURNAL_EXPORT_TOO_LARGE: {"http": 413, "severity": "warning"},
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user