Skip to content
This repository was archived by the owner on Sep 3, 2025. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/dispatch/data/source/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from datetime import datetime
from pydantic import Field, AnyHttpUrl
from pydantic import Field

from sqlalchemy import (
JSON,
Expand Down Expand Up @@ -101,11 +101,13 @@ class QueryReadMinimal(DispatchBase):
description: str


# Note: href is str since it must be serialized to JSON
# We validate the URL in the API layer
class Link(DispatchBase):
id: int | None
name: str | None
description: str | None = None
href: AnyHttpUrl | None
href: str | None


# Pydantic models
Expand Down
54 changes: 45 additions & 9 deletions src/dispatch/data/source/service.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
from pydantic import ValidationError
import logging

from pydantic import AnyHttpUrl, BaseModel, ValidationError

from fastapi import HTTPException, status

from dispatch.project import service as project_service
from dispatch.incident import service as incident_service
Expand All @@ -7,13 +11,16 @@
from dispatch.data.query import service as query_service
from dispatch.data.source.environment import service as environment_service
from dispatch.data.source.data_format import service as data_format_service
from dispatch.data.source.models import Link
from dispatch.data.source.status import service as status_service
from dispatch.data.source.type import service as type_service
from dispatch.data.source.transport import service as transport_service


from .models import Source, SourceCreate, SourceUpdate, SourceRead

log = logging.getLogger(__name__)


def get(*, db_session, source_id: int) -> Source | None:
"""Gets a source by its id."""
Expand All @@ -35,14 +42,16 @@ def get_by_name_or_raise(*, db_session, project_id, source_in: SourceRead) -> So
source = get_by_name(db_session=db_session, project_id=project_id, name=source_in.name)

if not source:
raise ValidationError([
{
"loc": ("source",),
"msg": f"Source not found: {source_in.name}",
"type": "value_error",
"input": source_in.name,
}
])
raise ValidationError(
[
{
"loc": ("source",),
"msg": f"Source not found: {source_in.name}",
"type": "value_error",
"input": source_in.name,
}
]
)

return source

Expand All @@ -52,8 +61,29 @@ def get_all(*, db_session, project_id: int) -> list[Source | None]:
return db_session.query(Source).filter(Source.project_id == project_id)


class LinkValidator(BaseModel):
href: AnyHttpUrl | None = None


def all_links_are_valid(links: list[Link]) -> bool:
"""Checks if all links are valid using AnyHttpUrl."""
for link in links:
try:
LinkValidator(href=link.href)
except ValidationError as e:
log.warning(f"Invalid URL found in source link: {link.href}, Error: {e}")
return False
return True


def create(*, db_session, source_in: SourceCreate) -> Source:
"""Creates a new source."""
if not all_links_are_valid(source_in.links):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=[{"msg": "One or more links are not valid URLs"}],
)

project = project_service.get_by_name_or_raise(
db_session=db_session, project_in=source_in.project
)
Expand Down Expand Up @@ -163,6 +193,12 @@ def get_or_create(*, db_session, source_in: SourceCreate) -> Source:

def update(*, db_session, source: Source, source_in: SourceUpdate) -> Source:
"""Updates an existing source."""
if not all_links_are_valid(source_in.links):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=[{"msg": "One or more links are not valid URLs"}],
)

source_data = source.dict()

update_data = source_in.dict(
Expand Down
Loading