Skip to content

Commit 8b75e25

Browse files
authored
Add methods for managing capsules (#10)
These methods map to the "PUT /api/plugins" and "DELETE /api/plugins/{capsule_name}" endpoints, allowing clients to manage capsules without file system access to the server computer.
1 parent 4804be1 commit 8b75e25

5 files changed

Lines changed: 81 additions & 6 deletions

File tree

brainframe/api/bf_errors.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,17 @@ class RemoteConnectionError(BaseAPIError):
292292
"""
293293

294294

295+
@_server_origin_error()
296+
class InvalidCapsuleError(BaseAPIError):
297+
"""The provided capsule could not be loaded."""
298+
299+
300+
@_server_origin_error()
301+
class IncompatibleCapsuleError(BaseAPIError):
302+
"""The provided capsule is not compatible with this version of BrainFrame.
303+
"""
304+
305+
295306
class ServerNotReadyError(BaseAPIError):
296307
"""The client was able to communicate with the server, but the server had
297308
not completed startup or was in an invalid state"""

brainframe/api/stubs/base_stub.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,13 @@ def _get_json(self, api_url, timeout, params=None) -> Tuple[Any, dict]:
5858
return json.loads(resp.content), resp.headers
5959
return None, resp.headers
6060

61-
def _put_json(self, api_url, timeout, json_data):
61+
def _put_json(self, api_url, timeout, json_data) -> Any:
6262
"""Send a PUT request to the given URL.
6363
6464
:param api_url: The /api/blah/blah to append to the base_url
6565
:param timeout: The timeout to use for this request
6666
:param json_data: Pre-formatted JSON to send
67-
:return: The JSON response as a dict, or None if none was sent
67+
:return: The parsed response, or None if none was sent
6868
"""
6969
resp = self._put(api_url,
7070
timeout,

brainframe/api/stubs/capsules.py

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import json
2-
from typing import Dict, List, Optional
2+
from pathlib import Path
3+
from time import time
4+
from typing import Dict, List, Optional, BinaryIO, Union
5+
6+
import requests.exceptions
37

48
from brainframe.api.bf_codecs import Capsule
5-
from .base_stub import BaseStub, DEFAULT_TIMEOUT
9+
from .base_stub import DEFAULT_TIMEOUT
10+
from .storage import StorageStubMixin
611

712

8-
class CapsuleStubMixin(BaseStub):
13+
class CapsuleStubMixin(StorageStubMixin):
914
"""Provides stubs to call APIs to inspect and configure capsules."""
1015

1116
def get_capsule(self, name,
@@ -28,6 +33,61 @@ def get_capsules(self, timeout=DEFAULT_TIMEOUT) -> List[Capsule]:
2833
capsules, _ = self._get_json(req, timeout)
2934
return [Capsule.from_dict(d) for d in capsules]
3035

36+
def load_capsule(self, storage_id: int,
37+
source_path: Optional[Path] = None,
38+
timeout: float = DEFAULT_TIMEOUT) -> Capsule:
39+
"""Loads and initializes a capsule from capsule data in storage.
40+
41+
:param storage_id: The ID of the raw capsule data in storage
42+
:param source_path: If available, the path to the source code on the
43+
development machine can be provided. Doing so allows stack traces
44+
to point to the correct source location.
45+
:param timeout: The timeout to use for this request
46+
:return: The loaded capsule
47+
"""
48+
req = f"/api/plugins"
49+
req_object = {
50+
"storage_id": storage_id,
51+
"dev_options": {
52+
"source_path": str(source_path),
53+
},
54+
}
55+
capsule = self._put_json(req, timeout, json.dumps(req_object))
56+
return Capsule.from_dict(capsule)
57+
58+
def upload_and_load_capsule(self, data: Union[bytes, BinaryIO],
59+
source_path: Optional[Path] = None,
60+
timeout: float = DEFAULT_TIMEOUT) -> Capsule:
61+
"""Uploads capsule data to storage, then loads and initializes a
62+
capsule from it. This is a utility method that combines the
63+
functionality of `new_storage` and `load_capsule`.
64+
65+
:param data: The data to store, either as bytes or as a file-like
66+
:param source_path: If available, the path to the source code on the
67+
development machine can be provided. Doing so allows stack traces
68+
to point to the correct source location.
69+
:param timeout: The timeout to use for each request
70+
:return: The loaded capsule
71+
"""
72+
storage_id = self.new_storage(data,
73+
mime_type="application/octet-stream",
74+
timeout=timeout)
75+
76+
return self.load_capsule(storage_id,
77+
source_path,
78+
timeout=timeout)
79+
80+
def unload_capsule(self, capsule_name: str,
81+
timeout: float = DEFAULT_TIMEOUT) -> None:
82+
"""Unloads a capsule and deletes its data from storage. This method may
83+
only be used with capsules that were loaded through the REST API.
84+
85+
:param capsule_name: The name of the capsule to unload
86+
:param timeout: The timeout to use for this request
87+
"""
88+
req = f"/api/plugins/{capsule_name}"
89+
self._delete(req, timeout)
90+
3191
def get_capsule_option_vals(self, capsule_name, stream_id=None,
3292
timeout=DEFAULT_TIMEOUT) \
3393
-> Dict[str, object]:

brainframe/api/stubs/storage.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
import json
12
from io import BytesIO
23
from typing import BinaryIO, Iterable, Tuple, Union
34

45
import numpy as np
5-
import json
66
from PIL import Image
77

88
from brainframe.api.bf_codecs import image_utils

docs/capsules.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ API Methods
1212

1313
.. automethod:: brainframe.api.BrainFrameAPI.get_capsules
1414

15+
.. automethod:: brainframe.api.BrainFrameAPI.set_capsule
16+
17+
.. automethod:: brainframe.api.BrainFrameAPI.delete_capsule
18+
1519
.. automethod:: brainframe.api.BrainFrameAPI.get_capsule_option_vals
1620

1721
.. automethod:: brainframe.api.BrainFrameAPI.set_capsule_option_vals

0 commit comments

Comments
 (0)