Skip to content
Draft
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
40 changes: 40 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,46 @@ CHANGELOG
This project uses `semantic versioning <http://semver.org/>`_.
This change log uses principles from `keep a changelog <http://keepachangelog.com/>`_.

[0.23.0] - 2025-12-08
---------------------

Added
^^^^^

- Health check endpoint ``GET /config/health`` for container orchestration
(does not require authorization)
- Tag manipulation routes:

- ``GET /tags/<uri>`` - Get dataset tags
- ``PUT /tags/<uri>`` - Set all dataset tags (replaces existing)
- ``POST /tags/<uri>/<tag>`` - Add a single tag
- ``DELETE /tags/<uri>/<tag>`` - Remove a single tag

- Annotation manipulation routes:

- ``GET /annotations/<uri>`` - Get dataset annotations
- ``PUT /annotations/<uri>`` - Set all annotations (replaces existing)
- ``PUT /annotations/<uri>/<name>`` - Set a single annotation
- ``DELETE /annotations/<uri>/<name>`` - Delete a single annotation

- README manipulation routes:

- ``GET /readmes/<uri>`` - Get dataset README
- ``PUT /readmes/<uri>`` - Update dataset README

Changed
^^^^^^^

- Switched build system to flit
- Tags and annotations are now updated both in the database and in storage
(previously only updated in database)

Removed
^^^^^^^

- Removed unused legacy code


[0.22.0]
------------

Expand Down
1 change: 0 additions & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
include README.rst
include LICENSE
include dserver/templates/*
64 changes: 64 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,57 @@ URI ``s3://dtool-demo/ba92a5fa-d3b4-4f10-bcb9-947f62e652db``::
http://localhost:5000/manifests/s3/dtool-demo/ba92a5fa-d3b4-4f10-bcb9-947f62e652db


Modifying dataset tags
~~~~~~~~~~~~~~~~~~~~~~

Add a single tag to a dataset::

$ curl -H "$HEADER" -X POST \
http://localhost:5000/tags/s3/dtool-demo/ba92a5fa-d3b4-4f10-bcb9-947f62e652db/new-tag

Replace all tags on a dataset::

$ curl -H "$HEADER" -H "Content-Type: application/json" \
-X PUT -d '{"tags": ["tag1", "tag2"]}' \
http://localhost:5000/tags/s3/dtool-demo/ba92a5fa-d3b4-4f10-bcb9-947f62e652db

Remove a tag from a dataset::

$ curl -H "$HEADER" -X DELETE \
http://localhost:5000/tags/s3/dtool-demo/ba92a5fa-d3b4-4f10-bcb9-947f62e652db/old-tag


Modifying dataset annotations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Set a single annotation on a dataset::

$ curl -H "$HEADER" -H "Content-Type: application/json" \
-X PUT -d '{"value": "some value"}' \
http://localhost:5000/annotations/s3/dtool-demo/ba92a5fa-d3b4-4f10-bcb9-947f62e652db/my-annotation

Replace all annotations on a dataset::

$ curl -H "$HEADER" -H "Content-Type: application/json" \
-X PUT -d '{"annotations": {"key1": "value1", "key2": 42}}' \
http://localhost:5000/annotations/s3/dtool-demo/ba92a5fa-d3b4-4f10-bcb9-947f62e652db

Delete an annotation from a dataset::

$ curl -H "$HEADER" -X DELETE \
http://localhost:5000/annotations/s3/dtool-demo/ba92a5fa-d3b4-4f10-bcb9-947f62e652db/my-annotation


Modifying dataset README
~~~~~~~~~~~~~~~~~~~~~~~~

Update the README of a dataset::

$ curl -H "$HEADER" -H "Content-Type: application/json" \
-X PUT -d '{"readme": "---\ndescription: Updated README content\n"}' \
http://localhost:5000/readmes/s3/dtool-demo/ba92a5fa-d3b4-4f10-bcb9-947f62e652db


Getting information about one's own permissions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down Expand Up @@ -727,6 +778,19 @@ and extension plugins with their versions, i.e.::

This request does not require any authorization.

The request::

$ curl http://localhost:5000/config/health

will return a simple health check status for container orchestration::

{
"status": "healthy"
}

This request does not require any authorization and can be used for
Kubernetes liveness/readiness probes or Docker health checks.

Creating a plugin
-----------------

Expand Down
105 changes: 95 additions & 10 deletions dservercore/annotations_routes.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,45 @@
"""Route for retrieving dataset annotations by URI."""
"""Routes for retrieving and modifying dataset annotations by URI."""
from flask import (
abort,
jsonify,
current_app
current_app,
request
)
from dservercore.utils_auth import (
jwt_required,
get_jwt_identity,
)

from dservercore import UnknownURIError
from dservercore import UnknownURIError, AuthorizationError
from dservercore.blueprint import Blueprint
from dservercore.schemas import AnnotationSchema
from dservercore.schemas import AnnotationSchema, SingleAnnotationSchema
import dservercore.utils_auth
from dservercore.utils import (
url_suffix_to_uri,
get_annotations_from_uri_by_user
get_annotations_from_uri_by_user,
set_annotations_for_uri_by_user,
set_annotation_for_uri_by_user,
delete_annotation_for_uri_by_user
)

bp = Blueprint("annotations", __name__, url_prefix="/annotations")


@bp.route("/<path:uri>", methods=["GET"])
@bp.response(200, AnnotationSchema)
@bp.alt_response(401, description="Not registered")
@bp.alt_response(401, description="Unauthorized")
@bp.alt_response(403, description="No permissions")
@bp.alt_response(400, description="Unknown URI")
@bp.alt_response(404, description="Not found")
@jwt_required()
def annotations(uri):
def get_annotations(uri):
"""Request the dataset annotations."""
username = get_jwt_identity()
if not dservercore.utils_auth.user_exists(username):
# Unregistered users should see 401.
abort(401)

uri = url_suffix_to_uri(uri)

if not dservercore.utils_auth.may_access(username, uri):
# Authorization errors should return 403.
abort(403)

try:
Expand All @@ -46,4 +48,87 @@ def annotations(uri):
current_app.logger.info("UnknownURIError")
abort(404)

return {"annotations": annotations}


@bp.route("/<path:uri>", methods=["PUT"])
@bp.arguments(AnnotationSchema)
@bp.response(200, AnnotationSchema)
@bp.alt_response(401, description="Unauthorized")
@bp.alt_response(403, description="No permissions")
@bp.alt_response(404, description="Not found")
@jwt_required()
def set_annotations(data, uri):
"""Set all dataset annotations (replaces existing annotations)."""
username = get_jwt_identity()
if not dservercore.utils_auth.user_exists(username):
abort(401)

uri = url_suffix_to_uri(uri)

try:
annotations = set_annotations_for_uri_by_user(
username, uri, data.get("annotations", {})
)
except AuthorizationError:
abort(403)
except UnknownURIError:
current_app.logger.info("UnknownURIError")
abort(404)

return {"annotations": annotations}


@bp.route("/<path:uri>/<annotation_name>", methods=["PUT"])
@bp.arguments(SingleAnnotationSchema)
@bp.response(200, AnnotationSchema)
@bp.alt_response(401, description="Unauthorized")
@bp.alt_response(403, description="No permissions")
@bp.alt_response(404, description="Not found")
@jwt_required()
def set_annotation(data, uri, annotation_name):
"""Set a single annotation (creates or updates)."""
username = get_jwt_identity()
if not dservercore.utils_auth.user_exists(username):
abort(401)

uri = url_suffix_to_uri(uri)

try:
annotations = set_annotation_for_uri_by_user(
username, uri, annotation_name, data.get("value")
)
except AuthorizationError:
abort(403)
except UnknownURIError:
current_app.logger.info("UnknownURIError")
abort(404)

return {"annotations": annotations}


@bp.route("/<path:uri>/<annotation_name>", methods=["DELETE"])
@bp.response(200, AnnotationSchema)
@bp.alt_response(401, description="Unauthorized")
@bp.alt_response(403, description="No permissions")
@bp.alt_response(404, description="Not found")
@jwt_required()
def delete_annotation(uri, annotation_name):
"""Delete a single annotation."""
username = get_jwt_identity()
if not dservercore.utils_auth.user_exists(username):
abort(401)

uri = url_suffix_to_uri(uri)

try:
annotations = delete_annotation_for_uri_by_user(
username, uri, annotation_name
)
except AuthorizationError:
abort(403)
except UnknownURIError:
current_app.logger.info("UnknownURIError")
abort(404)

return {"annotations": annotations}
13 changes: 12 additions & 1 deletion dservercore/config_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import dservercore
import dservercore.utils_auth
from dservercore.blueprint import Blueprint
from dservercore.schemas import ConfigSchema, VersionSchema
from dservercore.schemas import ConfigSchema, HealthSchema, VersionSchema
from dservercore.utils import versions_to_dict, obj_to_lowercase_key_dict


Expand Down Expand Up @@ -45,3 +45,14 @@ def server_versions():
This does not require authorization."""

return jsonify({"versions": versions_to_dict()})


@bp.route("/health", methods=["GET"])
@bp.response(200, HealthSchema)
def health():
"""Health check endpoint for container orchestration.

Returns a simple status indicating the service is running.
This does not require authorization."""

return jsonify({"status": "healthy"}), 200
37 changes: 31 additions & 6 deletions dservercore/me_routes.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
"""Routes for me (information on currently authenticated user)"""
from flask import (
abort,
jsonify,
)
from flask import abort

from dservercore.utils_auth import (
jwt_required,
get_jwt_identity,
)

import dservercore
import dservercore.utils
import dservercore.utils_auth

from dservercore.blueprint import Blueprint
from dservercore.sql_models import UserSchema, UserWithPermissionsSchema
from dservercore.schemas import SummarySchema
from dservercore.sql_models import User, UserWithPermissionsSchema
from dservercore.schemas import SummarySchema, MeUpdateSchema
from dservercore.utils import summary_of_datasets_by_user


Expand All @@ -34,6 +33,32 @@ def me_get():
return dservercore.utils.get_user_info(identity)


@bp.route("", methods=["PATCH"])
@bp.arguments(MeUpdateSchema)
@bp.response(200, UserWithPermissionsSchema)
@bp.alt_response(401, description="Not registered")
@jwt_required()
def me_patch(data: MeUpdateSchema):
"""Update the current user's profile.

Currently supports updating:
- display_name: string (can be null to clear)
"""
identity = get_jwt_identity()

if not dservercore.utils_auth.user_exists(identity):
abort(401)

user = User.query.filter_by(username=identity).first()

if "display_name" in data:
user.display_name = data.get("display_name")

dservercore.sql_db.session.commit()

return dservercore.utils.get_user_info(identity)


@bp.route("/summary", methods=["GET"])
@bp.response(200, SummarySchema)
@bp.alt_response(401, description="Not registered")
Expand Down
Loading
Loading