Skip to content

Commit f5c7d98

Browse files
author
Goran Vlaovic
committed
Merge branch 'feature/files-search' into 'develop'
File search supported See merge request core/sevenbridges-python!112
2 parents 0310e36 + 4e30d13 commit f5c7d98

5 files changed

Lines changed: 191 additions & 12 deletions

File tree

docs/quickstart.rst

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -907,6 +907,36 @@ There are certain restrictions on using this parameter:
907907
3. The continuation token pagination and offset pagination are mutually exclusive, so if there are passed both
908908
:code:`cont_token` and :code:`offset` parameters to :code:`query()`/:code:`list_files()`, :code:`SbgError` will be returned.
909909

910+
Search Files using SBG query language
911+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
912+
913+
Files can be searched based on a query criterion written in a special query language. The query syntax is explained in
914+
`the documentation <https://docs.sevenbridges.com/reference/query-syntax>`_. This can be achieved using :code:`search()` method:
915+
916+
.. code:: python
917+
918+
search_response = api.files.search(query='IN "user/example-project" WHERE type = "FILE"')
919+
920+
search_response.count # Gets the number of returned files/folders (files in this concrete example)
921+
search_response.cont_token # Gets continuation token that is used to fetch the next page of data
922+
search_response.result_set # Gets the list of resulting files/folders
923+
924+
Pagination parameters
925+
^^^^^^^^^^^^^^^^^^^^^
926+
927+
Token-based pagination can be achieved in one of the ways:
928+
929+
1. By using :code:`cont_token` and :code:`limit` parameters in :code:`search()` method:
930+
931+
.. code:: python
932+
933+
search_response = api.files.search(query='IN "user/example-project" WHERE type = "FILE"', limit=100, cont_token=start)
934+
935+
2. With :code:`TOKEN` and :code:`LIMIT` parameters in the provided query:
936+
937+
.. code:: python
938+
939+
search_response = api.files.search(query='IN "user/example-project" WHERE type = "FILE" LIMIT 100 TOKEN start')
910940
911941
Managing file upload and download
912942
---------------------------------

sevenbridges/models/file.py

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import os
21
import logging
2+
import os
33
import tempfile
44

5+
from sevenbridges.decorators import inplace_reload
56
from sevenbridges.errors import (
67
SbgError,
78
ResourceNotModified,
@@ -12,16 +13,15 @@
1213
BasicListField
1314
)
1415
from sevenbridges.meta.resource import Resource
15-
from sevenbridges.models.bulk import BulkRecord
16-
from sevenbridges.transfer.upload import Upload
17-
from sevenbridges.decorators import inplace_reload
18-
from sevenbridges.transfer.download import Download
1916
from sevenbridges.meta.transformer import Transform
17+
from sevenbridges.models.bulk import BulkRecord
18+
from sevenbridges.models.compound.files.download_info import DownloadInfo
19+
from sevenbridges.models.compound.files.file_origin import FileOrigin
20+
from sevenbridges.models.compound.files.file_storage import FileStorage
2021
from sevenbridges.models.compound.files.metadata import Metadata
2122
from sevenbridges.models.enums import PartSize, RequestParameters
22-
from sevenbridges.models.compound.files.file_storage import FileStorage
23-
from sevenbridges.models.compound.files.file_origin import FileOrigin
24-
from sevenbridges.models.compound.files.download_info import DownloadInfo
23+
from sevenbridges.transfer.download import Download
24+
from sevenbridges.transfer.upload import Upload
2525

2626
logger = logging.getLogger(__name__)
2727

@@ -52,6 +52,8 @@ class File(Resource):
5252
'scroll_folder': '/files/{id}/scroll',
5353
'copy_to_folder': '/files/{file_id}/actions/copy',
5454
'move_to_folder': '/files/{file_id}/actions/move',
55+
56+
'search': '/files/search',
5557
}
5658

5759
href = HrefField(read_only=True)
@@ -612,9 +614,45 @@ def move_to_folder(self, parent, name=None, api=None):
612614
).json()
613615
return File(api=api, **response)
614616

617+
@classmethod
618+
def search(cls, query, cont_token=None, limit=None, api=None):
619+
"""
620+
Search files by a query.
621+
:param query: Query written in SBG query language.
622+
:param cont_token: Continuation token value.
623+
:param limit: Limit value.
624+
:param api: Api instance.
625+
"""
626+
627+
if not query:
628+
raise SbgError('Query must be provided.')
629+
630+
if limit is not None and limit <= 0:
631+
raise SbgError('Limit must be greater than zero.')
632+
633+
api = api or cls._API
634+
635+
data = {'query': query}
636+
params = {
637+
'cont_token': cont_token,
638+
'limit': limit
639+
}
640+
641+
response = api.post(url=cls._URL['search'],
642+
data=data,
643+
params=params).json()
644+
645+
return SearchResponse(**response)
646+
615647

616648
class FileBulkRecord(BulkRecord):
617649
resource = CompoundField(cls=File, read_only=False)
618650

619651
def __str__(self):
620652
return f'<FileBulkRecord valid={self.valid}>'
653+
654+
655+
class SearchResponse(Resource):
656+
count = IntegerField(read_only=True)
657+
cont_token = StringField(read_only=True)
658+
result_set = BasicListField(read_only=True)

tests/providers.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ def paginated_projects(self, limit, num_of_projects):
163163

164164
if i > limit:
165165
prev_url = (
166-
f'/projects/?offset={i - limit}&limit={limit}&fields=_all'
166+
f'/projects/?offset={i - limit}&limit={limit}&fields=_all'
167167
)
168168
prev = {
169169
'method': 'GET',
@@ -286,6 +286,20 @@ def download_info():
286286
'url': generator.url()
287287
}
288288

289+
@staticmethod
290+
def default_search_file():
291+
return {
292+
'id': generator.uuid4(),
293+
'name': generator.name(),
294+
'metadata': {
295+
'sample': generator.name()
296+
},
297+
'tags': [
298+
generator.name()
299+
],
300+
'type': generator.slug()
301+
}
302+
289303
def exists(self, **kwargs):
290304
file_ = FileProvider.default_file()
291305
file_.update(kwargs)
@@ -480,6 +494,19 @@ def can_move_to_folder(self, id=None, parent=None, name=None):
480494

481495
self.request_mocker.post(f'/files/{id}/actions/move', json=result)
482496

497+
def files_to_search(self, num_of_files):
498+
items = [
499+
FileProvider.default_search_file()
500+
for _ in range(num_of_files)
501+
]
502+
href = f'{self.base_url}/files/search'
503+
response = {
504+
'count': num_of_files,
505+
'cont_token': generator.text(max_nb_chars=10),
506+
'result_set': items
507+
}
508+
self.request_mocker.post(href, json=response)
509+
483510

484511
class AppProvider:
485512
def __init__(self, request_mocker, base_url):
@@ -834,7 +861,7 @@ def paginated_file_list(self, limit, num_of_files, volume_id, volume_data):
834861
links = []
835862
if i + limit < num_of_files:
836863
next_page_link = {
837-
'next': (
864+
'next': (
838865
f'{self.base_url}/storage/volumes/{volume_id}/list/'
839866
f'?offset={i + limit}&limit={limit}&fields=_all'
840867
)
@@ -944,13 +971,13 @@ def default_copy_result():
944971
return copy_result
945972

946973
def feedback_set(self):
947-
url = f'{self.base_url }/action/notifications/feedback'
974+
url = f'{self.base_url}/action/notifications/feedback'
948975
self.request_mocker.post(url)
949976

950977
def can_bulk_copy(self, **kwargs):
951978
result = self.default_copy_result()
952979
result.update(kwargs)
953-
url = f'{self.base_url }/action/files/copy'
980+
url = f'{self.base_url}/action/files/copy'
954981
self.request_mocker.post(url, json=result)
955982

956983

tests/test_files.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,3 +427,65 @@ def test_move_to_folder(api, given, verifier):
427427

428428
# verification
429429
verifier.file.moved_to_folder(id=file_id)
430+
431+
432+
def test_search_files(api, given, verifier):
433+
total = 10
434+
query = 'some query'
435+
given.file.files_to_search(total)
436+
437+
# action
438+
response = api.files.search(query)
439+
440+
# verification
441+
assert response.count == total
442+
443+
verifier.file.searched(query)
444+
445+
446+
def test_search_files_paginated(api, given, verifier):
447+
total = 10
448+
query = 'some query'
449+
given.file.files_to_search(total)
450+
451+
# action
452+
response = api.files.search(query, cont_token='start', limit=10)
453+
454+
# verification
455+
assert response.count == total
456+
457+
verifier.file.searched_with_pagination(query, 'start', 10)
458+
459+
460+
def test_search_files_paginated_limit(api, given, verifier):
461+
total = 10
462+
query = 'some query'
463+
given.file.files_to_search(total)
464+
465+
# action
466+
response = api.files.search(query, limit=10)
467+
468+
# verification
469+
assert response.count == total
470+
471+
verifier.file.searched_with_limit(query, 10)
472+
473+
474+
def test_search_files_with_no_query(api, given, verifier):
475+
given.file.files_to_search(1)
476+
477+
with pytest.raises(SbgError):
478+
api.files.search(None)
479+
480+
with pytest.raises(SbgError):
481+
api.files.search("")
482+
483+
484+
def test_search_files_with_invalid_limit(api, given, verifier):
485+
given.file.files_to_search(1)
486+
487+
with pytest.raises(SbgError):
488+
api.files.search(query='some query', limit=0)
489+
490+
with pytest.raises(SbgError):
491+
api.files.search(query='some query', limit=-1)

tests/verifiers.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ def check_post_data(self):
2828
for hist in self.request_mocker._adapter.request_history:
2929
print(hist)
3030

31+
def check_body(self, body):
32+
for hist in self.request_mocker._adapter.request_history:
33+
if hist.json() == body:
34+
return True
35+
assert False, f'Body not matched \n{body} != \n{hist.body}'
36+
3137

3238
class ProjectVerifier:
3339
def __init__(self, request_mocker):
@@ -204,6 +210,22 @@ def copied_to_folder(self, id):
204210
def moved_to_folder(self, id):
205211
self.checker.check_url(f'/files/{id}/actions/move')
206212

213+
def searched(self, query):
214+
self.checker.check_url('/files/search')
215+
self.checker.check_body({'query': query})
216+
217+
def searched_with_pagination(self, query, cont_token, limit):
218+
qs = {'cont_token': [cont_token], 'limit': [f'{limit}']}
219+
self.checker.check_url('/files/search')
220+
self.checker.check_query(qs)
221+
self.checker.check_body({'query': query})
222+
223+
def searched_with_limit(self, query, limit):
224+
qs = {'limit': [f'{limit}']}
225+
self.checker.check_url('/files/search')
226+
self.checker.check_query(qs)
227+
self.checker.check_body({'query': query})
228+
207229

208230
class AppVerifier:
209231
def __init__(self, request_mocker):

0 commit comments

Comments
 (0)