diff --git a/src/citrine/__version__.py b/src/citrine/__version__.py index c4b4b2a36..425110fb6 100644 --- a/src/citrine/__version__.py +++ b/src/citrine/__version__.py @@ -1 +1 @@ -__version__ = "3.11.4" +__version__ = "3.11.5" diff --git a/src/citrine/_session.py b/src/citrine/_session.py index c403a574b..e7033cf35 100644 --- a/src/citrine/_session.py +++ b/src/citrine/_session.py @@ -151,8 +151,6 @@ def checked_request(self, method: str, path: str, logger.debug('\tmethod: {}'.format(method)) logger.debug('\tpath: {}'.format(path)) logger.debug('\tversion: {}'.format(version)) - for k, v in kwargs.items(): - logger.debug(f'\t{k}: {v}') if self._is_access_token_expired(): self._refresh_access_token() diff --git a/src/citrine/resources/project.py b/src/citrine/resources/project.py index 08108aa33..01401dc36 100644 --- a/src/citrine/resources/project.py +++ b/src/citrine/resources/project.py @@ -1,9 +1,9 @@ """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 +from deprecation import deprecated from gemd.entity.base_entity import BaseEntity from gemd.entity.link_by_uid import LinkByUID @@ -46,6 +46,7 @@ from citrine.resources.project_member import ProjectMember from citrine.resources.response import Response from citrine.resources.table_config import TableConfigCollection +from warnings import warn class Project(Resource['Project']): @@ -518,16 +519,14 @@ class ProjectCollection(Collection[Project]): """ - @property - def _path_template(self): - if self.team_id is None: - return '/projects' - else: - return '/teams/{team_id}/projects' + _path_template = '/projects' _individual_key = 'project' _collection_key = 'projects' _resource = Project - _api_version = 'v3' + + @property + def _api_version(self): + return 'v3' def __init__(self, session: Session, *, team_id: Optional[UUID] = None): self.session = session @@ -554,22 +553,6 @@ def build(self, data) -> Project: project.team_id = self.team_id return project - def get(self, uid: Union[UUID, str]) -> Project: - """ - Get a particular project. - - Parameters - ---------- - uid: UUID or str - The uid of the project to get. - - """ - # Only the team-agnostic project get is implemented - if self.team_id is None: - return super().get(uid) - else: - return ProjectCollection(session=self.session).get(uid) - def register(self, name: str, *, description: Optional[str] = None) -> Project: """ Create and upload new project. @@ -581,19 +564,15 @@ def register(self, name: str, *, description: Optional[str] = None) -> Project: description: str Long-form description of the project to be created. - Return - ------- - Project - The newly registered project. - """ if self.team_id is None: raise NotImplementedError("Cannot register a project without a team ID. " "Use team.projects.register.") + path = format_escaped_url('teams/{team_id}/projects', team_id=self.team_id) project = Project(name, description=description) try: - data = self.session.post_resource(self._get_path(), project.dump()) + data = self.session.post_resource(path, project.dump(), version=self._api_version) data = data[self._individual_key] return self.build(data) except NonRetryableException as e: @@ -616,7 +595,15 @@ def list(self, *, per_page: int = 1000) -> Iterator[Project]: Projects in this collection. """ - return super().list(per_page=per_page) + if self.team_id is None: + path = '/projects' + else: + path = format_escaped_url('/teams/{team_id}/projects', team_id=self.team_id) + + fetcher = partial(self._fetch_page, path=path) + return self._paginator.paginate(page_fetcher=fetcher, + collection_builder=self._build_collection_elements, + per_page=per_page) def search_all(self, search_params: Optional[Dict]) -> Iterable[Dict]: """ @@ -660,13 +647,15 @@ def search_all(self, search_params: Optional[Dict]) -> Iterable[Dict]: """ collections = [] + path = self._get_path(action="search") query_params = {'userId': ""} json = {} if search_params is None else {'search_params': search_params} - data = self.session.post_resource(self._get_path(action="search"), + data = self.session.post_resource(path, params=query_params, - json=json) + json=json, + version=self._api_version) if self._collection_key is not None: collections = data[self._collection_key] @@ -741,11 +730,7 @@ def delete(self, uid: Union[UUID, str]) -> Response: If the project is not empty, then the Response will contain a list of all of the project's resources. These must be deleted before the project can be deleted. """ - # Only the team-agnostic project get is implemented - if self.team_id is None: - return super().delete(uid) - else: - return ProjectCollection(session=self.session).delete(uid) + return super().delete(uid) def update(self, model: Project) -> Project: """Projects cannot be updated.""" diff --git a/tests/resources/test_project.py b/tests/resources/test_project.py index 28a869860..caae9496b 100644 --- a/tests/resources/test_project.py +++ b/tests/resources/test_project.py @@ -353,6 +353,36 @@ def test_failed_register_no_team(session): project_collection.register("Project") +def test_project_registration(collection: ProjectCollection, session): + # Given + create_time = parse('2019-09-10T00:00:00+00:00') + project_data = ProjectDataFactory( + name='testing', + description='A sample project', + created_at=int(create_time.timestamp() * 1000) # The lib expects ms since epoch, which is really odd + ) + session.set_response({'project': project_data}) + + # When + with pytest.warns(DeprecationWarning): + created_project = collection.register('testing') + + # Then + assert 1 == session.num_calls + expected_call = FakeCall( + method='POST', + path='/projects', + json={ + 'name': 'testing' + } + ) + assert expected_call == session.last_call + + assert 'A sample project' == created_project.description + assert 'CREATED' == created_project.status + assert create_time == created_project.created_at + + def test_project_registration(collection: ProjectCollection, session): # Given create_time = parse('2019-09-10T00:00:00+00:00') @@ -424,7 +454,7 @@ def test_list_no_team(session): projects = list(project_collection.list()) assert 1 == session.num_calls - expected_call = FakeCall(method='GET', path='/projects', params={'per_page': 1000, 'page': 1}) + expected_call = FakeCall(method='GET', path=f'/projects', params={'per_page': 1000, 'page': 1}) assert expected_call == session.last_call assert 5 == len(projects) @@ -442,27 +472,6 @@ def test_list_projects_with_page_params(collection, session): expected_call = FakeCall(method='GET', path=f'/teams/{collection.team_id}/projects', params={'per_page': 10, 'page': 1}) assert expected_call == session.last_call -def test_search_all_no_team(session): - project_collection = ProjectCollection(session=session) - projects_data = ProjectDataFactory.create_batch(2) - project_name_to_match = projects_data[0]['name'] - - search_params = { - 'name': { - 'value': project_name_to_match, - 'search_method': 'EXACT'}} - expected_response = [p for p in projects_data if p["name"] == project_name_to_match] - - project_collection.session.set_response({'projects': expected_response}) - - # Then - results = list(project_collection.search_all(search_params=search_params)) - - expected_call = FakeCall(method='POST', path='/projects/search', params={'userId': ''}, json={'search_params': search_params}) - - assert 1 == project_collection.session.num_calls - assert expected_call == project_collection.session.last_call - assert 1 == len(results) def test_search_all(collection: ProjectCollection): # Given @@ -481,7 +490,7 @@ def test_search_all(collection: ProjectCollection): results = list(collection.search_all(search_params=search_params)) expected_call = FakeCall(method='POST', - path=f'/teams/{collection.team_id}/projects/search', + path='/projects/search', params={'userId': ''}, json={'search_params': { 'name': { @@ -504,7 +513,7 @@ def test_search_all_no_search_params(collection: ProjectCollection): result = list(collection.search_all(search_params=None)) expected_call = FakeCall(method='POST', - path=f'/teams/{collection.team_id}/projects/search', + path='/projects/search', params={'userId': ''}, json={}) @@ -530,7 +539,7 @@ def test_search_projects(collection: ProjectCollection): result = list(collection.search(search_params=search_params)) expected_call = FakeCall(method='POST', - path=f'/teams/{collection.team_id}/projects/search', + path='/projects/search', params={'userId': ''}, json={'search_params': { 'name': { @@ -552,7 +561,7 @@ def test_search_projects_no_search_params(collection: ProjectCollection): # Then result = list(collection.search()) - expected_call = FakeCall(method='POST', path=f'/teams/{collection.team_id}/projects/search', params={'userId': ''}, json={}) + expected_call = FakeCall(method='POST', path='/projects/search', params={'userId': ''}, json={}) assert 1 == collection.session.num_calls assert expected_call == collection.session.last_call @@ -568,7 +577,7 @@ def test_delete_project(collection, session): # Then assert 1 == session.num_calls - expected_call = FakeCall(method='DELETE', path=f'/projects/{uid}') + expected_call = FakeCall(method='DELETE', path='/projects/{}'.format(uid)) assert expected_call == session.last_call @@ -598,8 +607,11 @@ def test_list_members(project, session): # Then assert 2 == session.num_calls - expect_call_1 = FakeCall(method='GET', path=f'/teams/{team_data["id"]}') - expect_call_2 = FakeCall(method='GET', path=f'/teams/{project.team_id}/users') + expect_call_1 = FakeCall( + method='GET', + path='/teams/{}'.format(team_data['id']), + ) + expect_call_2 = FakeCall(method='GET', path='/teams/{}/users'.format(project.team_id)) assert expect_call_1 == session.calls[0] assert expect_call_2 == session.calls[1] assert isinstance(members[0], TeamMember) diff --git a/tests/utils/session.py b/tests/utils/session.py index 0a6a7b97d..c74d7b278 100644 --- a/tests/utils/session.py +++ b/tests/utils/session.py @@ -36,13 +36,11 @@ def __eq__(self, other) -> bool: if not isinstance(other, FakeCall): return NotImplemented - return ( - self.method == other.method and - self.path.lstrip('/') == other.path.lstrip('/') and # Leading slashes don't affect results - self.json == other.json and - self.params == other.params and - (not self.version or not other.version or self.version == other.version) # Allows users to check the URL version without forcing everyone to. - ) + return self.method == other.method and \ + self.path == other.path and \ + self.json == other.json and \ + self.params == other.params and \ + (not self.version or not other.version or self.version == other.version) # Allows users to check the URL version without forcing everyone to. class FakeSession(Session):