Add governed RSS and Atom connectors
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from dataclasses import asdict
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.audit.logging import audit_event
|
||||
@@ -13,9 +15,18 @@ from govoplan_core.core.tabular_sources import (
|
||||
TabularSourceError,
|
||||
TabularSourceNotFoundError,
|
||||
)
|
||||
from govoplan_core.core.feeds import (
|
||||
FeedCapabilityError,
|
||||
FeedEntry,
|
||||
FeedRenderRequest,
|
||||
)
|
||||
from govoplan_core.core.sanctions import SanctionsSnapshotReference
|
||||
from govoplan_core.db.session import get_session
|
||||
from govoplan_connectors.backend.schemas import (
|
||||
FeedAcquireRequest,
|
||||
FeedDocumentResponse,
|
||||
FeedImportRequest,
|
||||
FeedRenderPayload,
|
||||
SanctionsAcquisitionRunListResponse,
|
||||
SanctionsAcquisitionRunResponse,
|
||||
SanctionsRefreshResponse,
|
||||
@@ -30,6 +41,7 @@ from govoplan_connectors.backend.schemas import (
|
||||
TabularSourcePreviewResponse,
|
||||
TabularSourceResponse,
|
||||
)
|
||||
from govoplan_connectors.backend.feeds import ConnectorFeedProvider, feed_rows
|
||||
from govoplan_connectors.backend.sanctions_sources import (
|
||||
SANCTIONS_READ_SCOPE,
|
||||
SANCTIONS_REFRESH_SCOPE,
|
||||
@@ -50,6 +62,7 @@ from govoplan_connectors.backend.tabular_sources import (
|
||||
router = APIRouter(prefix="/connectors", tags=["connectors"])
|
||||
provider = SqlTabularSourceProvider()
|
||||
sanctions_provider = SqlSanctionsSnapshotProvider()
|
||||
feed_transport = ConnectorFeedProvider()
|
||||
|
||||
|
||||
def _require_any_scope(principal: ApiPrincipal, *scopes: str) -> None:
|
||||
@@ -88,6 +101,132 @@ def _sanctions_http_error(
|
||||
)
|
||||
|
||||
|
||||
def _feed_http_error(exc: FeedCapabilityError) -> HTTPException:
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||
detail=str(exc),
|
||||
)
|
||||
|
||||
|
||||
@router.post("/feeds/preview", response_model=FeedDocumentResponse)
|
||||
def api_preview_feed(
|
||||
payload: FeedAcquireRequest,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> FeedDocumentResponse:
|
||||
_require_any_scope(principal, READ_SCOPE, ADMIN_SCOPE)
|
||||
try:
|
||||
document = feed_transport.fetch(
|
||||
payload.url,
|
||||
max_entries=payload.max_entries,
|
||||
)
|
||||
except FeedCapabilityError as exc:
|
||||
raise _feed_http_error(exc) from exc
|
||||
return FeedDocumentResponse.model_validate(asdict(document))
|
||||
|
||||
|
||||
@router.post(
|
||||
"/feeds/import",
|
||||
response_model=TabularSourceResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def api_import_feed_snapshot(
|
||||
payload: FeedImportRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> TabularSourceResponse:
|
||||
_require_any_scope(principal, WRITE_SCOPE, ADMIN_SCOPE)
|
||||
try:
|
||||
document = feed_transport.fetch(
|
||||
payload.url,
|
||||
max_entries=payload.max_entries,
|
||||
)
|
||||
source = provider.create_snapshot(
|
||||
session,
|
||||
principal,
|
||||
snapshot=TabularSnapshotInput(
|
||||
name=payload.name,
|
||||
source_name=payload.source_name,
|
||||
description=payload.description or document.description,
|
||||
rows=feed_rows(document),
|
||||
metadata={
|
||||
"import_format": document.format,
|
||||
"feed": {
|
||||
"source_url": document.source_url,
|
||||
"home_url": document.home_url,
|
||||
"acquired_at": (
|
||||
document.acquired_at.isoformat()
|
||||
if document.acquired_at
|
||||
else None
|
||||
),
|
||||
"fresh_until": (
|
||||
document.fresh_until.isoformat()
|
||||
if document.fresh_until
|
||||
else None
|
||||
),
|
||||
"etag": document.etag,
|
||||
"last_modified": document.last_modified,
|
||||
"content_type": document.content_type,
|
||||
"sha256": document.sha256,
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
except (FeedCapabilityError, TabularSourceError) as exc:
|
||||
if isinstance(exc, FeedCapabilityError):
|
||||
raise _feed_http_error(exc) from exc
|
||||
raise _http_error(exc) from exc
|
||||
audit_event(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
user_id=getattr(principal.user, "id", None),
|
||||
api_key_id=principal.api_key_id,
|
||||
action="connectors.feed_snapshot.created",
|
||||
object_type="connector_tabular_source",
|
||||
object_id=source.ref,
|
||||
details={
|
||||
"source_url": document.source_url,
|
||||
"format": document.format,
|
||||
"sha256": document.sha256,
|
||||
"row_count": source.row_count,
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
return _source_response(source)
|
||||
|
||||
|
||||
@router.post("/feeds/render")
|
||||
def api_render_feed(
|
||||
payload: FeedRenderPayload,
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
) -> Response:
|
||||
_require_any_scope(principal, READ_SCOPE, ADMIN_SCOPE)
|
||||
try:
|
||||
rendered = feed_transport.render(
|
||||
FeedRenderRequest(
|
||||
format=payload.format,
|
||||
title=payload.title,
|
||||
feed_url=payload.feed_url,
|
||||
home_url=payload.home_url,
|
||||
description=payload.description,
|
||||
language=payload.language,
|
||||
entries=tuple(
|
||||
FeedEntry(**item.model_dump()) for item in payload.entries
|
||||
),
|
||||
allowed_visibilities=frozenset(payload.allowed_visibilities),
|
||||
)
|
||||
)
|
||||
except FeedCapabilityError as exc:
|
||||
raise _feed_http_error(exc) from exc
|
||||
return Response(
|
||||
content=rendered.body,
|
||||
media_type=rendered.content_type,
|
||||
headers={
|
||||
"X-GovOPlaN-Feed-Included": str(rendered.included_entries),
|
||||
"X-GovOPlaN-Feed-Excluded": str(rendered.excluded_entries),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tabular-sources", response_model=TabularSourceListResponse)
|
||||
def api_list_tabular_sources(
|
||||
query: str = Query(default="", max_length=200),
|
||||
|
||||
Reference in New Issue
Block a user