From aeb25f8e2c24dd7b7cc293e697d15b5c5b550df4 Mon Sep 17 00:00:00 2001 From: ellieayla <1447600+me@users.noreply.github.com> Date: Mon, 24 Aug 2026 09:38:11 -0400 Subject: [PATCH] List projects filtered by key=value tags. The TFE project list api supports filter[i][key/value] query parameters, documented since v1.2.x and working since at least v202507-1. Expose tag filter via new field in ProjectListOptions, and serialize it to query parameters in Projects.list(). --- src/pytfe/models/project.py | 2 ++ src/pytfe/resources/projects.py | 4 +++ tests/units/test_project.py | 46 +++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/src/pytfe/models/project.py b/src/pytfe/models/project.py index d1aa67b2..c98af8ee 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 ac973171..f0756989 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 2f4a3321..18e083cf 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"