Skip to content

Commit

Permalink
honor pagination when getting tags and adjust API to match types
Browse files Browse the repository at this point in the history
add raise_for_status and test getting many tags from ghcr.io

Signed-off-by: Wolf Vollprecht <[email protected]>
  • Loading branch information
wolfv committed Feb 3, 2023
1 parent cb88582 commit 7781e31
Show file tree
Hide file tree
Showing 7 changed files with 45 additions and 12 deletions.
1 change: 0 additions & 1 deletion examples/conda-mirror.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ class CondaMirror(oras.provider.Registry):
}

def inspect(self, name):

# Parse the name into a container
container = self.get_container(name)

Expand Down
2 changes: 1 addition & 1 deletion oras/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def get_tags(self, name: str, N: int = 10_000) -> List[str]:
:param N: number of tags
:type N: int
"""
return self.remote.get_tags(name, N=N).json()
return self.remote.get_tags(name, N=N)

def push(self, *args, **kwargs):
"""
Expand Down
4 changes: 3 additions & 1 deletion oras/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ def get_blob_url(self, digest: str) -> str:
def upload_blob_url(self) -> str:
return f"{self.registry}/v2/{self.api_prefix}/blobs/uploads/"

def tags_url(self, N=10_000) -> str:
def tags_url(self, N=10_000, query=None) -> str:
if query:
return f"{self.registry}/v2/{self.api_prefix}/tags/list?{query}"
return f"{self.registry}/v2/{self.api_prefix}/tags/list?n={N}"

def put_manifest_url(self) -> str:
Expand Down
33 changes: 29 additions & 4 deletions oras/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import copy
import os
import urllib
from typing import List, Optional, Tuple, Union

import jsonschema
Expand Down Expand Up @@ -236,8 +237,34 @@ def get_tags(
:param N: number of tags
:type N: int
"""
tags_url = f"{self.prefix}://{container.tags_url(N)}" # type: ignore
return self.do_request(tags_url, "GET", headers=self.headers)
tags_url = f"{self.prefix}://{container.tags_url(N=N)}" # type: ignore

tags: List[str] = []
has_next_link = True
# get all tags using the pagination
while len(tags) < N and has_next_link:
res = self.do_request(tags_url, "GET", headers=self.headers)

# raise before trying to get `json` value
res.raise_for_status()

if res.headers.get("Link"):
link = res.headers.get("Link")
# if we have a next link, that looks something like:
# </v2/channel-mirrors/conda-forge/linux-64/arrow-cpp/tags/list?last=2.0.0-py37h5a62f8a_45_cuda&n=100000000>; rel="next"
# we want to extract the url and get the rest of the tags
assert link.endswith('; rel="next"')
next_link = link[link.find("<") + 1 : link.find(">")]
query = urllib.parse.urlparse(next_link).query
tags_url = f"{self.prefix}://{container.tags_url(query=query)}" # type: ignore
else:
has_next_link = False

# if the package does not exist, the response is an
# {"errors":[{"code":"NAME_UNKNOWN","message":"repository name not known to registry"}]}
tags += res.json().get("tags", [])

return tags

@ensure_container
def get_blob(
Expand Down Expand Up @@ -548,7 +575,6 @@ def push(self, *args, **kwargs) -> requests.Response:

# Upload files as blobs
for blob in kwargs.get("files", []):

# You can provide a blob + content type
if ":" in str(blob):
blob, media_type = str(blob).split(":", 1)
Expand Down Expand Up @@ -809,7 +835,6 @@ def authenticate_request(self, originalResponse: requests.Response) -> bool:
h = oras.auth.parse_auth_header(authHeaderRaw)

if "Authorization" not in headers:

# First try to request an anonymous token
logger.debug("No Authorization, requesting anonymous token")
if self.request_anonymous_token(h):
Expand Down
15 changes: 12 additions & 3 deletions oras/tests/test_oras.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,7 @@ def test_basic_push_pull(tmp_path):

# Test getting tags
tags = client.get_tags(target)
for key in ["name", "tags"]:
assert key in tags
assert "v1" in tags["tags"]
assert "v1" in tags

# Test pulling elsewhere
files = client.pull(target=target, outdir=tmp_path)
Expand All @@ -94,6 +92,17 @@ def test_basic_push_pull(tmp_path):
assert res.status_code == 201


def test_get_many_tags():
"""
Test getting many tags
"""
client = oras.client.OrasClient(hostname="ghcr.io", insecure=False)
tags = client.get_tags(
"channel-mirrors/conda-forge/linux-aarch64/arrow-cpp", N=100000
)
assert len(tags) > 1000


@pytest.mark.skipif(with_auth, reason="token auth is needed for push and pull")
def test_directory_push_pull(tmp_path):
"""
Expand Down
1 change: 0 additions & 1 deletion oras/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ def test_write_bad_json(tmp_path):


def test_write_json(tmp_path):

good_json = {"Wakkawakkawakka": [True, "2", 3]}
tmpfile = str(tmp_path / "good_json_file.txt")

Expand Down
1 change: 0 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ def get_reqs(lookup=None, key="INSTALL_REQUIRES"):
################################################################################

if __name__ == "__main__":

INSTALL_REQUIRES = get_reqs(lookup)
TESTS_REQUIRES = get_reqs(lookup, "TESTS_REQUIRES")
INSTALL_REQUIRES_ALL = get_reqs(lookup, "INSTALL_REQUIRES_ALL")
Expand Down

0 comments on commit 7781e31

Please sign in to comment.