Repository: digiLab-ai/uncertainty-engine-sdk
Pull request #27: Class refactor
Author: login: jacantwell
Created: 2025-05-12T10:24:04Z | Merged: 2025-05-14T09:30:55Z



Diff:
diff --git a/examples/demo_resource.ipynb b/examples/demo_resource.ipynb
index 138d2a2f..2b12dec8 100644
--- a/examples/demo_resource.ipynb
+++ b/examples/demo_resource.ipynb
@@ -65,7 +65,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "client.auth(\"YOUR-ACCCOUNT-ID\")"
+    "client.authenticate(\"YOUR-ACCCOUNT-ID\")"
    ]
   },
   {
diff --git a/tests/unit/api_providers/conftest.py b/tests/unit/api_providers/conftest.py
new file mode 100644
index 00000000..7b41136e
--- /dev/null
+++ b/tests/unit/api_providers/conftest.py
@@ -0,0 +1,97 @@
+from unittest.mock import MagicMock, mock_open, patch
+
+import pytest
+from uncertainty_engine_resource_client.api import ProjectRecordsApi, ResourcesApi
+
+from uncertainty_engine.api_providers.resource_provider import ResourceProvider
+
+
+@pytest.fixture
+def mock_auth_service():
+    """Fixture for a mock authentication provider."""
+    auth_service = MagicMock()
+    auth_service.account_id = "test-account-id"
+    return auth_service
+
+
+@pytest.fixture
+def mock_projects_client():
+    """Fixture for a mock projects client."""
+    return MagicMock()
+
+
+@pytest.fixture
+def mock_resources_client():
+    """Fixture for a mock resources client."""
+    return MagicMock()
+
+
+@pytest.fixture
+def patched_api_classes():
+    """Patch all API-related classes."""
+    with patch(
+        "uncertainty_engine_resource_client.api_client.ApiClient"
+    ) as patched_api_client:
+        with patch.object(
+            ProjectRecordsApi, "__init__", return_value=None
+        ) as patched_project_api:
+            with patch.object(
+                ResourcesApi, "__init__", return_value=None
+            ) as patched_resource_api:
+                yield {
+                    "api_client": patched_api_client,
+                    "project_api": patched_project_api,
+                    "resource_api": patched_resource_api,
+                }
+
+
+@pytest.fixture
+def resource_provider(
+    mock_auth_service, mock_projects_client, mock_resources_client, patched_api_classes
+):
+    """Create a ResourceProvider with mocked dependencies."""
+    provider = ResourceProvider(auth_service=mock_auth_service)
+    provider.projects_client = mock_projects_client
+    provider.resources_client = mock_resources_client
+    return provider
+
+
+@pytest.fixture
+def mock_file_content():
+    """Default mock file content."""
+    return b"test file content"
+
+
+@pytest.fixture
+def mock_file(mock_file_content):
+    """Mock the open function for file operations."""
+    _mock_file = mock_open(read_data=mock_file_content)
+    with patch("builtins.open", _mock_file):
+        yield _mock_file
+
+
+@pytest.fixture
+def mock_resource_record(
+    resource_id="test-resource-id", name="Test Resource", versions=None
+):
+    """Create a mock resource record."""
+    if versions is None:
+        versions = ["v1"]
+
+    record = MagicMock()
+    record.id = resource_id
+    record.name = name
+    record.versions = versions
+
+    response = MagicMock()
+    response.resource_record = record
+    return response
+
+
+@pytest.fixture
+def mock_version_response(url="https://upload-url.com", pending_id="test-pending-id"):
+    """Create a mock version response."""
+    response = MagicMock()
+    response.url = url
+    response.pending_record_id = pending_id
+    return response
diff --git a/tests/unit/api_providers/test_resource_provider.py b/tests/unit/api_providers/test_resource_provider.py
new file mode 100644
index 00000000..03f5d0fb
--- /dev/null
+++ b/tests/unit/api_providers/test_resource_provider.py
@@ -0,0 +1,699 @@
+import os
+from datetime import datetime
+from unittest.mock import MagicMock, patch
+
+import pytest
+import requests
+from uncertainty_engine_resource_client.exceptions import ApiException
+from uncertainty_engine_resource_client.models import (
+    PostResourceRecordRequest,
+    PostResourceVersionRequest,
+    ResourceRecordInput,
+    ResourceVersionRecordInput,
+)
+
+from uncertainty_engine.api_providers.resource_provider import (
+    DATETIME_STRING_FORMAT,
+    ResourceProvider,
+)
+
+### __init__ ###
+
+
+def test_init_default(mock_auth_service):
+    """Test initializing with default parameters."""
+
+    provider = ResourceProvider(mock_auth_service)
+
+    assert provider.auth_service is mock_auth_service
+    assert provider.client is not None
+    assert provider.projects_client is not None
+    assert provider.resources_client is not None
+
+
+def test_init_custom(mock_auth_service, patched_api_classes):
+    """Test initializing with custom parameters."""
+
+    custom_url = "http://custom-url.com"
+
+    provider = ResourceProvider(deployment=custom_url, auth_service=mock_auth_service)
+
+    assert provider.auth_service is mock_auth_service
+    assert provider.client is not None
+
+
+### resource_provider.account_id ###
+
+
+def test_account_id_with_auth_service(resource_provider, mock_auth_service):
+    """Test the account_id property when auth_service is available."""
+    assert resource_provider.account_id == mock_auth_service.account_id
+
+
+### resource_provider.upload ###
+
+
+def test_upload_success(
+    resource_provider,
+    mock_resources_client,
+    mock_file,
+    mock_resource_record,
+    mock_version_response,
+):
+    """Test the upload method when successful."""
+    # Setup mock responses
+    mock_resources_client.post_resource_record.return_value = mock_resource_record
+    mock_resources_client.post_resource_version.return_value = mock_version_response
+
+    # Setup requests mock
+    mock_put_response = MagicMock()
+    mock_put_response.status_code = 200
+
+    # Test parameters
+    project_id = "test-project"
+    name = "Test Resource"
+    resource_type = "dataset"
+    file_path = "path/to/test_file.csv"
+
+    with patch("requests.put", return_value=mock_put_response):
+        # Call the method
+        result = resource_provider.upload(
+            project_id=project_id,
+            name=name,
+            resource_type=resource_type,
+            file_path=file_path,
+        )
+
+        # Verify the result
+        assert result == "test-resource-id"
+
+        # Verify method calls
+        mock_resources_client.post_resource_record.assert_called_once_with(
+            project_id,
+            resource_type,
+            PostResourceRecordRequest(
+                resource_record=ResourceRecordInput(
+                    name=name, owner_id=resource_provider.account_id
+                )
+            ),
+        )
+
+        mock_resources_client.post_resource_version.assert_called_once_with(
+            project_id,
+            resource_type,
+            "test-resource-id",
+            PostResourceVersionRequest(
+                resource_version_record=ResourceVersionRecordInput(
+                    name=f"{name}-v1", owner_id=resource_provider.account_id
+                ),
+                resource_file_extension="csv",
+            ),
+        )
+
+        mock_file.assert_called_once_with(file_path, "rb")
+        requests.put.assert_called_once_with("https://upload-url.com", data=mock_file())
+
+        mock_resources_client.put_upload_resource_version.assert_called_once_with(
+            project_id, resource_type, "test-resource-id", "test-pending-id"
+        )
+
+
+def test_upload_no_auth(resource_provider):
+    """Test upload fails when not authenticated."""
+    # Set auth provider to None
+    with patch.object(
+        resource_provider,
+        "auth_service",
+        MagicMock(is_authenticated=False, account_id=None),
+    ):
+        # Call and verify exception
+        with pytest.raises(
+            ValueError, match="Authentication required before uploading resources"
+        ):
+            resource_provider.upload("project-id", "name", "type", "path")
+
+
+def test_upload_api_exception_on_record_creation(
+    resource_provider, mock_resources_client
+):
+    """Test handling of ApiException during resource record creation."""
+    # Setup mock error
+    api_exception = ApiException(status=400, reason="Bad Request")
+    mock_resources_client.post_resource_record.side_effect = api_exception
+
+    # Call and verify exception
+    with pytest.raises(Exception, match="Error creating resource record:"):
+        resource_provider.upload("project-id", "name", "type", "path/to/file.txt")
+
+
+def test_upload_api_exception_on_version_creation(
+    resource_provider, mock_resources_client, mock_resource_record
+):
+    """Test handling of ApiException during version record creation."""
+    # Setup mocks
+    mock_resources_client.post_resource_record.return_value = mock_resource_record
+
+    # Setup exception
+    api_exception = ApiException(status=400, reason="Bad Request")
+    mock_resources_client.post_resource_version.side_effect = api_exception
+
+    # Call and verify exception
+    with pytest.raises(Exception, match="Error creating version record:"):
+        resource_provider.upload("project-id", "name", "type", "path/to/file.txt")
+
+
+@pytest.mark.parametrize(
+    "status_code,error_text,expected_error_msg",
+    [
+        (403, "Forbidden", "Upload failed with status 403: Forbidden"),
+        (500, "Server Error", "Upload failed with status 500: Server Error"),
+    ],
+)
+def test_upload_file_upload_error(
+    resource_provider,
+    mock_resources_client,
+    mock_resource_record,
+    mock_version_response,
+    mock_file,
+    status_code,
+    error_text,
+    expected_error_msg,
+):
+    """Test handling of failed upload to presigned URL with various error codes."""
+    # Setup mocks
+    mock_resources_client.post_resource_record.return_value = mock_resource_record
+    mock_resources_client.post_resource_version.return_value = mock_version_response
+
+    # Setup requests mock with error
+    mock_put_response = MagicMock()
+    mock_put_response.status_code = status_code
+    mock_put_response.text = error_text
+
+    with patch("requests.put", return_value=mock_put_response):
+        # Call and verify exception
+        with pytest.raises(Exception, match=expected_error_msg):
+            resource_provider.upload("project-id", "name", "type", "path/to/file.txt")
+
+
+def test_upload_failed_completion(
+    resource_provider,
+    mock_resources_client,
+    mock_resource_record,
+    mock_version_response,
+    mock_file,
+):
+    """Test handling of exception during upload completion."""
+    # Setup mocks
+    mock_resources_client.post_resource_record.return_value = mock_resource_record
+    mock_resources_client.post_resource_version.return_value = mock_version_response
+
+    # Setup requests mock
+    mock_put_response = MagicMock()
+    mock_put_response.status_code = 200
+
+    # Setup exception on completion
+    api_exception = ApiException(status=400, reason="Bad Request")
+    mock_resources_client.put_upload_resource_version.side_effect = api_exception
+
+    with patch("requests.put", return_value=mock_put_response):
+        # Call and verify exception
+        with pytest.raises(Exception, match="Error completing upload:"):
+            resource_provider.upload("project-id", "name", "type", "path/to/file.txt")
+
+
+### resource_provider.download ###
+
+
+def test_download_success_with_filepath(
+    resource_provider, mock_file, mock_version_response
+):
+    """Test downloading a resource with a specified filepath."""
+    # Setup mocks
+    resource_provider.resources_client.get_latest_resource_version.return_value = (
+        mock_version_response
+    )
+
+    # Setup requests mock
+    mock_get_response = MagicMock()
+    mock_get_response.content = b"test file content"
+
+    with patch("os.makedirs") as mock_makedirs:
+        with patch("requests.get", return_value=mock_get_response):
+            # Call the method
+            resource_provider.download(
+                project_id="test-project",
+                resource_type="dataset",
+                resource_id="test-resource-id",
+                file_path="path/to/download/file.csv",
+            )
+
+            # Verify method calls
+            resource_provider.resources_client.get_latest_resource_version.assert_called_once_with(
+                "test-project", "dataset", "test-resource-id"
+            )
+
+            mock_makedirs.assert_called_once_with(
+                os.path.dirname(os.path.abspath("path/to/download/file.csv")),
+                exist_ok=True,
+            )
+            requests.get.assert_called_once_with("https://upload-url.com")
+            mock_get_response.raise_for_status.assert_called_once()
+            mock_file.assert_called_once_with("path/to/download/file.csv", "wb")
+            mock_file().write.assert_called_once_with(b"test file content")
+
+
+def test_download_success_without_filepath(resource_provider, mock_version_response):
+    """Test downloading a resource without a specified filepath (return content)."""
+    # Setup mocks
+    resource_provider.resources_client.get_latest_resource_version.return_value = (
+        mock_version_response
+    )
+
+    # Setup requests mock
+    mock_get_response = MagicMock()
+    expected_content = b'{"data": "test content"}'
+    mock_get_response.content = expected_content
+
+    with patch("requests.get", return_value=mock_get_response):
+        # Call the method
+        result = resource_provider.download(
+            project_id="test-project",
+            resource_type="dataset",
+            resource_id="test-resource-id",
+        )
+
+        # Verify result and method calls
+        assert result == expected_content
+        resource_provider.resources_client.get_latest_resource_version.assert_called_once_with(
+            "test-project", "dataset", "test-resource-id"
+        )
+        requests.get.assert_called_once_with("https://upload-url.com")
+        mock_get_response.raise_for_status.assert_called_once()
+
+
+def test_download_no_auth(resource_provider):
+    """Test download fails when not authenticated."""
+    # Set auth provider to None
+    with patch.object(
+        resource_provider,
+        "auth_service",
+        MagicMock(is_authenticated=False, account_id=None),
+    ):
+        # Call and verify exception
+        with pytest.raises(
+            ValueError, match="Authentication required before downloading resources"
+        ):
+            resource_provider.download("project-id", "type", "resource-id")
+
+
+def test_download_api_exception(resource_provider):
+    """Test handling of ApiException during resource version retrieval."""
+    # Setup exception
+    api_exception = ApiException(status=404, reason="Not Found")
+    resource_provider.resources_client.get_latest_resource_version.side_effect = (
+        api_exception
+    )
+
+    # Call and verify exception
+    with pytest.raises(Exception, match="Error retrieving resource:"):
+        resource_provider.download(
+            "project-id", "type", "resource-id", "path/to/file.txt"
+        )
+
+
+def test_download_generic_exception_on_retrieval(resource_provider):
+    """Test handling of generic Exception during resource retrieval."""
+    # Setup exception
+    generic_exception = Exception("Random error")
+    resource_provider.resources_client.get_latest_resource_version.side_effect = (
+        generic_exception
+    )
+
+    # Call and verify exception
+    with pytest.raises(Exception, match="Error retrieving resource: Random error"):
+        resource_provider.download(
+            "project-id", "type", "resource-id", "path/to/file.txt"
+        )
+
+
+def test_download_http_error(resource_provider, mock_version_response):
+    """Test handling of HTTP error during download."""
+    # Setup mocks
+    resource_provider.resources_client.get_latest_resource_version.return_value = (
+        mock_version_response
+    )
+
+    # Setup requests mock with error
+    mock_get_response = MagicMock()
+    http_error = requests.exceptions.HTTPError("404 Client Error")
+    mock_get_response.raise_for_status.side_effect = http_error
+
+    with patch("requests.get", return_value=mock_get_response):
+        # Call and verify exception
+        with pytest.raises(Exception, match="Error saving downloaded file:"):
+            resource_provider.download(
+                "project-id", "type", "resource-id", "path/to/file.txt"
+            )
+
+
+def test_download_file_not_found_error(
+    resource_provider, mock_version_response, mock_file
+):
+    """Test handling of FileNotFoundError when writing to a file."""
+    # Setup mocks
+    resource_provider.resources_client.get_latest_resource_version.return_value = (
+        mock_version_response
+    )
+
+    # Setup requests mock
+    mock_get_response = MagicMock()
+    mock_get_response.content = b"test file content"
+
+    # Setup file mock to raise FileNotFoundError
+    mock_file.side_effect = FileNotFoundError("No such file or directory")
+
+    with patch("os.makedirs"):
+        with patch("requests.get", return_value=mock_get_response):
+            # Call and verify exception
+            with pytest.raises(Exception, match="Invalid filepath provided"):
+                resource_provider.download(
+                    project_id="test-project",
+                    resource_type="dataset",
+                    resource_id="test-resource-id",
+                    file_path="path/to/download/file.csv",
+                )
+
+            mock_get_response.raise_for_status.assert_called_once()
+
+
+def test_download_other_file_exception(
+    resource_provider, mock_version_response, mock_file
+):
+    """Test handling of generic exceptions when writing to a file."""
+    # Setup mocks
+    resource_provider.resources_client.get_latest_resource_version.return_value = (
+        mock_version_response
+    )
+
+    # Setup requests mock
+    mock_get_response = MagicMock()
+    mock_get_response.content = b"test file content"
+
+    # Configure the write method to raise the exception
+    mock_file.return_value.write.side_effect = PermissionError("Permission denied")
+
+    with patch("os.makedirs"):
+        with patch("requests.get", return_value=mock_get_response):
+            # Call and verify exception
+            with pytest.raises(PermissionError, match="Permission denied"):
+                resource_provider.download(
+                    project_id="test-project",
+                    resource_type="dataset",
+                    resource_id="test-resource-id",
+                    file_path="path/to/download/file.csv",
+                )
+
+
+### resource_provider.update ###
+
+
+def test_update_success(
+    resource_provider,
+    mock_resources_client,
+    mock_resource_record,
+    mock_version_response,
+    mock_file,
+):
+    """Test updating a resource successfully."""
+    # Setup mocks for resource info - simulate two existing versions
+    mock_resource_record.resource_record.versions = ["v1", "v2"]
+    mock_resources_client.get_resource_record.return_value = mock_resource_record
+    mock_resources_client.post_resource_version.return_value = mock_version_response
+
+    # Setup requests mock
+    mock_put_response = MagicMock()
+    mock_put_response.status_code = 200
+
+    with patch("os.path.exists", return_value=True):
+        with patch("requests.put", return_value=mock_put_response):
+            # Call the method
+            resource_provider.update(
+                project_id="test-project",
+                resource_type="dataset",
+                resource_id="test-resource-id",
+                file_path="path/to/updated_file.json",
+            )
+
+            # Verify method calls
+            mock_resources_client.get_resource_record.assert_called_once_with(
+                "test-project", "dataset", "test-resource-id"
+            )
+
+            # Expected version name should be resource name + v3 (since there are two existing versions)
+            mock_resources_client.post_resource_version.assert_called_once_with(
+                "test-project",
+                "dataset",
+                "test-resource-id",
+                PostResourceVersionRequest(
+                    resource_version_record=ResourceVersionRecordInput(
+                        name="Test Resource-v3",
+                        owner_id=resource_provider.account_id,
+                    ),
+                    resource_file_extension="json",
+                ),
+            )
+
+            mock_file.assert_called_once_with("path/to/updated_file.json", "rb")
+            requests.put.assert_called_once_with(
+                "https://upload-url.com", data=mock_file()
+            )
+
+            mock_resources_client.put_upload_resource_version.assert_called_once_with(
+                "test-project", "dataset", "test-resource-id", "test-pending-id"
+            )
+
+
+def test_update_resource_not_found(
+    resource_provider, mock_resources_client, mock_version_response, mock_file
+):
+    """Test handling when the resource to update is not found."""
+    # Setup exception for resource lookup
+    api_exception = ApiException(status=404, reason="Not Found")
+    mock_resources_client.get_resource_record.side_effect = api_exception
+    mock_resources_client.post_resource_version.return_value = mock_version_response
+
+    # Setup requests mock
+    mock_put_response = MagicMock()
+    mock_put_response.status_code = 200
+
+    with patch("os.path.exists", return_value=True):
+        with patch("requests.put", return_value=mock_put_response):
+            with patch(
+                "uncertainty_engine.api_providers.resource_provider.uuid4",
+                return_value="test-uuid",
+            ):
+                # Call the method
+                resource_provider.update(
+                    project_id="test-project",
+                    resource_type="dataset",
+                    resource_id="test-resource-id",
+                    file_path="path/to/updated_file.json",
+                )
+
+                # Verify the generic version name is used
+                mock_resources_client.post_resource_version.assert_called_once_with(
+                    "test-project",
+                    "dataset",
+                    "test-resource-id",
+                    PostResourceVersionRequest(
+                        resource_version_record=ResourceVersionRecordInput(
+                            name="version-test-uuid",
+                            owner_id=resource_provider.account_id,
+                        ),
+                        resource_file_extension="json",
+                    ),
+                )
+
+
+def test_update_no_auth(resource_provider):
+    """Test update fails when not authenticated."""
+    # Set auth provider to None
+    with patch.object(
+        resource_provider,
+        "auth_service",
+        MagicMock(is_authenticated=False, account_id=None),
+    ):
+        # Call and verify exception
+        with pytest.raises(
+            ValueError, match="Authentication required before updating resources"
+        ):
+            resource_provider.update(
+                "project-id", "type", "resource-id", "path/to/file.txt"
+            )
+
+
+def test_update_file_not_found(resource_provider):
+    """Test update fails when the file doesn't exist."""
+    with patch("os.path.exists", return_value=False):
+        # Call and verify exception
+        with pytest.raises(FileNotFoundError, match="File not found:"):
+            resource_provider.update(
+                "project-id", "type", "resource-id", "nonexistent/file.txt"
+            )
+
+
+def test_update_api_exception_on_version_creation(
+    resource_provider, mock_resources_client, mock_resource_record
+):
+    """Test handling of ApiException during version record creation."""
+    # Setup mocks for resource info
+    mock_resources_client.get_resource_record.return_value = mock_resource_record
+
+    # Setup exception for version creation
+    api_exception = ApiException(status=400, reason="Bad Request")
+    mock_resources_client.post_resource_version.side_effect = api_exception
+
+    with patch("os.path.exists", return_value=True):
+        # Call and verify exception
+        with pytest.raises(Exception, match="Error creating version record:"):
+            resource_provider.update(
+                "project-id", "type", "resource-id", "path/to/file.txt"
+            )
+
+
+@pytest.mark.parametrize(
+    "status_code,error_text,expected_error_msg",
+    [
+        (403, "Forbidden", "Upload failed with status 403: Forbidden"),
+        (500, "Server Error", "Upload failed with status 500: Server Error"),
+    ],
+)
+def test_update_upload_error(
+    resource_provider,
+    mock_resources_client,
+    mock_resource_record,
+    mock_version_response,
+    mock_file,
+    status_code,
+    error_text,
+    expected_error_msg,
+):
+    """Test handling of error during upload to presigned URL."""
+    # Setup mocks for resource info
+    mock_resources_client.get_resource_record.return_value = mock_resource_record
+    mock_resources_client.post_resource_version.return_value = mock_version_response
+
+    # Setup requests mock with error
+    mock_put_response = MagicMock()
+    mock_put_response.status_code = status_code
+    mock_put_response.text = error_text
+
+    with patch("os.path.exists", return_value=True):
+        with patch("requests.put", return_value=mock_put_response):
+            # Call and verify exception
+            with pytest.raises(Exception, match=expected_error_msg):
+                resource_provider.update(
+                    "test-project",
+                    "dataset",
+                    "test-resource-id",
+                    "path/to/updated_file.json",
+                )
+
+
+def test_update_finalize_error(
+    resource_provider,
+    mock_resources_client,
+    mock_resource_record,
+    mock_version_response,
+    mock_file,
+):
+    """Test handling of error during finalization of upload."""
+    # Setup mocks for resource info
+    mock_resources_client.get_resource_record.return_value = mock_resource_record
+    mock_resources_client.post_resource_version.return_value = mock_version_response
+
+    # Setup requests mock
+    mock_put_response = MagicMock()
+    mock_put_response.status_code = 200
+
+    # Setup exception for finalization
+    api_exception = ApiException(status=400, reason="Bad Request")
+    mock_resources_client.put_upload_resource_version.side_effect = api_exception
+
+    with patch("os.path.exists", return_value=True):
+        with patch("requests.put", return_value=mock_put_response):
+            # Call and verify exception
+            with pytest.raises(Exception, match="Error finalizing upload:"):
+                resource_provider.update(
+                    "test-project",
+                    "dataset",
+                    "test-resource-id",
+                    "path/to/updated_file.json",
+                )
+
+
+### resource_provider.list_resources ###
+
+
+def test_list_resources_success(resource_provider, mock_resources_client):
+    """Test listing resources successfully."""
+    # Setup mock response
+    created_time = datetime.now()
+
+    # Create mock resource records
+    record1 = MagicMock()
+    record1.id = "resource-1"
+    record1.name = "Resource 1"
+    record1.created_at = created_time
+
+    record2 = MagicMock()
+    record2.id = "resource-2"
+    record2.name = "Resource 2"
+    record2.created_at = created_time
+
+    # Create response with list of resource records
+    response = MagicMock()
+    response.resource_records = [record1, record2]
+    mock_resources_client.get_project_resource_records.return_value = response
+
+    # Call the method
+    result = resource_provider.list_resources("test-project", "dataset")
+
+    # Verify result
+    expected_result = [
+        {
+            "id": "resource-1",
+            "name": "Resource 1",
+            "created_at": created_time.strftime(DATETIME_STRING_FORMAT),
+        },
+        {
+            "id": "resource-2",
+            "name": "Resource 2",
+            "created_at": created_time.strftime(DATETIME_STRING_FORMAT),
+        },
+    ]
+    assert result == expected_result
+
+    # Verify method call
+    mock_resources_client.get_project_resource_records.assert_called_once_with(
+        "test-project", "dataset"
+    )
+
+
+def test_list_resources_empty(resource_provider, mock_resources_client):
+    """Test listing resources when none exist."""
+    # Setup mock response with empty list
+    response = MagicMock()
+    response.resource_records = []
+    mock_resources_client.get_project_resource_records.return_value = response
+
+    # Call the method
+    result = resource_provider.list_resources("test-project", "dataset")
+
+    # Verify result is empty list
+    assert result == []
+
+    # Verify method call
+    mock_resources_client.get_project_resource_records.assert_called_once_with(
+        "test-project", "dataset"
+    )
diff --git a/tests/unit/test_auth_provider.py b/tests/unit/test_auth_provider.py
deleted file mode 100644
index d204432f..00000000
--- a/tests/unit/test_auth_provider.py
+++ /dev/null
@@ -1,74 +0,0 @@
-import pytest
-
-from uncertainty_engine.auth_provider import AuthProvider
-
-
-@pytest.fixture
-def auth_provider() -> AuthProvider:
-    """Fixture for an AuthProvider instance."""
-    return AuthProvider()
-
-
-def test_init(auth_provider: AuthProvider):
-    """
-    Test that a new AuthProvider has no account_id set.
-
-    Args:
-        auth_provider: An instance of AuthProvider.
-    """
-    assert auth_provider.account_id is None
-    assert auth_provider.is_authenticated is False
-
-
-def test_authenticate(auth_provider: AuthProvider):
-    """
-    Test that calling authenticate sets the account_id.
-
-    Args:
-        auth_provider: An instance of AuthProvider.
-    """
-    # Setup
-    test_account_id = "test-account-123"
-
-    # Call the method
-    auth_provider.authenticate(test_account_id)
-
-    # Verify result
-    assert auth_provider.account_id == test_account_id
-    assert auth_provider.is_authenticated is True
-
-
-def test_authenticate_multiple_calls(auth_provider: AuthProvider):
-    """
-    Test that authenticate can be called multiple times, updating the account_id.
-
-    Args:
-        auth_provider: An instance of AuthProvider.
-    """
-    # Setup
-    first_account_id = "first-account"
-    second_account_id = "second-account"
-
-    # Call authenticate with first ID
-    auth_provider.authenticate(first_account_id)
-    assert auth_provider.account_id == first_account_id
-    assert auth_provider.is_authenticated is True
-
-    # Call authenticate with second ID
-    auth_provider.authenticate(second_account_id)
-    assert auth_provider.account_id == second_account_id
-    assert auth_provider.is_authenticated is True
-
-
-def test_is_authenticated_property_when_authenticated(auth_provider: AuthProvider):
-    """
-    Test is_authenticated property returns True when account_id is set.
-
-    Args:
-        auth_provider: An instance of AuthProvider.
-    """
-    # Setup
-    auth_provider.account_id = "test-account"
-
-    # Verify result
-    assert auth_provider.is_authenticated is True
diff --git a/tests/unit/test_auth_service.py b/tests/unit/test_auth_service.py
new file mode 100644
index 00000000..ee638911
--- /dev/null
+++ b/tests/unit/test_auth_service.py
@@ -0,0 +1,61 @@
+import pytest
+
+from uncertainty_engine.auth_service import AuthService
+
+
+@pytest.fixture
+def auth_service():
+    """Fixture for an AuthService instance."""
+    return AuthService()
+
+
+def test_init(auth_service):
+    """Test that a new AuthService has no account_id set."""
+    assert auth_service.account_id is None
+    assert auth_service.is_authenticated is False
+
+
+def test_authenticate(auth_service):
+    """Test that calling authenticate sets the account_id."""
+    # Setup
+    test_account_id = "test-account-123"
+
+    # Call the method
+    auth_service.authenticate(test_account_id)
+
+    # Verify result
+    assert auth_service.account_id == test_account_id
+    assert auth_service.is_authenticated is True
+
+
+def test_authenticate_multiple_calls(auth_service):
+    """Test that authenticate can be called multiple times, updating the account_id."""
+    # Setup
+    first_account_id = "first-account"
+    second_account_id = "second-account"
+
+    # Call authenticate with first ID
+    auth_service.authenticate(first_account_id)
+    assert auth_service.account_id == first_account_id
+    assert auth_service.is_authenticated is True
+
+    # Call authenticate with second ID
+    auth_service.authenticate(second_account_id)
+    assert auth_service.account_id == second_account_id
+    assert auth_service.is_authenticated is True
+
+
+def test_is_authenticated_property_when_not_authenticated(auth_service):
+    """Test is_authenticated property returns False when account_id is None."""
+    # Verify initial state
+    assert auth_service.account_id is None
+    assert auth_service.is_authenticated is False
+
+
+def test_is_authenticated_property_when_authenticated(auth_service):
+    """Test is_authenticated property returns True when account_id is set."""
+    # Setup
+    auth_service.account_id = "test-account"
+
+    # Verify result
+    assert auth_service.is_authenticated is True
diff --git a/tests/unit/test_resource_provider.py b/tests/unit/test_resource_provider.py
deleted file mode 100644
index 2607a8ec..00000000
--- a/tests/unit/test_resource_provider.py
+++ /dev/null
@@ -1,806 +0,0 @@
-import os
-from datetime import datetime
-from unittest.mock import MagicMock, mock_open, patch
-
-import pytest
-import requests
-from uncertainty_engine_resource_client.api import ProjectRecordsApi, ResourcesApi
-from uncertainty_engine_resource_client.exceptions import ApiException
-from uncertainty_engine_resource_client.models import (
-    PostResourceRecordRequest,
-    PostResourceVersionRequest,
-    ResourceRecordInput,
-    ResourceVersionRecordInput,
-)
-
-from uncertainty_engine.resource_provider import (
-    DATETIME_STRING_FORMAT,
-    DEFAULT_RESOURCE_DEPLOYMENT,
-    ResourceProvider,
-)
-
-
-@pytest.fixture
-def mock_auth_provider():
-    """Fixture for a mock authentication provider."""
-    auth_provider = MagicMock()
-    auth_provider.account_id = "test-account-id"
-    return auth_provider
-
-
-@pytest.fixture
-def mock_api_client():
-    """Fixture for a mock API client."""
-    return MagicMock()
-
-
-@pytest.fixture
-def mock_projects_client():
-    """Fixture for a mock projects client."""
-    return MagicMock()
-
-
-@pytest.fixture
-def mock_resources_client():
-    """Fixture for a mock resources client."""
-    return MagicMock()
-
-
-@pytest.fixture
-def resource_provider(
-    mock_auth_provider, mock_api_client, mock_projects_client, mock_resources_client
-):
-    """Fixture for a ResourceProvider with mocked dependencies."""
-    with patch(
-        "uncertainty_engine_resource_client.api_client.ApiClient",
-        return_value=mock_api_client,
-    ):
-        with patch.object(ProjectRecordsApi, "__init__", return_value=None):
-            with patch.object(ResourcesApi, "__init__", return_value=None):
-                provider = ResourceProvider(auth_provider=mock_auth_provider)
-                provider.projects_client = mock_projects_client
-                provider.resources_client = mock_resources_client
-                return provider
-
-
-def test_init_default():
-    """Test initializing with default parameters."""
-    with patch(
-        "uncertainty_engine.resource_provider.Configuration"
-    ) as mock_config_class:
-        mock_config_instance = MagicMock()
-        mock_config_class.return_value = mock_config_instance
-
-        with patch("uncertainty_engine.resource_provider.ApiClient") as mock_client:
-            with patch.object(ProjectRecordsApi, "__init__", return_value=None):
-                with patch.object(ResourcesApi, "__init__", return_value=None):
-                    provider = ResourceProvider()
-
-                    # Verify configuration is created with default URL
-                    mock_config_class.assert_called_once_with(
-                        host=DEFAULT_RESOURCE_DEPLOYMENT
-                    )
-
-                    # Verify clients are created
-                    mock_client.assert_called_once_with(
-                        configuration=mock_config_instance
-                    )
-                    assert provider.auth_provider is None
-                    assert provider.client is not None
-                    assert provider.projects_client is not None
-                    assert provider.resources_client is not None
-
-
-def test_init_custom(mock_auth_provider):
-    """Test initializing with custom parameters."""
-    custom_url = "http://custom-url.com"
-
-    with patch(
-        "uncertainty_engine.resource_provider.Configuration"
-    ) as mock_config_class:
-        mock_config_instance = MagicMock()
-        mock_config_class.return_value = mock_config_instance
-
-        with patch("uncertainty_engine.resource_provider.ApiClient") as mock_client:
-            with patch.object(ProjectRecordsApi, "__init__", return_value=None):
-                with patch.object(ResourcesApi, "__init__", return_value=None):
-                    provider = ResourceProvider(
-                        deployment=custom_url, auth_provider=mock_auth_provider
-                    )
-
-                    # Verify configuration is created with custom URL
-                    mock_config_class.assert_called_once_with(host=custom_url)
-
-                    # Verify clients are created
-                    mock_client.assert_called_once_with(
-                        configuration=mock_config_instance
-                    )
-                    assert provider.auth_provider is mock_auth_provider
-                    assert provider.client is not None
-
-
-def test_account_id_with_auth_provider(resource_provider, mock_auth_provider):
-    """Test the account_id property when auth_provider is available."""
-    assert resource_provider.account_id == mock_auth_provider.account_id
-
-
-def test_account_id_without_auth_provider():
-    """Test the account_id property when auth_provider is not available."""
-    with patch("uncertainty_engine_resource_client.api_client.ApiClient"):
-        with patch.object(ProjectRecordsApi, "__init__", return_value=None):
-            with patch.object(ResourcesApi, "__init__", return_value=None):
-                provider = ResourceProvider(auth_provider=None)
-                assert provider.account_id is None
-
-
-def test_upload_success(resource_provider, mock_resources_client):
-    """Test the upload method when successful."""
-    # Setup mock responses
-    resource_record = MagicMock(id="test-resource-id")
-    resource_response = MagicMock(resource_record=resource_record)
-    mock_resources_client.post_resource_record.return_value = resource_response
-
-    version_response = MagicMock(
-        url="https://upload-url.com", pending_record_id="test-pending-id"
-    )
-    mock_resources_client.post_resource_version.return_value = version_response
-
-    # Setup file mock
-    mock_file = mock_open(read_data=b"test file content")
-
-    # Setup requests mock
-    mock_put_response = MagicMock()
-    mock_put_response.status_code = 200
-
-    # Test parameters
-    project_id = "test-project"
-    name = "Test Resource"
-    resource_type = "dataset"
-    file_path = "path/to/test_file.csv"
-
-    with patch("builtins.open", mock_file):
-        with patch("requests.put", return_value=mock_put_response):
-            # Call the method
-            result = resource_provider.upload(
-                project_id=project_id,
-                name=name,
-                resource_type=resource_type,
-                file_path=file_path,
-            )
-
-            # Verify the result
-            assert result == "test-resource-id"
-
-            # Verify method calls
-            mock_resources_client.post_resource_record.assert_called_once_with(
-                project_id,
-                resource_type,
-                PostResourceRecordRequest(
-                    resource_record=ResourceRecordInput(
-                        name=name, owner_id=resource_provider.account_id
-                    )
-                ),
-            )
-
-            mock_resources_client.post_resource_version.assert_called_once_with(
-                project_id,
-                resource_type,
-                "test-resource-id",
-                PostResourceVersionRequest(
-                    resource_version_record=ResourceVersionRecordInput(
-                        name=f"{name}-v1", owner_id=resource_provider.account_id
-                    ),
-                    resource_file_extension="csv",
-                ),
-            )
-
-            mock_file.assert_called_once_with(file_path, "rb")
-            requests.put.assert_called_once_with(
-                "https://upload-url.com", data=mock_file()
-            )
-
-            mock_resources_client.put_upload_resource_version.assert_called_once_with(
-                project_id, resource_type, "test-resource-id", "test-pending-id"
-            )
-
-
-def test_upload_no_auth(resource_provider):
-    """Test upload fails when not authenticated."""
-
-    # Set auth provider to None
-    with patch.object(resource_provider, "auth_provider", None):
-
-        # Call and verify exception
-        with pytest.raises(
-            ValueError, match="Authentication required before uploading resources"
-        ):
-            resource_provider.upload("project-id", "name", "type", "path")
-
-
-def test_upload_api_exception_on_record_creation(
-    resource_provider, mock_resources_client
-):
-    """Test handling of ApiException during resource record creation."""
-    # Setup mock error
-    api_exception = ApiException(status=400, reason="Bad Request")
-    mock_resources_client.post_resource_record.side_effect = api_exception
-
-    # Call and verify exception
-    with pytest.raises(Exception, match="Error creating resource record:"):
-        resource_provider.upload("project-id", "name", "type", "path/to/file.txt")
-
-
-def test_upload_api_exception_on_version_creation(
-    resource_provider, mock_resources_client
-):
-    """Test handling of ApiException during version record creation."""
-    # Setup mocks
-    resource_record = MagicMock(id="test-resource-id")
-    resource_response = MagicMock(resource_record=resource_record)
-    mock_resources_client.post_resource_record.return_value = resource_response
-
-    # Setup exception
-    api_exception = ApiException(status=400, reason="Bad Request")
-    mock_resources_client.post_resource_version.side_effect = api_exception
-
-    # Call and verify exception
-    with pytest.raises(Exception, match="Error creating version record:"):
-        resource_provider.upload("project-id", "name", "type", "path/to/file.txt")
-
-
-def test_upload_failed_file_upload(resource_provider, mock_resources_client):
-    """Test handling of failed upload to presigned URL."""
-    # Setup mocks
-    resource_record = MagicMock(id="test-resource-id")
-    resource_response = MagicMock(resource_record=resource_record)
-    mock_resources_client.post_resource_record.return_value = resource_response
-
-    version_response = MagicMock(
-        url="https://upload-url.com", pending_record_id="test-pending-id"
-    )
-    mock_resources_client.post_resource_version.return_value = version_response
-
-    # Setup file mock
-    mock_file = mock_open(read_data=b"test file content")
-
-    # Setup requests mock with error
-    mock_put_response = MagicMock()
-    mock_put_response.status_code = 403
-    mock_put_response.text = "Forbidden"
-
-    with patch("builtins.open", mock_file):
-        with patch("requests.put", return_value=mock_put_response):
-            # Call and verify exception
-            with pytest.raises(
-                Exception, match="Upload failed with status 403: Forbidden"
-            ):
-                resource_provider.upload(
-                    "project-id", "name", "type", "path/to/file.txt"
-                )
-
-
-def test_upload_failed_completion(resource_provider, mock_resources_client):
-    """Test handling of exception during upload completion."""
-    # Setup mocks
-    resource_record = MagicMock(id="test-resource-id")
-    resource_response = MagicMock(resource_record=resource_record)
-    mock_resources_client.post_resource_record.return_value = resource_response
-
-    version_response = MagicMock(
-        url="https://upload-url.com", pending_record_id="test-pending-id"
-    )
-    mock_resources_client.post_resource_version.return_value = version_response
-
-    # Setup file mock
-    mock_file = mock_open(read_data=b"test file content")
-
-    # Setup requests mock
-    mock_put_response = MagicMock()
-    mock_put_response.status_code = 200
-
-    # Setup exception on completion
-    api_exception = ApiException(status=400, reason="Bad Request")
-    mock_resources_client.put_upload_resource_version.side_effect = api_exception
-
-    with patch("builtins.open", mock_file):
-        with patch("requests.put", return_value=mock_put_response):
-            # Call and verify exception
-            with pytest.raises(Exception, match="Error completing upload:"):
-                resource_provider.upload(
-                    "project-id", "name", "type", "path/to/file.txt"
-                )
-
-
-def test_download_success_with_filepath(resource_provider):
-    """Test downloading a resource with a specified filepath."""
-    # Setup mocks
-    version_response = MagicMock()
-    version_response.url = "https://download-url.com"
-    resource_provider.resources_client.get_latest_resource_version.return_value = (
-        version_response
-    )
-
-    # Setup requests mock
-    mock_get_response = MagicMock()
-    mock_get_response.content = b"test file content"
-
-    # Setup file mock
-    mock_file = mock_open()
-
-    with patch("os.makedirs") as mock_makedirs:
-        with patch("builtins.open", mock_file):
-            with patch("requests.get", return_value=mock_get_response):
-                # Call the method
-                resource_provider.download(
-                    project_id="test-project",
-                    resource_type="dataset",
-                    resource_id="test-resource-id",
-                    file_path="path/to/download/file.csv",
-                )
-
-                # Verify method calls
-                resource_provider.resources_client.get_latest_resource_version.assert_called_once_with(
-                    "test-project", "dataset", "test-resource-id"
-                )
-
-                mock_makedirs.assert_called_once_with(
-                    os.path.dirname(os.path.abspath("path/to/download/file.csv")),
-                    exist_ok=True,
-                )
-                requests.get.assert_called_once_with("https://download-url.com")
-                mock_get_response.raise_for_status.assert_called_once()
-                mock_file.assert_called_once_with("path/to/download/file.csv", "wb")
-                mock_file().write.assert_called_once_with(b"test file content")
-
-
-def test_download_success_without_filepath(resource_provider):
-    """Test downloading a resource without a specified filepath (return content)."""
-    # Setup mocks
-    version_response = MagicMock()
-    version_response.url = "https://download-url.com"
-    resource_provider.resources_client.get_latest_resource_version.return_value = (
-        version_response
-    )
-
-    # Setup requests mock
-    mock_get_response = MagicMock()
-    expected_content = b'{"data": "test content"}'
-    mock_get_response.content = expected_content
-
-    with patch("requests.get", return_value=mock_get_response):
-        # Call the method
-        result = resource_provider.download(
-            project_id="test-project",
-            resource_type="dataset",
-            resource_id="test-resource-id",
-        )
-
-        # Verify result and method calls
-        assert result == expected_content
-        resource_provider.resources_client.get_latest_resource_version.assert_called_once_with(
-            "test-project", "dataset", "test-resource-id"
-        )
-        requests.get.assert_called_once_with("https://download-url.com")
-        mock_get_response.raise_for_status.assert_called_once()
-
-
-def test_download_no_auth(resource_provider):
-    """Test download fails when not authenticated."""
-    # Set auth provider to None
-    with patch.object(resource_provider, "auth_provider", None):
-
-        # Call and verify exception
-        with pytest.raises(
-            ValueError, match="Authentication required before downloading resources"
-        ):
-            resource_provider.download("project-id", "type", "resource-id")
-
-
-def test_download_api_exception(resource_provider):
-    """Test handling of ApiException during resource version retrieval."""
-    # Setup exception
-    api_exception = ApiException(status=404, reason="Not Found")
-    resource_provider.resources_client.get_latest_resource_version.side_effect = (
-        api_exception
-    )
-
-    # Call and verify exception
-    with pytest.raises(Exception, match="Error retrieving resource:"):
-        resource_provider.download(
-            "project-id", "type", "resource-id", "path/to/file.txt"
-        )
-
-
-def test_download_generic_exception_on_retrieval(resource_provider):
-    """Test handling of generic Exception during resource retrieval."""
-    # Setup exception
-    generic_exception = Exception("Random error")
-    resource_provider.resources_client.get_latest_resource_version.side_effect = (
-        generic_exception
-    )
-
-    # Call and verify exception
-    with pytest.raises(Exception, match="Error retrieving resource: Random error"):
-        resource_provider.download(
-            "project-id", "type", "resource-id", "path/to/file.txt"
-        )
-
-
-def test_download_http_error(resource_provider):
-    """Test handling of HTTP error during download."""
-    # Setup mocks
-    version_response = MagicMock()
-    version_response.url = "https://download-url.com"
-    resource_provider.resources_client.get_latest_resource_version.return_value = (
-        version_response
-    )
-
-    # Setup requests mock with error
-    mock_get_response = MagicMock()
-    http_error = requests.exceptions.HTTPError("404 Client Error")
-    mock_get_response.raise_for_status.side_effect = http_error
-
-    with patch("requests.get", return_value=mock_get_response):
-        # Call and verify exception
-        with pytest.raises(Exception, match="Error saving downloaded file:"):
-            resource_provider.download(
-                "project-id", "type", "resource-id", "path/to/file.txt"
-            )
-
-
-def test_download_file_not_found_error(resource_provider):
-    """Test handling of FileNotFoundError when writing to a file."""
-    # Setup mocks
-    version_response = MagicMock()
-    version_response.url = "https://download-url.com"
-    resource_provider.resources_client.get_latest_resource_version.return_value = (
-        version_response
-    )
-
-    # Setup requests mock
-    mock_get_response = MagicMock()
-    mock_get_response.content = b"test file content"
-
-    # Setup file mock to raise FileNotFoundError
-    mock_file = mock_open()
-    mock_file.side_effect = FileNotFoundError("No such file or directory")
-
-    with patch("os.makedirs"):
-        with patch("builtins.open", mock_file):
-            with patch("requests.get", return_value=mock_get_response):
-                # Call and verify exception
-                with pytest.raises(Exception, match="Invalid filepath provided"):
-                    resource_provider.download(
-                        project_id="test-project",
-                        resource_type="dataset",
-                        resource_id="test-resource-id",
-                        file_path="path/to/download/file.csv",
-                    )
-
-                mock_get_response.raise_for_status.assert_called_once()
-
-
-def test_download_other_file_exception(resource_provider):
-    """Test handling of generic exceptions when writing to a file."""
-    # Setup mocks
-    version_response = MagicMock()
-    version_response.url = "https://download-url.com"
-    resource_provider.resources_client.get_latest_resource_version.return_value = (
-        version_response
-    )
-
-    # Setup requests mock
-    mock_get_response = MagicMock()
-    mock_get_response.content = b"test file content"
-
-    # Setup file mock to raise a permission error - this is more specific and appropriate
-    mock_file = mock_open()
-    # Configure the write method to raise the exception
-    mock_file.return_value.write.side_effect = PermissionError("Permission denied")
-
-    with patch("os.makedirs"):
-        with patch("builtins.open", mock_file):
-            with patch("requests.get", return_value=mock_get_response):
-                # Call and verify exception
-                with pytest.raises(PermissionError, match="Permission denied"):
-                    resource_provider.download(
-                        project_id="test-project",
-                        resource_type="dataset",
-                        resource_id="test-resource-id",
-                        file_path="path/to/download/file.csv",
-                    )
-
-
-def test_update_success(resource_provider, mock_resources_client):
-    """Test updating a resource successfully."""
-    # Setup mocks for resource info with direct attribute assignment
-    resource_record = MagicMock()
-    resource_record.name = "Test Resource"  # Direct assignment for string value
-    resource_record.versions = ["v1", "v2"]
-
-    resource_response = MagicMock()
-    resource_response.resource_record = resource_record
-    mock_resources_client.get_resource_record.return_value = resource_response
-
-    # Setup mocks for version creation
-    version_response = MagicMock()
-    version_response.url = "https://upload-url.com"
-    version_response.pending_record_id = "test-pending-id"
-    mock_resources_client.post_resource_version.return_value = version_response
-
-    # Setup file mock
-    mock_file = mock_open(read_data=b"updated content")
-
-    # Setup requests mock
-    mock_put_response = MagicMock()
-    mock_put_response.status_code = 200
-
-    with patch("os.path.exists", return_value=True):
-        with patch("builtins.open", mock_file):
-            with patch("requests.put", return_value=mock_put_response):
-                # Call the method
-                resource_provider.update(
-                    project_id="test-project",
-                    resource_type="dataset",
-                    resource_id="test-resource-id",
-                    file_path="path/to/updated_file.json",
-                )
-
-                # Verify method calls
-                mock_resources_client.get_resource_record.assert_called_once_with(
-                    "test-project", "dataset", "test-resource-id"
-                )
-
-                # Expected version name should be resource name + v3 (since there are two existing versions)
-                mock_resources_client.post_resource_version.assert_called_once_with(
-                    "test-project",
-                    "dataset",
-                    "test-resource-id",
-                    PostResourceVersionRequest(
-                        resource_version_record=ResourceVersionRecordInput(
-                            name="Test Resource-v3",
-                            owner_id=resource_provider.account_id,
-                        ),
-                        resource_file_extension="json",
-                    ),
-                )
-
-                mock_file.assert_called_once_with("path/to/updated_file.json", "rb")
-                requests.put.assert_called_once_with(
-                    "https://upload-url.com", data=mock_file()
-                )
-
-                mock_resources_client.put_upload_resource_version.assert_called_once_with(
-                    "test-project", "dataset", "test-resource-id", "test-pending-id"
-                )
-
-
-def test_update_resource_not_found(resource_provider, mock_resources_client):
-    """Test handling when the resource to update is not found."""
-    # Setup exception for resource lookup
-    api_exception = ApiException(status=404, reason="Not Found")
-    mock_resources_client.get_resource_record.side_effect = api_exception
-
-    # Setup mocks for version creation
-    version_response = MagicMock(
-        url="https://upload-url.com", pending_record_id="test-pending-id"
-    )
-    mock_resources_client.post_resource_version.return_value = version_response
-
-    # Setup file mock
-    mock_file = mock_open(read_data=b"updated content")
-
-    # Setup requests mock
-    mock_put_response = MagicMock()
-    mock_put_response.status_code = 200
-
-    with patch("os.path.exists", return_value=True):
-        with patch("builtins.open", mock_file):
-            with patch("requests.put", return_value=mock_put_response):
-                with patch(
-                    "uncertainty_engine.resource_provider.uuid4",
-                    return_value="test-uuid",
-                ):
-                    # Call the method
-                    resource_provider.update(
-                        project_id="test-project",
-                        resource_type="dataset",
-                        resource_id="test-resource-id",
-                        file_path="path/to/updated_file.json",
-                    )
-
-                    # Verify the generic version name is used
-                    mock_resources_client.post_resource_version.assert_called_once_with(
-                        "test-project",
-                        "dataset",
-                        "test-resource-id",
-                        PostResourceVersionRequest(
-                            resource_version_record=ResourceVersionRecordInput(
-                                name="version-test-uuid",
-                                owner_id=resource_provider.account_id,
-                            ),
-                            resource_file_extension="json",
-                        ),
-                    )
-
-
-def test_update_no_auth(resource_provider):
-    """Test update fails when not authenticated."""
-    # Set auth provider to None
-    with patch.object(resource_provider, "auth_provider", None):
-        resource_provider.auth_provider = None
-
-        # Call and verify exception
-        with pytest.raises(
-            ValueError, match="Authentication required before updating resources"
-        ):
-            resource_provider.update(
-                "project-id", "type", "resource-id", "path/to/file.txt"
-            )
-
-
-def test_update_file_not_found(resource_provider):
-    """Test update fails when the file doesn't exist."""
-    with patch("os.path.exists", return_value=False):
-        # Call and verify exception
-        with pytest.raises(FileNotFoundError, match="File not found:"):
-            resource_provider.update(
-                "project-id", "type", "resource-id", "nonexistent/file.txt"
-            )
-
-
-def test_update_api_exception_on_version_creation(
-    resource_provider, mock_resources_client
-):
-    """Test handling of ApiException during version record creation."""
-    # Setup mocks for resource info
-    resource_record = MagicMock(name="Test Resource", versions=["v1"])
-    resource_response = MagicMock(resource_record=resource_record)
-    mock_resources_client.get_resource_record.return_value = resource_response
-
-    # Setup exception for version creation
-    api_exception = ApiException(status=400, reason="Bad Request")
-    mock_resources_client.post_resource_version.side_effect = api_exception
-
-    with patch("os.path.exists", return_value=True):
-        # Call and verify exception
-        with pytest.raises(Exception, match="Error creating version record:"):
-            resource_provider.update(
-                "project-id", "type", "resource-id", "path/to/file.txt"
-            )
-
-
-def test_update_upload_error(resource_provider, mock_resources_client):
-    """Test handling of error during upload to presigned URL."""
-    # Setup mocks for resource info
-    resource_record = MagicMock(name="Test Resource", versions=["v1"])
-    resource_response = MagicMock(resource_record=resource_record)
-    mock_resources_client.get_resource_record.return_value = resource_response
-
-    # Setup mocks for version creation
-    version_response = MagicMock(
-        url="https://upload-url.com", pending_record_id="test-pending-id"
-    )
-    mock_resources_client.post_resource_version.return_value = version_response
-
-    # Setup file mock
-    mock_file = mock_open(read_data=b"updated content")
-
-    # Setup requests mock with error
-    mock_put_response = MagicMock()
-    mock_put_response.status_code = 403
-    mock_put_response.text = "Forbidden"
-
-    with patch("os.path.exists", return_value=True):
-        with patch("builtins.open", mock_file):
-            with patch("requests.put", return_value=mock_put_response):
-                # Call and verify exception
-                with pytest.raises(
-                    Exception, match="Upload failed with status 403: Forbidden"
-                ):
-                    resource_provider.update(
-                        "test-project",
-                        "dataset",
-                        "test-resource-id",
-                        "path/to/updated_file.json",
-                    )
-
-
-def test_update_finalize_error(resource_provider, mock_resources_client):
-    """Test handling of error during finalization of upload."""
-    # Setup mocks for resource info
-    resource_record = MagicMock(name="Test Resource", versions=["v1"])
-    resource_response = MagicMock(resource_record=resource_record)
-    mock_resources_client.get_resource_record.return_value = resource_response
-
-    # Setup mocks for version creation
-    version_response = MagicMock(
-        url="https://upload-url.com", pending_record_id="test-pending-id"
-    )
-    mock_resources_client.post_resource_version.return_value = version_response
-
-    # Setup file mock
-    mock_file = mock_open(read_data=b"updated content")
-
-    # Setup requests mock
-    mock_put_response = MagicMock()
-    mock_put_response.status_code = 200
-
-    # Setup exception for finalization
-    api_exception = ApiException(status=400, reason="Bad Request")
-    mock_resources_client.put_upload_resource_version.side_effect = api_exception
-
-    with patch("os.path.exists", return_value=True):
-        with patch("builtins.open", mock_file):
-            with patch("requests.put", return_value=mock_put_response):
-                # Call and verify exception
-                with pytest.raises(Exception, match="Error finalizing upload:"):
-                    resource_provider.update(
-                        "test-project",
-                        "dataset",
-                        "test-resource-id",
-                        "path/to/updated_file.json",
-                    )
-
-
-def test_list_resources_success(resource_provider, mock_resources_client):
-    """Test listing resources successfully."""
-    # Setup mock response
-    created_time = datetime.now()
-
-    # Create proper mock objects with attributes that return expected values
-    record1 = MagicMock()
-    record1.id = "resource-1"  # Direct assignment to make it return the string
-    record1.name = "Resource 1"
-    record1.created_at = created_time
-
-    record2 = MagicMock()
-    record2.id = "resource-2"
-    record2.name = "Resource 2"
-    record2.created_at = created_time
-
-    resource_records = [record1, record2]
-    response = MagicMock(resource_records=resource_records)
-    mock_resources_client.get_project_resource_records.return_value = response
-
-    # Call the method
-    result = resource_provider.list_resources("test-project", "dataset")
-
-    # Verify result
-    expected_result = [
-        {
-            "id": "resource-1",
-            "name": "Resource 1",
-            "created_at": created_time.strftime(DATETIME_STRING_FORMAT),
-        },
-        {
-            "id": "resource-2",
-            "name": "Resource 2",
-            "created_at": created_time.strftime(DATETIME_STRING_FORMAT),
-        },
-    ]
-    assert result == expected_result
-
-    # Verify method call
-    mock_resources_client.get_project_resource_records.assert_called_once_with(
-        "test-project", "dataset"
-    )
-
-
-def test_list_resources_empty(resource_provider, mock_resources_client):
-    """Test listing resources when none exist."""
-    # Setup mock response with empty list
-    response = MagicMock()
-    response.resource_records = []  # Direct assignment for clean mocking
-    mock_resources_client.get_project_resource_records.return_value = response
-
-    # Call the method
-    result = resource_provider.list_resources("test-project", "dataset")
-
-    # Verify result is empty list
-    assert result == []
-
-    # Verify method call
-    mock_resources_client.get_project_resource_records.assert_called_once_with(
-        "test-project", "dataset"
-    )
diff --git a/uncertainty_engine/api_providers/__init__.py b/uncertainty_engine/api_providers/__init__.py
new file mode 100644
index 00000000..5d09c763
--- /dev/null
+++ b/uncertainty_engine/api_providers/__init__.py
@@ -0,0 +1,2 @@
+from uncertainty_engine.api_providers.api_provider import ApiProviderBase
+from uncertainty_engine.api_providers.resource_provider import ResourceProvider
diff --git a/uncertainty_engine/api_providers/api_provider.py b/uncertainty_engine/api_providers/api_provider.py
new file mode 100644
index 00000000..2fb579cd
--- /dev/null
+++ b/uncertainty_engine/api_providers/api_provider.py
@@ -0,0 +1,9 @@
+from uncertainty_engine.auth_service import AuthService
+
+
+class ApiProviderBase:
+    """Base class for all API clients with automatic token refresh"""
+
+    def __init__(self, deployment: str, auth_service: AuthService):
+        self.deployment = deployment
+        self.auth_service = auth_service
diff --git a/uncertainty_engine/resource_provider.py b/uncertainty_engine/api_providers/resource_provider.py
similarity index 93%
rename from uncertainty_engine/resource_provider.py
rename to uncertainty_engine/api_providers/resource_provider.py
index b3b6fb80..440a75b9 100644
--- a/uncertainty_engine/resource_provider.py
+++ b/uncertainty_engine/api_providers/resource_provider.py
@@ -14,14 +14,17 @@
     ResourceVersionRecordInput,
 )
 
-from uncertainty_engine.auth_provider import AuthProvider
+from uncertainty_engine.api_providers import ApiProviderBase
+from uncertainty_engine.auth_service import AuthService
 from uncertainty_engine.utils import format_api_error
 
 DATETIME_STRING_FORMAT = "%H:%M:%S %Y-%m-%d"
 DEFAULT_RESOURCE_DEPLOYMENT = "http://localhost:8001/api"
 
 
-class ResourceProvider:
+class ResourceProvider(ApiProviderBase):
+    deployment: str
+    auth_service: AuthService
     """
     Client for managing resources in the Uncertainty Engine platform.
 
@@ -37,9 +40,7 @@ class ResourceProvider:
     """
 
     def __init__(
-        self,
-        deployment: str = DEFAULT_RESOURCE_DEPLOYMENT,
-        auth_provider: Optional[AuthProvider] = None,
+        self, auth_service: AuthService, deployment: str = DEFAULT_RESOURCE_DEPLOYMENT
     ):
         """
         Create an instance of a ResourceProvider
@@ -47,19 +48,33 @@ def __init__(
         Args:
             deployment: The URL of the resource service. You typically won't need
                         to change this unless instructed by support.
-            auth_provider: Handles your authentication. This is usually provided
-                           by the parent application.
+            auth_service: Handles your authentication.
         """
-        self.auth_provider = auth_provider
-        config = Configuration(host=deployment)
-        self.client = ApiClient(configuration=config)
+        super().__init__(deployment, auth_service)
+
+        # Initialize the generated API client
+        self.client = ApiClient(configuration=Configuration(host=deployment))
         self.projects_client = ProjectRecordsApi(self.client)
         self.resources_client = ResourcesApi(self.client)
 
+    def authenticate(self, account_id: str) -> None:
+        """
+        Set the account ID
+
+        Args:
+            account_id: The account ID to authenticate with.
+        """
+        self.auth_service.authenticate(account_id)
+
     @property
     def account_id(self) -> Optional[str]:
-        """Get the current account ID from the auth provider"""
-        return None if self.auth_provider is None else self.auth_provider.account_id
+        """
+        Get the current account ID from the auth provider
+
+        Returns:
+            The account ID if authenticated, otherwise None.
+        """
+        return self.auth_service.account_id
 
     def upload(
         self,
diff --git a/uncertainty_engine/auth_provider.py b/uncertainty_engine/auth_service.py
similarity index 83%
rename from uncertainty_engine/auth_provider.py
rename to uncertainty_engine/auth_service.py
index 6af2de0a..8d08b6c7 100644
--- a/uncertainty_engine/auth_provider.py
+++ b/uncertainty_engine/auth_service.py
@@ -1,5 +1,5 @@
-class AuthProvider:
-    """Authentication state management"""
+class AuthService:
+    """Authentication service that manages tokens and provides them to API clients"""
 
     def __init__(self):
         self.account_id = None
diff --git a/uncertainty_engine/client.py b/uncertainty_engine/client.py
index d24e7ebb..30352a59 100644
--- a/uncertainty_engine/client.py
+++ b/uncertainty_engine/client.py
@@ -6,9 +6,9 @@
 from pydantic import BaseModel
 from typeguard import typechecked
 
-from uncertainty_engine.auth_provider import AuthProvider
+from uncertainty_engine.api_providers import ResourceProvider
+from uncertainty_engine.auth_service import AuthService
 from uncertainty_engine.nodes.base import Node
-from uncertainty_engine.resource_provider import ResourceProvider
 
 DEFAULT_DEPLOYMENT = "http://localhost:8000/api"
 DEFAULT_RESOURCE_DEPLOYMENT = "http://localhost:8001/api"
@@ -58,19 +58,17 @@ def __init__(
         """
         self.email = email
         self.deployment = deployment
-        self.auth_provider = AuthProvider()
-        self.resources = ResourceProvider(
-            deployment=resource_deployment, auth_provider=self.auth_provider
-        )
+        self.auth_service = AuthService()
+        self.resources = ResourceProvider(self.auth_service, resource_deployment)
 
-    def auth(self, account_id: str) -> None:
+    def authenticate(self, account_id: str) -> None:
         """
         Authenticate the user with the Uncertainty Engine"
 
         Args:
             account_id : The account ID to authenticate with.
         """
-        self.auth_provider.authenticate(account_id)
+        self.resources.authenticate(account_id)
 
     def list_nodes(self, category: Optional[str] = None) -> list:
         """


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR refactors the authentication and resource management layers by replacing use of AuthProvider with AuthService and updating method names and test fixtures accordingly. Key changes include:
- Changing the client’s auth() method to authenticate() and updating its implementation.
- Refactoring ResourceProvider to extend ApiProviderBase and use an injected AuthService.
- Adjusting tests and fixtures throughout the codebase to align with these changes.

### Reviewed Changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated no comments.

<details>
<summary>Show a summary per file</summary>

| File                                          | Description                                                    |
| --------------------------------------------- | -------------------------------------------------------------- |
| uncertainty_engine/client.py                  | Updated authentication initialization and method renaming.   |
| uncertainty_engine/auth_service.py            | Renamed the authentication class and updated its docstring.    |
| uncertainty_engine/api_providers/resource_provider.py | Modified constructor signature and property access for auth. |
| tests/unit/api_providers/test_resource_provider.py | Updated tests for upload, download, update, and authentication.  |
| tests/unit/api_providers/conftest.py           | Adjusted fixtures to support AuthService and ResourceProvider refactor.  |
| examples/demo_resource.ipynb                  | Updated example usage to call authenticate() instead of auth().  |
</details>



<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**tests/unit/api_providers/test_resource_provider.py:272**
* [nitpick] The expected URL for the download test is set to 'https://upload-url.com', which may be confusing since downloads typically use a URL that indicates a download endpoint. Consider revising the mock URL to clearly reflect its role as a download endpoint if applicable.
```
requests.get.assert_called_once_with("https://upload-url.com")
```
**tests/unit/api_providers/conftest.py:90**
* [nitpick] The mock_version_response fixture uses 'https://upload-url.com' as its default URL, which may cause ambiguity in tests for download scenarios. Consider renaming or parameterizing the URL to better differentiate upload and download endpoints.
```
def mock_version_response(url="https://upload-url.com", pending_id="test-pending-id"):
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T10:04:39Z
user: login: natalietf
body: Really nice refactoring changes Jasper - very satisfying (especially `test_resource_provider`!) 
Let me know if you have any questions about my comments!
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T13:40:24Z
user: login: jacantwell
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T16:16:50Z
user: login: jacantwell
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T16:17:51Z
user: login: jacantwell
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T16:26:43Z
user: login: JDonaldM
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T19:51:44Z
user: login: JDonaldM
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T19:59:23Z
user: login: JDonaldM
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T20:04:51Z
user: login: jacantwell
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T08:14:52Z
user: login: jacantwell
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T08:56:27Z
user: login: jacantwell
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T09:11:43Z
user: login: jacantwell
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T09:12:21Z

Review comments:
diff_hunk: @@ -37,29 +40,31 @@ class ResourceProvider:
     """
 
     def __init__(
-        self,
-        deployment: str = DEFAULT_RESOURCE_DEPLOYMENT,
-        auth_provider: Optional[AuthProvider] = None,
+        self, auth_service: AuthService, deployment: str = DEFAULT_RESOURCE_DEPLOYMENT
     ):
         """
         Create an instance of a ResourceProvider
 
         Args:
             deployment: The URL of the resource service. You typically won't need
                         to change this unless instructed by support.
-            auth_provider: Handles your authentication. This is usually provided
-                           by the parent application.
+            auth_service: Handles your authentication.
         """
-        self.auth_provider = auth_provider
-        config = Configuration(host=deployment)
-        self.client = ApiClient(configuration=config)
+        super().__init__(deployment, auth_service)
+
+        # Initialize the generated API client
+        self.client = ApiClient(configuration=Configuration(host=deployment))
         self.projects_client = ProjectRecordsApi(self.client)
         self.resources_client = ResourcesApi(self.client)
 
+    def authenticate(self, account_id: str) -> None:
+        """Set the account ID"""
+        self.auth_service.authenticate(account_id)
+
     @property
     def account_id(self) -> Optional[str]:
         """Get the current account ID from the auth provider"""
-        return None if self.auth_provider is None else self.auth_provider.account_id
+        return None if self.auth_service is None else self.auth_service.account_id
path: uncertainty_engine/api_providers/resource_provider.py
user: login: natalietf
body: Is the `None` check needed? `auth_service` isn't `Optional` so shouldn't be `None` and if the `account_id` is `None` then this will return `None` anyway? Lmk if I'm being an idiot 😊
```suggestion
        return self.auth_service.account_id
```
created_at: 2025-05-13T10:17:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,19 +58,17 @@ def __init__(
         """
         self.email = email
         self.deployment = deployment
-        self.auth_provider = AuthProvider()
-        self.resources = ResourceProvider(
-            deployment=resource_deployment, auth_provider=self.auth_provider
-        )
+        self.auth_service = AuthService()
+        self.resources = ResourceProvider(self.auth_service, resource_deployment)
path: uncertainty_engine/client.py
user: login: natalietf
body: Is it worth passing `AuthService()` directly to `ResourcesProvider` just as `self.auth_service` isn't accessed anywhere else currently? Unless its there for a future purpose?
```suggestion
        self.resources = ResourceProvider(AuthService(), resource_deployment)
```
created_at: 2025-05-13T10:41:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,61 @@
+import pytest
+
+from uncertainty_engine.auth_service import AuthService
+
+
+@pytest.fixture
+def auth_service():
+    """Fixture for an AuthService instance."""
+    return AuthService()
+
+
+def test_init(auth_service):
+    """Test that a new AuthService has no account_id set."""
+    assert auth_service.account_id is None
+    assert auth_service.is_authenticated is False
+
+
+def test_authenticate(auth_service):
+    """Test that calling authenticate sets the account_id."""
+    # Setup
+    test_account_id = "test-account-123"
+
+    # Call the method
+    auth_service.authenticate(test_account_id)
+
+    # Verify result
+    assert auth_service.account_id == test_account_id
+    assert auth_service.is_authenticated is True
+
+
+def test_authenticate_multiple_calls(auth_service):
+    """Test that authenticate can be called multiple times, updating the account_id."""
+    # Setup
+    first_account_id = "first-account"
+    second_account_id = "second-account"
+
+    # Call authenticate with first ID
+    auth_service.authenticate(first_account_id)
+    assert auth_service.account_id == first_account_id
+    assert auth_service.is_authenticated is True
+
+    # Call authenticate with second ID
+    auth_service.authenticate(second_account_id)
+    assert auth_service.account_id == second_account_id
+    assert auth_service.is_authenticated is True
path: tests/unit/test_auth_service.py
user: login: natalietf
body: Just looking at this test - is it worth having a way of resetting `auth_service.account_id` back to `None` or even a `clear_authentication` method?Once again out of scope and I know there is the auth PR coming in soon anyway. I guess you can also just create another instance of it for this anyway - it just seemed that if you can update it easily like this maybe there should be a way to reset it as well :)
created_at: 2025-05-13T10:53:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,61 @@
+import pytest
+
+from uncertainty_engine.auth_service import AuthService
+
+
+@pytest.fixture
+def auth_service():
+    """Fixture for an AuthService instance."""
+    return AuthService()
+
+
+def test_init(auth_service):
+    """Test that a new AuthService has no account_id set."""
+    assert auth_service.account_id is None
+    assert auth_service.is_authenticated is False
+
+
+def test_authenticate(auth_service):
+    """Test that calling authenticate sets the account_id."""
+    # Setup
+    test_account_id = "test-account-123"
+
+    # Call the method
+    auth_service.authenticate(test_account_id)
+
+    # Verify result
+    assert auth_service.account_id == test_account_id
+    assert auth_service.is_authenticated is True
+
+
+def test_authenticate_multiple_calls(auth_service):
+    """Test that authenticate can be called multiple times, updating the account_id."""
+    # Setup
+    first_account_id = "first-account"
+    second_account_id = "second-account"
+
+    # Call authenticate with first ID
+    auth_service.authenticate(first_account_id)
+    assert auth_service.account_id == first_account_id
+    assert auth_service.is_authenticated is True
+
+    # Call authenticate with second ID
+    auth_service.authenticate(second_account_id)
+    assert auth_service.account_id == second_account_id
+    assert auth_service.is_authenticated is True
+
+
+def test_is_authenticated_property_when_not_authenticated(auth_service):
+    """Test is_authenticated property returns False when account_id is None."""
+    # Verify initial state
+    assert auth_service.account_id is None
+    assert auth_service.is_authenticated is False
path: tests/unit/test_auth_service.py
user: login: natalietf
body: Is this not the same as the `test_init` test?
created_at: 2025-05-13T10:57:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,61 @@
+import pytest
+
+from uncertainty_engine.auth_service import AuthService
+
+
+@pytest.fixture
+def auth_service():
+    """Fixture for an AuthService instance."""
+    return AuthService()
+
+
+def test_init(auth_service):
+    """Test that a new AuthService has no account_id set."""
+    assert auth_service.account_id is None
+    assert auth_service.is_authenticated is False
+
+
+def test_authenticate(auth_service):
+    """Test that calling authenticate sets the account_id."""
+    # Setup
+    test_account_id = "test-account-123"
+
+    # Call the method
+    auth_service.authenticate(test_account_id)
+
+    # Verify result
+    assert auth_service.account_id == test_account_id
+    assert auth_service.is_authenticated is True
+
+
+def test_authenticate_multiple_calls(auth_service):
+    """Test that authenticate can be called multiple times, updating the account_id."""
+    # Setup
+    first_account_id = "first-account"
+    second_account_id = "second-account"
+
+    # Call authenticate with first ID
+    auth_service.authenticate(first_account_id)
+    assert auth_service.account_id == first_account_id
+    assert auth_service.is_authenticated is True
+
+    # Call authenticate with second ID
+    auth_service.authenticate(second_account_id)
+    assert auth_service.account_id == second_account_id
+    assert auth_service.is_authenticated is True
+
+
+def test_is_authenticated_property_when_not_authenticated(auth_service):
+    """Test is_authenticated property returns False when account_id is None."""
+    # Verify initial state
+    assert auth_service.account_id is None
+    assert auth_service.is_authenticated is False
+
+
+def test_is_authenticated_property_when_authenticated(auth_service):
+    """Test is_authenticated property returns True when account_id is set."""
+    # Setup
+    auth_service.account_id = "test-account"
+
+    # Verify result
+    assert auth_service.is_authenticated is True
path: tests/unit/test_auth_service.py
user: login: natalietf
body: Same with this one - I feel like it's already done as part of `test_authenticate`
created_at: 2025-05-13T11:56:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,95 @@
+from unittest.mock import MagicMock, mock_open, patch
+
+import pytest
+from uncertainty_engine_resource_client.api import ProjectRecordsApi, ResourcesApi
+
+from uncertainty_engine.api_providers.resource_provider import ResourceProvider
+
+
+@pytest.fixture
+def mock_auth_service():
+    """Fixture for a mock authentication provider."""
+    auth_service = MagicMock()
+    auth_service.account_id = "test-account-id"
+    return auth_service
+
+
+@pytest.fixture
+def mock_projects_client():
+    """Fixture for a mock projects client."""
+    return MagicMock()
+
+
+@pytest.fixture
+def mock_resources_client():
+    """Fixture for a mock resources client."""
+    return MagicMock()
+
+
+@pytest.fixture
+def patched_api_classes():
+    """Patch all API-related classes."""
+    with patch(
+        "uncertainty_engine_resource_client.api_client.ApiClient"
+    ) as patched_api_client:
+        with patch.object(
+            ProjectRecordsApi, "__init__", return_value=None
+        ) as patched_project_api:
+            with patch.object(
+                ResourcesApi, "__init__", return_value=None
+            ) as patched_resource_api:
+                yield {
+                    "api_client": patched_api_client,
+                    "project_api": patched_project_api,
+                    "resource_api": patched_resource_api,
+                }
path: tests/unit/api_providers/conftest.py
user: login: natalietf
body: Don't know whether using pytest's `monkeypatch` might be a bit cleaner here? Haven't got loads of experience using it but [pytest docs](https://docs.pytest.org/en/stable/how-to/monkeypatch.html) seem to recommend it and seems a bit easier to follow - once again a matter of preference though so feel free to disagree
```suggestion
@pytest.fixture
def patched_api_classes(monkeypatch: MonkeyPatch):
    """Patch all API-related classes using monkeypatch."""
    # Create mocks for each class/method
    api_client_mock = MagicMock()
    project_api_init_mock = MagicMock(return_value=None)
    resource_api_init_mock = MagicMock(return_value=None)

    # Apply patches
    monkeypatch.setattr(
        "uncertainty_engine_resource_client.api_client.ApiClient", api_client_mock
    )
    monkeypatch.setattr(ProjectRecordsApi, "__init__", project_api_init_mock)
    monkeypatch.setattr(ResourcesApi, "__init__", resource_api_init_mock)

    # Return mocks for assertions if needed
    return {
        "api_client": api_client_mock,
        "project_api": project_api_init_mock,
        "resource_api": resource_api_init_mock,
    }
```
created_at: 2025-05-13T12:40:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -13,140 +12,65 @@
     ResourceVersionRecordInput,
 )
 
-from uncertainty_engine.resource_provider import (
+from uncertainty_engine.api_providers.resource_provider import (
     DATETIME_STRING_FORMAT,
-    DEFAULT_RESOURCE_DEPLOYMENT,
     ResourceProvider,
 )
 
+### __init__ ###
path: tests/unit/api_providers/test_resource_provider.py
user: login: natalietf
body: I appreciate these 😊😊
created_at: 2025-05-13T12:42:00Z
author_association: CONTRIBUTOR
path: tests/unit/api_providers/test_resource_provider.py
user: login: natalietf
body: This file is much cleaner and easier to follow - thank you!
Same suggestion about using `pytest.monkeypatch` goes for this file (I think this [link](https://docs.pytest.org/en/stable/how-to/monkeypatch.html#monkeypatching-returned-objects-building-mock-classes) show quite a nice way of mocking http requests) just in case its useful but I don't think it would alter the functionality so either works :)
created_at: 2025-05-13T13:03:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -248,51 +168,53 @@ def test_upload_api_exception_on_version_creation(
         resource_provider.upload("project-id", "name", "type", "path/to/file.txt")
 
 
-def test_upload_failed_file_upload(resource_provider, mock_resources_client):
-    """Test handling of failed upload to presigned URL."""
+@pytest.mark.parametrize(
+    "status_code,error_text,expected_error_msg",
+    [
+        (403, "Forbidden", "Upload failed with status 403: Forbidden"),
+        (500, "Server Error", "Upload failed with status 500: Server Error"),
+    ],
+)
+def test_upload_failed_file_upload(
path: tests/unit/api_providers/test_resource_provider.py
user: login: natalietf
body: Nice!!
Very nitpicky one: Do you reckon we could change the name of this to `test_upload_file_upload_error` just so it matches the naming convention of other tests testing for same errors (ie `test_update_upload_error`) :)
```suggestion
@pytest.mark.parametrize(
    "status_code,error_text,expected_error_msg",
    [
        (403, "Forbidden", "Upload failed with status 403: Forbidden"),
        (500, "Server Error", "Upload failed with status 500: Server Error"),
    ],
)
def test_upload_file_upload_error(
```
created_at: 2025-05-13T13:27:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,95 @@
+from unittest.mock import MagicMock, mock_open, patch
+
+import pytest
+from uncertainty_engine_resource_client.api import ProjectRecordsApi, ResourcesApi
+
+from uncertainty_engine.api_providers.resource_provider import ResourceProvider
+
+
+@pytest.fixture
+def mock_auth_service():
+    """Fixture for a mock authentication provider."""
+    auth_service = MagicMock()
+    auth_service.account_id = "test-account-id"
+    return auth_service
+
+
+@pytest.fixture
+def mock_projects_client():
+    """Fixture for a mock projects client."""
+    return MagicMock()
+
+
+@pytest.fixture
+def mock_resources_client():
+    """Fixture for a mock resources client."""
+    return MagicMock()
+
+
+@pytest.fixture
+def patched_api_classes():
+    """Patch all API-related classes."""
+    with patch(
+        "uncertainty_engine_resource_client.api_client.ApiClient"
+    ) as patched_api_client:
+        with patch.object(
+            ProjectRecordsApi, "__init__", return_value=None
+        ) as patched_project_api:
+            with patch.object(
+                ResourcesApi, "__init__", return_value=None
+            ) as patched_resource_api:
+                yield {
+                    "api_client": patched_api_client,
+                    "project_api": patched_project_api,
+                    "resource_api": patched_resource_api,
+                }
+
+
+@pytest.fixture
+def resource_provider(
+    mock_auth_service, mock_projects_client, mock_resources_client, patched_api_classes
+):
+    """Create a ResourceProvider with mocked dependencies."""
+    provider = ResourceProvider(auth_service=mock_auth_service)
+    provider.projects_client = mock_projects_client
+    provider.resources_client = mock_resources_client
+    return provider
+
+
+@pytest.fixture
+def mock_file_content():
+    """Default mock file content."""
+    return b"test file content"
+
+
+@pytest.fixture
+def mock_file(mock_file_content):
+    """Mock the open function for file operations."""
+    return mock_open(read_data=mock_file_content)
path: tests/unit/api_providers/conftest.py
user: login: natalietf
body: Looks like the `builtins.open` patch is always used with the `mock_file` fixture - is it worth including the patch within the fixture?
created_at: 2025-05-13T13:39:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,19 +58,17 @@ def __init__(
         """
         self.email = email
         self.deployment = deployment
-        self.auth_provider = AuthProvider()
-        self.resources = ResourceProvider(
-            deployment=resource_deployment, auth_provider=self.auth_provider
-        )
+        self.auth_service = AuthService()
+        self.resources = ResourceProvider(self.auth_service, resource_deployment)
path: uncertainty_engine/client.py
user: login: jacantwell
body: In the future the Core API will also be authenticate and we will want to use the same instance of the AuthService for Resources and Core so i would be in favour of keeping it outside of the init for now.
created_at: 2025-05-13T16:16:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,61 @@
+import pytest
+
+from uncertainty_engine.auth_service import AuthService
+
+
+@pytest.fixture
+def auth_service():
+    """Fixture for an AuthService instance."""
+    return AuthService()
+
+
+def test_init(auth_service):
+    """Test that a new AuthService has no account_id set."""
+    assert auth_service.account_id is None
+    assert auth_service.is_authenticated is False
+
+
+def test_authenticate(auth_service):
+    """Test that calling authenticate sets the account_id."""
+    # Setup
+    test_account_id = "test-account-123"
+
+    # Call the method
+    auth_service.authenticate(test_account_id)
+
+    # Verify result
+    assert auth_service.account_id == test_account_id
+    assert auth_service.is_authenticated is True
+
+
+def test_authenticate_multiple_calls(auth_service):
+    """Test that authenticate can be called multiple times, updating the account_id."""
+    # Setup
+    first_account_id = "first-account"
+    second_account_id = "second-account"
+
+    # Call authenticate with first ID
+    auth_service.authenticate(first_account_id)
+    assert auth_service.account_id == first_account_id
+    assert auth_service.is_authenticated is True
+
+    # Call authenticate with second ID
+    auth_service.authenticate(second_account_id)
+    assert auth_service.account_id == second_account_id
+    assert auth_service.is_authenticated is True
path: tests/unit/test_auth_service.py
user: login: jacantwell
body: Good suggestion, the authenticate method does change a lot in the next PR so reckon I will leave it for now.
created_at: 2025-05-13T16:17:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,61 @@
+import pytest
+
+from uncertainty_engine.auth_service import AuthService
+
+
+@pytest.fixture
+def auth_service():
+    """Fixture for an AuthService instance."""
+    return AuthService()
+
+
+def test_init(auth_service):
+    """Test that a new AuthService has no account_id set."""
+    assert auth_service.account_id is None
+    assert auth_service.is_authenticated is False
+
+
+def test_authenticate(auth_service):
+    """Test that calling authenticate sets the account_id."""
+    # Setup
+    test_account_id = "test-account-123"
+
+    # Call the method
+    auth_service.authenticate(test_account_id)
+
+    # Verify result
+    assert auth_service.account_id == test_account_id
+    assert auth_service.is_authenticated is True
+
+
+def test_authenticate_multiple_calls(auth_service):
+    """Test that authenticate can be called multiple times, updating the account_id."""
+    # Setup
+    first_account_id = "first-account"
+    second_account_id = "second-account"
+
+    # Call authenticate with first ID
+    auth_service.authenticate(first_account_id)
+    assert auth_service.account_id == first_account_id
+    assert auth_service.is_authenticated is True
+
+    # Call authenticate with second ID
+    auth_service.authenticate(second_account_id)
+    assert auth_service.account_id == second_account_id
+    assert auth_service.is_authenticated is True
+
+
+def test_is_authenticated_property_when_not_authenticated(auth_service):
+    """Test is_authenticated property returns False when account_id is None."""
+    # Verify initial state
+    assert auth_service.account_id is None
+    assert auth_service.is_authenticated is False
path: tests/unit/test_auth_service.py
user: login: jacantwell
body: For now yes, this one will change when the auth stuff goes in so gonna leave it for now 
created_at: 2025-05-13T16:26:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -37,29 +40,31 @@ class ResourceProvider:
     """
 
     def __init__(
-        self,
-        deployment: str = DEFAULT_RESOURCE_DEPLOYMENT,
-        auth_provider: Optional[AuthProvider] = None,
+        self, auth_service: AuthService, deployment: str = DEFAULT_RESOURCE_DEPLOYMENT
     ):
         """
         Create an instance of a ResourceProvider
 
         Args:
             deployment: The URL of the resource service. You typically won't need
                         to change this unless instructed by support.
-            auth_provider: Handles your authentication. This is usually provided
-                           by the parent application.
+            auth_service: Handles your authentication.
         """
-        self.auth_provider = auth_provider
-        config = Configuration(host=deployment)
-        self.client = ApiClient(configuration=config)
+        super().__init__(deployment, auth_service)
+
+        # Initialize the generated API client
+        self.client = ApiClient(configuration=Configuration(host=deployment))
         self.projects_client = ProjectRecordsApi(self.client)
         self.resources_client = ResourcesApi(self.client)
 
+    def authenticate(self, account_id: str) -> None:
+        """Set the account ID"""
path: uncertainty_engine/api_providers/resource_provider.py
user: login: JDonaldM
body: Can we add args to this docstring
created_at: 2025-05-13T19:51:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -37,29 +40,31 @@ class ResourceProvider:
     """
 
     def __init__(
-        self,
-        deployment: str = DEFAULT_RESOURCE_DEPLOYMENT,
-        auth_provider: Optional[AuthProvider] = None,
+        self, auth_service: AuthService, deployment: str = DEFAULT_RESOURCE_DEPLOYMENT
     ):
         """
         Create an instance of a ResourceProvider
 
         Args:
             deployment: The URL of the resource service. You typically won't need
                         to change this unless instructed by support.
-            auth_provider: Handles your authentication. This is usually provided
-                           by the parent application.
+            auth_service: Handles your authentication.
         """
-        self.auth_provider = auth_provider
-        config = Configuration(host=deployment)
-        self.client = ApiClient(configuration=config)
+        super().__init__(deployment, auth_service)
+
+        # Initialize the generated API client
+        self.client = ApiClient(configuration=Configuration(host=deployment))
         self.projects_client = ProjectRecordsApi(self.client)
         self.resources_client = ResourcesApi(self.client)
 
+    def authenticate(self, account_id: str) -> None:
+        """Set the account ID"""
+        self.auth_service.authenticate(account_id)
+
     @property
     def account_id(self) -> Optional[str]:
         """Get the current account ID from the auth provider"""
path: uncertainty_engine/api_providers/resource_provider.py
user: login: JDonaldM
body: Can we add returns to the docstring.
Sorry I made the same comment on the feature branch. Happy for it to just go in here.
created_at: 2025-05-13T19:59:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,19 +58,17 @@ def __init__(
         """
         self.email = email
         self.deployment = deployment
-        self.auth_provider = AuthProvider()
-        self.resources = ResourceProvider(
-            deployment=resource_deployment, auth_provider=self.auth_provider
-        )
+        self.auth_service = AuthService()
+        self.resources = ResourceProvider(self.auth_service, resource_deployment)
 
-    def auth(self, account_id: str) -> None:
+    def authenticate(self, account_id: str) -> None:
         """
         Authenticate the user with the Uncertainty Engine"
 
         Args:
             account_id : The account ID to authenticate with.
         """
-        self.auth_provider.authenticate(account_id)
+        self.resources.authenticate(account_id)
path: uncertainty_engine/client.py
user: login: JDonaldM
body: Why is the auth provider now being authenticated via the resource service here? If we did `self.auth_provider.authenticate(account_id)` would the result not be the same?
created_at: 2025-05-13T20:04:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -37,29 +40,31 @@ class ResourceProvider:
     """
 
     def __init__(
-        self,
-        deployment: str = DEFAULT_RESOURCE_DEPLOYMENT,
-        auth_provider: Optional[AuthProvider] = None,
+        self, auth_service: AuthService, deployment: str = DEFAULT_RESOURCE_DEPLOYMENT
     ):
         """
         Create an instance of a ResourceProvider
 
         Args:
             deployment: The URL of the resource service. You typically won't need
                         to change this unless instructed by support.
-            auth_provider: Handles your authentication. This is usually provided
-                           by the parent application.
+            auth_service: Handles your authentication.
         """
-        self.auth_provider = auth_provider
-        config = Configuration(host=deployment)
-        self.client = ApiClient(configuration=config)
+        super().__init__(deployment, auth_service)
+
+        # Initialize the generated API client
+        self.client = ApiClient(configuration=Configuration(host=deployment))
         self.projects_client = ProjectRecordsApi(self.client)
         self.resources_client = ResourcesApi(self.client)
 
+    def authenticate(self, account_id: str) -> None:
+        """Set the account ID"""
+        self.auth_service.authenticate(account_id)
+
     @property
     def account_id(self) -> Optional[str]:
         """Get the current account ID from the auth provider"""
path: uncertainty_engine/api_providers/resource_provider.py
user: login: jacantwell
body: When a function returns None do we always want to specify in the docstring as?
```
Returns:
    None
```
created_at: 2025-05-14T08:14:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,19 +58,17 @@ def __init__(
         """
         self.email = email
         self.deployment = deployment
-        self.auth_provider = AuthProvider()
-        self.resources = ResourceProvider(
-            deployment=resource_deployment, auth_provider=self.auth_provider
-        )
+        self.auth_service = AuthService()
+        self.resources = ResourceProvider(self.auth_service, resource_deployment)
 
-    def auth(self, account_id: str) -> None:
+    def authenticate(self, account_id: str) -> None:
         """
         Authenticate the user with the Uncertainty Engine"
 
         Args:
             account_id : The account ID to authenticate with.
         """
-        self.auth_provider.authenticate(account_id)
+        self.resources.authenticate(account_id)
path: uncertainty_engine/client.py
user: login: jacantwell
body: Talked about this, will tidy up in auth PR :)
created_at: 2025-05-14T08:56:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,95 @@
+from unittest.mock import MagicMock, mock_open, patch
+
+import pytest
+from uncertainty_engine_resource_client.api import ProjectRecordsApi, ResourcesApi
+
+from uncertainty_engine.api_providers.resource_provider import ResourceProvider
+
+
+@pytest.fixture
+def mock_auth_service():
+    """Fixture for a mock authentication provider."""
+    auth_service = MagicMock()
+    auth_service.account_id = "test-account-id"
+    return auth_service
+
+
+@pytest.fixture
+def mock_projects_client():
+    """Fixture for a mock projects client."""
+    return MagicMock()
+
+
+@pytest.fixture
+def mock_resources_client():
+    """Fixture for a mock resources client."""
+    return MagicMock()
+
+
+@pytest.fixture
+def patched_api_classes():
+    """Patch all API-related classes."""
+    with patch(
+        "uncertainty_engine_resource_client.api_client.ApiClient"
+    ) as patched_api_client:
+        with patch.object(
+            ProjectRecordsApi, "__init__", return_value=None
+        ) as patched_project_api:
+            with patch.object(
+                ResourcesApi, "__init__", return_value=None
+            ) as patched_resource_api:
+                yield {
+                    "api_client": patched_api_client,
+                    "project_api": patched_project_api,
+                    "resource_api": patched_resource_api,
+                }
+
+
+@pytest.fixture
+def resource_provider(
+    mock_auth_service, mock_projects_client, mock_resources_client, patched_api_classes
+):
+    """Create a ResourceProvider with mocked dependencies."""
+    provider = ResourceProvider(auth_service=mock_auth_service)
+    provider.projects_client = mock_projects_client
+    provider.resources_client = mock_resources_client
+    return provider
+
+
+@pytest.fixture
+def mock_file_content():
+    """Default mock file content."""
+    return b"test file content"
+
+
+@pytest.fixture
+def mock_file(mock_file_content):
+    """Mock the open function for file operations."""
+    return mock_open(read_data=mock_file_content)
path: tests/unit/api_providers/conftest.py
user: login: jacantwell
body: Great spot!
created_at: 2025-05-14T09:11:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,95 @@
+from unittest.mock import MagicMock, mock_open, patch
+
+import pytest
+from uncertainty_engine_resource_client.api import ProjectRecordsApi, ResourcesApi
+
+from uncertainty_engine.api_providers.resource_provider import ResourceProvider
+
+
+@pytest.fixture
+def mock_auth_service():
+    """Fixture for a mock authentication provider."""
+    auth_service = MagicMock()
+    auth_service.account_id = "test-account-id"
+    return auth_service
+
+
+@pytest.fixture
+def mock_projects_client():
+    """Fixture for a mock projects client."""
+    return MagicMock()
+
+
+@pytest.fixture
+def mock_resources_client():
+    """Fixture for a mock resources client."""
+    return MagicMock()
+
+
+@pytest.fixture
+def patched_api_classes():
+    """Patch all API-related classes."""
+    with patch(
+        "uncertainty_engine_resource_client.api_client.ApiClient"
+    ) as patched_api_client:
+        with patch.object(
+            ProjectRecordsApi, "__init__", return_value=None
+        ) as patched_project_api:
+            with patch.object(
+                ResourcesApi, "__init__", return_value=None
+            ) as patched_resource_api:
+                yield {
+                    "api_client": patched_api_client,
+                    "project_api": patched_project_api,
+                    "resource_api": patched_resource_api,
+                }
path: tests/unit/api_providers/conftest.py
user: login: jacantwell
body: Going to have to change the tests in the auth PR. Will give monkey patching a go there :)
created_at: 2025-05-14T09:12:21Z
author_association: CONTRIBUTOR

PR comments:
user: login: natalietf
created_at: 2025-05-13T10:00:06Z
body: Just taking a look :)
author_association: CONTRIBUTOR
user: login: jacantwell
created_at: 2025-05-13T10:06:39Z
body: > Just taking a look :)

Sorry for lack of pr description. This is just rejigging the class structure in prep for the authentication stuff. Injecting the authservice into the provider will make the handling of tokens and authentication much easier.
author_association: CONTRIBUTOR