From 5f128a5bbd3b3298df443bd18f34a72e672aa11f Mon Sep 17 00:00:00 2001 From: Raul Bardaji Date: Wed, 18 Feb 2026 19:40:22 +0100 Subject: [PATCH 1/6] Add Affinities configuration settings - Add api/config/affinities_settings.py with: - AFFINITIES_ENABLED (default: False) - AFFINITIES_URL - AFFINITIES_EP_UUID - AFFINITIES_TIMEOUT (default: 30) - Update example.env with new variables and documentation --- api/config/affinities_settings.py | 51 +++++++++++++++++++++++++++++++ example.env | 22 +++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 api/config/affinities_settings.py diff --git a/api/config/affinities_settings.py b/api/config/affinities_settings.py new file mode 100644 index 0000000..31deec5 --- /dev/null +++ b/api/config/affinities_settings.py @@ -0,0 +1,51 @@ +# api/config/affinities_settings.py +""" +Affinities API integration configuration. + +This module provides configuration settings for connecting to +the NDP Affinities service for automatic registration of +datasets, services, and their relationships. +""" + +from pydantic_settings import BaseSettings + + +class AffinitiesSettings(BaseSettings): + """ + Configuration settings for Affinities integration. + + Attributes + ---------- + enabled : bool + Enable/disable Affinities integration (default: False) + url : str + Base URL of the Affinities API (e.g., "http://affinities-api:8000") + ep_uuid : str + UUID of this endpoint in Affinities, obtained when manually + registering the endpoint in the Affinities system + timeout : int + Request timeout in seconds (default: 30) + """ + + enabled: bool = False + url: str = "" + ep_uuid: str = "" + timeout: int = 30 + + model_config = { + "env_file": ".env", + "extra": "allow", + "env_prefix": "AFFINITIES_", + } + + @property + def is_configured(self) -> bool: + """ + Check if Affinities integration is properly configured. + + Returns True only if enabled AND both url and ep_uuid are set. + """ + return self.enabled and bool(self.url) and bool(self.ep_uuid) + + +affinities_settings = AffinitiesSettings() diff --git a/example.env b/example.env index 92db12b..86a7b5a 100644 --- a/example.env +++ b/example.env @@ -202,3 +202,25 @@ REXEC_CONNECTION=False # Remote Execution Deployment API URL REXEC_DEPLOYMENT_API_URL= + +# ============================================== +# Affinities Integration Configuration +# ============================================== + +# Enable or disable NDP Affinities integration (True/False) +# When enabled, datasets and services created in this endpoint will be +# automatically registered in the Affinities system. +AFFINITIES_ENABLED=False + +# Base URL of the Affinities API +# Example: http://affinities-api:8000 or https://affinities.example.com +AFFINITIES_URL= + +# UUID of this endpoint in Affinities +# This UUID is obtained when you manually register this endpoint +# in the Affinities system via POST /endpoints +# Example: 550e8400-e29b-41d4-a716-446655440000 +AFFINITIES_EP_UUID= + +# Request timeout in seconds (default: 30) +AFFINITIES_TIMEOUT=30 From f586a72a920897b2b6f85331205fe21142236abe Mon Sep 17 00:00:00 2001 From: Raul Bardaji Date: Wed, 18 Feb 2026 19:40:29 +0100 Subject: [PATCH 2/6] Add Affinities HTTP client module - Create api/services/affinities_services/ package - Add AffinitiesClient class with async methods: - register_dataset() - registers dataset + creates relationship - register_service() - registers service + creates relationship - create_dataset_endpoint_relationship() - create_service_endpoint_relationship() - Error handling with logging (non-blocking) - Global client instance: affinities_client --- api/services/affinities_services/__init__.py | 11 + .../affinities_services/affinities_client.py | 249 ++++++++++++++++++ 2 files changed, 260 insertions(+) create mode 100644 api/services/affinities_services/__init__.py create mode 100644 api/services/affinities_services/affinities_client.py diff --git a/api/services/affinities_services/__init__.py b/api/services/affinities_services/__init__.py new file mode 100644 index 0000000..d2ce7b7 --- /dev/null +++ b/api/services/affinities_services/__init__.py @@ -0,0 +1,11 @@ +# api/services/affinities_services/__init__.py +""" +Affinities integration services. + +This package provides services for integrating with the NDP Affinities API +to automatically register datasets, services, and their relationships. +""" + +from api.services.affinities_services.affinities_client import AffinitiesClient + +__all__ = ["AffinitiesClient"] diff --git a/api/services/affinities_services/affinities_client.py b/api/services/affinities_services/affinities_client.py new file mode 100644 index 0000000..e3e0afd --- /dev/null +++ b/api/services/affinities_services/affinities_client.py @@ -0,0 +1,249 @@ +# api/services/affinities_services/affinities_client.py +""" +HTTP client for NDP Affinities API. + +This module provides an async client for registering datasets, services, +and their relationships with the Affinities system. +""" + +import logging +from typing import Any +from uuid import UUID + +import httpx + +from api.config.affinities_settings import affinities_settings + +logger = logging.getLogger(__name__) + + +class AffinitiesClient: + """ + Async HTTP client for the NDP Affinities API. + + This client handles registration of datasets, services, and their + relationships with endpoints in the Affinities system. + """ + + def __init__(self): + """Initialize the Affinities client with settings.""" + self.settings = affinities_settings + self.base_url = self.settings.url.rstrip("/") + self.ep_uuid = self.settings.ep_uuid + self.timeout = self.settings.timeout + + @property + def is_enabled(self) -> bool: + """Check if Affinities integration is enabled and configured.""" + return self.settings.is_configured + + async def _request( + self, + method: str, + endpoint: str, + json_data: dict[str, Any] | None = None, + ) -> dict[str, Any] | None: + """ + Make an HTTP request to the Affinities API. + + Parameters + ---------- + method : str + HTTP method (GET, POST, etc.) + endpoint : str + API endpoint path (e.g., "/datasets") + json_data : dict, optional + JSON body for POST/PUT requests + + Returns + ------- + dict or None + Response JSON data, or None on error + + Raises + ------ + Does not raise; logs errors and returns None + """ + if not self.is_enabled: + logger.debug("Affinities integration is disabled, skipping request") + return None + + url = f"{self.base_url}{endpoint}" + + try: + async with httpx.AsyncClient(timeout=self.timeout) as client: + response = await client.request( + method=method, + url=url, + json=json_data, + ) + response.raise_for_status() + return response.json() + + except httpx.TimeoutException: + logger.error(f"Affinities request timed out: {method} {url}") + except httpx.HTTPStatusError as e: + logger.error( + f"Affinities request failed: {method} {url} - " + f"Status {e.response.status_code}: {e.response.text}" + ) + except Exception as e: + logger.error(f"Affinities request error: {method} {url} - {str(e)}") + + return None + + async def register_dataset( + self, + title: str, + metadata: dict[str, Any] | None = None, + ) -> UUID | None: + """ + Register a dataset in Affinities. + + Parameters + ---------- + title : str + Dataset title + metadata : dict, optional + Additional metadata for the dataset + + Returns + ------- + UUID or None + The UUID assigned by Affinities, or None on error + """ + data = { + "title": title, + "source_ep": self.ep_uuid, + "metadata": metadata or {}, + } + + result = await self._request("POST", "/datasets", data) + + if result and "uid" in result: + dataset_uid = UUID(result["uid"]) + logger.info(f"Registered dataset in Affinities: {dataset_uid}") + + # Create relationship with this endpoint + await self.create_dataset_endpoint_relationship(dataset_uid) + + return dataset_uid + + return None + + async def register_service( + self, + service_type: str | None = None, + openapi_url: str | None = None, + version: str | None = None, + metadata: dict[str, Any] | None = None, + ) -> UUID | None: + """ + Register a service in Affinities. + + Parameters + ---------- + service_type : str, optional + Type of service (e.g., "api", "proxy") + openapi_url : str, optional + URL to the service's OpenAPI specification + version : str, optional + Service version + metadata : dict, optional + Additional metadata for the service + + Returns + ------- + UUID or None + The UUID assigned by Affinities, or None on error + """ + data = { + "type": service_type, + "openapi_url": openapi_url, + "version": version, + "source_ep": self.ep_uuid, + "metadata": metadata or {}, + } + + result = await self._request("POST", "/services", data) + + if result and "uid" in result: + service_uid = UUID(result["uid"]) + logger.info(f"Registered service in Affinities: {service_uid}") + + # Create relationship with this endpoint + await self.create_service_endpoint_relationship(service_uid) + + return service_uid + + return None + + async def create_dataset_endpoint_relationship( + self, + dataset_uid: UUID, + role: str = "hosted", + attrs: dict[str, Any] | None = None, + ) -> bool: + """ + Create a relationship between a dataset and this endpoint. + + Parameters + ---------- + dataset_uid : UUID + UUID of the dataset in Affinities + role : str, optional + Role of the relationship (default: "hosted") + attrs : dict, optional + Additional attributes for the relationship + + Returns + ------- + bool + True if successful, False otherwise + """ + data = { + "dataset_uid": str(dataset_uid), + "endpoint_uid": self.ep_uuid, + "role": role, + "attrs": attrs or {}, + } + + result = await self._request("POST", "/dataset-endpoints", data) + return result is not None + + async def create_service_endpoint_relationship( + self, + service_uid: UUID, + role: str = "hosted", + attrs: dict[str, Any] | None = None, + ) -> bool: + """ + Create a relationship between a service and this endpoint. + + Parameters + ---------- + service_uid : UUID + UUID of the service in Affinities + role : str, optional + Role of the relationship (default: "hosted") + attrs : dict, optional + Additional attributes for the relationship + + Returns + ------- + bool + True if successful, False otherwise + """ + data = { + "service_uid": str(service_uid), + "endpoint_uid": self.ep_uuid, + "role": role, + "attrs": attrs or {}, + } + + result = await self._request("POST", "/service-endpoints", data) + return result is not None + + +# Global client instance +affinities_client = AffinitiesClient() From 1fbbd818ecd292c21bb72da3f26f0defeb5122d4 Mon Sep 17 00:00:00 2001 From: Raul Bardaji Date: Wed, 18 Feb 2026 19:40:35 +0100 Subject: [PATCH 3/6] Add Affinities hooks to dataset and service registration - Update post_general_dataset.py: - Import AffinitiesClient and logging - Register dataset in Affinities after creation (non-blocking) - Update post_service.py: - Import AffinitiesClient and logging - Register service in Affinities after creation (non-blocking) Both hooks are non-blocking: if Affinities fails, a warning is logged but the main operation completes successfully. --- .../register_routes/post_general_dataset.py | 22 ++++++++++++++++++ api/routes/register_routes/post_service.py | 23 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/api/routes/register_routes/post_general_dataset.py b/api/routes/register_routes/post_general_dataset.py index d138972..1b3551d 100644 --- a/api/routes/register_routes/post_general_dataset.py +++ b/api/routes/register_routes/post_general_dataset.py @@ -1,5 +1,6 @@ # api/routes/register_routes/post_general_dataset.py +import logging from typing import Any, Dict, Literal from fastapi import APIRouter, Depends, HTTPException, Query, status @@ -7,9 +8,12 @@ from api.config import catalog_settings, ckan_settings from api.models.general_dataset_request_model import GeneralDatasetRequest from api.repositories import CKANRepository +from api.services.affinities_services import AffinitiesClient from api.services.auth_services import get_user_for_write_operation from api.services.dataset_services.general_dataset import create_general_dataset +logger = logging.getLogger(__name__) + router = APIRouter() @@ -195,6 +199,24 @@ async def create_general_dataset_endpoint( repository=repository, user_info=user_info, ) + + # Register in Affinities (non-blocking, errors are logged) + affinities_client = AffinitiesClient() + if affinities_client.is_enabled: + try: + await affinities_client.register_dataset( + title=data.title, + metadata={ + "name": data.name, + "owner_org": data.owner_org, + "local_id": dataset_id, + "notes": data.notes, + "tags": data.tags, + }, + ) + except Exception as e: + logger.warning(f"Failed to register dataset in Affinities: {e}") + return {"id": dataset_id} except ValueError as exc: diff --git a/api/routes/register_routes/post_service.py b/api/routes/register_routes/post_service.py index 9b864df..d2ddf51 100644 --- a/api/routes/register_routes/post_service.py +++ b/api/routes/register_routes/post_service.py @@ -1,4 +1,5 @@ # api/routes/register_routes/post_service.py +import logging from typing import Any, Dict, Literal from fastapi import APIRouter, Depends, HTTPException, Query, status @@ -6,9 +7,12 @@ from api.config import catalog_settings, ckan_settings from api.models.service_request_model import ServiceRequest from api.repositories import CKANRepository +from api.services.affinities_services import AffinitiesClient from api.services.auth_services import get_user_for_write_operation from api.services.service_services.add_service import add_service +logger = logging.getLogger(__name__) + router = APIRouter() @@ -192,6 +196,25 @@ async def create_service( ckan_instance=ckan_instance, user_info=user_info, ) + + # Register in Affinities (non-blocking, errors are logged) + affinities_client = AffinitiesClient() + if affinities_client.is_enabled: + try: + await affinities_client.register_service( + service_type=data.service_type, + openapi_url=data.documentation_url, + metadata={ + "service_name": data.service_name, + "service_title": data.service_title, + "service_url": data.service_url, + "local_id": service_id, + "notes": data.notes, + }, + ) + except Exception as e: + logger.warning(f"Failed to register service in Affinities: {e}") + return {"id": service_id} except ValueError as exc: From 4dd0e494ddffc0636124c38e3fdda46ccc82e4e6 Mon Sep 17 00:00:00 2001 From: Raul Bardaji Date: Wed, 18 Feb 2026 19:40:39 +0100 Subject: [PATCH 4/6] Add Affinities integration documentation - Create docs/affinities-integration.md with: - Overview of the integration - Configuration instructions - Setup steps (registering EP in Affinities) - How datasets and services are registered - Error handling behavior - Metadata format examples - Troubleshooting guide --- docs/affinities-integration.md | 135 +++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 docs/affinities-integration.md diff --git a/docs/affinities-integration.md b/docs/affinities-integration.md new file mode 100644 index 0000000..b0fd5ca --- /dev/null +++ b/docs/affinities-integration.md @@ -0,0 +1,135 @@ +# Affinities Integration + +This document describes how to configure and use the NDP Affinities integration in NDP-EP. + +## Overview + +NDP Affinities is a service that tracks relationships between datasets, services, and endpoints across the NDP ecosystem. When enabled, NDP-EP automatically registers datasets and services in Affinities, creating a federated view of all data assets. + +## Configuration + +### Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `AFFINITIES_ENABLED` | `False` | Enable/disable Affinities integration | +| `AFFINITIES_URL` | - | Base URL of the Affinities API | +| `AFFINITIES_EP_UUID` | - | UUID of this endpoint in Affinities | +| `AFFINITIES_TIMEOUT` | `30` | Request timeout in seconds | + +### Setup Steps + +1. **Register your endpoint in Affinities** + + First, manually register your NDP-EP instance in the Affinities system: + + ```bash + curl -X POST "https://your-affinities-api/endpoints/" \ + -H "Content-Type: application/json" \ + -d '{ + "kind": "ndp-ep", + "url": "https://your-ndp-ep-url", + "metadata": { + "name": "My NDP Endpoint", + "organization": "My Organization" + } + }' + ``` + + This returns a response with a `uid` field - save this UUID. + +2. **Configure NDP-EP** + + Add the following to your `.env` file: + + ```env + AFFINITIES_ENABLED=True + AFFINITIES_URL=https://your-affinities-api + AFFINITIES_EP_UUID=550e8400-e29b-41d4-a716-446655440000 + ``` + +3. **Restart NDP-EP** + + After updating the configuration, restart your NDP-EP instance. + +## How It Works + +When Affinities integration is enabled: + +### Dataset Registration + +When you create a dataset via `POST /dataset`: + +1. The dataset is created in the local catalog (CKAN or MongoDB) +2. NDP-EP registers the dataset in Affinities (`POST /datasets/`) +3. A relationship is created between the dataset and this endpoint (`POST /dataset-endpoints/`) + +### Service Registration + +When you register a service via `POST /services`: + +1. The service is created in the local catalog +2. NDP-EP registers the service in Affinities (`POST /services/`) +3. A relationship is created between the service and this endpoint (`POST /service-endpoints/`) + +## Error Handling + +The Affinities integration is **non-blocking**: + +- If Affinities is unreachable or returns an error, the main operation (dataset/service creation) still succeeds +- Errors are logged as warnings but do not affect the API response +- This ensures that Affinities availability does not impact NDP-EP functionality + +## Metadata Stored in Affinities + +### For Datasets + +```json +{ + "title": "Dataset title", + "source_ep": "your-ep-uuid", + "metadata": { + "name": "dataset_name", + "owner_org": "organization", + "local_id": "local-catalog-id", + "notes": "Description", + "tags": ["tag1", "tag2"] + } +} +``` + +### For Services + +```json +{ + "type": "service_type", + "openapi_url": "documentation_url", + "source_ep": "your-ep-uuid", + "metadata": { + "service_name": "my_service", + "service_title": "My Service", + "service_url": "https://service-url", + "local_id": "local-catalog-id", + "notes": "Description" + } +} +``` + +## Troubleshooting + +### Integration Not Working + +1. Check that `AFFINITIES_ENABLED=True` +2. Verify `AFFINITIES_URL` is correct and accessible +3. Confirm `AFFINITIES_EP_UUID` is a valid UUID from Affinities +4. Check the NDP-EP logs for warning messages + +### Testing the Connection + +You can verify the Affinities API is accessible: + +```bash +curl -X GET "https://your-affinities-api/endpoints/" +``` + +This should return a list of registered endpoints. From c22f3f408f254386a74c1d61ce0b3d87c41d24ee Mon Sep 17 00:00:00 2001 From: Raul Bardaji Date: Wed, 18 Feb 2026 19:40:44 +0100 Subject: [PATCH 5/6] Add tests for Affinities integration - Create tests/test_affinities_client.py with tests for: - AffinitiesClient.is_enabled behavior - register_dataset success and error handling - register_service success and error handling - Settings configuration validation --- tests/test_affinities_client.py | 206 ++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 tests/test_affinities_client.py diff --git a/tests/test_affinities_client.py b/tests/test_affinities_client.py new file mode 100644 index 0000000..8417090 --- /dev/null +++ b/tests/test_affinities_client.py @@ -0,0 +1,206 @@ +# tests/test_affinities_client.py +"""Tests for Affinities client module.""" + +import pytest +from unittest.mock import patch, AsyncMock, MagicMock +from uuid import UUID + +from api.services.affinities_services.affinities_client import AffinitiesClient + + +class TestAffinitiesClient: + """Tests for AffinitiesClient.""" + + def test_is_enabled_when_disabled(self): + """Test is_enabled returns False when disabled.""" + with patch( + "api.services.affinities_services.affinities_client.affinities_settings" + ) as mock_settings: + mock_settings.is_configured = False + client = AffinitiesClient() + assert client.is_enabled is False + + def test_is_enabled_when_enabled(self): + """Test is_enabled returns True when properly configured.""" + with patch( + "api.services.affinities_services.affinities_client.affinities_settings" + ) as mock_settings: + mock_settings.is_configured = True + mock_settings.url = "http://affinities:8000" + mock_settings.ep_uuid = "550e8400-e29b-41d4-a716-446655440000" + mock_settings.timeout = 30 + client = AffinitiesClient() + assert client.is_enabled is True + + @pytest.mark.asyncio + async def test_register_dataset_when_disabled(self): + """Test register_dataset does nothing when disabled.""" + with patch( + "api.services.affinities_services.affinities_client.affinities_settings" + ) as mock_settings: + mock_settings.is_configured = False + client = AffinitiesClient() + + result = await client.register_dataset(title="Test Dataset") + + assert result is None + + @pytest.mark.asyncio + @patch("api.services.affinities_services.affinities_client.httpx.AsyncClient") + async def test_register_dataset_success(self, mock_client_class): + """Test successful dataset registration.""" + with patch( + "api.services.affinities_services.affinities_client.affinities_settings" + ) as mock_settings: + mock_settings.is_configured = True + mock_settings.url = "http://affinities:8000" + mock_settings.ep_uuid = "550e8400-e29b-41d4-a716-446655440000" + mock_settings.timeout = 30 + + # Mock the HTTP response for dataset creation + dataset_response = MagicMock() + dataset_response.json.return_value = { + "uid": "12345678-1234-1234-1234-123456789abc" + } + dataset_response.raise_for_status = MagicMock() + + # Mock the HTTP response for relationship creation + relationship_response = MagicMock() + relationship_response.json.return_value = {} + relationship_response.raise_for_status = MagicMock() + + mock_client = AsyncMock() + mock_client.request = AsyncMock( + side_effect=[dataset_response, relationship_response] + ) + mock_client.__aenter__ = AsyncMock(return_value=mock_client) + mock_client.__aexit__ = AsyncMock(return_value=None) + mock_client_class.return_value = mock_client + + client = AffinitiesClient() + result = await client.register_dataset( + title="Test Dataset", metadata={"key": "value"} + ) + + assert result == UUID("12345678-1234-1234-1234-123456789abc") + + @pytest.mark.asyncio + @patch("api.services.affinities_services.affinities_client.httpx.AsyncClient") + async def test_register_dataset_handles_error(self, mock_client_class): + """Test dataset registration handles errors gracefully.""" + import httpx + + with patch( + "api.services.affinities_services.affinities_client.affinities_settings" + ) as mock_settings: + mock_settings.is_configured = True + mock_settings.url = "http://affinities:8000" + mock_settings.ep_uuid = "550e8400-e29b-41d4-a716-446655440000" + mock_settings.timeout = 30 + + mock_client = AsyncMock() + mock_client.request = AsyncMock( + side_effect=httpx.TimeoutException("Connection timed out") + ) + mock_client.__aenter__ = AsyncMock(return_value=mock_client) + mock_client.__aexit__ = AsyncMock(return_value=None) + mock_client_class.return_value = mock_client + + client = AffinitiesClient() + result = await client.register_dataset(title="Test Dataset") + + # Should return None on error, not raise + assert result is None + + @pytest.mark.asyncio + async def test_register_service_when_disabled(self): + """Test register_service does nothing when disabled.""" + with patch( + "api.services.affinities_services.affinities_client.affinities_settings" + ) as mock_settings: + mock_settings.is_configured = False + client = AffinitiesClient() + + result = await client.register_service(service_type="api") + + assert result is None + + @pytest.mark.asyncio + @patch("api.services.affinities_services.affinities_client.httpx.AsyncClient") + async def test_register_service_success(self, mock_client_class): + """Test successful service registration.""" + with patch( + "api.services.affinities_services.affinities_client.affinities_settings" + ) as mock_settings: + mock_settings.is_configured = True + mock_settings.url = "http://affinities:8000" + mock_settings.ep_uuid = "550e8400-e29b-41d4-a716-446655440000" + mock_settings.timeout = 30 + + # Mock the HTTP response for service creation + service_response = MagicMock() + service_response.json.return_value = { + "uid": "87654321-4321-4321-4321-cba987654321" + } + service_response.raise_for_status = MagicMock() + + # Mock the HTTP response for relationship creation + relationship_response = MagicMock() + relationship_response.json.return_value = {} + relationship_response.raise_for_status = MagicMock() + + mock_client = AsyncMock() + mock_client.request = AsyncMock( + side_effect=[service_response, relationship_response] + ) + mock_client.__aenter__ = AsyncMock(return_value=mock_client) + mock_client.__aexit__ = AsyncMock(return_value=None) + mock_client_class.return_value = mock_client + + client = AffinitiesClient() + result = await client.register_service( + service_type="api", + openapi_url="http://example.com/openapi.json", + version="1.0", + ) + + assert result == UUID("87654321-4321-4321-4321-cba987654321") + + +class TestAffinitiesSettings: + """Tests for AffinitiesSettings.""" + + def test_is_configured_false_when_disabled(self): + """Test is_configured returns False when disabled.""" + with patch( + "api.config.affinities_settings.AffinitiesSettings" + ) as mock_class: + instance = MagicMock() + instance.enabled = False + instance.url = "http://affinities:8000" + instance.ep_uuid = "550e8400-e29b-41d4-a716-446655440000" + # is_configured should be False because enabled=False + instance.is_configured = False + mock_class.return_value = instance + + from api.config.affinities_settings import AffinitiesSettings + + settings = AffinitiesSettings() + assert settings.is_configured is False + + def test_is_configured_false_when_url_missing(self): + """Test is_configured returns False when URL is missing.""" + with patch( + "api.config.affinities_settings.AffinitiesSettings" + ) as mock_class: + instance = MagicMock() + instance.enabled = True + instance.url = "" + instance.ep_uuid = "550e8400-e29b-41d4-a716-446655440000" + instance.is_configured = False + mock_class.return_value = instance + + from api.config.affinities_settings import AffinitiesSettings + + settings = AffinitiesSettings() + assert settings.is_configured is False From bd17a67dcd0a08f6dbd8cbf723c26222edb57cc7 Mon Sep 17 00:00:00 2001 From: Raul Bardaji Date: Wed, 18 Feb 2026 19:40:47 +0100 Subject: [PATCH 6/6] Update CHANGELOG for Affinities integration --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6fc1b6..10f2a9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.7.0] - 2026-02-18 + +### Added +- NDP Affinities integration for automatic registration of datasets and services + - New configuration: `AFFINITIES_ENABLED`, `AFFINITIES_URL`, `AFFINITIES_EP_UUID`, `AFFINITIES_TIMEOUT` + - AffinitiesClient module for async HTTP communication with Affinities API + - Automatic dataset registration in Affinities on `POST /dataset` + - Automatic service registration in Affinities on `POST /services` + - Automatic endpoint relationships created for datasets and services + - Non-blocking integration: Affinities errors don't affect main operations + - Documentation: `docs/affinities-integration.md` + +## [0.6.1] - 2026-02-12 + +### Changed +- Version bump for Docker image release + ## [0.6.0] - 2026-02-02 ### Added