Skip to content

Commit eccbf9c

Browse files
hoshimuraCopilot
andauthored
Fixart ls root (#55)
* Expand bare art:// to the configured Artifactory root With ARTIFACTORY_BASE_URL (or ART_BASE_URL) set, `art://` now resolves to the configured service root, so `asanypath ls art://` lists repositories with the scoped environment token. Without a base URL, a host-less Artifactory path raises an error naming the setting instead of the misleading "token required" error. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use one Artifactory keyring entry per service The service key is now derived from the normalized endpoint, so `art://host/artifactory` and paths below it share the cached token entry `art:host/artifactory`. Previously the bare service root used `art:host`; a token cached only through the root must be supplied once more. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 15730c5 commit eccbf9c

3 files changed

Lines changed: 69 additions & 6 deletions

File tree

‎src/asanypath/artifactory.py‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,11 @@ def __init__(self, *args, token: str | None = None, **kwargs):
137137
"""
138138
super().__init__(*args, **kwargs)
139139
self._bind_path_attrs()
140+
if not self._service_key:
141+
raise ValueError(
142+
"Artifactory path needs a host (art://<host>/artifactory/...), "
143+
"or set ARTIFACTORY_BASE_URL to use art:// shorthand."
144+
)
140145

141146
from asanypath import _credstore
142147

@@ -191,12 +196,10 @@ def _env_token(self) -> str | None:
191196

192197
@property
193198
def _service_key(self) -> str:
194-
"""``host/artifactory``: the service key for the keyring, the JFrog CLI
195-
config and token scoping. Derived as before endpoint normalization so
196-
stored credentials keep matching."""
197-
raw = str(self)[len("art://") :]
198-
idx = raw.find("/artifactory/")
199-
return raw[: idx + len("/artifactory")] if idx >= 0 else raw.split("/")[0]
199+
"""``host/artifactory`` (or ``host``): the service key for the keyring,
200+
the JFrog CLI config and token scoping. Same for the service root and
201+
every path below it."""
202+
return self._endpoint_url.removeprefix("https://")
200203

201204
@property
202205
def _item_path(self) -> str:

‎src/asanypath/common.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,7 @@ def _expand_protocol_base_url(self, part: str) -> str:
472472
473473
Example with ARTIFACTORY_BASE_URL=artifacts.example.com/artifactory:
474474
art://repo/path -> art://artifacts.example.com/artifactory/repo/path
475+
art:// -> art://artifacts.example.com/artifactory
475476
"""
476477
if self.protocol == "file":
477478
return part
@@ -494,6 +495,9 @@ def _expand_protocol_base_url(self, part: str) -> str:
494495
normalized = scheme_re.sub(f"{self.protocol}{SCHEME_SEP}", part, count=1)
495496
normalized_rest = normalized.split(SCHEME_SEP, 1)[1].lstrip("/")
496497
base_prefix = base_prefix.rstrip("/")
498+
if not normalized_rest:
499+
# The bare root (e.g. ``art://``) is the configured service root.
500+
return f"{self.protocol}{SCHEME_SEP}{base_prefix}"
497501
if normalized_rest == base_prefix or normalized_rest.startswith(f"{base_prefix}/"):
498502
return normalized
499503

‎tests/test_artifactory.py‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,40 @@ def test_art_scheme_dispatch():
361361
assert isinstance(p, ArtifactoryPath)
362362

363363

364+
@pytest.mark.parametrize("setting", ["ARTIFACTORY_BASE_URL", "ART_BASE_URL"])
365+
async def test_bare_scheme_is_configured_service_root(monkeypatch, setting):
366+
"""``art://`` is the configured service root: env token allowed, repos listed."""
367+
from asanypath import AsAnyPath
368+
from asanypath.common import _BASE_URL_PREFIX_CACHE
369+
370+
_BASE_URL_PREFIX_CACHE.clear()
371+
monkeypatch.setattr(ArtifactoryPath, "_env_config", None)
372+
monkeypatch.setenv(setting, "https://artifactory.example.com/artifactory/")
373+
monkeypatch.setenv("ARTIFACTORY_IDENTITY_TOKEN", "env-token")
374+
try:
375+
root = AsAnyPath("art://")
376+
assert str(root) == "art://artifactory.example.com/artifactory"
377+
assert root._token == "env-token"
378+
with patch(
379+
"asanypath.artifactory.art_list_repos",
380+
new=AsyncMock(return_value=["generic"]),
381+
) as mock:
382+
assert [str(x) async for x in root.iterdir()] == [
383+
"art://artifactory.example.com/artifactory/generic"
384+
]
385+
assert mock.await_args.kwargs["base_url"] == "https://artifactory.example.com/artifactory"
386+
finally:
387+
_BASE_URL_PREFIX_CACHE.clear()
388+
389+
390+
def test_bare_scheme_without_base_url_names_the_setting(monkeypatch):
391+
from asanypath.common import _BASE_URL_PREFIX_CACHE
392+
393+
_BASE_URL_PREFIX_CACHE.clear()
394+
with pytest.raises(ValueError, match="ARTIFACTORY_BASE_URL"):
395+
ArtifactoryPath("art://", token="tok")
396+
397+
364398
# ----------------------------------------------------------------------
365399
# Credstore wiring
366400
# ----------------------------------------------------------------------
@@ -392,6 +426,28 @@ def test_artifactory_explicit_token_is_saved(monkeypatch):
392426
assert saved[0][0].startswith("art:")
393427

394428

429+
@pytest.mark.parametrize(
430+
"path",
431+
[
432+
"art://h.example.com/artifactory",
433+
"art://h.example.com/artifactory/",
434+
"art://h.example.com/artifactory/repo",
435+
"art://h.example.com/artifactory/repo/dir/file.txt",
436+
],
437+
)
438+
def test_artifactory_cached_token_key_is_per_service(monkeypatch, path):
439+
"""The service root and paths below it share one keyring entry."""
440+
from asanypath import _credstore
441+
442+
monkeypatch.setattr(ArtifactoryPath, "_service_tokens", {})
443+
loaded: list = []
444+
monkeypatch.setattr(_credstore, "load", lambda key, **_: loaded.append(key) or "cached")
445+
with patch.dict("os.environ", {}, clear=True):
446+
ArtifactoryPath._env_config = None
447+
ArtifactoryPath(path)
448+
assert loaded == ["art:h.example.com/artifactory"]
449+
450+
395451
def test_artifactory_loads_token_from_jfrog_cli_conf(tmp_path, monkeypatch):
396452
"""No token/env/credstore → falls back to the JFrog CLI config."""
397453
import json

0 commit comments

Comments
 (0)