Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/registry index #184

Merged
merged 9 commits into from
Dec 22, 2021
29 changes: 29 additions & 0 deletions lifemonitor/api/controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,35 @@ def workflow_registries_get_by_uuid(registry_uuid):
return serializers.WorkflowRegistrySchema().dump(registry)


@authorized
@cached(timeout=Timeout.REQUEST)
def registry_index(registry_uuid):
if not current_user:
return lm_exceptions.report_problem(401, "Unauthorized")
registry = lm.get_workflow_registry_by_uuid(registry_uuid)
if not registry:
return lm_exceptions.report_problem(404, "Not Found",
detail=messages.no_registry_found.format(registry_uuid))
workflows = registry.get_index(current_user)
return serializers.ListOfRegistryIndexItemsSchema().dump(workflows)


@authorized
@cached(timeout=Timeout.REQUEST)
def registry_index_workflow(registry_uuid, registry_workflow_identifier):
if not current_user:
return lm_exceptions.report_problem(401, "Unauthorized")
registry = lm.get_workflow_registry_by_uuid(registry_uuid)
if not registry:
return lm_exceptions.report_problem(404, "Not Found",
detail=messages.no_registry_found.format(registry_uuid))
workflow = registry.get_index_workflow(current_user, registry_workflow_identifier)
if not workflow:
return lm_exceptions.report_problem(404, "Not Found",
detail=messages.workflow_not_found.format(registry_workflow_identifier, 'latest'))
return serializers.RegistryIndexItemSchema().dump(workflow)


@authorized
@cached(timeout=Timeout.REQUEST)
def workflow_registries_get_current():
Expand Down
3 changes: 2 additions & 1 deletion lifemonitor/api/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from .status import Status, AggregateTestStatus, WorkflowStatus, SuiteStatus

# 'registries' package
from .registries import WorkflowRegistry, WorkflowRegistryClient
from .registries import RegistryWorkflow, WorkflowRegistry, WorkflowRegistryClient

# 'workflows' package
from .workflows import Workflow, WorkflowVersion
Expand Down Expand Up @@ -72,6 +72,7 @@
"WorkflowRegistryClient",
"WorkflowStatus",
"WorkflowVersion",
"RegistryWorkflow"
]

# set module level logger
Expand Down
4 changes: 2 additions & 2 deletions lifemonitor/api/models/registries/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@
from __future__ import annotations

from lifemonitor.utils import ClassManager
from .registry import WorkflowRegistry, WorkflowRegistryClient
from .registry import RegistryWorkflow, WorkflowRegistry, WorkflowRegistryClient


__all__ = [WorkflowRegistry, WorkflowRegistryClient] + \
__all__ = [RegistryWorkflow, WorkflowRegistry, WorkflowRegistryClient] + \
ClassManager('lifemonitor.api.models.registries',
class_suffix="WorkflowRegistry", skip=["registry"], lazy=False).get_classes()
57 changes: 57 additions & 0 deletions lifemonitor/api/models/registries/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,51 @@
logger = logging.getLogger(__name__)


class RegistryWorkflow(object):
_registry: WorkflowRegistry
_identifier: str
_name: str
_latest_version: str
_versions: List[str] = None

def __init__(self,
registry: WorkflowRegistry,
identifier: str,
name: str,
latest_version: str = None,
versions: List[str] = None) -> None:
self._registry = registry
self._identifier = identifier
self._name = name
self._latest_version = latest_version
if versions:
self._versions = versions.copy()

@property
def registry(self) -> WorkflowRegistry:
return self._registry

@property
def identifier(self) -> str:
return self._identifier

@property
def name(self) -> str:
return self._name

@property
def latest_version(self) -> str:
return self._latest_version

@property
def versions(self) -> List[str]:
return self._versions.copy()

@property
def external_link(self) -> str:
return self._registry.get_external_link(self._identifier, self._latest_version)


class WorkflowRegistryClient(ABC):

client_types = ClassManager('lifemonitor.api.models.registries', class_suffix="WorkflowRegistryClient", skip=["registry"])
Expand Down Expand Up @@ -78,6 +123,12 @@ def _get(self, user, *args, **kwargs):
response.raise_for_status()
return response

def get_index(self, user: auth_models.User) -> List[RegistryWorkflow]:
pass

def get_index_workflow(self, user: auth_models.User, workflow_identifier: str) -> RegistryWorkflow:
pass

def download_url(self, url, user, target_path=None):
return download_url(url, target_path,
authorization=f'Bearer {self._get_access_token(user.id)["access_token"]}')
Expand Down Expand Up @@ -240,6 +291,12 @@ def get_user_workflows(self, user: auth_models.User) -> List[models.Workflow]:
def get_user_workflow_versions(self, user: auth_models.User) -> List[models.WorkflowVersion]:
return self.client.filter_by_user(self.registered_workflow_versions, user)

def get_index(self, user: auth_models.User) -> List[RegistryWorkflow]:
return self.client.get_index(user)

def get_index_workflow(self, user: auth_models.User, workflow_identifier: str) -> RegistryWorkflow:
return self.client.get_index_workflow(user, workflow_identifier)

@classmethod
def all(cls) -> List[WorkflowRegistry]:
return cls.query.all()
Expand Down
29 changes: 25 additions & 4 deletions lifemonitor/api/models/registries/seek.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,16 @@
from __future__ import annotations

import logging
from typing import Union
from typing import List, Union

import requests
from lifemonitor.api import models
from lifemonitor.auth.models import User
from lifemonitor.exceptions import EntityNotFoundException
from lifemonitor.exceptions import (EntityNotFoundException,
LifeMonitorException)

from .registry import WorkflowRegistry, WorkflowRegistryClient
from .registry import (RegistryWorkflow, WorkflowRegistry,
WorkflowRegistryClient)

# set module level logger
logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -63,8 +66,26 @@ def get_workflow_metadata(self, user, w: Union[models.WorkflowVersion, str]):
raise RuntimeError(f"ERROR: unable to get workflow (status code: {r.status_code})")
return r.json()['data']

def get_index(self, user: User) -> List[RegistryWorkflow]:
result = []
for w in self.get_workflows_metadata(user):
result.append(RegistryWorkflow(self.registry, w['id'], w['attributes']['title']))
return result

def get_index_workflow(self, user: User, workflow_identifier: str) -> RegistryWorkflow:
try:
w = self.get_workflow_metadata(user, workflow_identifier)
return RegistryWorkflow(self.registry, w['id'], w['attributes']['title'],
latest_version=w['attributes']['version'],
versions=[_['version'] for _ in w['attributes']['versions']]) if w else None
except requests.exceptions.HTTPError as e:
if e.response.status_code == 404:
raise EntityNotFoundException(WorkflowRegistry, entity_id=workflow_identifier)
raise LifeMonitorException(original_error=e)

def get_external_link(self, external_id: str, version: str) -> str:
return f"{self.registry.uri}/workflows/{external_id}?version={version}"
version_param = '' if not version or version == 'latest' else f"?version={version}"
return f"{self.registry.uri}/workflows/{external_id}{version_param}"

def get_rocrate_external_link(self, external_id: str, version: str) -> str:
return f'{self.registry.uri}/workflows/{external_id}/ro_crate?version={version}'
Expand Down
37 changes: 36 additions & 1 deletion lifemonitor/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,41 @@ class ListOfWorkflowRegistriesSchema(ListOfItems):
__item_scheme__ = WorkflowRegistrySchema


class RegistryIndexItemSchema(ResourceMetadataSchema):
__envelope__ = {"single": None, "many": "items"}
__model__ = models.RegistryWorkflow

class Meta:
model = models.RegistryWorkflow

identifier = fields.String(attribute="identifier")
name = fields.String(attribute="name")
latest_version = fields.String(attribute="latest_version")
versions = fields.List(fields.String, attribute="versions")
registry = ma.Nested(WorkflowRegistrySchema(exclude=('meta', 'links')), attribute="registry")
links = fields.Method('get_links')

def get_links(self, obj):
links = ResourceMetadataSchema.get_links(self, obj)
if links is not None:
links['origin'] = obj.external_link
return links
return {
'origin': obj.external_link
}

@post_dump
def remove_skip_values(self, data, **kwargs):
return {
key: value for key, value in data.items()
if value is not None
}


class ListOfRegistryIndexItemsSchema(ListOfItems):
__item_scheme__ = RegistryIndexItemSchema


class WorkflowSchema(ResourceMetadataSchema):
__envelope__ = {"single": None, "many": "items"}
__model__ = models.WorkflowVersion
Expand Down Expand Up @@ -273,7 +308,7 @@ def format_availability_issues(status: models.WorkflowStatus):
issues = status.availability_issues
logger.info(issues)
if 'not_available' == status.aggregated_status and len(issues) > 0:
return ', '.join([f"{i['issue']}: Unable to get resource '{i['resource']}' from service '{i['service']}'" if 'service' in i else i['issue'] for i in issues])
return ', '.join([f"{i['issue']}: Unable to get resource '{i['resource']}' from service '{i['service']}'" if 'service' in i and 'resource' in i else i['issue'] for i in issues])
return None


Expand Down
109 changes: 109 additions & 0 deletions specs/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,65 @@ paths:
"404":
$ref: "#/components/responses/NotFound"

/registries/{registry_uuid}/index:
get:
tags: ["Registries"]
x-openapi-router-controller: lifemonitor.api.controllers
operationId: "registry_index"
summary: "Get registry index"
description: "Get the index of workflows available on the registry"
security:
- apiKey: ["user.workflow.read"]
- RegistryCodeFlow: ["user.workflow.read"]
- AuthorizationCodeFlow: ["user.workflow.read"]
parameters:
- $ref: "#/components/parameters/registry_uuid"
responses:
"200":
description: List of workflows on the registry
content:
application/json:
schema:
$ref: "#/components/schemas/ListOfRegistryWorkflows"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"

/registries/{registry_uuid}/index/{registry_workflow_identifier}:
get:
tags: ["Registries"]
x-openapi-router-controller: lifemonitor.api.controllers
operationId: "registry_index_workflow"
summary: "Get registry index workflow"
description: "Get information about the specified workflow of the registry index"
security:
- apiKey: ["user.workflow.read"]
- RegistryCodeFlow: ["user.workflow.read"]
- AuthorizationCodeFlow: ["user.workflow.read"]
parameters:
- $ref: "#/components/parameters/registry_uuid"
- $ref: "#/components/parameters/registry_workflow_identifier"
responses:
"200":
description: List of workflows on the registry
content:
application/json:
schema:
$ref: "#/components/schemas/RegistryWorkflow"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"

/users/current:
get:
tags: ["Users"]
Expand Down Expand Up @@ -1118,6 +1177,14 @@ components:
type: string
required: true
example: aa16c6a9-571f-4a62-976f-60ea514ad2c1
registry_workflow_identifier:
name: "registry_workflow_identifier"
description: "Identifier of the workflow on the registry"
in: path
schema:
type: string
required: true
example: 123e4567-e89b-12d3-a456-426614174000
user_id:
name: "user_id"
description: "Registry user's identifier"
Expand Down Expand Up @@ -1547,6 +1614,48 @@ components:
# (e.g., `authorization: "ApiKey 1234567890"`).
# example: "Bearer xxx__ZRBhqf9eeRasjqMw90pgEeMpTZ7__"

RegistryWorkflowBase:
allOf:
- $ref: "#/components/schemas/WorkflowBase"
- type: object
description: Registry workflow
properties:
identifier:
type: string
description: |
The identifier of the workflow on the registry
example: 148
registry:
description:
$ref: "#/components/schemas/Registry"

RegistryWorkflow:
allOf:
- $ref: "#/components/schemas/RegistryWorkflowBase"
- type: object
description: Registry workflow
properties:
latest_version:
type: string
description: The workflow identifier on the registry
example: '1.0'
versions:
description: The list of workflow versions
type: array
items:
type: string
example: ['1.0', "1.0-dev"]

ListOfRegistryWorkflows:
type: object
properties:
items:
type: array
items:
$ref: "#/components/schemas/RegistryWorkflowBase"
required:
- items

WorkflowVersionBase:
allOf:
- $ref: "#/components/schemas/WorkflowBase"
Expand Down