Skip to content
Merged
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
4 changes: 2 additions & 2 deletions skore-hub-project/src/skore_hub_project/artifact/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def upload(project: Project, content: str | bytes, content_type: str) -> str:
):
# Ask for upload urls.
response = hub_client.post(
url=f"projects/{project.tenant}/{project.name}/artifacts",
url=f"projects/{project.quoted_tenant}/{project.quoted_name}/artifacts",
json=[
{
"checksum": serializer.checksum,
Expand Down Expand Up @@ -181,7 +181,7 @@ def upload(project: Project, content: str | bytes, content_type: str) -> str:

# Acknowledge the upload, to let the hub/storage rebuild the whole.
hub_client.post(
url=f"projects/{project.tenant}/{project.name}/artifacts/complete",
url=f"projects/{project.quoted_tenant}/{project.quoted_name}/artifacts/complete",
json=[
{
"checksum": serializer.checksum,
Expand Down
35 changes: 27 additions & 8 deletions skore-hub-project/src/skore_hub_project/project/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@

import itertools
import re
from functools import wraps
from functools import cached_property, wraps
from operator import itemgetter
from tempfile import TemporaryFile
from types import SimpleNamespace
from typing import TYPE_CHECKING
from urllib.parse import quote

import joblib
import orjson
Expand Down Expand Up @@ -46,7 +47,9 @@ def ensure_project_is_created(method):
def wrapper(project: Project, *args, **kwargs):
if not project.created:
with HUBClient() as hub_client:
hub_client.post(f"projects/{project.tenant}/{project.name}")
hub_client.post(
f"projects/{project.quoted_tenant}/{project.quoted_name}"
)

project.created = True

Expand Down Expand Up @@ -126,6 +129,16 @@ def name(self) -> str:
"""The name of the project."""
return self.__name

@cached_property
def quoted_tenant(self) -> str:
"""The quoted tenant of the project."""
return quote(self.__tenant, safe="")

@cached_property
def quoted_name(self) -> str:
"""The quoted name of the project."""
return quote(self.__name, safe="")

@ensure_project_is_created
def put(self, key: str, report: EstimatorReport | CrossValidationReport):
"""
Expand Down Expand Up @@ -170,7 +183,7 @@ def put(self, key: str, report: EstimatorReport | CrossValidationReport):

with HUBClient() as hub_client:
hub_client.post(
url=f"projects/{self.tenant}/{self.name}/{endpoint}",
url=f"projects/{self.quoted_tenant}/{self.quoted_name}/{endpoint}",
content=payload_json_bytes,
headers={
"Content-Length": str(len(payload_json_bytes)),
Expand All @@ -182,7 +195,11 @@ def put(self, key: str, report: EstimatorReport | CrossValidationReport):
def get(self, urn: str) -> EstimatorReport | CrossValidationReport:
"""Get a persisted report by its URN."""
if m := re.match(Project.__REPORT_URN_PATTERN, urn):
url = f"projects/{self.tenant}/{self.name}/{m['type']}-reports/{m['id']}"
tenant = self.quoted_tenant
name = self.quoted_name
type = m["type"]
id = m["id"]
url = f"projects/{tenant}/{name}/{type}-reports/{id}"
else:
raise ValueError(
f"URN '{urn}' format does not match '{Project.__REPORT_URN_PATTERN}'"
Expand Down Expand Up @@ -247,13 +264,13 @@ def dto(response):
zip(
itertools.repeat("estimator"),
hub_client.get(
f"projects/{self.tenant}/{self.name}/estimator-reports/"
f"projects/{self.quoted_tenant}/{self.quoted_name}/estimator-reports/"
).json(),
),
zip(
itertools.repeat("cross-validation"),
hub_client.get(
f"projects/{self.tenant}/{self.name}/cross-validation-reports/"
f"projects/{self.quoted_tenant}/{self.quoted_name}/cross-validation-reports/"
).json(),
),
)
Expand Down Expand Up @@ -309,11 +326,13 @@ def delete(tenant: str, name: str):
"""
with HUBClient() as hub_client:
try:
hub_client.delete(f"projects/{tenant}/{name}")
hub_client.delete(
f"projects/{quote(tenant, safe='')}/{quote(name, safe='')}"
)
except HTTPStatusError as e:
if e.response.status_code == 403:
raise PermissionError(
f"Failed to delete the project; "
f"Failed to delete the project '{name}'; "
f"please contact the '{tenant}' owner"
) from e
raise
15 changes: 12 additions & 3 deletions skore-hub-project/tests/unit/project/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,16 @@ def monkeypatch_table_report_representation(monkeypatch):

class TestProject:
def test_tenant(self):
assert Project("<tenant>", "<name>").tenant == "<tenant>"
assert Project("my/ tenant", "my/ name").tenant == "my/ tenant"

def test_quoted_tenant(self):
assert Project("my/ tenant", "my/ name").quoted_tenant == "my%2F%20tenant"

def test_name(self):
assert Project("<tenant>", "<name>").name == "<name>"
assert Project("my/ tenant", "my/ name").name == "my/ name"

def test_quoted_name(self):
assert Project("my/ tenant", "my/ name").quoted_name == "my%2F%20name"

def test_put_exception(self, respx_mock):
respx_mock.post("projects/<tenant>/<name>").mock(Response(200))
Expand Down Expand Up @@ -336,6 +342,9 @@ def test_delete_exception(self, respx_mock):

with raises(
PermissionError,
match="Failed to delete the project; please contact the '<tenant>' owner",
match=(
"Failed to delete the project '<name>'; "
"please contact the '<tenant>' owner"
),
):
Project.delete("<tenant>", "<name>")