Skip to content
Merged
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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.8.0] - 2026-03-03

### Added
- New endpoint `POST /dataset/{dataset_id}/publish` to copy datasets from local catalog to PRE-CKAN
- Copies dataset metadata and all associated resources
- Proper error handling for disabled PRE-CKAN and duplicate names
- Unit tests for all scenarios
- New `PRE_CKAN_ORGANIZATION` environment variable
- When set, overrides the owner_org when publishing to PRE-CKAN
- Required when PRE-CKAN API credentials are tied to a specific organization
- Local catalog can use any organization; PRE-CKAN uses the configured one

## [0.7.2] - 2026-02-23

### Added
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,13 @@ PRE_CKAN_URL=http://XX.XX.XX.XXX:5000/
# Obtain this from the NDP team or your Pre-CKAN user profile
PRE_CKAN_API_KEY=

# Organization for Pre-CKAN publishing (Optional)
# When set, all datasets published to PRE-CKAN will use this organization,
# regardless of their original owner_org in the local catalog.
# Required when your PRE-CKAN API key is tied to a specific organization.
# Format: ep-XXXXXXXXXXXXXXXXXXXXXXXX (assigned by NDP)
PRE_CKAN_ORGANIZATION=

# ==============================================
# STREAMING CONFIGURATION
# ==============================================
Expand Down Expand Up @@ -329,6 +336,7 @@ CKAN_API_KEY=your-local-ckan-api-key
PRE_CKAN_ENABLED=True
PRE_CKAN_URL=https://preckan.nationaldataplatform.org
PRE_CKAN_API_KEY=your-ndp-preckan-api-key
PRE_CKAN_ORGANIZATION=ep-your-assigned-org-id
```

## 🔒 Group-Based Access Control
Expand Down
1 change: 1 addition & 0 deletions api/config/ckan_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class Settings(BaseSettings):
pre_ckan_url: str = "https://ndp-test.sdsc.edu/catalog2"
pre_ckan_api_key: str = ""
pre_ckan_verify_ssl: bool = True
pre_ckan_organization: str = ""

def _get_session(self, verify_ssl: bool) -> requests.Session:
"""Create a requests session with SSL verification setting."""
Expand Down
2 changes: 2 additions & 0 deletions api/routes/register_routes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .post_s3 import router as post_s3_router
from .post_service import router as post_service_router
from .post_url import router as post_url_router
from .publish_dataset import router as publish_dataset_router

router = APIRouter()

Expand All @@ -17,3 +18,4 @@
router.include_router(post_s3_router)
router.include_router(post_service_router)
router.include_router(post_general_dataset_router)
router.include_router(publish_dataset_router)
167 changes: 167 additions & 0 deletions api/routes/register_routes/publish_dataset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# api/routes/register_routes/publish_dataset.py

"""Endpoint for publishing datasets from local catalog to PRE-CKAN."""

import logging
from typing import Any, Dict

from fastapi import APIRouter, Depends, HTTPException, Path, status

from api.services.auth_services import get_user_for_write_operation
from api.services.dataset_services.publish_dataset import publish_dataset_to_preckan

logger = logging.getLogger(__name__)

router = APIRouter()


@router.post(
"/dataset/{dataset_id}/publish",
response_model=dict,
status_code=status.HTTP_201_CREATED,
summary="Publish dataset from local catalog to PRE-CKAN",
description=(
"Publish (copy) a dataset from the local catalog to PRE-CKAN.\n\n"
"### Behavior\n"
"1. Fetches the dataset from the local catalog\n"
"2. Creates the dataset in PRE-CKAN with the same metadata\n"
"3. Creates all associated resources in PRE-CKAN\n\n"
"### Requirements\n"
"- PRE-CKAN must be enabled in the configuration\n"
"- The dataset must exist in the local catalog\n"
"- The dataset name must not already exist in PRE-CKAN\n\n"
"### Authorization\n"
"This endpoint requires authentication.\n\n"
"### Example Response\n"
"```json\n"
"{\n"
' "id": "12345678-abcd-efgh-ijkl-1234567890ab",\n'
' "message": "Dataset published to PRE-CKAN successfully"\n'
"}\n"
"```\n"
),
responses={
201: {
"description": "Dataset published successfully",
"content": {
"application/json": {
"example": {
"id": "12345678-abcd-efgh-ijkl-1234567890ab",
"message": "Dataset published to PRE-CKAN successfully",
}
}
},
},
400: {
"description": "Bad Request",
"content": {
"application/json": {
"examples": {
"preckan_disabled": {
"summary": "PRE-CKAN disabled",
"value": {
"detail": "PRE-CKAN is disabled and cannot be used."
},
},
"duplicate": {
"summary": "Dataset already exists",
"value": {
"detail": (
"A dataset with name 'my-dataset' "
"already exists in PRE-CKAN."
)
},
},
}
}
},
},
401: {
"description": "Unauthorized - Authentication required",
"content": {
"application/json": {"example": {"detail": "Invalid or expired token"}}
},
},
404: {
"description": "Dataset not found in local catalog",
"content": {
"application/json": {
"example": {"detail": "Dataset not found in local catalog: ..."}
}
},
},
500: {
"description": "Internal server error",
"content": {
"application/json": {
"example": {"detail": "Error creating dataset in PRE-CKAN: ..."}
}
},
},
},
)
async def publish_dataset_endpoint(
dataset_id: str = Path(..., description="ID or name of the dataset to publish"),
user_info: Dict[str, Any] = Depends(get_user_for_write_operation),
):
"""
Publish a dataset from the local catalog to PRE-CKAN.

This endpoint copies a dataset and its resources from the local
catalog to PRE-CKAN, enabling promotion of datasets from
development/local environment to pre-production.

Parameters
----------
dataset_id : str
The ID or name of the dataset to publish.
user_info : Dict[str, Any]
User authentication and authorization information.

Returns
-------
dict
A dictionary containing the new dataset ID in PRE-CKAN and
a success message.

Raises
------
HTTPException
- 400: PRE-CKAN disabled or duplicate dataset
- 401: Authentication required
- 404: Dataset not found in local catalog
- 500: Error during publication
"""
try:
new_dataset_id = publish_dataset_to_preckan(
dataset_id=dataset_id,
user_info=user_info,
)

logger.info(
f"Dataset '{dataset_id}' published to PRE-CKAN "
f"with new ID: {new_dataset_id}"
)

return {
"id": new_dataset_id,
"message": "Dataset published to PRE-CKAN successfully",
}

except ValueError as exc:
error_msg = str(exc)
if "not found" in error_msg.lower():
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=error_msg,
)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=error_msg,
)
except Exception as exc:
logger.error(f"Error publishing dataset to PRE-CKAN: {exc}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error publishing dataset: {str(exc)}",
)
1 change: 1 addition & 0 deletions api/services/dataset_services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
)
from .get_resource import get_resource # noqa: F401
from .patch_resource import patch_resource # noqa: F401
from .publish_dataset import publish_dataset_to_preckan # noqa: F401
from .search_resources import search_resources # noqa: F401
Loading
Loading