From 2764ac1999f869791beb19570369f2a0061940ed Mon Sep 17 00:00:00 2001 From: Austin Noto-Moniz Date: Thu, 25 Sep 2025 14:34:46 -0400 Subject: [PATCH] [PNE-7426] Expose Project.archived. Allow users to view the archive status of a project. --- src/citrine/__version__.py | 2 +- src/citrine/resources/project.py | 55 ++++++++++++++++++++++++++++++-- tests/resources/test_project.py | 41 +++++++++++++++++++++++- 3 files changed, 94 insertions(+), 4 deletions(-) diff --git a/src/citrine/__version__.py b/src/citrine/__version__.py index a4dde6d81..e1609ca35 100644 --- a/src/citrine/__version__.py +++ b/src/citrine/__version__.py @@ -1 +1 @@ -__version__ = "3.24.1" +__version__ = "3.25.0" diff --git a/src/citrine/resources/project.py b/src/citrine/resources/project.py index 6dedfede6..4e9777f1f 100644 --- a/src/citrine/resources/project.py +++ b/src/citrine/resources/project.py @@ -1,5 +1,6 @@ """Resources that represent both individual and collections of projects.""" from deprecation import deprecated +from functools import partial from typing import Optional, Dict, List, Union, Iterable, Tuple, Iterator from uuid import UUID from warnings import warn @@ -77,6 +78,8 @@ class Project(Resource['Project']): """str: Status of the project.""" created_at = properties.Optional(properties.Datetime(), 'created_at') """int: Time the project was created, in seconds since epoch.""" + archived = properties.Optional(properties.Boolean, 'archived') + """bool: Whether the project is archived.""" _team_id = properties.Optional(properties.UUID, "team.id", serializable=False) def __init__(self, @@ -599,9 +602,57 @@ def register(self, name: str, *, description: Optional[str] = None) -> Project: project = Project(name, description=description) return super().register(project) + def _list_base(self, *, per_page: int = 1000, archived: Optional[bool] = None): + filters = {} + if archived is not None: + filters["archived"] = str(archived).lower() + + fetcher = partial(self._fetch_page, additional_params=filters, version=self._api_version) + return self._paginator.paginate(page_fetcher=fetcher, + collection_builder=self._build_collection_elements, + per_page=per_page) + def list(self, *, per_page: int = 1000) -> Iterator[Project]: """ - List projects using pagination. + List all projects using pagination. + + Parameters + --------- + per_page: int, optional + Max number of results to return per page. Default is 1000. This parameter + is used when making requests to the backend service. If the page parameter + is specified it limits the maximum number of elements in the response. + + Returns + ------- + Iterator[Project] + Projects in this collection. + + """ + return self._list_base(per_page=per_page) + + def list_active(self, *, per_page: int = 1000) -> Iterator[Project]: + """ + List non-archived projects using pagination. + + Parameters + --------- + per_page: int, optional + Max number of results to return per page. Default is 1000. This parameter + is used when making requests to the backend service. If the page parameter + is specified it limits the maximum number of elements in the response. + + Returns + ------- + Iterator[Project] + Projects in this collection. + + """ + return self._list_base(per_page=per_page, archived=False) + + def list_archived(self, *, per_page: int = 1000) -> Iterable[Project]: + """ + List archived projects using pagination. Parameters --------- @@ -616,7 +667,7 @@ def list(self, *, per_page: int = 1000) -> Iterator[Project]: Projects in this collection. """ - return super().list(per_page=per_page) + return self._list_base(per_page=per_page, archived=True) def search_all(self, search_params: Optional[Dict]) -> Iterable[Dict]: """ diff --git a/tests/resources/test_project.py b/tests/resources/test_project.py index cf1053ec0..0a1905d74 100644 --- a/tests/resources/test_project.py +++ b/tests/resources/test_project.py @@ -418,7 +418,46 @@ def test_list_projects(collection, session): # Then assert 1 == session.num_calls - expected_call = FakeCall(method='GET', path=f'/teams/{collection.team_id}/projects', params={'per_page': 1000, 'page': 1}) + expected_call = FakeCall(method='GET', + path=f'/teams/{collection.team_id}/projects', + params={'per_page': 1000, 'page': 1}, + version="v3") + assert expected_call == session.last_call + assert 5 == len(projects) + + +def test_list_archived_projects(collection, session): + # Given + projects_data = ProjectDataFactory.create_batch(5) + session.set_response({'projects': projects_data}) + + # When + projects = list(collection.list_archived()) + + # Then + assert 1 == session.num_calls + expected_call = FakeCall(method='GET', + path=f'/teams/{collection.team_id}/projects', + params={'per_page': 1000, 'page': 1, 'archived': "true"}, + version="v3") + assert expected_call == session.last_call + assert 5 == len(projects) + + +def test_list_active_projects(collection, session): + # Given + projects_data = ProjectDataFactory.create_batch(5) + session.set_response({'projects': projects_data}) + + # When + projects = list(collection.list_active()) + + # Then + assert 1 == session.num_calls + expected_call = FakeCall(method='GET', + path=f'/teams/{collection.team_id}/projects', + params={'per_page': 1000, 'page': 1, 'archived': "false"}, + version="v3") assert expected_call == session.last_call assert 5 == len(projects)