Skip to content

Commit 1603894

Browse files
authored
Merge pull request #772 from CitrineInformatics/PLA-9759
PLA-9759 - Changing how to get files using filters
2 parents e572f32 + 85f9a85 commit 1603894

File tree

4 files changed

+200
-77
lines changed

4 files changed

+200
-77
lines changed

src/citrine/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '1.36.0'
1+
__version__ = '1.36.1'

src/citrine/resources/file_link.py

Lines changed: 166 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,35 @@
2121
from citrine._session import Session
2222
from citrine._utils.functions import rewrite_s3_links_locally
2323
from citrine._utils.functions import write_file_locally, format_escaped_url
24-
from citrine.exceptions import NotFound
24+
2525
from citrine.jobs.job import JobSubmissionResponse, _poll_for_job_completion
2626
from citrine.resources.response import Response
2727
from gemd.entity.bounds.base_bounds import BaseBounds
2828
from gemd.entity.file_link import FileLink as GEMDFileLink
29+
from gemd.enumeration.base_enumeration import BaseEnumeration
2930

3031
logger = getLogger(__name__)
3132

3233

34+
class SearchFileFilterTypeEnum(BaseEnumeration):
35+
"""
36+
The type of the filter used to search for files.
37+
38+
* SEARCH_BY_NAME:
39+
Search a file by name in a specific dataset,
40+
returns by default the last version or a specific one
41+
* SEARCH_BY_VERSION_ID:
42+
Search by a specific file version id
43+
* SEARCH_BY_DATASET_FILE_ID:
44+
Search either the last version or a specific version number for a specific dataset file id
45+
46+
"""
47+
48+
NAME_SEARCH = "search_by_name"
49+
VERSION_ID_SEARCH = "search_by_version_id"
50+
DATASET_FILE_ID_SEARCH = "search_by_dataset_file_id"
51+
52+
3353
class _Uploader:
3454
"""Holds the many parameters that are generated and used during file upload."""
3555

@@ -199,7 +219,7 @@ def _get_path_from_file_link(self, file_link: FileLink,
199219
# Use this sessions project/dataset credentials and the URL's file / version
200220
file_id, version_id = self._get_ids_from_url(file_link.url)
201221
if file_id is None:
202-
raise ValueError(f"FileLink did not contain a Citrine platform file URL.")
222+
raise ValueError("FileLink did not contain a Citrine platform file URL.")
203223
return self._get_path(uid=file_id, version=version_id, action=action)
204224

205225
def build(self, data: dict) -> FileLink:
@@ -263,9 +283,8 @@ def _as_dict_from_resource(self, file: dict):
263283
# FIXME While the 'id' field is supposed to be the file ID, it contains the version
264284
# for some reason. Needs to be fixed on back end. PLA-9482
265285
filename = file['filename']
266-
# file_id = file['id']
267-
# version_id = file['version']
268-
file_id, version_id = self._get_ids_from_url(file['versioned_url'])
286+
file_id = file['id']
287+
version_id = file['version']
269288

270289
file_dict = {
271290
'url': self._get_path(uid=file_id, version=version_id),
@@ -303,59 +322,40 @@ def get(self,
303322
raise TypeError(f"Version can only be resolved from str, int or UUID."
304323
f"Instead got {type(uid)} {uid}.")
305324

306-
try: # Check if the uid string is actually a UUID
307-
if isinstance(uid, str):
325+
if isinstance(uid, str):
326+
try: # Check if the uid string is actually a UUID
308327
uid = UUID(uid)
309-
except ValueError:
310-
pass
311-
312-
try: # Check if the version string is actually a UUID
313-
if isinstance(version, str):
314-
version = UUID(version)
315-
except ValueError:
316-
pass
317-
318-
try: # Check if the version string is actually an int / version number
319-
if isinstance(version, str):
320-
version = int(version)
321-
except ValueError:
322-
pass
328+
except ValueError:
329+
pass
323330

324331
if isinstance(version, str):
325-
raise ValueError(
326-
f"Version {version} could not be converted to either an int or a UUID"
327-
)
332+
try: # Check if the version string is actually a UUID
333+
version = UUID(version)
334+
except ValueError:
335+
try: # Check if the version string is actually an int / version number
336+
version = int(version)
337+
except ValueError:
338+
raise ValueError(
339+
f"Version {version} could not be converted to either an int or a UUID"
340+
)
328341

329342
if isinstance(uid, str):
330-
# Assume it's the filename on platform; resolve to UUID
331-
match = next((f for f in self.list() if uid == f.filename), None)
332-
if match is None:
333-
raise NotFound(f"Found no file named {uid}")
334-
match_file, match_version = self._get_ids_from_url(match.url)
335-
if version is None or version == match_version:
336-
# Done; the list endpoint always returns the most recent version
337-
return match
338-
339-
# Stash the now-resolved UUID to match with a version
340-
uid = match_file
341-
342-
if isinstance(version, UUID):
343-
# Can only return 0 or 1 result
344-
path = self._get_path(uid=uid, version=version)
345-
data = self.session.get_resource(path, version=self._api_version)
346-
return self.build(self._as_dict_from_resource(data))
347-
348-
# version is an int / version number
349-
path = self._get_path(uid=uid)
350-
data = self.session.get_resource(path, version=self._api_version)['files']
351-
if version is None:
352-
recent = max(data, key=lambda x: x['version_number'])
353-
return self.build(self._as_dict_from_resource(recent))
354-
for result in data:
355-
if result['version_number'] == version:
356-
return self.build(self._as_dict_from_resource(result))
357-
358-
raise NotFound(f"Found file, but no version {version}")
343+
# Assume it's the filename on platform;
344+
if version is None or isinstance(version, int):
345+
file = self._search_by_file_version_id(file_version_id=version)
346+
else: # We did our type checks earlier; version is an int or None
347+
file = self._search_by_file_name(dset_id=self.dataset_id,
348+
file_name=uid,
349+
file_version_number=version)
350+
else: # We did our type checks earlier; uid is a UUID
351+
if isinstance(version, UUID):
352+
file = self._search_by_file_version_id(file_version_id=version)
353+
else: # We did our type checks earlier; version is an int or None
354+
file = self._search_by_dataset_file_id(dataset_file_id=uid,
355+
dset_id=self.dataset_id,
356+
file_version_number=version)
357+
358+
return file
359359

360360
def upload(self, *, file_path: Union[str, Path], dest_name: str = None) -> FileLink:
361361
"""
@@ -447,6 +447,119 @@ def _make_upload_request(self, file_path: Path, dest_name: str):
447447
"{}".format(upload_request))
448448
return uploader
449449

450+
def _search_by_file_name(self,
451+
file_name: str,
452+
dset_id: UUID,
453+
file_version_number: Optional[int] = None
454+
) -> Optional[FileLink]:
455+
"""
456+
Make a request to the backend to search a file by name.
457+
458+
Note that you can specify a version number, in case you don't, it will
459+
return the last version by default.
460+
461+
Parameters
462+
----------
463+
file_name: str
464+
The name of the file.
465+
dset_id: UUID
466+
UUID that represents a dataset.
467+
file_version_number: Optional[int]
468+
As optional, you can send a specific version number.
469+
470+
Returns
471+
-------
472+
FileLink
473+
All the data needed for a file.
474+
475+
"""
476+
path = self._get_path() + "/search"
477+
478+
search_json = {
479+
'fileSearchFilter':
480+
{
481+
'type': SearchFileFilterTypeEnum.NAME_SEARCH.value,
482+
'datasetId': str(dset_id),
483+
'fileName': file_name,
484+
'fileVersionNumber': file_version_number
485+
}
486+
}
487+
488+
data = self.session.post_resource(path=path, json=search_json)
489+
490+
return self.build(self._as_dict_from_resource(data['files'][0]))
491+
492+
def _search_by_file_version_id(self,
493+
file_version_id: UUID
494+
) -> Optional[FileLink]:
495+
"""
496+
Make a request to the backend to search a file by file version id.
497+
498+
Parameters
499+
----------
500+
file_version_id: UUID
501+
UUID that represents a file version id.
502+
503+
Returns
504+
-------
505+
FileLink
506+
All the data needed for a file.
507+
508+
"""
509+
path = self._get_path() + "/search"
510+
511+
search_json = {
512+
'fileSearchFilter': {
513+
'type': SearchFileFilterTypeEnum.VERSION_ID_SEARCH.value,
514+
'fileVersionUuid': str(file_version_id)
515+
}
516+
}
517+
518+
data = self.session.post_resource(path=path, json=search_json)
519+
520+
return self.build(self._as_dict_from_resource(data['files'][0]))
521+
522+
def _search_by_dataset_file_id(self,
523+
dataset_file_id: UUID,
524+
dset_id: UUID,
525+
file_version_number: Optional[int] = None
526+
) -> Optional[FileLink]:
527+
"""
528+
Make a request to the backend to search a file by dataset file id.
529+
530+
Note that you can specify a version number, in case you don't, it will
531+
return the last version by default.
532+
533+
Parameters
534+
----------
535+
dataset_file_id: UUID
536+
UUID that represents a dataset file id.
537+
dset_id: UUID
538+
UUID that represents a dataset.
539+
file_version_number: Optional[int]
540+
As optional, you can send a specific version number
541+
542+
Returns
543+
-------
544+
FileLink
545+
All the data needed for a file.
546+
547+
"""
548+
path = self._get_path() + "/search"
549+
550+
search_json = {
551+
'fileSearchFilter': {
552+
'type': SearchFileFilterTypeEnum.DATASET_FILE_ID_SEARCH.value,
553+
'datasetId': str(dset_id),
554+
'datasetFileId': str(dataset_file_id),
555+
'fileVersionNumber': file_version_number
556+
}
557+
}
558+
559+
data = self.session.post_resource(path=path, json=search_json)
560+
561+
return self.build(self._as_dict_from_resource(data['files'][0]))
562+
450563
@staticmethod
451564
def _mime_type(file_path: Path):
452565
# This string coercion is for supporting pathlib.Path objects in python 3.6

src/citrine/resources/project.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -505,7 +505,7 @@ def owned_table_config_ids(self) -> List[str]:
505505
return result["table_definition_ids"]
506506

507507
@use_teams("team.list_members", deprecated=True)
508-
def list_members(self) -> Union[List[ProjectMember], List["TeamMember"]]:
508+
def list_members(self) -> Union[List[ProjectMember], List["TeamMember"]]: # noqa: F821
509509
"""
510510
List all of the members in the current project.
511511

tests/resources/test_file_link.py

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
from uuid import uuid4, UUID
66

77
import requests_mock
8+
9+
from citrine.resources.api_error import ValidationError
810
from citrine.resources.file_link import FileCollection, FileLink, _Uploader, \
911
FileProcessingType
1012
from citrine.exceptions import NotFound
1113

1214
from tests.utils.factories import FileLinkDataFactory, _UploaderFactory
13-
from tests.utils.session import FakeSession, FakeS3Client, FakeCall
15+
from tests.utils.session import FakeSession, FakeS3Client, FakeCall, FakeRequestResponseApiError
1416

1517

1618
@pytest.fixture
@@ -560,16 +562,14 @@ def test_resolve_file_link(collection: FileCollection, session):
560562
assert session.num_calls == 0, "No-op still hit server"
561563

562564
session.set_response({
563-
'files': file1_versions
565+
'files': [raw_files[1]]
564566
})
565-
566567
assert collection._resolve_file_link(UUID(raw_files[1]['id'])) == file1, "UUID didn't resolve"
567568
assert session.num_calls == 1
568569

569570
session.set_response({
570-
'files': raw_files
571+
'files': [raw_files[1]]
571572
})
572-
573573
assert collection._resolve_file_link(raw_files[1]['id']) == file1, "String UUID didn't resolve"
574574
assert session.num_calls == 2
575575

@@ -580,13 +580,15 @@ def test_resolve_file_link(collection: FileCollection, session):
580580
assert collection._resolve_file_link(abs_link).filename == "web.pdf"
581581
assert collection._resolve_file_link(abs_link).url == abs_link
582582

583-
session.set_response(raw_files[1])
584-
583+
session.set_response({
584+
'files': [raw_files[1]]
585+
})
585586
assert collection._resolve_file_link(file1.url) == file1, "Relative path didn't resolve"
586587
assert session.num_calls == 4
587588

588-
session.set_response({"files": raw_files})
589-
589+
session.set_response({
590+
'files': [raw_files[1]]
591+
})
590592
assert collection._resolve_file_link(file1.filename) == file1, "Filename didn't resolve"
591593
assert session.num_calls == 5
592594

@@ -674,25 +676,32 @@ def test_get(collection: FileCollection, session):
674676
for f1 in file1_versions:
675677
f1['unversioned_url'] = f"http://test.domain.net:8002/api/v1/files/{f1['id']}"
676678
f1['versioned_url'] = f"http://test.domain.net:8002/api/v1/files/{f1['id']}/versions/{f1['version']}"
679+
file0 = FileLink.build(collection._as_dict_from_resource(raw_files[0]))
677680
file1 = FileLink.build(collection._as_dict_from_resource(raw_files[1]))
678681

679-
session.set_response(raw_files[1])
682+
session.set_response({
683+
'files': [raw_files[1]]
684+
})
680685
assert collection.get(uid=raw_files[1]['id'], version=raw_files[1]['version']) == file1
681686

682687
session.set_response({
683-
'files': file1_versions
688+
'files': [raw_files[0]]
684689
})
685-
assert collection.get(uid=raw_files[1]['id'], version=raw_files[1]['version_number']) == file1
690+
assert collection.get(uid=raw_files[0]['id'], version=raw_files[0]['version_number']) == file0
686691

687-
session.set_responses(
688-
{'files': raw_files},
689-
{'files': file1_versions}
690-
)
692+
session.set_response({
693+
'files': [raw_files[1]]
694+
})
691695
assert collection.get(uid=raw_files[1]['filename'], version=raw_files[1]['version_number']) == file1
692696

693-
session.set_responses(
694-
{'files': raw_files},
695-
{'files': file1_versions}
697+
session.set_response({
698+
'files': [raw_files[1]]
699+
})
700+
assert collection.get(uid=raw_files[1]['filename'], version=raw_files[1]['version']) == file1
701+
702+
validation_error = ValidationError(failure_message="file not found", failure_id="failure_id")
703+
session.set_response(
704+
NotFound("path", FakeRequestResponseApiError(400, "Not found", [validation_error]))
696705
)
697706
with pytest.raises(NotFound):
698707
collection.get(uid=raw_files[1]['filename'], version=4)
@@ -712,8 +721,9 @@ def test_exceptions(collection: FileCollection, session):
712721
with pytest.raises(ValueError):
713722
collection.get(uid=uuid4(), version="Words!")
714723

715-
session.set_response({
716-
'files': []
717-
})
724+
validation_error = ValidationError(failure_message="file not found", failure_id="failure_id")
725+
session.set_response(
726+
NotFound("path", FakeRequestResponseApiError(400, "Not found", [validation_error]))
727+
)
718728
with pytest.raises(NotFound):
719729
collection.get(uid="name")

0 commit comments

Comments
 (0)