diff --git a/src/pytfe/models/project.py b/src/pytfe/models/project.py index d1aa67b..c98af8e 100644 --- a/src/pytfe/models/project.py +++ b/src/pytfe/models/project.py @@ -48,6 +48,8 @@ class ProjectListOptions(BaseModel): # Optional: String used to filter results by complete project name name: str | None = None + # Optional: Tags used to filter results by key=value pairs + tags: dict[str, str] | None = None # Optional: Query string to search projects by names query: str | None = None # Optional: Include related resources diff --git a/src/pytfe/resources/projects.py b/src/pytfe/resources/projects.py index ac97317..f075698 100644 --- a/src/pytfe/resources/projects.py +++ b/src/pytfe/resources/projects.py @@ -160,6 +160,10 @@ def list( params["q"] = options.query if options.name: params["filter[names]"] = options.name + if options.tags: + for i, (tag_name, tag_value) in enumerate(options.tags.items()): + params[f"filter[tagged][{i}][key]"] = _safe_str(tag_name) + params[f"filter[tagged][{i}][value]"] = _safe_str(tag_value) if options.page_size: params["page[size]"] = options.page_size diff --git a/tests/units/test_project.py b/tests/units/test_project.py index 2f4a332..18e083c 100644 --- a/tests/units/test_project.py +++ b/tests/units/test_project.py @@ -10,6 +10,7 @@ Project, ProjectAddTagBindingsOptions, ProjectCreateOptions, + ProjectListOptions, ProjectUpdateOptions, TagBinding, ) @@ -109,6 +110,51 @@ def test_list_projects_success(self): expected_path = f"/api/v2/organizations/{organization}/projects" self.projects_service._list.assert_called_once_with(expected_path) + def test_list_projects_by_tags_query_params_serialized_success(self): + """Test successful serialization of project list tag filter into query parameters.""" + organization = "test-org" + expected_path = f"/api/v2/organizations/{organization}/projects" + + # Mock the _list method so we can introspect the query parameters + self.projects_service._list = Mock(return_value=[]) + + # Call the method under test + list( + self.projects_service.list( + organization, options=ProjectListOptions(tags={"department": "five"}) + ) + ) + + self.projects_service._list.assert_called_once_with( + expected_path, + params={ + "filter[tagged][0][key]": "department", + "filter[tagged][0][value]": "five", + }, + ) + + def test_list_projects_by_name_query_params_serialized_success(self): + """Test successful serialization of project list name filter into query parameters.""" + organization = "test-org" + expected_path = f"/api/v2/organizations/{organization}/projects" + + # Mock the _list method so we can introspect the query parameters + self.projects_service._list = Mock(return_value=[]) + + # Call the method under test + list( + self.projects_service.list( + organization, options=ProjectListOptions(name="prj-456") + ) + ) + + self.projects_service._list.assert_called_once_with( + expected_path, + params={ + "filter[names]": "prj-456", + }, + ) + def test_create_project_success(self): """Test successful project creation""" organization = "test-org"