Skip to content

Commit f486086

Browse files
committed
adding api-endpoint client for singularity hub, still needs tests
1 parent 3caa141 commit f486086

File tree

7 files changed

+288
-1
lines changed

7 files changed

+288
-1
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Compare Singularity Hub containers
2+
3+
# This is a simple script to use the singularity command line tool to download containers
4+
# (using Singularity, Section 1) and compare build specs (using Singularity Hub API, Section 2) and to
5+
# compare the containers themselves using singularity python (Section 3)
6+
7+
container_names = ['vsoch/singularity-hello-world',
8+
'researchapps/quantum_state_diffusion',
9+
'vsoch/pefinder']
10+
11+
from singularity.hub.client import Client
12+
13+
client = Client()
14+
results = dict()
15+
16+
for container_name in container_names:
17+
18+
# Retrieve the container based on the name
19+
collection = client.get_collection(container_name)
20+
container_ids = collection['container_set']
21+
containers = []
22+
for container_id in container_ids:
23+
container = client.get_container(container_id)
24+
containers.append(container)

singularity/hub/__init__.py

Whitespace-only changes.

singularity/hub/auth.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/usr/bin/env python
2+
3+
'''
4+
auth.py: authentication functions for singularity hub api
5+
currently no token / auth for private collections
6+
'''
7+
8+
from singularity.logman import bot
9+
import os
10+
import sys
11+
12+
13+
def get_headers(token=None):
14+
'''get_headers will return a simple default header for a json
15+
post. This function will be adopted as needed.
16+
:param token: an optional token to add for auth
17+
'''
18+
headers = dict()
19+
headers["Content-Type"] = "application/json"
20+
if token!=None:
21+
headers["Authorization"] = "Bearer %s" %(token)
22+
23+
header_names = ",".join(list(headers.keys()))
24+
bot.logger.debug("Headers found: %s",header_names)
25+
return headers

singularity/hub/base.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/usr/bin/env python
2+
3+
'''
4+
base.py: base module for working with singularity hub api. Right
5+
now serves to hold defaults.
6+
7+
'''
8+
9+
from singularity.hub.utils import (
10+
parse_container_name,
11+
is_number,
12+
api_get,
13+
api_post
14+
)
15+
16+
api_base = "https://singularity-hub.org/api"
17+
18+
def get_template(container_name,get_type=None):
19+
'''get a container/collection or return None.
20+
'''
21+
if get_type == None:
22+
get_type = "container"
23+
get_type = get_type.lower().replace(' ','')
24+
25+
result = None
26+
image = parse_container_name(container_name)
27+
if is_number(image):
28+
url = "%s/%ss/%s" %(api_base,get_type,image)
29+
30+
elif image['user'] is not None and image['repo_name'] is not None:
31+
url = "%s/%s/%s/%s" %(api_base,
32+
get_type,
33+
image['user'],
34+
image['repo_name'])
35+
36+
if image['repo_tag'] is not None and get_type is not "collection":
37+
url = "%s:%s" %(url,image['repo_tag'])
38+
39+
result = api_get(url)
40+
41+
return result

singularity/hub/client.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#!/usr/bin/env python
2+
3+
'''
4+
client.py: simple client for singularity hub api
5+
6+
'''
7+
8+
from singularity.hub.auth import (
9+
get_headers
10+
)
11+
12+
from singularity.logman import bot
13+
from singularity.hub.base import get_template
14+
15+
16+
class Client(object):
17+
18+
19+
def __init__(self, token=None):
20+
21+
if token is not None:
22+
self.token = token
23+
# currently not used
24+
self.headers = get_headers(token=token)
25+
26+
27+
def update_headers(self, headers):
28+
'''update_headers will add headers to the client
29+
:param headers: should be a dictionary of key,value to update/add to header
30+
'''
31+
for key,value in headers.items():
32+
self.client.headers[key] = item
33+
34+
35+
def get_container(self,container_name):
36+
'''get a container or return None.
37+
'''
38+
return get_template(container_name,"container")
39+
40+
41+
def get_collection(self,container_name):
42+
'''get a container collection or return None.
43+
'''
44+
return get_template(container_name,"collection")

singularity/hub/utils.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
#!/usr/bin/env python
2+
3+
'''
4+
utils.py: general http functions (utils) for som api
5+
6+
'''
7+
8+
from singularity.logman import bot
9+
from singularity.hub.auth import get_headers
10+
11+
import requests
12+
import os
13+
import sys
14+
15+
16+
def api_get(url,headers=None,token=None,data=None, return_json=True):
17+
'''api_get will use requests to get a particular url
18+
:param url: the url to send file to
19+
:param headers: a dictionary with headers for the request
20+
:param putdata: additional data to add to the request
21+
:param return_json: return json if successful
22+
'''
23+
bot.logger.debug("GET %s",url)
24+
25+
if headers == None:
26+
headers = get_headers(token=token)
27+
if data == None:
28+
response = requests.get(url,
29+
headers=headers)
30+
else:
31+
response = requests.get(url,
32+
headers=headers,
33+
json=data)
34+
35+
if response.status_code == 200 and return_json:
36+
return response.json()
37+
38+
return response
39+
40+
41+
def api_put(url,headers=None,token=None,data=None, return_json=True):
42+
'''api_put will send a read file (spec) to Singularity Hub with a particular set of headers
43+
:param url: the url to send file to
44+
:param headers: the headers to get
45+
:param headers: a dictionary with headers for the request
46+
:param data: additional data to add to the request
47+
:param return_json: return json if successful
48+
'''
49+
bot.logger.debug("PUT %s",url)
50+
51+
if headers == None:
52+
headers = get_headers(token=token)
53+
if data == None:
54+
response = requests.put(url,
55+
headers=headers)
56+
else:
57+
response = requests.put(url,
58+
headers=headers,
59+
json=data)
60+
61+
if response.status_code == 200 and return_json:
62+
return response.json()
63+
64+
return response
65+
66+
67+
def api_post(url,headers=None,data=None,token=None,return_json=True):
68+
'''api_get will use requests to get a particular url
69+
:param url: the url to send file to
70+
:param headers: a dictionary with headers for the request
71+
:param data: additional data to add to the request
72+
:param return_json: return json if successful
73+
'''
74+
bot.logger.debug("POST %s",url)
75+
76+
if headers == None:
77+
headers = get_headers(token=token)
78+
if data == None:
79+
response = requests.post(url,
80+
headers=headers)
81+
else:
82+
response = requests.post(url,
83+
headers=headers,
84+
json=data)
85+
86+
if response.status_code == 200 and return_json:
87+
return response.json()
88+
89+
return response
90+
91+
92+
######################################################################
93+
# OS/IO and Formatting Functions
94+
######################################################################
95+
96+
97+
def is_number(container_name):
98+
'''is_number determines if the user is providing a singularity hub
99+
number (meaning the id of an image to download) vs a full name)
100+
'''
101+
if isinstance(container_name,dict):
102+
return False
103+
try:
104+
float(container_name)
105+
return True
106+
except ValueError:
107+
return False
108+
109+
110+
def parse_container_name(image):
111+
'''parse_container_name will return a json structure with a repo name, tag, user.
112+
'''
113+
container_name = image
114+
if not is_number(image):
115+
image = image.replace(' ','')
116+
117+
# If the user provided a number (unique id for an image), return it
118+
if is_number(image) == True:
119+
logger.info("Numeric image ID %s found.", image)
120+
return int(image)
121+
122+
image = image.split('/')
123+
124+
# If there are two parts, we have username with repo (and maybe tag)
125+
if len(image) >= 2:
126+
user = image[0]
127+
image = image[1]
128+
129+
# Otherwise, we trigger error (not supported just usernames yet)
130+
else:
131+
bot.logger.error('You must specify a repo name and username, %s is not valid',container_name)
132+
sys.exit(1)
133+
134+
# Now split the name by : in case there is a tag
135+
image = image.split(':')
136+
if len(image) == 2:
137+
repo_name = image[0]
138+
repo_tag = image[1]
139+
140+
# Otherwise, assume latest of an image
141+
else:
142+
repo_name = image[0]
143+
repo_tag = "latest"
144+
145+
bot.logger.info("User: %s", user)
146+
bot.logger.info("Repo Name: %s", repo_name)
147+
bot.logger.info("Repo Tag: %s", repo_tag)
148+
149+
parsed = {'repo_name':repo_name,
150+
'repo_tag':repo_tag,
151+
'user':user }
152+
153+
return parsed

singularity/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "1.1.1"
1+
__version__ = "1.1.2"
22
AUTHOR = 'Vanessa Sochat'
33
AUTHOR_EMAIL = '[email protected]'
44
NAME = 'singularity'

0 commit comments

Comments
 (0)