feat(notifications): added notifications presentation layer
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class NotificationsApiConfig(AppConfig):
|
||||
name = "api.v1.notifications"
|
||||
label = "api_v1_notifications"
|
||||
@@ -0,0 +1,226 @@
|
||||
from http import HTTPStatus
|
||||
from uuid import UUID
|
||||
|
||||
from django.http import Http404, HttpRequest
|
||||
from ninja import Router
|
||||
|
||||
from api.v1.notifications.schemas import (
|
||||
ChannelCreateIn,
|
||||
ChannelOut,
|
||||
ChannelUpdateIn,
|
||||
FlushResultOut,
|
||||
LogOut,
|
||||
RuleCreateIn,
|
||||
RuleOut,
|
||||
RuleUpdateIn,
|
||||
)
|
||||
from apps.experiments.models import Experiment
|
||||
from apps.notifications.models import NotificationRule
|
||||
from apps.notifications.services import (
|
||||
channel_create,
|
||||
channel_delete,
|
||||
channel_get,
|
||||
channel_list,
|
||||
channel_update,
|
||||
flush_pending_notifications,
|
||||
log_list,
|
||||
rule_create,
|
||||
rule_delete,
|
||||
rule_list,
|
||||
rule_update,
|
||||
)
|
||||
from apps.users.auth.bearer import jwt_bearer
|
||||
|
||||
router = Router(tags=["notifications"], auth=jwt_bearer)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/notification-channels",
|
||||
response={HTTPStatus.CREATED: ChannelOut},
|
||||
summary="Create a notification channel",
|
||||
)
|
||||
def create_channel(
|
||||
request: HttpRequest,
|
||||
payload: ChannelCreateIn,
|
||||
) -> tuple[int, ChannelOut]:
|
||||
ch = channel_create(
|
||||
channel_type=payload.channel_type,
|
||||
name=payload.name,
|
||||
config=payload.config,
|
||||
)
|
||||
return HTTPStatus.CREATED, ChannelOut.model_validate(ch)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/notification-channels",
|
||||
response={HTTPStatus.OK: list[ChannelOut]},
|
||||
summary="List notification channels",
|
||||
)
|
||||
def list_channels(
|
||||
request: HttpRequest,
|
||||
) -> tuple[int, list[ChannelOut]]:
|
||||
channels = channel_list()
|
||||
return HTTPStatus.OK, [ChannelOut.model_validate(c) for c in channels]
|
||||
|
||||
|
||||
@router.get(
|
||||
"/notification-channels/{channel_id}",
|
||||
response={HTTPStatus.OK: ChannelOut},
|
||||
summary="Get notification channel",
|
||||
)
|
||||
def get_channel(
|
||||
request: HttpRequest,
|
||||
channel_id: UUID,
|
||||
) -> tuple[int, ChannelOut]:
|
||||
ch = channel_get(channel_id)
|
||||
if not ch:
|
||||
raise Http404
|
||||
return HTTPStatus.OK, ChannelOut.model_validate(ch)
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/notification-channels/{channel_id}",
|
||||
response={HTTPStatus.OK: ChannelOut},
|
||||
summary="Update notification channel",
|
||||
)
|
||||
def update_channel(
|
||||
request: HttpRequest,
|
||||
channel_id: UUID,
|
||||
payload: ChannelUpdateIn,
|
||||
) -> tuple[int, ChannelOut]:
|
||||
ch = channel_get(channel_id)
|
||||
if not ch:
|
||||
raise Http404
|
||||
ch = channel_update(
|
||||
channel=ch,
|
||||
**payload.dict(exclude_unset=True),
|
||||
)
|
||||
return HTTPStatus.OK, ChannelOut.model_validate(ch)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/notification-channels/{channel_id}",
|
||||
response={HTTPStatus.NO_CONTENT: None},
|
||||
summary="Delete notification channel",
|
||||
)
|
||||
def delete_channel(
|
||||
request: HttpRequest,
|
||||
channel_id: UUID,
|
||||
) -> tuple[int, None]:
|
||||
ch = channel_get(channel_id)
|
||||
if not ch:
|
||||
raise Http404
|
||||
channel_delete(channel=ch)
|
||||
return HTTPStatus.NO_CONTENT, None
|
||||
|
||||
|
||||
@router.post(
|
||||
"/notification-rules",
|
||||
response={HTTPStatus.CREATED: RuleOut},
|
||||
summary="Create a notification rule",
|
||||
)
|
||||
def create_rule(
|
||||
request: HttpRequest,
|
||||
payload: RuleCreateIn,
|
||||
) -> tuple[int, RuleOut]:
|
||||
ch = channel_get(payload.channel_id)
|
||||
if not ch:
|
||||
raise Http404
|
||||
|
||||
experiment = None
|
||||
if payload.experiment_id:
|
||||
try:
|
||||
experiment = Experiment.objects.get(pk=payload.experiment_id)
|
||||
except Experiment.DoesNotExist:
|
||||
raise Http404 from Experiment.DoesNotExist
|
||||
|
||||
r = rule_create(
|
||||
event_type=payload.event_type,
|
||||
channel=ch,
|
||||
experiment=experiment,
|
||||
)
|
||||
r = NotificationRule.objects.select_related("channel", "experiment").get(
|
||||
pk=r.pk
|
||||
)
|
||||
return HTTPStatus.CREATED, RuleOut.from_rule(r)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/notification-rules",
|
||||
response={HTTPStatus.OK: list[RuleOut]},
|
||||
summary="List notification rules",
|
||||
)
|
||||
def list_rules(
|
||||
request: HttpRequest,
|
||||
channel_id: UUID | None = None,
|
||||
) -> tuple[int, list[RuleOut]]:
|
||||
rules = rule_list(channel_id=channel_id)
|
||||
return HTTPStatus.OK, [RuleOut.from_rule(r) for r in rules]
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/notification-rules/{rule_id}",
|
||||
response={HTTPStatus.OK: RuleOut},
|
||||
summary="Update notification rule",
|
||||
)
|
||||
def update_rule(
|
||||
request: HttpRequest,
|
||||
rule_id: UUID,
|
||||
payload: RuleUpdateIn,
|
||||
) -> tuple[int, RuleOut]:
|
||||
try:
|
||||
r = NotificationRule.objects.select_related(
|
||||
"channel", "experiment"
|
||||
).get(pk=rule_id)
|
||||
except NotificationRule.DoesNotExist:
|
||||
raise Http404 from NotificationRule.DoesNotExist
|
||||
r = rule_update(
|
||||
rule=r,
|
||||
**payload.dict(exclude_unset=True),
|
||||
)
|
||||
r = NotificationRule.objects.select_related("channel", "experiment").get(
|
||||
pk=r.pk
|
||||
)
|
||||
return HTTPStatus.OK, RuleOut.from_rule(r)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/notification-rules/{rule_id}",
|
||||
response={HTTPStatus.NO_CONTENT: None},
|
||||
summary="Delete notification rule",
|
||||
)
|
||||
def delete_rule(
|
||||
request: HttpRequest,
|
||||
rule_id: UUID,
|
||||
) -> tuple[int, None]:
|
||||
try:
|
||||
r = NotificationRule.objects.get(pk=rule_id)
|
||||
except NotificationRule.DoesNotExist:
|
||||
raise Http404 from NotificationRule.DoesNotExist
|
||||
rule_delete(rule=r)
|
||||
return HTTPStatus.NO_CONTENT, None
|
||||
|
||||
|
||||
@router.get(
|
||||
"/notification-logs",
|
||||
response={HTTPStatus.OK: list[LogOut]},
|
||||
summary="List notification logs",
|
||||
)
|
||||
def list_logs(
|
||||
request: HttpRequest,
|
||||
status: str | None = None,
|
||||
) -> tuple[int, list[LogOut]]:
|
||||
logs = log_list(status=status)
|
||||
return HTTPStatus.OK, [LogOut.from_log(lg) for lg in logs]
|
||||
|
||||
|
||||
@router.post(
|
||||
"/notifications/flush",
|
||||
response={HTTPStatus.OK: FlushResultOut},
|
||||
summary="Manually flush pending notifications",
|
||||
)
|
||||
def flush_notifications(
|
||||
request: HttpRequest,
|
||||
) -> tuple[int, FlushResultOut]:
|
||||
results = flush_pending_notifications()
|
||||
return HTTPStatus.OK, FlushResultOut(**results)
|
||||
@@ -0,0 +1,133 @@
|
||||
from typing import ClassVar
|
||||
from uuid import UUID
|
||||
|
||||
from ninja import ModelSchema, Schema
|
||||
|
||||
from apps.notifications.models import (
|
||||
ChannelType,
|
||||
NotificationChannel,
|
||||
NotificationEventType,
|
||||
NotificationLog,
|
||||
NotificationRule,
|
||||
)
|
||||
|
||||
|
||||
class ChannelCreateIn(Schema):
|
||||
channel_type: ChannelType
|
||||
name: str
|
||||
config: dict = {}
|
||||
|
||||
|
||||
class ChannelUpdateIn(Schema):
|
||||
name: str | None = None
|
||||
config: dict | None = None
|
||||
is_active: bool | None = None
|
||||
|
||||
|
||||
class ChannelOut(ModelSchema):
|
||||
class Meta:
|
||||
model = NotificationChannel
|
||||
fields: ClassVar[tuple[str, ...]] = (
|
||||
NotificationChannel.id.field.name,
|
||||
NotificationChannel.channel_type.field.name,
|
||||
NotificationChannel.name.field.name,
|
||||
NotificationChannel.config.field.name,
|
||||
NotificationChannel.is_active.field.name,
|
||||
NotificationChannel.created_at.field.name,
|
||||
NotificationChannel.updated_at.field.name,
|
||||
)
|
||||
|
||||
|
||||
class RuleCreateIn(Schema):
|
||||
event_type: NotificationEventType
|
||||
channel_id: UUID
|
||||
experiment_id: UUID | None = None
|
||||
|
||||
|
||||
class RuleUpdateIn(Schema):
|
||||
event_type: NotificationEventType | None = None
|
||||
is_active: bool | None = None
|
||||
|
||||
|
||||
class ChannelBriefOut(Schema):
|
||||
id: UUID
|
||||
name: str
|
||||
channel_type: str
|
||||
|
||||
|
||||
class ExperimentBriefOut(Schema):
|
||||
id: UUID
|
||||
name: str
|
||||
|
||||
|
||||
class RuleOut(ModelSchema):
|
||||
channel: ChannelBriefOut
|
||||
experiment: ExperimentBriefOut | None = None
|
||||
|
||||
class Meta:
|
||||
model = NotificationRule
|
||||
fields: ClassVar[tuple[str, ...]] = (
|
||||
NotificationRule.id.field.name,
|
||||
NotificationRule.event_type.field.name,
|
||||
NotificationRule.is_active.field.name,
|
||||
NotificationRule.created_at.field.name,
|
||||
NotificationRule.updated_at.field.name,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_rule(cls, r: NotificationRule) -> "RuleOut":
|
||||
experiment_brief = None
|
||||
if r.experiment:
|
||||
experiment_brief = ExperimentBriefOut(
|
||||
id=r.experiment.pk,
|
||||
name=r.experiment.name,
|
||||
)
|
||||
return cls(
|
||||
id=r.pk,
|
||||
event_type=r.event_type,
|
||||
channel=ChannelBriefOut(
|
||||
id=r.channel.pk,
|
||||
name=r.channel.name,
|
||||
channel_type=r.channel.channel_type,
|
||||
),
|
||||
experiment=experiment_brief,
|
||||
is_active=r.is_active,
|
||||
created_at=r.created_at,
|
||||
updated_at=r.updated_at,
|
||||
)
|
||||
|
||||
|
||||
class LogOut(ModelSchema):
|
||||
channel_name: str | None = None
|
||||
|
||||
class Meta:
|
||||
model = NotificationLog
|
||||
fields: ClassVar[tuple[str, ...]] = (
|
||||
NotificationLog.id.field.name,
|
||||
NotificationLog.event_type.field.name,
|
||||
NotificationLog.event_key.field.name,
|
||||
NotificationLog.payload.field.name,
|
||||
NotificationLog.status.field.name,
|
||||
NotificationLog.error.field.name,
|
||||
NotificationLog.created_at.field.name,
|
||||
NotificationLog.sent_at.field.name,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_log(cls, log: NotificationLog) -> "LogOut":
|
||||
return cls(
|
||||
id=log.pk,
|
||||
event_type=log.event_type,
|
||||
event_key=log.event_key,
|
||||
payload=log.payload,
|
||||
status=log.status,
|
||||
error=log.error,
|
||||
created_at=log.created_at,
|
||||
sent_at=log.sent_at,
|
||||
channel_name=log.channel.name if log.channel else None,
|
||||
)
|
||||
|
||||
|
||||
class FlushResultOut(Schema):
|
||||
sent: int
|
||||
failed: int
|
||||
@@ -0,0 +1,251 @@
|
||||
import json
|
||||
import uuid
|
||||
from typing import override
|
||||
|
||||
from django.test import Client, TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from apps.notifications.models import (
|
||||
ChannelType,
|
||||
NotificationEventType,
|
||||
NotificationLog,
|
||||
NotificationStatus,
|
||||
)
|
||||
from apps.notifications.services import channel_create, rule_create
|
||||
from apps.reviews.tests.helpers import make_admin
|
||||
from apps.users.tests.helpers import auth_header
|
||||
|
||||
|
||||
class ChannelAPITest(TestCase):
|
||||
@override
|
||||
def setUp(self) -> None:
|
||||
self.client = Client()
|
||||
self.admin = make_admin("_nch")
|
||||
self.auth = auth_header(self.admin)
|
||||
|
||||
def _create_channel(self, **overrides) -> dict:
|
||||
payload = {
|
||||
"channel_type": ChannelType.TELEGRAM,
|
||||
"name": "Test Channel",
|
||||
"config": {"bot_token": "tok", "chat_id": "-100"},
|
||||
}
|
||||
payload.update(overrides)
|
||||
return self.client.post(
|
||||
reverse("api-1:create_channel"),
|
||||
data=json.dumps(payload),
|
||||
content_type="application/json",
|
||||
HTTP_AUTHORIZATION=self.auth,
|
||||
)
|
||||
|
||||
def test_create_channel(self) -> None:
|
||||
resp = self._create_channel()
|
||||
self.assertEqual(resp.status_code, 201)
|
||||
data = resp.json()
|
||||
self.assertEqual(data["channel_type"], ChannelType.TELEGRAM)
|
||||
self.assertEqual(data["name"], "Test Channel")
|
||||
self.assertTrue(data["is_active"])
|
||||
|
||||
def test_create_smtp_channel(self) -> None:
|
||||
resp = self._create_channel(
|
||||
channel_type=ChannelType.SMTP,
|
||||
name="Email",
|
||||
config={"recipient": "a@b.com"},
|
||||
)
|
||||
self.assertEqual(resp.status_code, 201)
|
||||
self.assertEqual(resp.json()["channel_type"], ChannelType.SMTP)
|
||||
|
||||
def test_list_channels(self) -> None:
|
||||
self._create_channel()
|
||||
resp = self.client.get(
|
||||
reverse("api-1:list_channels"),
|
||||
HTTP_AUTHORIZATION=self.auth,
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(len(resp.json()), 1)
|
||||
|
||||
def test_get_channel(self) -> None:
|
||||
create_resp = self._create_channel()
|
||||
ch_id = create_resp.json()["id"]
|
||||
resp = self.client.get(
|
||||
reverse("api-1:get_channel", args=[ch_id]),
|
||||
HTTP_AUTHORIZATION=self.auth,
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.json()["id"], ch_id)
|
||||
|
||||
def test_get_channel_not_found(self) -> None:
|
||||
resp = self.client.get(
|
||||
reverse("api-1:get_channel", args=[uuid.uuid4()]),
|
||||
HTTP_AUTHORIZATION=self.auth,
|
||||
)
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
def test_update_channel(self) -> None:
|
||||
create_resp = self._create_channel()
|
||||
ch_id = create_resp.json()["id"]
|
||||
resp = self.client.patch(
|
||||
reverse("api-1:update_channel", args=[ch_id]),
|
||||
data=json.dumps({"name": "Updated"}),
|
||||
content_type="application/json",
|
||||
HTTP_AUTHORIZATION=self.auth,
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.json()["name"], "Updated")
|
||||
|
||||
def test_delete_channel(self) -> None:
|
||||
create_resp = self._create_channel()
|
||||
ch_id = create_resp.json()["id"]
|
||||
resp = self.client.delete(
|
||||
reverse("api-1:delete_channel", args=[ch_id]),
|
||||
HTTP_AUTHORIZATION=self.auth,
|
||||
)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
get_resp = self.client.get(
|
||||
reverse("api-1:get_channel", args=[ch_id]),
|
||||
HTTP_AUTHORIZATION=self.auth,
|
||||
)
|
||||
self.assertEqual(get_resp.status_code, 404)
|
||||
|
||||
def test_delete_channel_not_found(self) -> None:
|
||||
resp = self.client.delete(
|
||||
reverse("api-1:delete_channel", args=[uuid.uuid4()]),
|
||||
HTTP_AUTHORIZATION=self.auth,
|
||||
)
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
|
||||
class RuleAPITest(TestCase):
|
||||
@override
|
||||
def setUp(self) -> None:
|
||||
self.client = Client()
|
||||
self.admin = make_admin("_nrul")
|
||||
self.auth = auth_header(self.admin)
|
||||
self.channel = channel_create(
|
||||
channel_type=ChannelType.TELEGRAM,
|
||||
name="Rule Test Channel",
|
||||
config={"bot_token": "tok", "chat_id": "-100"},
|
||||
)
|
||||
|
||||
def _create_rule(self, **overrides) -> dict:
|
||||
payload = {
|
||||
"event_type": NotificationEventType.GUARDRAIL_TRIGGERED,
|
||||
"channel_id": str(self.channel.pk),
|
||||
}
|
||||
payload.update(overrides)
|
||||
return self.client.post(
|
||||
reverse("api-1:create_rule"),
|
||||
data=json.dumps(payload),
|
||||
content_type="application/json",
|
||||
HTTP_AUTHORIZATION=self.auth,
|
||||
)
|
||||
|
||||
def test_create_rule(self) -> None:
|
||||
resp = self._create_rule()
|
||||
self.assertEqual(resp.status_code, 201)
|
||||
data = resp.json()
|
||||
self.assertEqual(
|
||||
data["event_type"], NotificationEventType.GUARDRAIL_TRIGGERED
|
||||
)
|
||||
self.assertEqual(data["channel"]["id"], str(self.channel.pk))
|
||||
self.assertIsNone(data["experiment"])
|
||||
|
||||
def test_create_rule_nonexistent_channel(self) -> None:
|
||||
resp = self._create_rule(channel_id=str(uuid.uuid4()))
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
def test_list_rules(self) -> None:
|
||||
self._create_rule()
|
||||
resp = self.client.get(
|
||||
reverse("api-1:list_rules"),
|
||||
HTTP_AUTHORIZATION=self.auth,
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(len(resp.json()), 1)
|
||||
|
||||
def test_update_rule(self) -> None:
|
||||
create_resp = self._create_rule()
|
||||
rule_id = create_resp.json()["id"]
|
||||
resp = self.client.patch(
|
||||
reverse("api-1:update_rule", args=[rule_id]),
|
||||
data=json.dumps({"is_active": False}),
|
||||
content_type="application/json",
|
||||
HTTP_AUTHORIZATION=self.auth,
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertFalse(resp.json()["is_active"])
|
||||
|
||||
def test_update_rule_not_found(self) -> None:
|
||||
resp = self.client.patch(
|
||||
reverse("api-1:update_rule", args=[uuid.uuid4()]),
|
||||
data=json.dumps({"is_active": False}),
|
||||
content_type="application/json",
|
||||
HTTP_AUTHORIZATION=self.auth,
|
||||
)
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
def test_delete_rule(self) -> None:
|
||||
create_resp = self._create_rule()
|
||||
rule_id = create_resp.json()["id"]
|
||||
resp = self.client.delete(
|
||||
reverse("api-1:delete_rule", args=[rule_id]),
|
||||
HTTP_AUTHORIZATION=self.auth,
|
||||
)
|
||||
self.assertEqual(resp.status_code, 204)
|
||||
|
||||
def test_delete_rule_not_found(self) -> None:
|
||||
resp = self.client.delete(
|
||||
reverse("api-1:delete_rule", args=[uuid.uuid4()]),
|
||||
HTTP_AUTHORIZATION=self.auth,
|
||||
)
|
||||
self.assertEqual(resp.status_code, 404)
|
||||
|
||||
|
||||
class LogAndFlushAPITest(TestCase):
|
||||
@override
|
||||
def setUp(self) -> None:
|
||||
self.client = Client()
|
||||
self.admin = make_admin("_nlog")
|
||||
self.auth = auth_header(self.admin)
|
||||
self.channel = channel_create(
|
||||
channel_type=ChannelType.TELEGRAM,
|
||||
name="Log Channel",
|
||||
config={"bot_token": "tok", "chat_id": "-100"},
|
||||
)
|
||||
|
||||
def test_list_logs_empty(self) -> None:
|
||||
resp = self.client.get(
|
||||
reverse("api-1:list_logs"),
|
||||
HTTP_AUTHORIZATION=self.auth,
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.json(), [])
|
||||
|
||||
def test_list_logs_with_data(self) -> None:
|
||||
rule = rule_create(
|
||||
event_type=NotificationEventType.EXPERIMENT_STARTED,
|
||||
channel=self.channel,
|
||||
)
|
||||
NotificationLog.objects.create(
|
||||
rule=rule,
|
||||
channel=self.channel,
|
||||
event_type=NotificationEventType.EXPERIMENT_STARTED,
|
||||
event_key="test:1",
|
||||
payload={"title": "Test"},
|
||||
status=NotificationStatus.SENT,
|
||||
)
|
||||
resp = self.client.get(
|
||||
reverse("api-1:list_logs"),
|
||||
HTTP_AUTHORIZATION=self.auth,
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(len(resp.json()), 1)
|
||||
|
||||
def test_flush_no_pending(self) -> None:
|
||||
resp = self.client.post(
|
||||
reverse("api-1:flush_notifications"),
|
||||
HTTP_AUTHORIZATION=self.auth,
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
data = resp.json()
|
||||
self.assertEqual(data["sent"], 0)
|
||||
self.assertEqual(data["failed"], 0)
|
||||
@@ -9,12 +9,15 @@ from ninja.renderers import BaseRenderer
|
||||
|
||||
from api.v1 import handlers
|
||||
from api.v1.auth.endpoints import router as auth_router
|
||||
from api.v1.conflicts.endpoints import router as conflicts_router
|
||||
from api.v1.decision.endpoints import router as decision_router
|
||||
from api.v1.events.endpoints import router as events_router
|
||||
from api.v1.experiments.endpoints import router as experiments_router
|
||||
from api.v1.flags.endpoints import router as flags_router
|
||||
from api.v1.guardrails.endpoints import router as guardrails_router
|
||||
from api.v1.learnings.endpoints import router as learnings_router
|
||||
from api.v1.metrics.endpoints import router as metrics_router
|
||||
from api.v1.notifications.endpoints import router as notifications_router
|
||||
from api.v1.reports.endpoints import router as reports_router
|
||||
from api.v1.reviews.endpoints import router as reviews_router
|
||||
from api.v1.users.endpoints import router as users_router
|
||||
@@ -96,5 +99,20 @@ router.add_router(
|
||||
guardrails_router,
|
||||
)
|
||||
|
||||
router.add_router(
|
||||
"",
|
||||
notifications_router,
|
||||
)
|
||||
|
||||
router.add_router(
|
||||
"",
|
||||
learnings_router,
|
||||
)
|
||||
|
||||
router.add_router(
|
||||
"conflicts",
|
||||
conflicts_router,
|
||||
)
|
||||
|
||||
for exception, handler in handlers.exception_handlers:
|
||||
router.add_exception_handler(exception, partial(handler, router=router))
|
||||
|
||||
Reference in New Issue
Block a user