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

WIP: V1 artifact files endpoint changes #658

Open
wants to merge 12 commits into
base: dev
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion src/dioptra/restapi/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ def create_parameters_schema(
location = "files"

parameters_schema = ParametersSchema(
name=cast(str, field.name),
name=cast(str, field.data_key or field.name),
type=parameter_type,
location=location,
required=field.required,
Expand Down
80 changes: 72 additions & 8 deletions src/dioptra/restapi/v1/artifacts/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@
from __future__ import annotations

import uuid
from pathlib import Path
from typing import cast
from urllib.parse import unquote

import structlog
from flask import request
from flask import request, send_file
from flask_accepts import accepts, responds
from flask_login import login_required
from flask_restx import Namespace, Resource
Expand All @@ -31,13 +32,16 @@

from dioptra.restapi.db import models
from dioptra.restapi.routes import V1_ARTIFACTS_ROUTE
from dioptra.restapi.utils import as_api_parser, as_parameters_schema_list
from dioptra.restapi.v1 import utils
from dioptra.restapi.v1.shared.snapshots.controller import (
generate_resource_snapshots_endpoint,
generate_resource_snapshots_id_endpoint,
)

from .schema import (
ArtifactContentsGetQueryParameters,
ArtifactFileSchema,
ArtifactGetQueryParameters,
ArtifactMutableFieldsSchema,
ArtifactPageSchema,
Expand All @@ -46,6 +50,7 @@
from .service import (
RESOURCE_TYPE,
SEARCHABLE_FIELDS,
ArtifactIdContentsService,
Copy link
Collaborator

Choose a reason for hiding this comment

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

causes an import error right now. I realize you haven't worked on the service layer yet, but I found it useful to create a class stub, so flask would run and I could view the endpoint and schema in swagger.

ArtifactIdService,
ArtifactService,
)
Expand All @@ -69,7 +74,6 @@ def __init__(self, artifact_service: ArtifactService, *args, **kwargs) -> None:
self._artifact_service = artifact_service
super().__init__(*args, **kwargs)

@login_required
@accepts(query_params_schema=ArtifactGetQueryParameters, api=api)
@responds(schema=ArtifactPageSchema, api=api)
def get(self):
Expand Down Expand Up @@ -110,21 +114,30 @@ def get(self):
)

@login_required
@accepts(schema=ArtifactSchema, api=api)
@api.expect(
as_api_parser(
api,
as_parameters_schema_list(
ArtifactSchema, operation="load", location="form"
),
)
)
@accepts(form_schema=ArtifactSchema, api=api)
@responds(schema=ArtifactSchema, api=api)
def post(self):
"""Creates an Artifact resource."""
log = LOGGER.new(
request_id=str(uuid.uuid4()), resource="Artifact", request_type="POST"
)
log.debug("Request received")
parsed_obj = request.parsed_obj # noqa: F841
parsed_form = request.parsed_form # noqa: F841

artifact = self._artifact_service.create(
uri=parsed_obj["uri"],
description=parsed_obj["description"],
group_id=parsed_obj["group_id"],
job_id=parsed_obj["job_id"],
artifact_file=request.files.get("artifactFile", None),
artifact_type=parsed_form["artifact_type"],
description=parsed_form["description"],
group_id=parsed_form["group_id"],
job_id=parsed_form["job_id"],
log=log,
)
return utils.build_artifact(artifact)
Expand Down Expand Up @@ -179,6 +192,57 @@ def put(self, id: int):
return utils.build_artifact(artifact)


@api.route("/<int:id>/contents")
@api.param("id", "ID for the Artifact resource.")
class ArtifactIdContentsEndpoint(Resource):
@inject
def __init__(
self, artifact_id_contents_service: ArtifactIdContentsService, *args, **kwargs
) -> None:
"""Initialize the artifact id contents resource.

All arguments are provided via dependency injection.

Args:
artifact_id_contents_service: A ArtifactIdContentsService object.
"""
self._artifact_id_contents_service = artifact_id_contents_service
super().__init__(*args, **kwargs)

@login_required
@accepts(query_params_schema=ArtifactContentsGetQueryParameters, api=api)
@responds(schema=ArtifactFileSchema(many=True), api=api)
def get(self, id: int):
"""Gets a list of all files associated with an Artifact resource."""
log = LOGGER.new(
request_id=str(uuid.uuid4()), resource="Artifact", request_type="GET", id=id
)
parsed_query_params = request.parsed_query_params # type: ignore # noqa: F841

path = parsed_query_params["path"]
download = parsed_query_params["download"]

contents, is_dir, artifact_name = self._artifact_id_contents_service.get(
artifact_id=id,
path=path,
download=download,
log=log,
)

if path is None and is_dir:
path = Path(f"artifact_{id}.json").name
elif path is None:
path = artifact_name
else:
path = Path(path).name

return send_file(
path_or_file=contents,
as_attachment=download,
download_name=path,
)


ArtifactSnapshotsResource = generate_resource_snapshots_endpoint(
api=api,
resource_model=models.Artifact,
Expand Down
69 changes: 64 additions & 5 deletions src/dioptra/restapi/v1/artifacts/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
# ACCESS THE FULL CC BY 4.0 LICENSE HERE:
# https://creativecommons.org/licenses/by/4.0/legalcode
"""The schemas for serializing/deserializing Artifact resources."""
from marshmallow import Schema, fields
from marshmallow import Schema, fields, validate
from dioptra.restapi.custom_schema_fields import FileUpload

from dioptra.restapi.v1.schemas import (
BasePageSchema,
Expand All @@ -42,6 +43,37 @@ class ArtifactRefSchema(ArtifactRefBaseSchema): # type: ignore
)


class ArtifactFileMetadataSchema(Schema):
"""The schema for the artifact file metadata."""

fileType = fields.String(
attribute="file_type",
metadata=dict(description="The type of the file."),
dump_only=True,
)
fileSize = fields.Integer(
attribute="file_size",
metadata=dict(description="The size in bytes of the file."),
dump_only=True,
)
fileUrl = fields.Url(
attribute="file_url",
metadata=dict(description="URL for accessing the contents of the file."),
relative=True,
dump_only=True,
)


class ArtifactFileSchema(ArtifactFileMetadataSchema):
"""The schema for an artifact file."""

relativePath = fields.String(
attribute="relative_path",
metadata=dict(description="Relative path to the Artifact URI."),
dump_only=True,
)


class ArtifactMutableFieldsSchema(Schema):
"""The fields schema for the mutable data in a Artifact resource."""

Expand All @@ -55,7 +87,7 @@ class ArtifactMutableFieldsSchema(Schema):
ArtifactBaseSchema = generate_base_resource_schema("Artifact", snapshot=True)


class ArtifactSchema(ArtifactMutableFieldsSchema, ArtifactBaseSchema): # type: ignore
class ArtifactSchema(ArtifactFileMetadataSchema, ArtifactMutableFieldsSchema, ArtifactBaseSchema): # type: ignore
"""The schema for the data stored in an Artifact resource."""

jobId = fields.Int(
Expand All @@ -64,9 +96,19 @@ class ArtifactSchema(ArtifactMutableFieldsSchema, ArtifactBaseSchema): # type:
metadata=dict(description="id of the job that produced this Artifact"),
required=True,
)
uri = fields.String(
attribute="uri",
metadata=dict(description="URL pointing to the location of the Artifact."),
artifactFile = FileUpload(
attribute="artifact_file",
metadata=dict(
type="file",
format="binary",
description="The artifact file.",
),
required=False,
)
artifactType = fields.String(
attribute="artifact_type",
validate=validate.OneOf(['file', 'dir']),
metadata=dict(description="Indicates what type of artifact this is (file or dir)."),
required=True,
)

Expand All @@ -81,6 +123,23 @@ class ArtifactPageSchema(BasePageSchema):
)


class ArtifactContentsGetQueryParameters(Schema):
"""A schema for adding artifact contents query parameters to a resource endpoint."""

path = fields.String(
attribute="path",
metadata=dict(description="Path of a specific artifact."),
load_default=None,
)
download = fields.Boolean(
attribute="download",
metadata=dict(
description="Determines whether the file will be downloaded or viewed."
),
load_default=None,
)


class ArtifactGetQueryParameters(
PagingQueryParametersSchema,
GroupIdQueryParametersSchema,
Expand Down
Loading
Loading