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: Add Group and Group Membership data models to restapi #329

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/dioptra/pyplugs/_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,7 @@
class NoutPlugin(Protocol):
_task_nout: int

def __call__(self, *args, **kwargs) -> Any:
... # pragma: nocover
def __call__(self, *args, **kwargs) -> Any: ... # pragma: nocover # noqa E704


# Type aliases
Expand Down
12 changes: 12 additions & 0 deletions src/dioptra/restapi/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ def bind_dependencies(binder: Binder) -> None:
binder: A :py:class:`~injector.Binder` object.
"""
from .experiment import bind_dependencies as attach_experiment_dependencies
from .group import bind_dependencies as attach_group_dependencies
from .group_membership import (
bind_dependencies as attach_group_membership_dependencies,
)
from .job import bind_dependencies as attach_job_dependencies
from .queue import bind_dependencies as attach_job_queue_dependencies
from .task_plugin import bind_dependencies as attach_task_plugin_dependencies
Expand All @@ -40,6 +44,8 @@ def bind_dependencies(binder: Binder) -> None:
attach_job_queue_dependencies(binder)
attach_task_plugin_dependencies(binder)
attach_user_dependencies(binder)
attach_group_dependencies(binder)
attach_group_membership_dependencies(binder)


def register_providers(modules: List[Callable[..., Any]]) -> None:
Expand All @@ -50,6 +56,10 @@ def register_providers(modules: List[Callable[..., Any]]) -> None:
environment.
"""
from .experiment import register_providers as attach_experiment_providers
from .group import register_providers as attach_group_providers
from .group_membership import (
register_providers as attach_group_membership_providers,
)
from .job import register_providers as attach_job_providers
from .queue import register_providers as attach_job_queue_providers
from .task_plugin import register_providers as attach_task_plugin_providers
Expand All @@ -61,3 +71,5 @@ def register_providers(modules: List[Callable[..., Any]]) -> None:
attach_job_queue_providers(modules)
attach_task_plugin_providers(modules)
attach_user_providers(modules)
attach_group_providers(modules)
attach_group_membership_providers(modules)
6 changes: 3 additions & 3 deletions src/dioptra/restapi/experiment/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,9 @@ def create(

def create_mlflow_experiment(self, experiment_name: str) -> int:
try:
experiment_id: Optional[
str
] = self._mlflow_tracking_service.create_experiment(experiment_name)
experiment_id: Optional[str] = (
self._mlflow_tracking_service.create_experiment(experiment_name)
)

except RestException as exc:
raise ExperimentMLFlowTrackingRegistrationError from exc
Expand Down
28 changes: 28 additions & 0 deletions src/dioptra/restapi/group/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# This Software (Dioptra) is being made available as a public service by the
# National Institute of Standards and Technology (NIST), an Agency of the United
# States Department of Commerce. This software was developed in part by employees of
# NIST and in part by NIST contractors. Copyright in portions of this software that
# were developed by NIST contractors has been licensed or assigned to NIST. Pursuant
# to Title 17 United States Code Section 105, works of NIST employees are not
# subject to copyright protection in the United States. However, NIST may hold
# international copyright in software created by its employees and domestic
# copyright (or licensing rights) in portions of software that were assigned or
# licensed to NIST. To the extent that NIST holds copyright in this software, it is
# being made available under the Creative Commons Attribution 4.0 International
# license (CC BY 4.0). The disclaimers of the CC BY 4.0 license apply to all parts
# of the software developed or licensed by NIST.
#
# ACCESS THE FULL CC BY 4.0 LICENSE HERE:
# https://creativecommons.org/licenses/by/4.0/legalcode
"""The group endpoint subpackage."""

from .dependencies import bind_dependencies, register_providers
from .errors import register_error_handlers
from .routes import register_routes

__all__ = [
"bind_dependencies",
"register_error_handlers",
"register_providers",
"register_routes",
]
112 changes: 112 additions & 0 deletions src/dioptra/restapi/group/controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# This Software (Dioptra) is being made available as a public service by the
# National Institute of Standards and Technology (NIST), an Agency of the United
# States Department of Commerce. This software was developed in part by employees of
# NIST and in part by NIST contractors. Copyright in portions of this software that
# were developed by NIST contractors has been licensed or assigned to NIST. Pursuant
# to Title 17 United States Code Section 105, works of NIST employees are not
# subject to copyright protection in the United States. However, NIST may hold
# international copyright in software created by its employees and domestic
# copyright (or licensing rights) in portions of software that were assigned or
# licensed to NIST. To the extent that NIST holds copyright in this software, it is
# being made available under the Creative Commons Attribution 4.0 International
# license (CC BY 4.0). The disclaimers of the CC BY 4.0 license apply to all parts
# of the software developed or licensed by NIST.
#
# ACCESS THE FULL CC BY 4.0 LICENSE HERE:
# https://creativecommons.org/licenses/by/4.0/legalcode
"""The module defining the group endpoints."""
from __future__ import annotations

import uuid
from typing import Any, cast

import structlog
from flask import request
from flask_accepts import accepts, responds
from flask_restx import Namespace, Resource
from injector import inject
from structlog.stdlib import BoundLogger

from .model import Group
from .schema import GroupSchema, IdStatusResponseSchema
from .service import GroupService

LOGGER: BoundLogger = structlog.stdlib.get_logger()

api: Namespace = Namespace(
"Group",
description="Group submission and management operations",
)


@api.route("/")
class GroupResource(Resource):
"""Shows a list of all Group, and lets you POST to create new groups."""

@inject
def __init__(
self,
*args,
group_service: GroupService,
**kwargs,
) -> None:
self._group_service = group_service
super().__init__(*args, **kwargs)

@responds(schema=GroupSchema(many=True), api=api)
def get(self) -> list[Group]:
"""Gets a list of all groups."""
log: BoundLogger = LOGGER.new(
request_id=str(uuid.uuid4()), resource="group", request_type="GET"
) # noqa: F841
log.info("Request received")
return self._group_service.get_all(log=log)

@accepts(schema=GroupSchema, api=api)
@responds(schema=GroupSchema, api=api)
def post(self) -> Group:
"""Creates a new Group via a group submission form with an attached file."""
log: BoundLogger = LOGGER.new(
request_id=str(uuid.uuid4()), resource="group", request_type="POST"
) # noqa: F841

log.info("Request received")

parsed_obj = request.parsed_obj # type: ignore
name = str(parsed_obj["group_name"])
return self._group_service.create(name=name, log=log)

@accepts(schema=GroupSchema, api=api)
@responds(schema=IdStatusResponseSchema, api=api)
def delete(self) -> dict[str, Any]:
log: BoundLogger = LOGGER.new(
request_id=str(uuid.uuid4()), resource="group", request_type="POST"
) # noqa: F841

log.info("Request received")

parsed_obj = request.parsed_obj # type: ignore
group_id = int(parsed_obj["id"])
return self._group_service.delete(id=group_id)


@api.route("/<int:groupId>")
@api.param("groupId", "A string specifying a group's UUID.")
class GroupIdResource(Resource):
"""Shows a single job."""

@inject
def __init__(self, *args, _service: GroupService, **kwargs) -> None:
self._group_service = _service
super().__init__(*args, **kwargs)

@responds(schema=GroupSchema, api=api)
def get(self, groupId: int) -> Group:
"""Gets a group by its unique identifier."""
log: BoundLogger = LOGGER.new(
request_id=str(uuid.uuid4()), resource="groupId", request_type="GET"
) # noqa: F841
log.info("Request received", group_id=groupId)
group = self._group_service.get(groupId, error_if_not_found=True, log=log)

return cast(Group, group)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if necessary, service returns Group type already.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mypy raises an error as it interprets the return from the query as Any.

51 changes: 51 additions & 0 deletions src/dioptra/restapi/group/dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# This Software (Dioptra) is being made available as a public service by the
# National Institute of Standards and Technology (NIST), an Agency of the United
# States Department of Commerce. This software was developed in part by employees of
# NIST and in part by NIST contractors. Copyright in portions of this software that
# were developed by NIST contractors has been licensed or assigned to NIST. Pursuant
# to Title 17 United States Code Section 105, works of NIST employees are not
# subject to copyright protection in the United States. However, NIST may hold
# international copyright in software created by its employees and domestic
# copyright (or licensing rights) in portions of software that were assigned or
# licensed to NIST. To the extent that NIST holds copyright in this software, it is
# being made available under the Creative Commons Attribution 4.0 International
# license (CC BY 4.0). The disclaimers of the CC BY 4.0 license apply to all parts
# of the software developed or licensed by NIST.
#
# ACCESS THE FULL CC BY 4.0 LICENSE HERE:
# https://creativecommons.org/licenses/by/4.0/legalcode
"""Binding configurations to shared services using dependency injection."""
from __future__ import annotations

from typing import Any, Callable

from injector import Binder, Module, provider

from .service import GroupService


class GroupServiceModule(Module):
@provider
def provide_queue_name_service_module(
self,
) -> GroupService:
return GroupService()


def bind_dependencies(binder: Binder) -> None:
"""Binds interfaces to implementations within the main application.

Args:
binder: A :py:class:`~injector.Binder` object.
"""
pass


def register_providers(modules: list[Callable[..., Any]]) -> None:
"""Registers type providers within the main application.

Args:
modules: A list of callables used for configuring the dependency injection
environment.
"""
modules.append(GroupServiceModule)
44 changes: 44 additions & 0 deletions src/dioptra/restapi/group/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# This Software (Dioptra) is being made available as a public service by the
# National Institute of Standards and Technology (NIST), an Agency of the United
# States Department of Commerce. This software was developed in part by employees of
# NIST and in part by NIST contractors. Copyright in portions of this software that
# were developed by NIST contractors has been licensed or assigned to NIST. Pursuant
# to Title 17 United States Code Section 105, works of NIST employees are not
# subject to copyright protection in the United States. However, NIST may hold
# international copyright in software created by its employees and domestic
# copyright (or licensing rights) in portions of software that were assigned or
# licensed to NIST. To the extent that NIST holds copyright in this software, it is
# being made available under the Creative Commons Attribution 4.0 International
# license (CC BY 4.0). The disclaimers of the CC BY 4.0 license apply to all parts
# of the software developed or licensed by NIST.
#
# ACCESS THE FULL CC BY 4.0 LICENSE HERE:
# https://creativecommons.org/licenses/by/4.0/legalcode
"""Error handlers for the group endpoints."""
from __future__ import annotations

from flask_restx import Api


class GroupDoesNotExistError(Exception):
"""The requested group does not exist."""


class GroupSubmissionError(Exception):
"""The Group submission form contains invalid parameters."""


def register_error_handlers(api: Api) -> None:
@api.errorhandler(GroupDoesNotExistError)
def handle_job_does_not_exist_error(error):
return {"message": "Not Found - The requested group does not exist"}, 404

@api.errorhandler(GroupSubmissionError)
def handle_job_submission_error(error):
return (
{
"message": "Bad Request - The group submission form contains "
"invalid parameters. Please verify and resubmit."
},
400,
)
99 changes: 99 additions & 0 deletions src/dioptra/restapi/group/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# This Software (Dioptra) is being made available as a public service by the
# National Institute of Standards and Technology (NIST), an Agency of the United
# States Department of Commerce. This software was developed in part by employees of
# NIST and in part by NIST contractors. Copyright in portions of this software that
# were developed by NIST contractors has been licensed or assigned to NIST. Pursuant
# to Title 17 United States Code Section 105, works of NIST employees are not
# subject to copyright protection in the United States. However, NIST may hold
# international copyright in software created by its employees and domestic
# copyright (or licensing rights) in portions of software that were assigned or
# licensed to NIST. To the extent that NIST holds copyright in this software, it is
# being made available under the Creative Commons Attribution 4.0 International
# license (CC BY 4.0). The disclaimers of the CC BY 4.0 license apply to all parts
# of the software developed or licensed by NIST.
#
# ACCESS THE FULL CC BY 4.0 LICENSE HERE:
# https://creativecommons.org/licenses/by/4.0/legalcode
"""The data models for the job endpoint objects."""
from __future__ import annotations

from dioptra.restapi.app import db
from dioptra.restapi.group_membership.model import GroupMembership
from dioptra.restapi.user.model import User


class Group(db.Model):
"""The Groups table.

Attributes:
group_id: The unique identifier of the group.
name: Human-readable name for the group.
creator_id: The id for the user that created the group.
owner_id: The id for the user that owns the group.
created_on: The time at which the group was created.
deleted: Whether the group has been deleted.
"""

__tablename__ = "groups"

group_id = db.Column(db.BigInteger(), primary_key=True)
name = db.Column(db.String(36))

creator_id = db.Column(db.BigInteger(), db.ForeignKey("users.user_id"), index=True)
owner_id = db.Column(db.BigInteger(), db.ForeignKey("users.user_id"), index=True)

created_on = db.Column(db.DateTime())
deleted = db.Column(db.Boolean)

creator = db.relationship("User", foreign_keys=[creator_id])
owner = db.relationship("User", foreign_keys=[owner_id])

@classmethod
def next_id(cls) -> int:
"""Generates the next id in the sequence."""
group: Group | None = cls.query.order_by(cls.group_id.desc()).first()

if group is None:
return 1

return int(group.id) + 1

@property
def users(self):
"""The users that are members of the group."""
return (
User.query.join(GroupMembership)
.filter(GroupMembership.group_id == self.group_id)
.all()
)

def check_membership(self, user: User) -> bool:
"""Check if the user has permission to perform the specified action.

Args:
user: The user to check.
action: The action to check.

Returns:
True if the user has permission to perform the action, False otherwise.
"""
membership = GroupMembership.query.filter_by(
GroupMembership.user_id == user.user_id,
GroupMembership.group_id == self.group_id,
)

if membership is None:
return False
else:
return True

def update(self, changes: dict):
"""Updates the record.

Args:
changes: A dictionary containing record updates.
"""
for key, val in changes.items():
setattr(self, key, val)

return self
Loading
Loading