Skip to content

Commit

Permalink
[MAINT] Deprecate /api/v1/environments/ (conda-incubator#1061)
Browse files Browse the repository at this point in the history
  • Loading branch information
peytondmurray authored Feb 5, 2025
1 parent a7e60b9 commit 6fbae46
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 23 deletions.
7 changes: 7 additions & 0 deletions conda-store-server/conda_store_server/_internal/server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,13 @@ async def conda_store_middleware(request: Request, call_next):
request.state.authentication = self.authentication
request.state.templates = self.templates
response = await call_next(request)

# Handle requests that are sent to deprecated endpoints;
# see conda_store_server._internal.server.views.api.deprecated
# for additional information
if hasattr(request.state, "deprecation_date"):
response.headers["Deprecation"] = "True"
response.headers["Sunset"] = request.state.deprecation_date
return response

@app.exception_handler(HTTPException)
Expand Down
40 changes: 27 additions & 13 deletions conda-store-server/conda_store_server/_internal/server/views/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,30 +107,43 @@ def paginated_api_response(


def deprecated(sunset_date: datetime.date) -> Callable:
"""Decorator to add deprecation headers to a HTTP response. These will include:
{
Deprecation: True
Sunset: <sunset_date>
}
"""Add deprecation headers to a HTTP request and response.
This will include the deprecation date on the request object,
which will then be picked up by the conda_store_middleware
to add the deprecation date to the response object.
See the conda-store backwards compatibility policy for appropriate use of
deprecations https://conda.store/community/policies/backwards-compatibility.
Note that decorated functions _must_ include the request parameter.
This is not an elegant way of doing this, but FastAPI has no other
way of achieving the same effect without confusing its request/response
inference machinery, which relies on the type annotations of the
routes.
Parameters
----------
sunset_date : datetime.date
the date that the endpoint will have it's functionality removed
Returns
-------
Callable
Decorator which wraps an endpoint
"""

def decorator(func):
@wraps(func)
def add_deprecated_headers(*args, **kwargs):
response = func(*args, **kwargs)
response.headers["Deprecation"] = "True"
response.headers["Sunset"] = sunset_date.strftime(
async def add_deprecated_headers(request: Request, *args, **kwargs):
# It's not possible to add the deprecation headers to the
# output of `func(*args, **kwargs)`, since that may be a
# simple dict object, not a Response
request.state.deprecation_date = sunset_date.strftime(
"%a, %d %b %Y 00:00:00 UTC"
)
return response
result = await func(*args, request=request, **kwargs)
return result

return add_deprecated_headers

Expand Down Expand Up @@ -635,10 +648,11 @@ async def api_delete_namespace(


@router_api.get(
"/environment/",
response_model=schema.APIListEnvironment,
"/environment/", response_model=schema.APIListEnvironment, deprecated=True
)
@deprecated(sunset_date=datetime.date(2025, 3, 17))
async def api_list_environments_v1(
request: Request,
auth: Authentication = Depends(dependencies.get_auth),
conda_store: CondaStore = Depends(dependencies.get_conda_store),
entity: AuthenticationToken = Depends(dependencies.get_entity),
Expand Down Expand Up @@ -1361,8 +1375,8 @@ async def api_get_build_archive(
@router_api.get("/build/{build_id}/docker/", deprecated=True)
@deprecated(sunset_date=datetime.date(2025, 3, 17))
async def api_get_build_docker_image_url(
build_id: int,
request: Request,
build_id: int,
conda_store=Depends(dependencies.get_conda_store),
server=Depends(dependencies.get_server),
auth=Depends(dependencies.get_auth),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ def replace_words(s, words):


@deprecated(sunset_date=datetime.date(2025, 3, 17))
def get_docker_image_manifest(conda_store, image, tag, timeout=10 * 60):
async def get_docker_image_manifest(
conda_store, image, tag, request: Request, timeout=10 * 60
):
namespace, *image_name = image.split("/")

# /v2/<image-name>/manifest/<tag>
Expand Down Expand Up @@ -131,14 +133,14 @@ def get_docker_image_manifest(conda_store, image, tag, timeout=10 * 60):


@deprecated(sunset_date=datetime.date(2025, 3, 17))
def get_docker_image_blob(conda_store, image, blobsum):
async def get_docker_image_blob(conda_store, image, blobsum, request: Request):
blob_key = f"docker/blobs/{blobsum}"
return RedirectResponse(conda_store.storage.get_url(blob_key))


@router_registry.get("/v2/", deprecated=True)
@deprecated(sunset_date=datetime.date(2025, 3, 17))
def v2(
async def v2(
request: Request,
entity=Depends(dependencies.get_entity),
):
Expand All @@ -150,7 +152,7 @@ def v2(

@router_registry.get("/v2/{rest:path}", deprecated=True)
@deprecated(sunset_date=datetime.date(2025, 3, 17))
def list_tags(
async def list_tags(
rest: str,
request: Request,
conda_store=Depends(dependencies.get_conda_store),
Expand Down Expand Up @@ -187,8 +189,8 @@ def list_tags(
# /v2/<image>/manifests/<tag>
elif parts[-2] == "manifests":
tag = parts[-1]
return get_docker_image_manifest(conda_store, image, tag)
return get_docker_image_manifest(conda_store, image, tag, request)
# /v2/<image>/blobs/<blobsum>
elif parts[-2] == "blobs":
blobsum = parts[-1].split(":")[1]
return get_docker_image_blob(conda_store, image, blobsum)
return get_docker_image_blob(conda_store, image, blobsum, request)
13 changes: 9 additions & 4 deletions conda-store-server/tests/_internal/server/views/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import pytest
import traitlets
import yaml
from fastapi import Request
from fastapi.testclient import TestClient

from conda_store_server import CONDA_STORE_DIR, __version__
Expand Down Expand Up @@ -47,19 +48,22 @@ def mock_get_entity():
testclient.app.dependency_overrides = {}


def test_deprecation_warning():
def test_deprecation_warning(testclient):
from fastapi.responses import JSONResponse

from conda_store_server._internal.server.views.api import deprecated

router = testclient.app.router

@router.get("/foo")
@deprecated(datetime.date(2024, 12, 17))
def api_status():
async def api_status(request: Request):
return JSONResponse(
status_code=400,
content={"ok": "ok"},
)

result = api_status()
result = testclient.get("/foo")
assert result.headers.get("Deprecation") == "True"
assert result.headers.get("Sunset") == "Tue, 17 Dec 2024 00:00:00 UTC"

Expand Down Expand Up @@ -257,6 +261,8 @@ def test_api_list_environments_auth(

r = model.model_validate(response.json())
assert r.status == schema.APIStatus.OK
if version == "v1":
assert response.headers.get("Deprecation")
assert sorted([_.name for _ in r.data]) == ["name1", "name2", "name3", "name4"]


Expand Down Expand Up @@ -1206,7 +1212,6 @@ def test_api_list_environments_paginate(
cursor = None
cursor_param = ""
while cursor is None or cursor != Cursor.end():
# breakpoint()
response = testclient.get(f"api/v2/environment/?limit={limit}{cursor_param}")
response.raise_for_status()

Expand Down

0 comments on commit 6fbae46

Please sign in to comment.