Skip to content

Commit c075ee6

Browse files
authored
WIP: Feature/Analysis 2d graphs and nodeports package (#114)
added notebooks for #3 use case for displaying 2D graphs enhanced simcore-sdk/nodeports package to connect to S3, postgres, download from S3 and compatible with the docker swarm.
1 parent 5b9efd8 commit c075ee6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+58241
-99
lines changed

.env-devel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
POSTGRES_ENDPOINT=postgres:5432
12
POSTGRES_USER=simcore
23
POSTGRES_PASSWORD=simcore
34
POSTGRES_DB=simcoredb

Makefile

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
PY_FILES = $(strip $(shell find services packages -iname '*.py'))
66

7-
export PYTHONPATH=${PWD}/packages/s3wrapper/src
7+
export PYTHONPATH=${CURDIR}/packages/s3wrapper/src:${CURDIR}/packages/simcore-sdk/src
88

99
build-devel:
1010
docker-compose -f services/docker-compose.yml -f services/docker-compose.devel.yml build
@@ -26,19 +26,18 @@ up:
2626

2727
up-swarm:
2828
docker swarm init
29-
docker-compose -f services/docker-compose.yml -f services/docker-compose.deploy.yml up
29+
docker stack deploy -c services/docker-compose.yml -c services/docker-compose.deploy.yml workbench
3030

3131
down:
3232
docker-compose -f services/docker-compose.yml down
3333
docker-compose -f services/docker-compose.yml -f services/docker-compose.devel.yml down
3434

3535
down-swarm:
36-
docker-compose -f services/docker-compose.yml -f services/docker-compose.deploy.yml down
36+
docker stack rm workbench
3737
docker swarm leave -f
3838

3939
stack-up:
4040
docker swarm init
41-
POSTGRES_USER=simcore POSTGRES_PASSWORD=simcore POSTGRES_DB=simcoredb RABBITMQ_USER=simcore RABBITMQ_PASSWORD=simcore RABBITMQ_PROGRESS_CHANNEL=comp.backend.channels.progress RABBITMQ_LOG_CHANNEL=comp.backend.channels.log S3_ENDPOINT=minio:9000 S3_ACCESS_KEY=12345678 S3_SECRET_KEY=12345678 S3_BUCKET_NAME=simcore docker stack deploy -c services/docker-compose.yml -c services/docker-compose.deploy.yml osparc
4241

4342
stack-down:
4443
docker stack rm osparc

packages/s3wrapper/src/s3wrapper/s3_client.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from minio import Minio
77
from minio.error import ResponseError
88

9-
logging.basicConfig(level=logging.DEBUG)
109
_LOGGER = logging.getLogger(__name__)
1110

1211

@@ -122,9 +121,9 @@ def get_metadata(self, bucket_name, object_name):
122121

123122
return {}
124123

125-
def list_objects(self, bucket_name, recursive=False):
124+
def list_objects(self, bucket_name, prefix=None, recursive=False):
126125
try:
127-
return self.client.list_objects(bucket_name, recursive=recursive)
126+
return self.client.list_objects(bucket_name, prefix=prefix, recursive=recursive)
128127
except ResponseError as _err:
129128
logging.exception("Could not list objects")
130129

@@ -152,7 +151,7 @@ def exists_object(self, bucket_name, object_name, recursive=False):
152151
''' This seems to be pretty heavy, should be used with care
153152
'''
154153
try:
155-
objects = self.list_objects(bucket_name, recursive)
154+
objects = self.list_objects(bucket_name, recursive=recursive)
156155
for obj in objects:
157156
if obj.object_name == object_name:
158157
return True

packages/s3wrapper/tests/test_s3_client.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ def test_file_upload_download(s3_client, bucket, text_files):
104104
filepath = text_files(1)[0]
105105
object_name = "1"
106106
assert s3_client.upload_file(bucket, object_name, filepath)
107-
filepath2 = filepath + "."
107+
filepath2 = filepath + ".rec"
108108
assert s3_client.download_file(bucket, object_name, filepath2)
109109
assert filecmp.cmp(filepath2, filepath)
110110

@@ -173,7 +173,7 @@ def test_presigned_put(s3_client, bucket, text_files):
173173
with urllib.request.urlopen(req) as _f:
174174
pass
175175

176-
filepath2 = filepath + "."
176+
filepath2 = filepath + ".rec"
177177
assert s3_client.download_file(bucket, object_name, filepath2)
178178
assert filecmp.cmp(filepath2, filepath)
179179

@@ -233,3 +233,28 @@ def test_object_exists(s3_client, bucket, text_files):
233233
assert s3_client.upload_file(bucket, object_name, file2)
234234
assert not s3_client.exists_object(bucket, object_name, False)
235235
assert s3_client.exists_object(bucket, object_name, True)
236+
237+
def test_list_objects(s3_client, bucket, text_files):
238+
files = text_files(2)
239+
file1 = files[0]
240+
file2 = files[1]
241+
object_name = "level1/level2/1"
242+
assert s3_client.upload_file(bucket, object_name, file1)
243+
object_name = "level2/level2/2"
244+
assert s3_client.upload_file(bucket, object_name, file2)
245+
246+
listed_objects = s3_client.list_objects(bucket)
247+
for s3_obj in listed_objects:
248+
assert s3_obj.object_name == "level1/" or s3_obj.object_name == "level2/"
249+
250+
listed_objects = s3_client.list_objects(bucket, prefix="level1")
251+
for s3_obj in listed_objects:
252+
assert s3_obj.object_name == "level1/"
253+
254+
listed_objects = s3_client.list_objects(bucket, prefix="level1", recursive=True)
255+
for s3_obj in listed_objects:
256+
assert s3_obj.object_name == "level1/level2/1"
257+
258+
listed_objects = s3_client.list_objects(bucket, recursive=True)
259+
for s3_obj in listed_objects:
260+
assert s3_obj.object_name == "level1/level2/1" or s3_obj.object_name == "level2/level2/2"

packages/simcore-sdk/requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
psycopg2==2.7.4
22
sqlalchemy==1.2.5
3+
networkx==2.1
4+
tenacity==4.12.0
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"version":"0.1",
3+
"inputs": [
4+
{
5+
"key": "in_1",
6+
"label": "computational data",
7+
"desc": "these are computed data out of a pipeline",
8+
"type": "file-url",
9+
"value": "/home/jovyan/data/outputControllerOut.dat",
10+
"timestamp": "2018-05-23T15:34:53.511Z"
11+
},
12+
{
13+
"key": "in_5",
14+
"label": "some number",
15+
"desc": "numbering things",
16+
"type": "int",
17+
"value": "666",
18+
"timestamp": "2018-05-23T15:34:53.511Z"
19+
}
20+
],
21+
"outputs": [
22+
{
23+
"key": "out_1",
24+
"label": "some boolean output",
25+
"desc": "could be true or false...",
26+
"type": "bool",
27+
"value": "null",
28+
"timestamp": "2018-05-23T15:34:53.511Z"
29+
}
30+
]
31+
}

packages/simcore-sdk/src/simcore_sdk/config/db.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"""
44
from os import environ as env
55

6-
POSTGRES_URL = "postgres:5432"
6+
POSTGRES_URL = env.get("POSTGRES_ENDPOINT", "postgres:5432")
77
POSTGRES_USER = env.get("POSTGRES_USER", "simcore")
88
POSTGRES_PW = env.get("POSTGRES_PASSWORD", "simcore")
99
POSTGRES_DB = env.get("POSTGRES_DB", "simcoredb")
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#pylint: disable=C0111
2+
#pylint: disable=C0103
3+
# import os
4+
# from simcore_sdk.nodeports import config
5+
# from simcore_sdk.nodeports import io
6+
# from simcore_sdk.nodeports import serialization
7+
8+
# _CONFIG = config.CONFIG[os.environ.get("simcoreapi_CONFIG", "default")]
9+
# _IO = io.IO(config=_CONFIG)
10+
# # create initial Simcore object
11+
# PORTS = serialization.create_from_json(_IO, auto_read=True, auto_write=True)
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""This module contains an item representing a node port"""
2+
3+
import logging
4+
import collections
5+
import datetime
6+
from simcore_sdk.nodeports import exceptions
7+
from simcore_sdk.nodeports import config
8+
from simcore_sdk.nodeports import filemanager
9+
10+
_LOGGER = logging.getLogger(__name__)
11+
12+
_DataItem = collections.namedtuple("_DataItem", config.DATA_ITEM_KEYS)
13+
14+
class DataItem(_DataItem):
15+
"""This class encapsulate a Data Item and provide accessors functions"""
16+
def __new__(cls, **kwargs):
17+
_LOGGER.debug("Creating new data item with %s", kwargs)
18+
self = super(DataItem, cls).__new__(cls, **kwargs)
19+
self.new_data_cb = None
20+
self.get_node_from_uuid_cb = None
21+
return self
22+
23+
def get(self):
24+
"""returns the data converted to the underlying type.
25+
26+
Can throw InvalidPtrotocolError if the underling type is unknown.
27+
Can throw ValueError if the conversion fails.
28+
returns the converted value or None if no value is defined
29+
"""
30+
_LOGGER.debug("Getting data item")
31+
if self.type not in config.TYPE_TO_PYTHON_TYPE_MAP:
32+
raise exceptions.InvalidProtocolError(self.type)
33+
if self.value == "null":
34+
_LOGGER.debug("Got empty data item")
35+
return None
36+
_LOGGER.debug("Got data item with value %s", self.value)
37+
38+
if isinstance(self.value, str) and self.value.startswith("link."):
39+
link = self.value.split(".")
40+
if len(link) != 3:
41+
raise exceptions.InvalidProtocolError(self.value, "Invalid link definition: " + str(self.value))
42+
other_node_uuid = link[1]
43+
other_port_key = link[2]
44+
45+
if self.type in config.TYPE_TO_S3_FILE_LIST:
46+
# try to fetch from S3 as a file
47+
_LOGGER.debug("Fetch file from S3 %s", self.value)
48+
return filemanager.download_file_from_S3(node_uuid=other_node_uuid,
49+
node_key=other_port_key,
50+
file_name=self.key)
51+
elif self.type in config.TYPE_TO_S3_FOLDER_LIST:
52+
# try to fetch from S3 as a folder
53+
_LOGGER.debug("Fetch folder from S3 %s", self.value)
54+
return filemanager.download_folder_from_s3(node_uuid=other_node_uuid,
55+
node_key=other_port_key,
56+
folder_name=self.key)
57+
else:
58+
# try to fetch link from database node
59+
_LOGGER.debug("Fetch value from other node %s", self.value)
60+
if not self.get_node_from_uuid_cb:
61+
raise exceptions.NodeportsException("callback to get other node information is not set")
62+
63+
other_nodeports = self.get_node_from_uuid_cb(other_node_uuid) #pylint: disable=not-callable
64+
_LOGGER.debug("Received node from DB %s, now returning value", other_nodeports)
65+
return other_nodeports.get(other_port_key)
66+
67+
68+
return config.TYPE_TO_PYTHON_TYPE_MAP[self.type](self.value)
69+
70+
def set(self, value):
71+
"""sets the data to the underlying port
72+
73+
Arguments:
74+
value {any type} -- must be convertible to a string, or an exception will be thrown.
75+
"""
76+
_LOGGER.info("Setting data item with value %s", value)
77+
# let's create a new data
78+
data_dct = self._asdict()
79+
new_value = str(value)
80+
if new_value != data_dct["value"]:
81+
data_dct["value"] = str(value)
82+
data_dct["timestamp"] = datetime.datetime.utcnow().isoformat()
83+
new_data = DataItem(**data_dct)
84+
if self.new_data_cb:
85+
_LOGGER.debug("calling new data callback")
86+
self.new_data_cb(new_data) #pylint: disable=not-callable
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
"""This module defines a container for port descriptions"""
2+
3+
# pylint: disable=too-many-ancestors
4+
import logging
5+
from collections import MutableSequence
6+
from simcore_sdk.nodeports import exceptions
7+
from simcore_sdk.nodeports._item import DataItem
8+
9+
_LOGGER = logging.getLogger(__name__)
10+
11+
class DataItemsList(MutableSequence):
12+
"""This class contains a list of Data Items."""
13+
14+
def __init__(self, data=None, read_only=False, change_cb=None, get_node_from_node_uuid_cb=None):
15+
_LOGGER.debug("Creating DataItemsList with %s", data)
16+
if data is None:
17+
data = []
18+
19+
data_keys = set()
20+
for item in data:
21+
if not isinstance(item, DataItem):
22+
raise TypeError
23+
data_keys.add(item.key)
24+
# check uniqueness... we could use directly a set for this as well
25+
if len(data_keys) != len(data):
26+
raise exceptions.InvalidProtocolError(data)
27+
self.__lst = data
28+
self.read_only = read_only
29+
30+
self.__change_notifier = change_cb
31+
self.__get_node_from_node_uuid_cb = get_node_from_node_uuid_cb
32+
self.__assign_change_notifier_to_data()
33+
34+
@property
35+
def change_notifier(self):
36+
"""Callback function to be set if client code wants notifications when
37+
an item is modified or replaced"""
38+
return self.__change_notifier
39+
40+
@change_notifier.setter
41+
def change_notifier(self, value):
42+
self.__change_notifier = value
43+
self.__assign_change_notifier_to_data()
44+
45+
@property
46+
def get_node_from_node_uuid_cb(self):
47+
return self.__get_node_from_node_uuid_cb
48+
49+
@get_node_from_node_uuid_cb.setter
50+
def get_node_from_node_uuid_cb(self, value):
51+
self.__get_node_from_node_uuid_cb = value
52+
self.__assign_change_notifier_to_data()
53+
54+
def __setitem__(self, index, value):
55+
_LOGGER.debug("Setting item %s with %s", index, value)
56+
if self.read_only:
57+
raise exceptions.ReadOnlyError(self)
58+
if not isinstance(value, DataItem):
59+
raise TypeError
60+
if isinstance(index, str):
61+
# it might be a key
62+
index = self.__find_index_from_key(index)
63+
if not index < len(self.__lst):
64+
raise exceptions.UnboundPortError(index)
65+
# check for uniqueness
66+
stored_index = self.__find_index_from_key(value.key)
67+
if stored_index != index:
68+
# not the same key not allowed
69+
raise exceptions.InvalidProtocolError(value._asdict())
70+
self.__lst[index] = value
71+
self.__assign_change_notifier_to_data()
72+
self.__notify_client()
73+
74+
75+
def __notify_client(self):
76+
if self.change_notifier and callable(self.change_notifier):
77+
self.change_notifier() #pylint: disable=not-callable
78+
79+
def __getitem__(self, index):
80+
_LOGGER.debug("Getting item %s", index)
81+
if isinstance(index, str):
82+
# it might be a key
83+
index = self.__find_index_from_key(index)
84+
if index < len(self.__lst):
85+
return self.__lst[index]
86+
raise exceptions.UnboundPortError(index)
87+
88+
def __len__(self):
89+
return len(self.__lst)
90+
91+
def __delitem__(self, index):
92+
_LOGGER.debug("Deleting item %s", index)
93+
if self.read_only:
94+
raise exceptions.ReadOnlyError(self)
95+
del self.__lst[index]
96+
97+
def insert(self, index, value):
98+
_LOGGER.debug("Inserting item %s at %s", value, index)
99+
if self.read_only:
100+
raise exceptions.ReadOnlyError(self)
101+
if not isinstance(value, DataItem):
102+
raise TypeError
103+
if self.__find_index_from_key(value.key) < len(self.__lst):
104+
# the key already exists
105+
raise exceptions.InvalidProtocolError(value._asdict())
106+
self.__lst.insert(index, value)
107+
self.__assign_change_notifier_to_data()
108+
self.__notify_client()
109+
110+
def __find_index_from_key(self, item_key):
111+
indices = [index for index in range(0, len(self.__lst)) if self.__lst[index].key == item_key]
112+
if not indices:
113+
return len(self.__lst)
114+
if len(indices) > 1:
115+
raise exceptions.InvalidProtocolError(indices)
116+
return indices[0]
117+
118+
def __assign_change_notifier_to_data(self):
119+
for data in self.__lst:
120+
data.new_data_cb = self.__item_value_updated_cb
121+
data.get_node_from_uuid_cb = self.get_node_from_node_uuid_cb
122+
123+
def __item_value_updated_cb(self, new_data_item):
124+
# a new item shall replace the current one
125+
self.__replace_item(new_data_item)
126+
self.__notify_client()
127+
128+
def __replace_item(self, new_data_item):
129+
item_index = self.__find_index_from_key(new_data_item.key)
130+
self.__lst[item_index] = new_data_item
131+
132+
@classmethod
133+
def __check_uniqueness(cls, dataitems):
134+
data_keys = set([item.key for item in dataitems])
135+
if len(dataitems) != len(data_keys):
136+
raise exceptions.InvalidProtocolError(dataitems)
137+

0 commit comments

Comments
 (0)