diff --git a/renga/api/client.py b/renga/api/client.py index b3eaca9f21..31e338cc07 100644 --- a/renga/api/client.py +++ b/renga/api/client.py @@ -94,10 +94,15 @@ def get(self, *args, **kwargs): @check_status_code def post(self, *args, **kwargs): - """Perform the ``GET`` request and check its status code.""" + """Perform the ``POST`` request and check its status code.""" return super(APIClient, self).post(*args, **kwargs) + @check_status_code + def put(self, *args, **kwargs): + """Perform the ``PUT`` request and check its status code.""" + return super(APIClient, self).put(*args, **kwargs) + @check_status_code def delete(self, *args, **kwargs): - """Perform the ``GET`` request and check its status code.""" + """Perform the ``DELETE`` request and check its status code.""" return super(APIClient, self).delete(*args, **kwargs) diff --git a/renga/api/storage.py b/renga/api/storage.py index d4732085e8..185aaf919e 100644 --- a/renga/api/storage.py +++ b/renga/api/storage.py @@ -36,6 +36,13 @@ def create_bucket(self, **kwargs): self._url('/api/storage/authorize/create_bucket'), json=kwargs) return resp.json() + def storage_bucket_metadata_replace(self, resource_id, data): + """Replace resource metadata.""" + return self.put( + self._url('/api/storage/bucket/{0}', resource_id), + json=data, + expected_status_code=200, ).json() + class FilesApiMixin(object): """Client for handling file objects in a bucket.""" @@ -76,7 +83,8 @@ def storage_file_metadata_replace(self, resource_id, data): """Replace resource metadata.""" return self.put( self._url('/api/storage/file/{0}', resource_id), - json=data, ).json() + json=data, + expected_status_code=200, ).json() def storage_io_write(self, data): """Write data to the file. diff --git a/renga/models/storage.py b/renga/models/storage.py index 0549f5344b..29635dbf8a 100644 --- a/renga/models/storage.py +++ b/renga/models/storage.py @@ -43,9 +43,24 @@ def name(self): """The bucket name.""" return self._properties.get('resource:bucket_name') + @name.setter + def name(self, value): + """Modify the name.""" + # FIXME check when the API is fixed + self._client.api.storage_bucket_metadata_replace( + self.id, { + 'file_name': value, + }) + + # Update if the service replace works + self._properties['resource:bucket_name'] = value + @property def _properties(self): """The internal bucket properties.""" + if self._response.get('properties') is None: + self._response = self._client.api.get_bucket(self.id) + assert self._response['properties'] return self._response.get('properties') @property @@ -72,6 +87,12 @@ def backends(self): def create(self, name=None, backend=None, **kwargs): """Create a new :class:`~renga.models.storage.Bucket` instance. + **Example** + + >>> bucket = client.buckets.create('bucket1') + >>> bucket.name + 'bucket1' + :param name: Bucket name. :param backend: Name of backend used to store data in the bucket. Defaults to the :envvar:`RENGA_STORAGE_BUCKET_BACKEND` @@ -139,27 +160,27 @@ def access_token(self): self._client.api.access_token) @property - def filename(self): - """Filename of the file.""" + def name(self): + """Name of the file.""" return self._properties.get('resource:file_name') - @filename.setter - def filename(self, value): - """Modify the filename value.""" - labels = self._properties.get('resource:labels', []) + @name.setter + def name(self, value): + """Modify the name.""" self._client.api.storage_file_metadata_replace(self.id, { 'file_name': value, - 'labels': labels, }) # Update if the service replace works self._properties['resource:file_name'] = value - def clone(self, filename=None, bucket=None): + filename = name + + def clone(self, name=None, filename=None, bucket=None): """Create an instance of the file for independent version tracking.""" resp = self._client.api.storage_copy_file( resource_id=self.id, - file_name=filename or 'clone_' + self.filename, + file_name=name or filename or 'clone_' + self.name, bucket_id=bucket.id if isinstance(bucket, Bucket) else bucket, ) return self.__class__( LazyResponse(lambda: self._client.api.get_file(resp['id']), resp), @@ -184,7 +205,7 @@ class Meta: model = File - headers = ('id', 'filename') + headers = ('id', 'name') def __init__(self, bucket, **kwargs): """Initialize collection of files in the bucket.""" @@ -203,14 +224,14 @@ def __iter__(self): return (self.Meta.model(f, client=self._client, collection=self) for f in self._client.api.get_bucket_files(self.bucket.id)) - def open(self, filename=None, mode='w'): + def open(self, name=None, filename=None, mode='w'): """Create an empty file in this bucket.""" if mode != 'w': raise NotImplemented('Only mode "w" is currently supported') resp = self._client.api.create_file( bucket_id=self.bucket.id, - file_name=filename, + file_name=name or filename, request_type='create_file', ) access_token = resp.pop('access_token') @@ -228,31 +249,32 @@ def open(self, filename=None, mode='w'): } return FileHandle(file_handle, client=client) - def create(self, filename=None): + def create(self, name=None, filename=None): """Create an empty file in this bucket.""" resp = self._client.api.create_file( bucket_id=self.bucket.id, - file_name=filename, + file_name=name or filename, request_type='create_file', ) return self.Meta.model( LazyResponse(lambda: self._client.api.get_file(resp['id']), resp), client=self._client, collection=self) - def from_url(self, url, filename=None): + def from_url(self, url, name=None, filename=None): """Create a file with data from the streamed GET response. **Example** - >>> file_ = client.buckets[1234].files.from_url( - ... 'https://example.com/tests/data', filename='hello') + >>> bucket = client.buckets.create('bucket1') + >>> file_ = bucket.files.from_url( + ... 'https://example.com/tests/data', name='hello') >>> file_.id 9876 - >>> client.buckets[1234].files[9876].open('r').read() + >>> client.buckets[bucket.id].files[file_.id].open('r').read() b'hello world' """ - with self.open(filename=filename or url, mode='w') as fp: + with self.open(name=name or filename or url, mode='w') as fp: fp.from_url(url) return self.__getitem__(fp.id) @@ -300,7 +322,8 @@ def from_url(self, url): **Example** - >>> with client.buckets[1234].files[9876].open('w') as fp: + >>> file_ = client.buckets.create('bucket1').files.create('data') + >>> with file_.open('w') as fp: ... fp.from_url('https://example.com/tests/data') """ @@ -321,10 +344,12 @@ class FileVersion(Model, FileMixin): IDENTIFIER_KEY = 'id' @property - def filename(self): - """Filename of the file.""" + def name(self): + """Name of the file.""" return self._properties.get('resource:file_name', - self._collection.file.filename) + self._collection.file.name) + + filename = name @property def created(self): @@ -342,7 +367,7 @@ class Meta: model = FileVersion - headers = ('id', 'filename', 'created') + headers = ('id', 'name', 'created') def __init__(self, file_, **kwargs): """Initialize a collection of file versions.""" diff --git a/tests/conftest.py b/tests/conftest.py index 9ae169d332..4ad6673383 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -311,15 +311,58 @@ def deployer_responses(auth_responses, renga_client): def storage_responses(auth_responses, renga_client): """Monkeypatch requests to immitate the storage service.""" rsps = auth_responses - rsps.add( + + buckets = rsps.buckets = [] + bucket_ = {'id': 1234} + + def create_bucket(request): + """Create a new instance of bucket.""" + resp = json.loads(request.body.decode('utf-8')) + id = bucket_['id'] + bucket_['id'] += 4444 + name = resp['name'] + buckets.append({ + "id": + id, + "types": ["resource:bucket"], + "properties": [{ + "key": + "resource:bucket_backend", + "data_type": + "string", + "cardinality": + "single", + "values": [{ + "key": "resource:bucket_backend", + "data_type": "string", + "value": "local", + "properties": [] + }] + }, { + "key": + "resource:bucket_name", + "data_type": + "string", + "cardinality": + "single", + "values": [{ + "key": "resource:bucket_name", + "data_type": "string", + "value": name, + "properties": [] + }] + }] + }) + return (201, {}, json.dumps({ + 'id': id, + 'name': name, + 'backend': 'local', + })) + + rsps.add_callback( responses.POST, renga_client.api._url('/api/storage/authorize/create_bucket'), - status=201, - json={ - 'id': 1234, - 'name': 'hello', - 'backend': 'local', - }) + callback=create_bucket, ) file_ = {'id': 9876} data = {} @@ -490,81 +533,30 @@ def io_read(request): def explorer_responses(auth_responses, renga_client): """Monkeypatch requests to immitate the explorer service.""" rsps = auth_responses - buckets = [{ - "id": - 1234, - "types": ["resource:bucket"], - "properties": [{ - "key": - "resource:bucket_backend", - "data_type": - "string", - "cardinality": - "single", - "values": [{ - "key": "resource:bucket_backend", - "data_type": "string", - "value": "local", - "properties": [] - }] - }, { - "key": - "resource:bucket_name", - "data_type": - "string", - "cardinality": - "single", - "values": [{ - "key": "resource:bucket_name", - "data_type": "string", - "value": "bucket1", - "properties": [] - }] - }] - }, { - "id": - 5678, - "types": ["resource:bucket"], - "properties": [{ - "key": - "resource:bucket_backend", - "data_type": - "string", - "cardinality": - "single", - "values": [{ - "key": "resource:bucket_backend", - "data_type": "string", - "value": "local", - "properties": [] - }] - }, { - "key": - "resource:bucket_name", - "data_type": - "string", - "cardinality": - "single", - "values": [{ - "key": "resource:bucket_name", - "data_type": "string", - "value": "bucket2", - "properties": [] - }] - }] - }] + buckets = rsps.buckets - rsps.add( + rsps.add_callback( responses.GET, renga_client.api._url('/api/explorer/storage/bucket'), - status=200, - json=buckets) + callback=lambda request: (200, {}, json.dumps(buckets)), ) - rsps.add( + def rename_bucket(request): + """Rename a bucket.""" + payload = json.loads(request.body.decode('utf-8')) + buckets[0]['properties'][1]['values'][0]['value'] = payload[ + 'file_name'] + return (200, {}, '{}') + + rsps.add_callback( + responses.PUT, + renga_client.api._url('/api/storage/bucket/1234'), + callback=rename_bucket, + content_type='application/json', ) + + rsps.add_callback( responses.GET, renga_client.api._url('/api/explorer/storage/bucket/1234'), - status=200, - json=buckets[0]) + callback=lambda request: (200, {}, json.dumps(buckets[0]))) def new_file(id, name): """Create new file.""" diff --git a/tests/test_client.py b/tests/test_client.py index 43d457f820..83dbd19e4e 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -77,6 +77,8 @@ def test_client_contexts(renga_client, deployer_responses, storage_responses, monkeypatch.setenv('RENGA_CONTEXT_ID', 'abcd') context = renga_client.contexts.create(image='hello-world') + renga_client.buckets.create('hello') + assert context.id == 'abcd' assert context.spec['image'] == 'hello-world' @@ -121,10 +123,16 @@ def test_client_buckets(renga_client, storage_responses): """Test client for managing buckets and files.""" bucket = renga_client.buckets.create(name='world', backend='local') assert bucket.id == 1234 + assert bucket.name == 'world' + + bucket.name = 'Earth' + + bucket = renga_client.buckets[1234] + assert bucket.name == 'Earth' - file_ = bucket.files.create(filename='hello.ipynb') + file_ = bucket.files.create(name='hello.ipynb') assert file_.id == 9876 - assert file_.filename == 'hello.ipynb' + assert file_.name == 'hello.ipynb' with file_.open('w') as fp: fp.write(b'hello world') @@ -149,11 +157,15 @@ def test_client_buckets_shortcut(renga_client, storage_responses): def test_bucket_listing(renga_client, explorer_responses): """Test storage explorer client.""" + renga_client.buckets.create(name='first') + renga_client.buckets.create(name='second') + buckets = renga_client.buckets.list() - assert buckets[0].id == 1234 - assert buckets[1].id == 5678 - assert renga_client.buckets[1234].id == 1234 + assert buckets[0].name == 'first' + assert buckets[1].name == 'second' + + assert renga_client.buckets[buckets[0].id].id == buckets[0].id def test_file_renaming(renga_client, storage_responses): @@ -161,15 +173,15 @@ def test_file_renaming(renga_client, storage_responses): bucket = renga_client.buckets.create(name='world', backend='local') assert bucket.id == 1234 - file_ = bucket.files.create(filename='hello.ipynb') + file_ = bucket.files.create(name='hello.ipynb') assert file_.id == 9876 - assert file_.filename == 'hello.ipynb' + assert file_.name == 'hello.ipynb' - file_.filename = 'hello-2' - assert file_.filename == 'hello-2' + file_.name = 'hello-2' + assert file_.name == 'hello-2' file_ = bucket.files[9876] - assert file_.filename == 'hello-2' + assert file_.name == 'hello-2' def test_file_cloning(renga_client, storage_responses): @@ -177,7 +189,7 @@ def test_file_cloning(renga_client, storage_responses): bucket = renga_client.buckets.create(name='world', backend='local') assert bucket.id == 1234 - file_ = bucket.files.create(filename='hello.ipynb') + file_ = bucket.files.create(name='hello.ipynb') with file_.open('w') as fp: fp.write(b'hello world') diff --git a/tests/test_notebook.py b/tests/test_notebook.py index e2ef3b9916..fbbd71b2f3 100644 --- a/tests/test_notebook.py +++ b/tests/test_notebook.py @@ -50,6 +50,7 @@ def test_file_manager_browse(instance_path, renga_client, monkeypatch, deployer_responses, storage_responses, notebook): """Test browsing in file manager.""" client = renga_client + client.buckets.create('bucket1') monkeypatch.setenv('RENGA_ENDPOINT', client.api.endpoint) monkeypatch.setenv('RENGA_ACCESS_TOKEN', client.api.token['access_token']) @@ -73,6 +74,7 @@ def test_file_manager(instance_path, renga_client, monkeypatch, deployer_responses, storage_responses, notebook): """Test file manager.""" client = renga_client + client.buckets.create('bucket1') monkeypatch.setenv('RENGA_ENDPOINT', client.api.endpoint) monkeypatch.setenv('RENGA_ACCESS_TOKEN', client.api.token['access_token'])