From 2ec0b6f1dc3f86aa528cb4d90c7a4944da451f00 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 26 May 2020 19:27:07 +0200 Subject: [PATCH 1/6] Bump pytest-cov from 2.8.1 to 2.9.0 in /packages/postgres-database (#1526) Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 2.8.1 to 2.9.0. - [Release notes](https://github.com/pytest-dev/pytest-cov/releases) - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v2.8.1...v2.9.0) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- packages/postgres-database/requirements/_test.txt | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/postgres-database/requirements/_test.txt b/packages/postgres-database/requirements/_test.txt index b279b68e4ee..7182a21c65b 100644 --- a/packages/postgres-database/requirements/_test.txt +++ b/packages/postgres-database/requirements/_test.txt @@ -18,9 +18,7 @@ coveralls==2.0.0 # via -r requirements/_test.in docker==4.2.0 # via -r requirements/_migration.txt docopt==0.6.2 # via coveralls faker==4.1.0 # via -r requirements/_test.in -idna-ssl==1.1.0 # via aiohttp idna==2.9 # via -r requirements/_migration.txt, requests, yarl -importlib-metadata==1.6.0 # via pluggy, pytest isort==4.3.21 # via pylint lazy-object-proxy==1.4.3 # via astroid mako==1.1.2 # via -r requirements/_migration.txt, alembic @@ -35,7 +33,7 @@ py==1.8.1 # via pytest pylint==2.5.0 # via -r requirements/_test.in pyparsing==2.4.7 # via packaging pytest-aiohttp==0.3.0 # via -r requirements/_test.in -pytest-cov==2.8.1 # via -r requirements/_test.in +pytest-cov==2.9.0 # via -r requirements/_test.in pytest-docker==0.7.2 # via -r requirements/_test.in pytest-instafail==0.4.1.post0 # via -r requirements/_test.in pytest-runner==5.2 # via -r requirements/_test.in @@ -49,11 +47,8 @@ sqlalchemy[postgresql_psycopg2binary]==1.3.17 # via -r requirements/_migration. tenacity==6.2.0 # via -r requirements/_migration.txt text-unidecode==1.3 # via faker toml==0.10.1 # via pylint -typed-ast==1.4.1 # via astroid -typing-extensions==3.7.4.2 # via aiohttp urllib3==1.25.9 # via -r requirements/_migration.txt, requests wcwidth==0.1.9 # via pytest websocket-client==0.57.0 # via -r requirements/_migration.txt, docker wrapt==1.12.1 # via astroid yarl==1.4.2 # via -r requirements/_migration.txt, aiohttp -zipp==3.1.0 # via importlib-metadata From c3dd0f6ce407d263bc27908a3ffe0153d4a6a8f3 Mon Sep 17 00:00:00 2001 From: Andrei Neagu Date: Thu, 28 May 2020 16:58:45 +0200 Subject: [PATCH 2/6] When the service is stopped errors are trapped (#1532) * stop service will no longer fial the tg task on erro * downgraded errors to warnings they are being handled Co-authored-by: Andrei Neagu --- .../resource_manager/garbage_collector.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/resource_manager/garbage_collector.py b/services/web/server/src/simcore_service_webserver/resource_manager/garbage_collector.py index 216d55897ee..0c361313eb8 100644 --- a/services/web/server/src/simcore_service_webserver/resource_manager/garbage_collector.py +++ b/services/web/server/src/simcore_service_webserver/resource_manager/garbage_collector.py @@ -27,6 +27,10 @@ get_running_interactive_services, stop_service, ) +from simcore_service_webserver.director.director_exceptions import ( + ServiceNotFoundError, + DirectorException, +) logger = logging.getLogger(__name__) @@ -127,7 +131,10 @@ async def remove_orphaned_services( or node_id not in currently_opened_projects_node_ids ): logger.info("Will remove service %s", interactive_service["service_host"]) - await stop_service(app, node_id) + try: + await stop_service(app, node_id) + except (ServiceNotFoundError, DirectorException) as e: + logger.warning("Error while stopping service: %s", e) logger.info("Finished orphaned services removal") @@ -170,10 +177,9 @@ async def garbage_collector_task(app: web.Application): keep_alive = False logger.info("Garbage collection task was cancelled, it will not restart!") except Exception: # pylint: disable=broad-except - logger.exception("Error during garbage collector, restarting...") - await asyncio.sleep( - 5 - ) # will wait 5 seconds before restarting to avoid restart loops + logger.warning("There was an error during garbage collection, restarting...") + # will wait 5 seconds before restarting to avoid restart loops + await asyncio.sleep(5) async def setup_garbage_collector_task(app: web.Application): From b0e2545621cc24a720381c0a958f52a35ebe22f0 Mon Sep 17 00:00:00 2001 From: Pedro Crespo <32402063+pcrespov@users.noreply.github.com> Date: Thu, 28 May 2020 20:51:44 +0200 Subject: [PATCH 3/6] Maintenance upgrades (#1530) * Updated pytest-cov 2.8.1 -> 2.9.0 * Updated pytest-cov in webserver * Minor upgrade six from 1.14.0 -> 1.15.0 * Upgrades six in webserver * prometheus-client 0.7.1->0.8.0 * aiopika 6.6.0->6.6.1 * engineio 3.12.1 -> 3.13.0, socketio= 4.5.1->4.6.0 * Missing schema update in storage service * Removes patch on engineio after library is upgraded * fixed python-socketio test (#28) an error now is raises on failed connection also the handler is still fired Co-authored-by: Andrei Neagu --- api/tests/requirements.txt | 4 +- .../requirements/_migration.txt | 2 +- .../postgres-database/requirements/_test.txt | 7 +- packages/s3wrapper/requirements/_base.txt | 2 +- packages/s3wrapper/requirements/_test.txt | 4 +- .../service-library/requirements/_base.txt | 4 +- .../service-library/requirements/_test.txt | 6 +- packages/simcore-sdk/requirements/_base.txt | 2 +- packages/simcore-sdk/requirements/_test.txt | 4 +- .../api/v0/openapi.yaml | 12 +- .../api/v0/schemas/project-v0.0.1.json | 28 +++- services/web/server/requirements/_base.txt | 10 +- services/web/server/requirements/_test.txt | 14 +- .../socketio/__init__.py | 143 ------------------ .../unit/with_dbs/test_resource_manager.py | 5 +- .../requirements/requirements.txt | 4 +- 16 files changed, 66 insertions(+), 185 deletions(-) diff --git a/api/tests/requirements.txt b/api/tests/requirements.txt index 97abad5eeb3..5491296abb1 100644 --- a/api/tests/requirements.txt +++ b/api/tests/requirements.txt @@ -27,12 +27,12 @@ py==1.8.1 # via pytest pyparsing==2.4.7 # via packaging pyrsistent==0.16.0 # via jsonschema pytest-aiohttp==0.3.0 # via -r requirements.in -pytest-cov==2.8.1 # via -r requirements.in +pytest-cov==2.9.0 # via -r requirements.in pytest-instafail==0.4.1.post0 # via -r requirements.in pytest-sugar==0.9.3 # via -r requirements.in pytest==5.4.2 # via -r requirements.in, pytest-aiohttp, pytest-cov, pytest-instafail, pytest-sugar pyyaml==5.3.1 # via openapi-spec-validator -six==1.14.0 # via isodate, jsonschema, openapi-core, openapi-schema-validator, openapi-spec-validator, packaging +six==1.15.0 # via isodate, jsonschema, openapi-core, openapi-schema-validator, openapi-spec-validator, packaging strict-rfc3339==0.7 # via openapi-schema-validator termcolor==1.1.0 # via pytest-sugar typing-extensions==3.7.4.2 # via aiohttp diff --git a/packages/postgres-database/requirements/_migration.txt b/packages/postgres-database/requirements/_migration.txt index 58e36765cfe..f41e54c7510 100644 --- a/packages/postgres-database/requirements/_migration.txt +++ b/packages/postgres-database/requirements/_migration.txt @@ -17,7 +17,7 @@ psycopg2-binary==2.8.5 # via -r requirements/_base.txt, sqlalchemy python-dateutil==2.8.1 # via alembic python-editor==1.0.4 # via alembic requests==2.23.0 # via docker -six==1.14.0 # via docker, python-dateutil, tenacity, websocket-client +six==1.15.0 # via docker, python-dateutil, tenacity, websocket-client sqlalchemy[postgresql_psycopg2binary]==1.3.17 # via -r requirements/_base.txt, alembic tenacity==6.2.0 # via -r requirements/_migration.in urllib3==1.25.9 # via -r requirements/_migration.in, requests diff --git a/packages/postgres-database/requirements/_test.txt b/packages/postgres-database/requirements/_test.txt index 7182a21c65b..351e7976abb 100644 --- a/packages/postgres-database/requirements/_test.txt +++ b/packages/postgres-database/requirements/_test.txt @@ -18,7 +18,9 @@ coveralls==2.0.0 # via -r requirements/_test.in docker==4.2.0 # via -r requirements/_migration.txt docopt==0.6.2 # via coveralls faker==4.1.0 # via -r requirements/_test.in +idna-ssl==1.1.0 # via aiohttp idna==2.9 # via -r requirements/_migration.txt, requests, yarl +importlib-metadata==1.6.0 # via pluggy, pytest isort==4.3.21 # via pylint lazy-object-proxy==1.4.3 # via astroid mako==1.1.2 # via -r requirements/_migration.txt, alembic @@ -42,13 +44,16 @@ python-dateutil==2.8.1 # via -r requirements/_migration.txt, alembic, faker python-editor==1.0.4 # via -r requirements/_migration.txt, alembic pyyaml==5.3.1 # via -r requirements/_test.in requests==2.23.0 # via -r requirements/_migration.txt, coveralls, docker -six==1.14.0 # via -r requirements/_migration.txt, astroid, docker, packaging, python-dateutil, tenacity, websocket-client +six==1.15.0 # via -r requirements/_migration.txt, astroid, docker, packaging, python-dateutil, tenacity, websocket-client sqlalchemy[postgresql_psycopg2binary]==1.3.17 # via -r requirements/_migration.txt, aiopg, alembic tenacity==6.2.0 # via -r requirements/_migration.txt text-unidecode==1.3 # via faker toml==0.10.1 # via pylint +typed-ast==1.4.1 # via astroid +typing-extensions==3.7.4.2 # via aiohttp urllib3==1.25.9 # via -r requirements/_migration.txt, requests wcwidth==0.1.9 # via pytest websocket-client==0.57.0 # via -r requirements/_migration.txt, docker wrapt==1.12.1 # via astroid yarl==1.4.2 # via -r requirements/_migration.txt, aiohttp +zipp==3.1.0 # via importlib-metadata diff --git a/packages/s3wrapper/requirements/_base.txt b/packages/s3wrapper/requirements/_base.txt index 739e3843e09..8aedfb9abb7 100644 --- a/packages/s3wrapper/requirements/_base.txt +++ b/packages/s3wrapper/requirements/_base.txt @@ -9,5 +9,5 @@ configparser==5.0.0 # via minio minio==5.0.10 # via -r requirements/_base.in python-dateutil==2.8.1 # via minio pytz==2020.1 # via minio -six==1.14.0 # via python-dateutil +six==1.15.0 # via python-dateutil urllib3==1.25.9 # via -r requirements/_base.in, minio diff --git a/packages/s3wrapper/requirements/_test.txt b/packages/s3wrapper/requirements/_test.txt index 5e89f9b8f98..20b8a33b19d 100644 --- a/packages/s3wrapper/requirements/_test.txt +++ b/packages/s3wrapper/requirements/_test.txt @@ -24,14 +24,14 @@ pluggy==0.13.1 # via pytest py==1.8.1 # via pytest pylint==2.5.0 # via -r requirements/_test.in pyparsing==2.4.7 # via packaging -pytest-cov==2.8.1 # via -r requirements/_test.in +pytest-cov==2.9.0 # via -r requirements/_test.in pytest-docker==0.7.2 # via -r requirements/_test.in pytest-runner==5.2 # via -r requirements/_test.in pytest==5.4.2 # via -r requirements/_test.in, pytest-cov python-dateutil==2.8.1 # via -r requirements/_base.txt, minio pytz==2020.1 # via -r requirements/_base.txt, minio requests==2.23.0 # via -r requirements/_test.in, coveralls -six==1.14.0 # via -r requirements/_base.txt, astroid, packaging, python-dateutil +six==1.15.0 # via -r requirements/_base.txt, astroid, packaging, python-dateutil toml==0.10.1 # via pylint typed-ast==1.4.1 # via astroid urllib3==1.25.9 # via -r requirements/_base.txt, minio, requests diff --git a/packages/service-library/requirements/_base.txt b/packages/service-library/requirements/_base.txt index 926a30ec79f..4ff283a4f3d 100644 --- a/packages/service-library/requirements/_base.txt +++ b/packages/service-library/requirements/_base.txt @@ -20,11 +20,11 @@ lazy-object-proxy==1.4.3 # via openapi-core multidict==4.7.6 # via aiohttp, yarl openapi-core==0.12.0 # via -r requirements/_base.in openapi-spec-validator==0.2.8 # via openapi-core -prometheus-client==0.7.1 # via -r requirements/_base.in +prometheus-client==0.8.0 # via -r requirements/_base.in psycopg2-binary==2.8.5 # via -r requirements/_base.in, aiopg, sqlalchemy pyrsistent==0.16.0 # via jsonschema pyyaml==5.3.1 # via -r requirements/_base.in, openapi-spec-validator -six==1.14.0 # via isodate, jsonschema, openapi-core, openapi-spec-validator, pyrsistent, tenacity +six==1.15.0 # via isodate, jsonschema, openapi-core, openapi-spec-validator, pyrsistent, tenacity sqlalchemy[postgresql_psycopg2binary]==1.3.17 # via -r requirements/_base.in, aiopg strict-rfc3339==0.7 # via openapi-core tenacity==6.2.0 # via -r requirements/_base.in diff --git a/packages/service-library/requirements/_test.txt b/packages/service-library/requirements/_test.txt index ecb089210cb..810e0010efe 100644 --- a/packages/service-library/requirements/_test.txt +++ b/packages/service-library/requirements/_test.txt @@ -30,14 +30,14 @@ openapi-core==0.12.0 # via -r requirements/_base.txt openapi-spec-validator==0.2.8 # via -r requirements/_base.txt, openapi-core packaging==20.4 # via pytest, pytest-sugar pluggy==0.13.1 # via pytest -prometheus-client==0.7.1 # via -r requirements/_base.txt +prometheus-client==0.8.0 # via -r requirements/_base.txt psycopg2-binary==2.8.5 # via -r requirements/_base.txt, aiopg, sqlalchemy py==1.8.1 # via pytest pylint==2.5.0 # via -r requirements/_test.in pyparsing==2.4.7 # via packaging pyrsistent==0.16.0 # via -r requirements/_base.txt, jsonschema pytest-aiohttp==0.3.0 # via -r requirements/_test.in -pytest-cov==2.8.1 # via -r requirements/_test.in +pytest-cov==2.9.0 # via -r requirements/_test.in pytest-docker==0.7.2 # via -r requirements/_test.in pytest-instafail==0.4.1.post0 # via -r requirements/_test.in pytest-mock==3.1.0 # via -r requirements/_test.in @@ -46,7 +46,7 @@ pytest-sugar==0.9.3 # via -r requirements/_test.in pytest==5.4.2 # via -r requirements/_test.in, pytest-aiohttp, pytest-cov, pytest-instafail, pytest-mock, pytest-sugar pyyaml==5.3.1 # via -r requirements/_base.txt, openapi-spec-validator requests==2.23.0 # via coveralls -six==1.14.0 # via -r requirements/_base.txt, astroid, isodate, jsonschema, openapi-core, openapi-spec-validator, packaging, pyrsistent, tenacity +six==1.15.0 # via -r requirements/_base.txt, astroid, isodate, jsonschema, openapi-core, openapi-spec-validator, packaging, pyrsistent, tenacity sqlalchemy[postgresql_psycopg2binary]==1.3.17 # via -r requirements/_base.txt, aiopg strict-rfc3339==0.7 # via -r requirements/_base.txt, openapi-core tenacity==6.2.0 # via -r requirements/_base.txt diff --git a/packages/simcore-sdk/requirements/_base.txt b/packages/simcore-sdk/requirements/_base.txt index a63769e76c5..06a4ff77b39 100644 --- a/packages/simcore-sdk/requirements/_base.txt +++ b/packages/simcore-sdk/requirements/_base.txt @@ -19,7 +19,7 @@ networkx==2.4 # via -r requirements/_base.in psycopg2-binary==2.8.5 # via -r requirements/_base.in, aiopg, sqlalchemy pydantic==1.5.1 # via -r requirements/_base.in pyyaml==5.3.1 # via trafaret-config -six==1.14.0 # via tenacity +six==1.15.0 # via tenacity sqlalchemy[postgresql_psycopg2binary]==1.3.17 # via aiopg tenacity==6.2.0 # via -r requirements/_base.in trafaret-config==2.0.2 # via -r requirements/_base.in diff --git a/packages/simcore-sdk/requirements/_test.txt b/packages/simcore-sdk/requirements/_test.txt index 211ed803f50..476f5dc7e50 100644 --- a/packages/simcore-sdk/requirements/_test.txt +++ b/packages/simcore-sdk/requirements/_test.txt @@ -36,7 +36,7 @@ pydantic==1.5.1 # via -r requirements/_base.txt pylint==2.5.0 # via -r requirements/_test.in pyparsing==2.4.7 # via packaging pytest-aiohttp==0.3.0 # via -r requirements/_test.in -pytest-cov==2.8.1 # via -r requirements/_test.in +pytest-cov==2.9.0 # via -r requirements/_test.in pytest-docker==0.7.2 # via -r requirements/_test.in pytest-instafail==0.4.1.post0 # via -r requirements/_test.in pytest-mock==3.1.0 # via -r requirements/_test.in @@ -45,7 +45,7 @@ pytest-sugar==0.9.3 # via -r requirements/_test.in pytest==5.4.2 # via -r requirements/_test.in, pytest-aiohttp, pytest-cov, pytest-instafail, pytest-mock, pytest-sugar pyyaml==5.3.1 # via -r requirements/_base.txt, trafaret-config requests==2.23.0 # via -r requirements/_test.in, coveralls, docker -six==1.14.0 # via -r requirements/_base.txt, astroid, docker, packaging, tenacity, websocket-client +six==1.15.0 # via -r requirements/_base.txt, astroid, docker, packaging, tenacity, websocket-client sqlalchemy[postgresql_psycopg2binary]==1.3.17 # via -r requirements/_base.txt, aiopg tenacity==6.2.0 # via -r requirements/_base.txt termcolor==1.1.0 # via pytest-sugar diff --git a/services/storage/src/simcore_service_storage/api/v0/openapi.yaml b/services/storage/src/simcore_service_storage/api/v0/openapi.yaml index 7e273419165..b0dfb02d379 100644 --- a/services/storage/src/simcore_service_storage/api/v0/openapi.yaml +++ b/services/storage/src/simcore_service_storage/api/v0/openapi.yaml @@ -1759,7 +1759,8 @@ paths: example: Dabbling in temporal transitions ... prjOwner: type: string - description: user uuid + format: idn-email + description: user email accessRights: type: object description: object containing the GroupID as key and read/write/execution permissions as value @@ -1970,7 +1971,8 @@ paths: example: Dabbling in temporal transitions ... prjOwner: type: string - description: user uuid + format: idn-email + description: user email accessRights: type: object description: object containing the GroupID as key and read/write/execution permissions as value @@ -2191,7 +2193,8 @@ paths: example: Dabbling in temporal transitions ... prjOwner: type: string - description: user uuid + format: idn-email + description: user email accessRights: type: object description: object containing the GroupID as key and read/write/execution permissions as value @@ -2512,7 +2515,8 @@ components: example: Dabbling in temporal transitions ... prjOwner: type: string - description: user uuid + format: idn-email + description: user email accessRights: type: object description: object containing the GroupID as key and read/write/execution permissions as value diff --git a/services/storage/src/simcore_service_storage/api/v0/schemas/project-v0.0.1.json b/services/storage/src/simcore_service_storage/api/v0/schemas/project-v0.0.1.json index 253d6ce7c47..d40e6ceb1a4 100644 --- a/services/storage/src/simcore_service_storage/api/v0/schemas/project-v0.0.1.json +++ b/services/storage/src/simcore_service_storage/api/v0/schemas/project-v0.0.1.json @@ -41,7 +41,8 @@ }, "prjOwner": { "type": "string", - "description": "user uuid" + "format": "idn-email", + "description": "user email" }, "accessRights": { "type": "object", @@ -106,7 +107,7 @@ "type": "string" }, "progress": { - "type":"number", + "type": "number", "maximum": 100, "minimum": 0, "description": "the node progress value" @@ -158,7 +159,10 @@ ], "properties": { "store": { - "type": ["string", "integer"] + "type": [ + "string", + "integer" + ] }, "dataset": { "type": "string" @@ -181,7 +185,11 @@ "patternProperties": { "^[-_a-zA-Z0-9]+$": { "type": "string", - "enum": ["Invisible", "ReadOnly", "ReadAndWrite"], + "enum": [ + "Invisible", + "ReadOnly", + "ReadAndWrite" + ], "default": "ReadAndWrite", "examples": [ "ReadOnly" @@ -224,7 +232,10 @@ ], "properties": { "store": { - "type": ["string", "integer"] + "type": [ + "string", + "integer" + ] }, "dataset": { "type": "string" @@ -258,7 +269,10 @@ ] }, "parent": { - "type": ["string", "null"], + "type": [ + "string", + "null" + ], "pattern": "^\\S+$", "description": "Parent's (group-nodes') node ID s.", "examples": [ @@ -293,4 +307,4 @@ } } } -} +} \ No newline at end of file diff --git a/services/web/server/requirements/_base.txt b/services/web/server/requirements/_base.txt index 607e0699f03..da1514401d2 100644 --- a/services/web/server/requirements/_base.txt +++ b/services/web/server/requirements/_base.txt @@ -4,7 +4,7 @@ # # pip-compile --output-file=requirements/_base.txt requirements/_base.in # -aio-pika==6.6.0 # via -r requirements/_base.in +aio-pika==6.6.1 # via -r requirements/_base.in aiodebug==1.1.2 # via -r requirements/../../../../packages/service-library/requirements/_base.in, -r requirements/_base.in aiohttp-jinja2==1.2.0 # via -r requirements/_base.in aiohttp-security==0.4.0 # via -r requirements/_base.in @@ -45,16 +45,16 @@ openapi-core==0.12.0 # via -r requirements/../../../../packages/service-lib openapi-spec-validator==0.2.8 # via openapi-core pamqp==2.3.0 # via aiormq passlib==1.7.2 # via -r requirements/_base.in -prometheus-client==0.7.1 # via -r requirements/../../../../packages/service-library/requirements/_base.in +prometheus-client==0.8.0 # via -r requirements/../../../../packages/service-library/requirements/_base.in psycopg2-binary==2.8.5 # via -r requirements/../../../../packages/service-library/requirements/_base.in, aiopg, sqlalchemy pycparser==2.20 # via cffi pyrsistent==0.16.0 # via jsonschema -python-engineio==3.12.1 # via python-socketio -python-socketio==4.5.1 # via -r requirements/_base.in +python-engineio==3.13.0 # via python-socketio +python-socketio==4.6.0 # via -r requirements/_base.in pytz==2020.1 # via celery pyyaml==5.3.1 # via -r requirements/../../../../packages/service-library/requirements/_base.in, aiohttp-swagger, openapi-spec-validator semantic-version==2.8.5 # via -r requirements/_base.in -six==1.14.0 # via cryptography, isodate, jsonschema, openapi-core, openapi-spec-validator, pyrsistent, python-engineio, python-socketio, tenacity +six==1.15.0 # via cryptography, isodate, jsonschema, openapi-core, openapi-spec-validator, pyrsistent, python-engineio, python-socketio, tenacity sqlalchemy[postgresql_psycopg2binary]==1.3.17 # via -r requirements/../../../../packages/postgres-database/requirements/_base.in, -r requirements/../../../../packages/service-library/requirements/_base.in, aiopg strict-rfc3339==0.7 # via openapi-core tenacity==6.2.0 # via -r requirements/../../../../packages/service-library/requirements/_base.in diff --git a/services/web/server/requirements/_test.txt b/services/web/server/requirements/_test.txt index 443ed9a8a3c..3487dd072fb 100644 --- a/services/web/server/requirements/_test.txt +++ b/services/web/server/requirements/_test.txt @@ -4,7 +4,7 @@ # # pip-compile --output-file=requirements/_test.txt requirements/_test.in # -aio-pika==6.6.0 # via -r requirements/_base.txt +aio-pika==6.6.1 # via -r requirements/_base.txt aiodebug==1.1.2 # via -r requirements/_base.txt aiohttp-jinja2==1.2.0 # via -r requirements/_base.txt aiohttp-security==0.4.0 # via -r requirements/_base.txt @@ -27,7 +27,7 @@ certifi==2020.4.5.1 # via requests cffi==1.14.0 # via -r requirements/_base.txt, cryptography change-case==0.5.2 # via -r requirements/_base.txt chardet==3.0.4 # via -r requirements/_base.txt, aiohttp, requests -codecov==2.1.0 # via -r requirements/_test.in +codecov==2.1.3 # via -r requirements/_test.in coverage==5.1 # via -r requirements/_test.in, codecov, coveralls, pytest-cov coveralls==2.0.0 # via -r requirements/_test.in cryptography==2.9.2 # via -r requirements/_base.txt, aiohttp-session @@ -59,7 +59,7 @@ packaging==20.4 # via pytest, pytest-sugar pamqp==2.3.0 # via -r requirements/_base.txt, aiormq passlib==1.7.2 # via -r requirements/_base.txt pluggy==0.13.1 # via pytest -prometheus-client==0.7.1 # via -r requirements/_base.txt +prometheus-client==0.8.0 # via -r requirements/_base.txt psycopg2-binary==2.8.5 # via -r requirements/_base.txt, aiopg, sqlalchemy ptvsd==4.3.2 # via -r requirements/_test.in py==1.8.1 # via pytest @@ -68,7 +68,7 @@ pylint==2.5.0 # via -r requirements/_test.in pyparsing==2.4.7 # via packaging pyrsistent==0.16.0 # via -r requirements/_base.txt, jsonschema pytest-aiohttp==0.3.0 # via -r requirements/_test.in -pytest-cov==2.8.1 # via -r requirements/_test.in +pytest-cov==2.9.0 # via -r requirements/_test.in pytest-docker==0.7.2 # via -r requirements/_test.in pytest-instafail==0.4.1.post0 # via -r requirements/_test.in pytest-mock==3.1.0 # via -r requirements/_test.in @@ -76,14 +76,14 @@ pytest-runner==5.2 # via -r requirements/_test.in pytest-sugar==0.9.3 # via -r requirements/_test.in pytest==5.4.2 # via -r requirements/_test.in, pytest-aiohttp, pytest-cov, pytest-instafail, pytest-mock, pytest-sugar python-dateutil==2.8.1 # via faker -python-engineio==3.12.1 # via -r requirements/_base.txt, python-socketio -python-socketio==4.5.1 # via -r requirements/_base.txt +python-engineio==3.13.0 # via -r requirements/_base.txt, python-socketio +python-socketio==4.6.0 # via -r requirements/_base.txt pytz==2020.1 # via -r requirements/_base.txt, celery pyyaml==5.3.1 # via -r requirements/_base.txt, aiohttp-swagger, openapi-spec-validator redis==3.5.2 # via -r requirements/_test.in requests==2.23.0 # via codecov, coveralls, docker semantic-version==2.8.5 # via -r requirements/_base.txt -six==1.14.0 # via -r requirements/_base.txt, astroid, cryptography, docker, isodate, jsonschema, openapi-core, openapi-spec-validator, packaging, pyrsistent, python-dateutil, python-engineio, python-socketio, tenacity, websocket-client +six==1.15.0 # via -r requirements/_base.txt, astroid, cryptography, docker, isodate, jsonschema, openapi-core, openapi-spec-validator, packaging, pyrsistent, python-dateutil, python-engineio, python-socketio, tenacity, websocket-client sqlalchemy[postgresql_psycopg2binary]==1.3.17 # via -r requirements/_base.txt, aiopg strict-rfc3339==0.7 # via -r requirements/_base.txt, openapi-core tenacity==6.2.0 # via -r requirements/_base.txt, -r requirements/_test.in diff --git a/services/web/server/src/simcore_service_webserver/socketio/__init__.py b/services/web/server/src/simcore_service_webserver/socketio/__init__.py index 2acc7280aea..eb45f64a61f 100644 --- a/services/web/server/src/simcore_service_webserver/socketio/__init__.py +++ b/services/web/server/src/simcore_service_webserver/socketio/__init__.py @@ -16,149 +16,6 @@ log = logging.getLogger(__name__) -def monkey_patch_engineio(): - # pylint: disable=too-many-statements - """Adrsses an issue where cookies containing '=' signs in the value fail to be parsed""" - REQUIRED_ENGINEIO_VERSION = '3.12.1' - from engineio.__init__ import __version__ - - if __version__ != REQUIRED_ENGINEIO_VERSION: - raise RuntimeError(f"The engineio version required for this monkey patch is {REQUIRED_ENGINEIO_VERSION}\n" - "Please check if the new release includes the following" - "PR [#175](https://github.com/miguelgrinberg/python-engineio/pull/175)") - - from engineio.asyncio_client import AsyncClient - - import ssl - - try: - import aiohttp - except ImportError: # pragma: no cover - aiohttp = None - - from engineio import client - from engineio import exceptions - from engineio import packet - - async def _connect_websocket(self, url, headers, engineio_path): - # pylint: disable=protected-access,no-else-return,broad-except,too-many-return-statements,too-many-branches - """Establish or upgrade to a WebSocket connection with the server.""" - if aiohttp is None: # pragma: no cover - self.logger.error('aiohttp package not installed') - return False - websocket_url = self._get_engineio_url(url, engineio_path, - 'websocket') - if self.sid: - self.logger.info( - 'Attempting WebSocket upgrade to ' + websocket_url) - upgrade = True - websocket_url += '&sid=' + self.sid - else: - upgrade = False - self.base_url = websocket_url - self.logger.info( - 'Attempting WebSocket connection to ' + websocket_url) - - if self.http is None or self.http.closed: # pragma: no cover - self.http = aiohttp.ClientSession() - - # extract any new cookies passed in a header so that they can also be - # sent the the WebSocket route - cookies = {} - for header, value in headers.items(): - if header.lower() == 'cookie': - cookies = dict( - [cookie.split('=', 1) for cookie in value.split('; ')]) - del headers[header] - break - self.http.cookie_jar.update_cookies(cookies) - - try: - if not self.ssl_verify: - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE - ws = await self.http.ws_connect( - websocket_url + self._get_url_timestamp(), - headers=headers, ssl=ssl_context) - else: - ws = await self.http.ws_connect( - websocket_url + self._get_url_timestamp(), - headers=headers) - except (aiohttp.client_exceptions.WSServerHandshakeError, - aiohttp.client_exceptions.ServerConnectionError): - if upgrade: - self.logger.warning( - 'WebSocket upgrade failed: connection error') - return False - else: - raise exceptions.ConnectionError('Connection error') - if upgrade: - p = packet.Packet(packet.PING, data='probe').encode( - always_bytes=False) - try: - await ws.send_str(p) - except Exception as e: # pragma: no cover - self.logger.warning( - 'WebSocket upgrade failed: unexpected send exception: %s', - str(e)) - return False - try: - p = (await ws.receive()).data - except Exception as e: # pragma: no cover - self.logger.warning( - 'WebSocket upgrade failed: unexpected recv exception: %s', - str(e)) - return False - pkt = packet.Packet(encoded_packet=p) - if pkt.packet_type != packet.PONG or pkt.data != 'probe': - self.logger.warning( - 'WebSocket upgrade failed: no PONG packet') - return False - p = packet.Packet(packet.UPGRADE).encode(always_bytes=False) - try: - await ws.send_str(p) - except Exception as e: # pragma: no cover - self.logger.warning( - 'WebSocket upgrade failed: unexpected send exception: %s', - str(e)) - return False - self.current_transport = 'websocket' - self.logger.info('WebSocket upgrade was successful') - else: - try: - p = (await ws.receive()).data - except Exception as e: # pragma: no cover - raise exceptions.ConnectionError( - 'Unexpected recv exception: ' + str(e)) - open_packet = packet.Packet(encoded_packet=p) - if open_packet.packet_type != packet.OPEN: - raise exceptions.ConnectionError('no OPEN packet') - self.logger.info( - 'WebSocket connection accepted with ' + str(open_packet.data)) - self.sid = open_packet.data['sid'] - self.upgrades = open_packet.data['upgrades'] - self.ping_interval = open_packet.data['pingInterval'] / 1000.0 - self.ping_timeout = open_packet.data['pingTimeout'] / 1000.0 - self.current_transport = 'websocket' - - self.state = 'connected' - client.connected_clients.append(self) - await self._trigger_event('connect', run_async=False) - - self.ws = ws - self.ping_loop_task = self.start_background_task(self._ping_loop) - self.write_loop_task = self.start_background_task(self._write_loop) - self.read_loop_task = self.start_background_task( - self._read_loop_websocket) - return True - - AsyncClient._connect_websocket = _connect_websocket # pylint: disable=protected-access - - -monkey_patch_engineio() - - @app_module_setup(__name__, ModuleCategory.SYSTEM, logger=log) def setup(app: web.Application): mgr = None diff --git a/services/web/server/tests/unit/with_dbs/test_resource_manager.py b/services/web/server/tests/unit/with_dbs/test_resource_manager.py index 79b33c3f72e..2b3cf1ff176 100644 --- a/services/web/server/tests/unit/with_dbs/test_resource_manager.py +++ b/services/web/server/tests/unit/with_dbs/test_resource_manager.py @@ -159,8 +159,9 @@ async def test_anonymous_websocket_connection( socket_connect_error = mocker.Mock() sio.on("connect_error", handler=socket_connect_error) - await sio.connect(url, headers=headers) - assert sio.sid + with pytest.raises(socketio.exceptions.ConnectionError): + await sio.connect(url, headers=headers) + assert sio.sid is None socket_connect_error.assert_called_once() await sio.disconnect() assert not sio.sid diff --git a/tests/swarm-deploy/requirements/requirements.txt b/tests/swarm-deploy/requirements/requirements.txt index 4ed25263d76..262f8f097a0 100644 --- a/tests/swarm-deploy/requirements/requirements.txt +++ b/tests/swarm-deploy/requirements/requirements.txt @@ -24,7 +24,7 @@ pluggy==0.13.1 # via pytest py==1.8.1 # via pytest pyparsing==2.4.7 # via packaging pytest-aiohttp==0.3.0 # via -r requirements/requirements.in -pytest-cov==2.8.1 # via -r requirements/requirements.in +pytest-cov==2.9.0 # via -r requirements/requirements.in pytest-instafail==0.4.1.post0 # via -r requirements/requirements.in pytest-mock==3.1.0 # via -r requirements/requirements.in pytest-runner==5.2 # via -r requirements/requirements.in @@ -32,7 +32,7 @@ pytest-sugar==0.9.3 # via -r requirements/requirements.in pytest==5.4.2 # via -r requirements/requirements.in, pytest-aiohttp, pytest-cov, pytest-instafail, pytest-mock, pytest-sugar pyyaml==5.3.1 # via -r requirements/requirements.in requests==2.23.0 # via docker -six==1.14.0 # via docker, packaging, tenacity, websocket-client +six==1.15.0 # via docker, packaging, tenacity, websocket-client tenacity==6.2.0 # via -r requirements/requirements.in termcolor==1.1.0 # via pytest-sugar typing-extensions==3.7.4.2 # via aiohttp From faad95eaa557ec895361a994eba0f44a580c61e2 Mon Sep 17 00:00:00 2001 From: Pedro Crespo <32402063+pcrespov@users.noreply.github.com> Date: Fri, 29 May 2020 15:09:55 +0200 Subject: [PATCH 4/6] [bugfix] storage service fails when blackfynn API is not responsive (#1536) Fixes issue #1506 * Catches exceptions raised by bf client * Tests datcore unavailable --- .../src/simcore_service_storage/datcore.py | 19 +-- .../datcore_wrapper.py | 122 +++++++++--------- services/storage/tests/test_datcore.py | 36 +++++- 3 files changed, 101 insertions(+), 76 deletions(-) diff --git a/services/storage/src/simcore_service_storage/datcore.py b/services/storage/src/simcore_service_storage/datcore.py index 12cc71c95f1..ddc72bc8cd0 100644 --- a/services/storage/src/simcore_service_storage/datcore.py +++ b/services/storage/src/simcore_service_storage/datcore.py @@ -22,14 +22,6 @@ DatasetMetaDataVec = List[DatasetMetaData] -# FIXME: W0611:Unused IOAPI imported from blackfynn.api.transfers -# from blackfynn.api.transfers import IOAPI - - -# FIXME: W0212:Access to a protected member _api of a client class -# pylint: disable=W0212 - - def _get_collection_id( folder: BaseCollection, _collections: List[str], collection_id: str ) -> str: @@ -53,6 +45,7 @@ def _get_collection_id( class DatcoreClient(object): def __init__(self, api_token=None, api_secret=None, host=None, streaming_host=None): + # WARNING: contruction raise exception if service is not available. Use datacore_wrapper for safe calls self.client = Blackfynn( profile=None, api_token=api_token, @@ -310,8 +303,9 @@ def upload_file(self, destination: str, filepath: str, meta_data=None): if collection is None: return False - files = [filepath] - # pylint: disable = E1101 + files = [ + filepath, + ] self.client._api.io.upload_files(collection, files, display_progress=True) collection.update() @@ -349,10 +343,9 @@ def download_file(self, source, filename, destination_path): destination__apth (str): Path on host for storing file """ - # pylint: disable = E1101 url = self.download_link(source, filename) if url: - _file = urllib.URLopener() + _file = urllib.URLopener() # nosec _file.retrieve(url, destination_path) return True return False @@ -362,7 +355,7 @@ def download_link(self, destination, filename): returns presigned url for download, destination is a dataset or collection """ collection, collection_id = self._collection_from_destination(destination) - # pylint: disable = E1101 + for item in collection: if isinstance(item, DataPackage): if Path(item.files[0].as_dict()["content"]["s3key"]).name == filename: diff --git a/services/storage/src/simcore_service_storage/datcore_wrapper.py b/services/storage/src/simcore_service_storage/datcore_wrapper.py index 1491a918ca6..46c756b29b6 100644 --- a/services/storage/src/simcore_service_storage/datcore_wrapper.py +++ b/services/storage/src/simcore_service_storage/datcore_wrapper.py @@ -2,6 +2,7 @@ import json import logging from concurrent.futures import ThreadPoolExecutor +from contextlib import contextmanager from functools import wraps from pathlib import Path from typing import List @@ -17,10 +18,21 @@ CURRENT_DIR = Path(__file__).resolve().parent logger = logging.getLogger(__name__) -# FIXME: W0703: Catching too general exception Exception (broad-except) # pylint: disable=W0703 +@contextmanager +def safe_call(error_msg: str = "", *, skip_logs: bool = False): + try: + yield + except AttributeError: + if not skip_logs: + logger.warning("Calling disabled client. %s", error_msg) + except Exception: # pylint: disable=broad-except + if error_msg and not skip_logs: + logger.warning(error_msg, exc_info=True) + + # TODO: Use async callbacks for retreival of progress and pass via rabbit to server def make_async(func): @wraps(func) @@ -41,10 +53,10 @@ class DatcoreWrapper: This can go away now. Next cleanup round... + NOTE: Auto-disables client + """ - # pylint: disable=R0913 - # Too many arguments def __init__( self, api_token: str, api_secret: str, loop: object, pool: ThreadPoolExecutor ): @@ -54,27 +66,44 @@ def __init__( self.loop = loop self.pool = pool - self.d_client = DatcoreClient( - api_token=api_token, api_secret=api_secret, host="https://api.blackfynn.io" - ) + try: + self.d_client = DatcoreClient( + api_token=api_token, + api_secret=api_secret, + host="https://api.blackfynn.io", + ) + except Exception: + self.d_client = None # Disabled: any call will raise AttributeError + logger.warning( + "Failed to setup datcore. Disabling client.", exc_info=True + ) + + @property + def is_communication_enabled(self) -> bool: + """ Wrapper class auto-disables if client cannot be created + + e.g. if endpoint service is down + + :return: True if communication with datcore is enabled + :rtype: bool + """ + return self.d_client is not None @make_async def list_files_recursively(self) -> FileMetaDataVec: # pylint: disable=W0613 files = [] - try: + + with safe_call(error_msg="Error listing datcore files"): files = self.d_client.list_files_recursively() - except Exception: - logger.exception("Error listing datcore files") return files @make_async def list_files_raw(self) -> FileMetaDataExVec: # pylint: disable=W0613 files = [] - try: + + with safe_call(error_msg="Error listing datcore files"): files = self.d_client.list_files_raw() - except Exception: - logger.exception("Error listing datcore files") return files @@ -83,35 +112,28 @@ def list_files_raw_dataset( self, dataset_id: str ) -> FileMetaDataExVec: # pylint: disable=W0613 files = [] - try: + with safe_call(error_msg="Error listing datcore files"): files = self.d_client.list_files_raw_dataset(dataset_id) - except Exception: - logger.exception("Error listing datcore files") return files @make_async def delete_file(self, destination: str, filename: str): # the object can be found in dataset/filename <-> bucket_name/object_name - try: + with safe_call(error_msg="Error deleting datcore file"): self.d_client.delete_file(destination, filename) - except Exception: - logger.exception("Error deleting datcore file") @make_async def delete_file_by_id(self, file_id: str): - try: + + with safe_call(error_msg="Error deleting datcore file"): self.d_client.delete_file_by_id(file_id) - except Exception: - logger.exception("Error deleting datcore file") @make_async def download_link(self, destination: str, filename: str): url = "" - try: + with safe_call(error_msg="Error getting datcore download link"): url = self.d_client.download_link(destination, filename) - except Exception: - logger.exception("Error getting datcore download link") return url @@ -119,91 +141,73 @@ def download_link(self, destination: str, filename: str): def download_link_by_id(self, file_id: str): url = "" filename = "" - try: + with safe_call(error_msg="Error getting datcore download link"): url, filename = self.d_client.download_link_by_id(file_id) - except Exception: - logger.exception("Error getting datcore download link") return url, filename @make_async def create_test_dataset(self, dataset): - try: + + with safe_call(error_msg="Error creating test dataset"): ds = self.d_client.get_dataset(dataset) if ds is not None: self.d_client.delete_files(dataset) else: ds = self.d_client.create_dataset(dataset) return ds.id - except Exception: - logger.exception("Error creating test dataset") - return "" @make_async def delete_test_dataset(self, dataset): - try: + + with safe_call(error_msg="Error deleting test dataset"): ds = self.d_client.get_dataset(dataset) if ds is not None: self.d_client.delete_files(dataset) - except Exception: - logger.exception("Error deleting test dataset") @make_async def upload_file( self, destination: str, local_path: str, meta_data: FileMetaData = None ): - json_meta = "" - if meta_data: - json_meta = json.dumps(attr.asdict(meta_data)) - try: - str_meta = json_meta - result = False + result = False + str_meta = json.dumps(attr.asdict(meta_data)) if meta_data else "" + + with safe_call(error_msg="Error uploading file to datcore"): if str_meta: meta_data = json.loads(str_meta) result = self.d_client.upload_file(destination, local_path, meta_data) else: result = self.d_client.upload_file(destination, local_path) - return result - except Exception: - logger.exception("Error uploading file to datcore") - return False + return result @make_async def upload_file_to_id(self, destination_id: str, local_path: str): _id = "" - try: + + with safe_call(error_msg="Error uploading file to datcore"): _id = self.d_client.upload_file_to_id(destination_id, local_path) - except Exception: - logger.exception("Error uploading file to datcore") return _id @make_async def create_collection(self, destination_id: str, collection_name: str): - _id = "" - try: + with safe_call(error_msg="Error creating collection in datcore"): _id = self.d_client.create_collection(destination_id, collection_name) - except Exception: - logger.exception("Error creating collection in datcore") return _id @make_async def list_datasets(self): data = [] - try: + with safe_call(error_msg="Error creating collection in datcore"): data = self.d_client.list_datasets() - except Exception: - logger.exception("Error creating collection in datcore") return data @make_async def ping(self): - try: + ok = False + with safe_call(skip_logs=True): profile = self.d_client.profile() ok = profile is not None - return ok - except Exception: - logger.exception("Error pinging") - return False + return ok diff --git a/services/storage/tests/test_datcore.py b/services/storage/tests/test_datcore.py index 90bcac0c3ca..0bc6244b98a 100644 --- a/services/storage/tests/test_datcore.py +++ b/services/storage/tests/test_datcore.py @@ -1,8 +1,8 @@ -# TODO: W0611:Unused import ... -# pylint: disable=W0611 -# TODO: W0613:Unused argument ... -# pylint: disable=W0613 +# pylint:disable=unused-variable +# pylint:disable=unused-argument +# pylint:disable=redefined-outer-name +import importlib import os from concurrent.futures import ThreadPoolExecutor from pathlib import Path @@ -10,9 +10,37 @@ import pytest import utils +from simcore_service_storage import datcore from simcore_service_storage.datcore_wrapper import DatcoreWrapper +@pytest.fixture() +def mocked_blackfynn_unavailable(mocker): + def raise_error(*args, **kargs): + raise RuntimeError("mocked_blackfynn_unavailable") + + mock = mocker.patch("blackfynn.Blackfynn", raise_error) + importlib.reload(datcore) + return mock + + +async def test_datcore_unavailable(loop, mocked_blackfynn_unavailable): + api_token = os.environ.get("BF_API_KEY", "none") + api_secret = os.environ.get("BF_API_SECRET", "none") + pool = ThreadPoolExecutor(2) + + # must NOT raise but only returns empties + dcw = DatcoreWrapper(api_token, api_secret, loop, pool) + + assert not dcw.is_communication_enabled + + responsive = await dcw.ping() + assert not responsive + + res = await dcw.list_files_raw() + assert res == [] + + async def test_datcore_ping(loop): if not utils.has_datcore_tokens(): return From 8d7fa6650ed850f2add64b2854b906374f517f33 Mon Sep 17 00:00:00 2001 From: sanderegg <35365065+sanderegg@users.noreply.github.com> Date: Fri, 29 May 2020 16:33:55 +0200 Subject: [PATCH 5/6] typo in instructions --- docs/staging-instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/staging-instructions.md b/docs/staging-instructions.md index 3d706b08eb7..8b0a1e6d085 100644 --- a/docs/staging-instructions.md +++ b/docs/staging-instructions.md @@ -17,7 +17,7 @@ The process of moving code from [Master](https://github.com/ITISFoundation/ospar git push --set-upstream origin FREEZE_${SPRINT_NAME} # create the log entries to be copied into the pull request #body=scripts/url-encoder.sh "$(git log --oneline staging..HEAD --no-decorate)" - body=$(scripts/url-encoder.sh "$(git log staging..HEAD --pretty="format:- %s")") + body=$(scripts/url-encoder.bash "$(git log staging..HEAD --pretty="format:- %s")") # open the PR on github website echo "https://github.com/ITISFoundation/osparc-simcore/compare/staging...FREEZE_${SPRINT_NAME}?expand=1&title=FREEZE_${SPRINT_NAME}&body=$body" From 8c2155d2ef01ea6ee128bbbd659c6e6858300339 Mon Sep 17 00:00:00 2001 From: sanderegg <35365065+sanderegg@users.noreply.github.com> Date: Fri, 29 May 2020 16:40:08 +0200 Subject: [PATCH 6/6] get data from origin --- docs/staging-instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/staging-instructions.md b/docs/staging-instructions.md index 8b0a1e6d085..4a54fcfca04 100644 --- a/docs/staging-instructions.md +++ b/docs/staging-instructions.md @@ -17,7 +17,7 @@ The process of moving code from [Master](https://github.com/ITISFoundation/ospar git push --set-upstream origin FREEZE_${SPRINT_NAME} # create the log entries to be copied into the pull request #body=scripts/url-encoder.sh "$(git log --oneline staging..HEAD --no-decorate)" - body=$(scripts/url-encoder.bash "$(git log staging..HEAD --pretty="format:- %s")") + body=$(scripts/url-encoder.bash "$(git log origin/staging..HEAD --pretty="format:- %s")") # open the PR on github website echo "https://github.com/ITISFoundation/osparc-simcore/compare/staging...FREEZE_${SPRINT_NAME}?expand=1&title=FREEZE_${SPRINT_NAME}&body=$body"