From ff9a3f5ca660a35994fa51256b8b804834d6f014 Mon Sep 17 00:00:00 2001 From: luke yeo <87899265+lukeskywalker22@users.noreply.github.com> Date: Thu, 14 May 2026 03:55:59 +0000 Subject: [PATCH 1/3] feat: Added list method functionality to API --- .gitignore | 3 + campus_python/api/v1/timetable.py | 18 ++++ tests/unit/test_timetable_list.py | 162 ++++++++++++++++++++++++++++++ 3 files changed, 183 insertions(+) create mode 100644 tests/unit/test_timetable_list.py diff --git a/.gitignore b/.gitignore index b37824c..f5e3533 100644 --- a/.gitignore +++ b/.gitignore @@ -254,3 +254,6 @@ Thumbs.db # Playwright auth state (contains session cookies/tokens) .playwright_auth_state.json + +# test resources +testing/ \ No newline at end of file diff --git a/campus_python/api/v1/timetable.py b/campus_python/api/v1/timetable.py index 4a28ef3..9e1a4d4 100644 --- a/campus_python/api/v1/timetable.py +++ b/campus_python/api/v1/timetable.py @@ -3,6 +3,8 @@ Campus API timetable resource (v1). """ +import typing + import campus.model from ...interface import Resource, ResourceCollection @@ -114,6 +116,22 @@ def new(self, metadata: dict, data: dict) -> dict: resp.raise_for_status() return resp.json() + def list(self, **filters: typing.Any) -> "list[campus.model.TimetableMetadata]": + """List timetables matching the provided filters. + + Args: + **filters: Arbitrary filter parameters applied to the query. + + Returns: + list[campus.model.TimetableMetadata]: Matching timetable metadata objects. + """ + resp = self.client.get(self.make_path(), query=filters if filters else None) + resp.raise_for_status() + return [ + campus.model.TimetableMetadata.from_resource(item) + for item in resp.json() + ] + class Timetable(Resource): """A single timetable with start date.""" diff --git a/tests/unit/test_timetable_list.py b/tests/unit/test_timetable_list.py new file mode 100644 index 0000000..b33fda9 --- /dev/null +++ b/tests/unit/test_timetable_list.py @@ -0,0 +1,162 @@ +import unittest +from unittest.mock import Mock, MagicMock + +from campus_python.api.v1.timetable import Timetables +from campus_python.interface import ResourceRoot +import campus.model + + +class TestTimetablesList(unittest.TestCase): + """Test Timetables.list() method.""" + + def setUp(self): + """Set up test fixtures.""" + # Create mock client + self.mock_client = Mock() + self.mock_client.base_url = "https://api.example.com" + + # Create resource root + self.root = ResourceRoot(self.mock_client) + self.root.url_prefix = "api/v1" + + # Create Timetables instance + self.timetables = Timetables(self.mock_client, root=self.root) + + def test_list_without_filters(self): + """Test list() returns timetables without filters.""" + # Mock the response - backend returns direct list + mock_response = Mock() + mock_response.json.return_value = [ + { + "id": "tt-123", + "filename": "schedule.xlsx", + "start_date": "2026-01-01T00:00:00Z", + "end_date": "2026-06-30T00:00:00Z", + "created_at": "2026-01-01T00:00:00Z" + }, + { + "id": "tt-456", + "filename": "other.xlsx", + "start_date": "2026-07-01T00:00:00Z", + "end_date": "2026-12-31T00:00:00Z", + "created_at": "2026-07-01T00:00:00Z" + } + ] + mock_response.raise_for_status = Mock() + + self.mock_client.get.return_value = mock_response + + # Call list() + result = self.timetables.list() + + # Verify the client was called correctly + self.mock_client.get.assert_called_once_with( + "/api/v1/timetable/", + query=None + ) + + # Verify results + self.assertEqual(len(result), 2) + self.assertEqual(result[0].id, "tt-123") + self.assertEqual(result[0].filename, "schedule.xlsx") + self.assertEqual(result[1].id, "tt-456") + self.assertEqual(result[1].filename, "other.xlsx") + + def test_list_with_filters(self): + """Test list() with filter parameters.""" + # Mock the response - backend returns direct list + mock_response = Mock() + mock_response.json.return_value = [ + { + "id": "tt-123", + "filename": "schedule.xlsx", + "start_date": "2026-01-01T00:00:00Z", + "end_date": "2026-06-30T00:00:00Z", + "created_at": "2026-01-01T00:00:00Z" + } + ] + mock_response.raise_for_status = Mock() + + self.mock_client.get.return_value = mock_response + + # Call list() with filters + result = self.timetables.list(filename="schedule.xlsx") + + # Verify the client was called with correct filters + self.mock_client.get.assert_called_once_with( + "/api/v1/timetable/", + query={"filename": "schedule.xlsx"} + ) + + # Verify results + self.assertEqual(len(result), 1) + self.assertEqual(result[0].filename, "schedule.xlsx") + + def test_list_empty_result(self): + """Test list() returns empty list when no timetables found.""" + # Mock the response - backend returns direct list + mock_response = Mock() + mock_response.json.return_value = [] + mock_response.raise_for_status = Mock() + + self.mock_client.get.return_value = mock_response + + # Call list() + result = self.timetables.list() + + # Verify results + self.assertEqual(len(result), 0) + + def test_list_with_multiple_filters(self): + """Test list() with multiple filter parameters.""" + # Mock the response - backend returns direct list + mock_response = Mock() + mock_response.json.return_value = [ + { + "id": "tt-789", + "filename": "test.xlsx", + "start_date": "2026-01-01T00:00:00Z", + "end_date": "2026-06-30T00:00:00Z", + "created_at": "2026-01-01T00:00:00Z" + } + ] + mock_response.raise_for_status = Mock() + + self.mock_client.get.return_value = mock_response + + # Call list() with multiple filters + result = self.timetables.list( + filename="test.xlsx", + start_date="2026-01-01T00:00:00Z" + ) + + # Verify the client was called with correct filters + self.mock_client.get.assert_called_once_with( + "/api/v1/timetable/", + query={ + "filename": "test.xlsx", + "start_date": "2026-01-01T00:00:00Z" + } + ) + + # Verify results + self.assertEqual(len(result), 1) + self.assertEqual(result[0].filename, "test.xlsx") + + def test_list_propagates_client_errors(self): + """Test list() propagates client errors.""" + # Mock response with raise_for_status that raises exception + mock_response = Mock() + mock_response.raise_for_status.side_effect = Exception("Client error") + + self.mock_client.get.return_value = mock_response + + # Call list() should raise exception + with self.assertRaises(Exception) as context: + self.timetables.list() + + self.assertEqual(str(context.exception), "Client error") + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 034e24092ce10a807fd6432bf1507130f23bcf48 Mon Sep 17 00:00:00 2001 From: luke yeo <87899265+lukeskywalker22@users.noreply.github.com> Date: Thu, 14 May 2026 04:18:18 +0000 Subject: [PATCH 2/3] Removed unnecessary list method tests --- .gitignore | 5 +- tests/unit/test_timetable_list.py | 162 ------------------------------ 2 files changed, 1 insertion(+), 166 deletions(-) delete mode 100644 tests/unit/test_timetable_list.py diff --git a/.gitignore b/.gitignore index f5e3533..740df9b 100644 --- a/.gitignore +++ b/.gitignore @@ -253,7 +253,4 @@ ehthumbs.db Thumbs.db # Playwright auth state (contains session cookies/tokens) -.playwright_auth_state.json - -# test resources -testing/ \ No newline at end of file +.playwright_auth_state.json \ No newline at end of file diff --git a/tests/unit/test_timetable_list.py b/tests/unit/test_timetable_list.py deleted file mode 100644 index b33fda9..0000000 --- a/tests/unit/test_timetable_list.py +++ /dev/null @@ -1,162 +0,0 @@ -import unittest -from unittest.mock import Mock, MagicMock - -from campus_python.api.v1.timetable import Timetables -from campus_python.interface import ResourceRoot -import campus.model - - -class TestTimetablesList(unittest.TestCase): - """Test Timetables.list() method.""" - - def setUp(self): - """Set up test fixtures.""" - # Create mock client - self.mock_client = Mock() - self.mock_client.base_url = "https://api.example.com" - - # Create resource root - self.root = ResourceRoot(self.mock_client) - self.root.url_prefix = "api/v1" - - # Create Timetables instance - self.timetables = Timetables(self.mock_client, root=self.root) - - def test_list_without_filters(self): - """Test list() returns timetables without filters.""" - # Mock the response - backend returns direct list - mock_response = Mock() - mock_response.json.return_value = [ - { - "id": "tt-123", - "filename": "schedule.xlsx", - "start_date": "2026-01-01T00:00:00Z", - "end_date": "2026-06-30T00:00:00Z", - "created_at": "2026-01-01T00:00:00Z" - }, - { - "id": "tt-456", - "filename": "other.xlsx", - "start_date": "2026-07-01T00:00:00Z", - "end_date": "2026-12-31T00:00:00Z", - "created_at": "2026-07-01T00:00:00Z" - } - ] - mock_response.raise_for_status = Mock() - - self.mock_client.get.return_value = mock_response - - # Call list() - result = self.timetables.list() - - # Verify the client was called correctly - self.mock_client.get.assert_called_once_with( - "/api/v1/timetable/", - query=None - ) - - # Verify results - self.assertEqual(len(result), 2) - self.assertEqual(result[0].id, "tt-123") - self.assertEqual(result[0].filename, "schedule.xlsx") - self.assertEqual(result[1].id, "tt-456") - self.assertEqual(result[1].filename, "other.xlsx") - - def test_list_with_filters(self): - """Test list() with filter parameters.""" - # Mock the response - backend returns direct list - mock_response = Mock() - mock_response.json.return_value = [ - { - "id": "tt-123", - "filename": "schedule.xlsx", - "start_date": "2026-01-01T00:00:00Z", - "end_date": "2026-06-30T00:00:00Z", - "created_at": "2026-01-01T00:00:00Z" - } - ] - mock_response.raise_for_status = Mock() - - self.mock_client.get.return_value = mock_response - - # Call list() with filters - result = self.timetables.list(filename="schedule.xlsx") - - # Verify the client was called with correct filters - self.mock_client.get.assert_called_once_with( - "/api/v1/timetable/", - query={"filename": "schedule.xlsx"} - ) - - # Verify results - self.assertEqual(len(result), 1) - self.assertEqual(result[0].filename, "schedule.xlsx") - - def test_list_empty_result(self): - """Test list() returns empty list when no timetables found.""" - # Mock the response - backend returns direct list - mock_response = Mock() - mock_response.json.return_value = [] - mock_response.raise_for_status = Mock() - - self.mock_client.get.return_value = mock_response - - # Call list() - result = self.timetables.list() - - # Verify results - self.assertEqual(len(result), 0) - - def test_list_with_multiple_filters(self): - """Test list() with multiple filter parameters.""" - # Mock the response - backend returns direct list - mock_response = Mock() - mock_response.json.return_value = [ - { - "id": "tt-789", - "filename": "test.xlsx", - "start_date": "2026-01-01T00:00:00Z", - "end_date": "2026-06-30T00:00:00Z", - "created_at": "2026-01-01T00:00:00Z" - } - ] - mock_response.raise_for_status = Mock() - - self.mock_client.get.return_value = mock_response - - # Call list() with multiple filters - result = self.timetables.list( - filename="test.xlsx", - start_date="2026-01-01T00:00:00Z" - ) - - # Verify the client was called with correct filters - self.mock_client.get.assert_called_once_with( - "/api/v1/timetable/", - query={ - "filename": "test.xlsx", - "start_date": "2026-01-01T00:00:00Z" - } - ) - - # Verify results - self.assertEqual(len(result), 1) - self.assertEqual(result[0].filename, "test.xlsx") - - def test_list_propagates_client_errors(self): - """Test list() propagates client errors.""" - # Mock response with raise_for_status that raises exception - mock_response = Mock() - mock_response.raise_for_status.side_effect = Exception("Client error") - - self.mock_client.get.return_value = mock_response - - # Call list() should raise exception - with self.assertRaises(Exception) as context: - self.timetables.list() - - self.assertEqual(str(context.exception), "Client error") - - -if __name__ == "__main__": - unittest.main() \ No newline at end of file From d5afb5724dcb5dddfc74097d9eef3d8be88aa276 Mon Sep 17 00:00:00 2001 From: luke yeo <87899265+lukeskywalker22@users.noreply.github.com> Date: Thu, 14 May 2026 04:19:31 +0000 Subject: [PATCH 3/3] Bumped version number from 0.1.59 to 0.1.60 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8cc5ba1..00888d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "campus-api-python" -version = "0.1.59" +version = "0.1.60" description = "Campus API for Python projects" authors = ["NYJC Computing "]