Skip to content

Commit 8425b24

Browse files
authored
ref_url and resolved_spec methods for repo providers (#974)
ref_url and resolved_spec methods for repo providers
2 parents a168d06 + 1efa7b8 commit 8425b24

File tree

3 files changed

+170
-26
lines changed

3 files changed

+170
-26
lines changed

binderhub/builder.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,16 +313,28 @@ async def get(self, provider_prefix, _unescaped_spec):
313313
push_secret = None
314314

315315
BuildClass = FakeBuild if self.settings.get('fake_build') else Build
316+
316317
binder_url = '{proto}://{host}{base_url}v2/{provider}/{spec}'.format(
317318
proto=self.request.protocol,
318319
host=self.request.host,
319320
base_url=self.settings['base_url'],
320321
provider=provider_prefix,
321322
spec=spec,
322323
)
324+
resolved_spec = await provider.get_resolved_spec()
325+
persistent_binder_url = '{proto}://{host}{base_url}v2/{provider}/{spec}'.format(
326+
proto=self.request.protocol,
327+
host=self.request.host,
328+
base_url=self.settings['base_url'],
329+
provider=provider_prefix,
330+
spec=resolved_spec,
331+
)
332+
ref_url = await provider.get_resolved_ref_url()
323333
appendix = self.settings['appendix'].format(
324334
binder_url=binder_url,
325335
repo_url=repo_url,
336+
persistent_binder_url=persistent_binder_url,
337+
ref_url=ref_url,
326338
)
327339

328340
self.build = build = BuildClass(

binderhub/repoproviders.py

Lines changed: 91 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -164,10 +164,20 @@ def repo_config(self, settings):
164164
def get_resolved_ref(self):
165165
raise NotImplementedError("Must be overridden in child class")
166166

167+
@gen.coroutine
168+
def get_resolved_spec(self):
169+
"""Return the spec with resolved ref."""
170+
raise NotImplementedError("Must be overridden in child class")
171+
167172
def get_repo_url(self):
168173
"""Return the git clone-able repo URL"""
169174
raise NotImplementedError("Must be overridden in the child class")
170175

176+
@gen.coroutine
177+
def get_resolved_ref_url(self):
178+
"""Return the URL of repository at this commit in history"""
179+
raise NotImplementedError("Must be overridden in child class")
180+
171181
def get_build_slug(self):
172182
"""Return a unique build slug"""
173183
raise NotImplementedError("Must be overriden in the child class")
@@ -185,9 +195,15 @@ class FakeProvider(RepoProvider):
185195
async def get_resolved_ref(self):
186196
return "1a2b3c4d5e6f"
187197

198+
async def get_resolved_spec(self):
199+
return "fake/repo/1a2b3c4d5e6f"
200+
188201
def get_repo_url(self):
189202
return "https://example.com/fake/repo.git"
190203

204+
async def get_resolved_ref_url(self):
205+
return "https://example.com/fake/repo/tree/1a2b3c4d5e6f"
206+
191207
def get_build_slug(self):
192208
return '{user}-{repo}'.format(user='Rick', repo='Morty')
193209

@@ -208,11 +224,25 @@ def get_resolved_ref(self):
208224
self.record_id = r.effective_url.rsplit("/", maxsplit=1)[1]
209225
return self.record_id
210226

227+
async def get_resolved_spec(self):
228+
if not hasattr(self, 'record_id'):
229+
self.record_id = await self.get_resolved_ref()
230+
# zenodo registers a DOI which represents all versions of a software package
231+
# and it always resolves to latest version
232+
# for that case, we have to replace the version number in DOIs with
233+
# the specific (resolved) version (record_id)
234+
resolved_spec = self.spec.split("zenodo")[0] + "zenodo." + self.record_id
235+
return resolved_spec
236+
211237
def get_repo_url(self):
212238
# While called repo URL, the return value of this function is passed
213239
# as argument to repo2docker, hence we return the spec as is.
214240
return self.spec
215241

242+
async def get_resolved_ref_url(self):
243+
resolved_spec = await self.get_resolved_spec()
244+
return f"https://doi.org/{resolved_spec}"
245+
216246
def get_build_slug(self):
217247
return "zenodo-{}".format(self.record_id)
218248

@@ -241,11 +271,25 @@ def get_resolved_ref(self):
241271

242272
return self.record_id
243273

274+
async def get_resolved_spec(self):
275+
if not hasattr(self, 'record_id'):
276+
self.record_id = await self.get_resolved_ref()
277+
278+
# spec without version is accepted as version 1 - check get_resolved_ref method
279+
# for that case, we have to replace the version number in DOIs with
280+
# the specific (resolved) version (record_id)
281+
resolved_spec = self.spec.split("figshare")[0] + "figshare." + self.record_id
282+
return resolved_spec
283+
244284
def get_repo_url(self):
245285
# While called repo URL, the return value of this function is passed
246286
# as argument to repo2docker, hence we return the spec as is.
247287
return self.spec
248288

289+
async def get_resolved_ref_url(self):
290+
resolved_spec = await self.get_resolved_spec()
291+
return f"https://doi.org/{resolved_spec}"
292+
249293
def get_build_slug(self):
250294
return "figshare-{}".format(self.record_id)
251295

@@ -270,8 +314,8 @@ class GitRepoProvider(RepoProvider):
270314

271315
def __init__(self, *args, **kwargs):
272316
super().__init__(*args, **kwargs)
273-
url, unresolved_ref = self.spec.split('/', 1)
274-
self.repo = urllib.parse.unquote(url)
317+
self.url, unresolved_ref = self.spec.split('/', 1)
318+
self.repo = urllib.parse.unquote(self.url)
275319
self.unresolved_ref = urllib.parse.unquote(unresolved_ref)
276320
if not self.unresolved_ref:
277321
raise ValueError("`unresolved_ref` must be specified as a query parameter for the basic git provider")
@@ -302,9 +346,18 @@ def get_resolved_ref(self):
302346

303347
return self.resolved_ref
304348

349+
async def get_resolved_spec(self):
350+
if not hasattr(self, 'resolved_ref'):
351+
self.resolved_ref = await self.get_resolved_ref()
352+
return f"{self.url}/{self.resolved_ref}"
353+
305354
def get_repo_url(self):
306355
return self.repo
307356

357+
async def get_resolved_ref_url(self):
358+
# not possible to construct ref url of unknown git provider
359+
return self.get_repo_url()
360+
308361
def get_build_slug(self):
309362
return self.repo
310363

@@ -374,8 +427,8 @@ def _default_git_credentials(self):
374427

375428
def __init__(self, *args, **kwargs):
376429
super().__init__(*args, **kwargs)
377-
quoted_namespace, unresolved_ref = self.spec.split('/', 1)
378-
self.namespace = urllib.parse.unquote(quoted_namespace)
430+
self.quoted_namespace, unresolved_ref = self.spec.split('/', 1)
431+
self.namespace = urllib.parse.unquote(self.quoted_namespace)
379432
self.unresolved_ref = urllib.parse.unquote(unresolved_ref)
380433
if not self.unresolved_ref:
381434
raise ValueError("An unresolved ref is required")
@@ -410,13 +463,22 @@ def get_resolved_ref(self):
410463
self.resolved_ref = ref_info['id']
411464
return self.resolved_ref
412465

466+
async def get_resolved_spec(self):
467+
if not hasattr(self, 'resolved_ref'):
468+
self.resolved_ref = await self.get_resolved_ref()
469+
return f"{self.quoted_namespace}/{self.resolved_ref}"
470+
413471
def get_build_slug(self):
414472
# escape the name and replace dashes with something else.
415473
return '-'.join(p.replace('-', '_-') for p in self.namespace.split('/'))
416474

417475
def get_repo_url(self):
418-
return "https://{hostname}/{namespace}.git".format(
419-
hostname=self.hostname, namespace=self.namespace)
476+
return f"https://{self.hostname}/{self.namespace}.git"
477+
478+
async def get_resolved_ref_url(self):
479+
if not hasattr(self, 'resolved_ref'):
480+
self.resolved_ref = await self.get_resolved_ref()
481+
return f"https://{self.hostname}/{self.namespace}/tree/{self.resolved_ref}"
420482

421483

422484
class GitHubRepoProvider(RepoProvider):
@@ -500,8 +562,12 @@ def __init__(self, *args, **kwargs):
500562
self.repo = strip_suffix(self.repo, ".git")
501563

502564
def get_repo_url(self):
503-
return "https://{hostname}/{user}/{repo}".format(
504-
hostname=self.hostname, user=self.user, repo=self.repo)
565+
return f"https://{self.hostname}/{self.user}/{self.repo}"
566+
567+
async def get_resolved_ref_url(self):
568+
if not hasattr(self, 'resolved_ref'):
569+
self.resolved_ref = await self.get_resolved_ref()
570+
return f"https://{self.hostname}/{self.user}/{self.repo}/tree/{self.resolved_ref}"
505571

506572
@gen.coroutine
507573
def github_api_request(self, api_url, etag=None):
@@ -621,6 +687,11 @@ def get_resolved_ref(self):
621687
)
622688
return self.resolved_ref
623689

690+
async def get_resolved_spec(self):
691+
if not hasattr(self, 'resolved_ref'):
692+
self.resolved_ref = await self.get_resolved_ref()
693+
return f"{self.user}/{self.repo}/{self.resolved_ref}"
694+
624695
def get_build_slug(self):
625696
return '{user}-{repo}'.format(user=self.user, repo=self.repo)
626697

@@ -639,6 +710,7 @@ class GistRepoProvider(GitHubRepoProvider):
639710
"""
640711

641712
name = Unicode('Gist')
713+
hostname = Unicode('gist.github.com')
642714

643715
allow_secret_gist = Bool(
644716
default_value=False,
@@ -657,7 +729,12 @@ def __init__(self, *args, **kwargs):
657729
self.unresolved_ref = ''
658730

659731
def get_repo_url(self):
660-
return f'https://gist.github.com/{self.user}/{self.gist_id}.git'
732+
return f'https://{self.hostname}/{self.user}/{self.gist_id}.git'
733+
734+
async def get_resolved_ref_url(self):
735+
if not hasattr(self, 'resolved_ref'):
736+
self.resolved_ref = await self.get_resolved_ref()
737+
return f'https://{self.hostname}/{self.user}/{self.gist_id}/{self.resolved_ref}'
661738

662739
@gen.coroutine
663740
def get_resolved_ref(self):
@@ -689,5 +766,10 @@ def get_resolved_ref(self):
689766

690767
return self.resolved_ref
691768

769+
async def get_resolved_spec(self):
770+
if not hasattr(self, 'resolved_ref'):
771+
self.resolved_ref = await self.get_resolved_ref()
772+
return f'{self.user}/{self.gist_id}/{self.resolved_ref}'
773+
692774
def get_build_slug(self):
693775
return self.gist_id

binderhub/tests/test_repoproviders.py

Lines changed: 67 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,45 +37,82 @@ def test_spec_processing(spec, raw_user, raw_repo, raw_ref):
3737
assert raw_ref == unresolved_ref
3838

3939

40-
async def test_zenodo():
41-
spec = '10.5281/zenodo.3242074'
42-
40+
@pytest.mark.parametrize('spec,resolved_spec,resolved_ref,resolved_ref_url,build_slug', [
41+
['10.5281/zenodo.3242074',
42+
'10.5281/zenodo.3242074',
43+
'3242074',
44+
'https://doi.org/10.5281/zenodo.3242074',
45+
'zenodo-3242074'],
46+
# 10.5281/zenodo.3242073 -> This DOI represents all versions, and will always resolve to the latest one
47+
# for now it is 3242074
48+
['10.5281/zenodo.3242073',
49+
'10.5281/zenodo.3242074',
50+
'3242074',
51+
'https://doi.org/10.5281/zenodo.3242074',
52+
'zenodo-3242074'],
53+
])
54+
async def test_zenodo(spec, resolved_spec, resolved_ref, resolved_ref_url, build_slug):
4355
provider = ZenodoProvider(spec=spec)
4456

4557
# have to resolve the ref first
4658
ref = await provider.get_resolved_ref()
47-
assert ref == '3242074'
59+
assert ref == resolved_ref
4860

4961
slug = provider.get_build_slug()
50-
assert slug == 'zenodo-3242074'
62+
assert slug == build_slug
5163
repo_url = provider.get_repo_url()
5264
assert repo_url == spec
53-
54-
55-
async def test_figshare():
56-
spec = '10.6084/m9.figshare.9782777.v1'
57-
65+
ref_url = await provider.get_resolved_ref_url()
66+
assert ref_url == resolved_ref_url
67+
spec = await provider.get_resolved_spec()
68+
assert spec == resolved_spec
69+
70+
71+
@pytest.mark.parametrize('spec,resolved_spec,resolved_ref,resolved_ref_url,build_slug', [
72+
['10.6084/m9.figshare.9782777.v1',
73+
'10.6084/m9.figshare.9782777.v1',
74+
'9782777.v1',
75+
'https://doi.org/10.6084/m9.figshare.9782777.v1',
76+
'figshare-9782777.v1'],
77+
# spec without version is accepted as version 1 - check FigshareProvider.get_resolved_ref()
78+
['10.6084/m9.figshare.9782777',
79+
'10.6084/m9.figshare.9782777.v1',
80+
'9782777.v1',
81+
'https://doi.org/10.6084/m9.figshare.9782777.v1',
82+
'figshare-9782777.v1'],
83+
])
84+
async def test_figshare(spec, resolved_spec, resolved_ref, resolved_ref_url, build_slug):
5885
provider = FigshareProvider(spec=spec)
5986

6087
# have to resolve the ref first
6188
ref = await provider.get_resolved_ref()
62-
assert ref == '9782777.v1'
89+
assert ref == resolved_ref
6390

6491
slug = provider.get_build_slug()
65-
assert slug == 'figshare-9782777.v1'
92+
assert slug == build_slug
6693
repo_url = provider.get_repo_url()
6794
assert repo_url == spec
95+
ref_url = await provider.get_resolved_ref_url()
96+
assert ref_url == resolved_ref_url
97+
spec = await provider.get_resolved_spec()
98+
assert spec == resolved_spec
6899

69100

70101
@pytest.mark.github_api
71102
def test_github_ref():
72-
provider = GitHubRepoProvider(spec='jupyterhub/zero-to-jupyterhub-k8s/v0.4')
103+
namespace = 'jupyterhub/zero-to-jupyterhub-k8s'
104+
spec = f'{namespace}/v0.4'
105+
provider = GitHubRepoProvider(spec=spec)
73106
slug = provider.get_build_slug()
74107
assert slug == 'jupyterhub-zero-to-jupyterhub-k8s'
75108
full_url = provider.get_repo_url()
76-
assert full_url == 'https://github.com/jupyterhub/zero-to-jupyterhub-k8s'
109+
assert full_url == f'https://github.com/{namespace}'
77110
ref = IOLoop().run_sync(provider.get_resolved_ref)
78111
assert ref == 'f7f3ff6d1bf708bdc12e5f10e18b2a90a4795603'
112+
ref_url = IOLoop().run_sync(provider.get_resolved_ref_url)
113+
assert ref_url == f'https://github.com/{namespace}/tree/{ref}'
114+
resolved_spec = IOLoop().run_sync(provider.get_resolved_spec)
115+
assert resolved_spec == f'{namespace}/{ref}'
79116

80117

81118
def test_not_banned():
@@ -249,20 +286,29 @@ def test_git_ref(url, unresolved_ref, resolved_ref):
249286
assert full_url == url
250287
ref = IOLoop().run_sync(provider.get_resolved_ref)
251288
assert ref == resolved_ref
289+
ref_url = IOLoop().run_sync(provider.get_resolved_ref_url)
290+
assert ref_url == full_url
291+
resolved_spec = IOLoop().run_sync(provider.get_resolved_spec)
292+
assert resolved_spec == quote(url, safe='') + f'/{resolved_ref}'
252293

253294

254295
def test_gitlab_ref():
296+
namespace = 'gitlab-org/gitlab-foss'
255297
spec = '{}/{}'.format(
256-
quote('gitlab-org/gitlab-foss', safe=''),
298+
quote(namespace, safe=''),
257299
quote('v10.0.6')
258300
)
259301
provider = GitLabRepoProvider(spec=spec)
260302
slug = provider.get_build_slug()
261303
assert slug == 'gitlab_-org-gitlab_-foss'
262304
full_url = provider.get_repo_url()
263-
assert full_url == 'https://gitlab.com/gitlab-org/gitlab-foss.git'
305+
assert full_url == f'https://gitlab.com/{namespace}.git'
264306
ref = IOLoop().run_sync(provider.get_resolved_ref)
265307
assert ref == 'b3344b7f17c335a817c5d7608c5e47fd7cabc023'
308+
ref_url = IOLoop().run_sync(provider.get_resolved_ref_url)
309+
assert ref_url == f'https://gitlab.com/{namespace}/tree/{ref}'
310+
resolved_spec = IOLoop().run_sync(provider.get_resolved_spec)
311+
assert resolved_spec == quote(namespace, safe='') + f'/{ref}'
266312

267313

268314
@pytest.mark.github_api
@@ -273,9 +319,13 @@ def test_gist_ref():
273319
slug = provider.get_build_slug()
274320
assert slug == '8a658f7f63b13768d1e75fa2464f5092'
275321
full_url = provider.get_repo_url()
276-
assert full_url == 'https://gist.github.com/mariusvniekerk/8a658f7f63b13768d1e75fa2464f5092.git'
322+
assert full_url == f'https://gist.github.com/{spec}.git'
277323
ref = IOLoop().run_sync(provider.get_resolved_ref)
278324
assert ref == '7daa381aae8409bfe28193e2ed8f767c26371237'
325+
ref_url = IOLoop().run_sync(provider.get_resolved_ref_url)
326+
assert ref_url == f'https://gist.github.com/{spec}/{ref}'
327+
resolved_spec = IOLoop().run_sync(provider.get_resolved_spec)
328+
assert resolved_spec == f'{spec}/{ref}'
279329

280330

281331
@pytest.mark.github_api

0 commit comments

Comments
 (0)