|
1
|
|
|
from __future__ import unicode_literals |
|
2
|
|
|
import base64 |
|
3
|
|
|
import collections |
|
4
|
|
|
import json |
|
5
|
|
|
import os |
|
6
|
|
|
import requests |
|
7
|
|
|
import warnings |
|
8
|
|
|
|
|
9
|
|
|
# For backwards compatibility |
|
10
|
|
|
from .errors import * |
|
11
|
|
|
from . import errors |
|
12
|
|
|
from .requests_ext import stream_multipart, NullAuth |
|
13
|
|
|
|
|
14
|
|
|
from .utils import compute_hash, jencode, pv |
|
15
|
|
|
from .utils.http_codes import STATUS_CODES |
|
16
|
|
|
|
|
17
|
|
|
from .mixins.organizations import OrgMixin |
|
18
|
|
|
from .mixins.channels import ChannelsMixin |
|
19
|
|
|
from .mixins.package import PackageMixin |
|
20
|
|
|
|
|
21
|
|
|
import logging |
|
22
|
|
|
import platform |
|
23
|
|
|
|
|
24
|
|
|
log = logging.getLogger('binstar') |
|
25
|
|
|
|
|
26
|
|
|
from ._version import get_versions |
|
27
|
|
|
|
|
28
|
|
|
__version__ = get_versions()['version'] |
|
29
|
|
|
del get_versions |
|
30
|
|
|
|
|
31
|
|
|
|
|
32
|
|
|
class Binstar(OrgMixin, ChannelsMixin, PackageMixin): |
|
33
|
|
|
''' |
|
34
|
|
|
An object that represents interfaces with the Anaconda Cloud restful API. |
|
35
|
|
|
|
|
36
|
|
|
:param token: a token generated by Binstar.authenticate or None for |
|
37
|
|
|
an anonymous user. |
|
38
|
|
|
''' |
|
39
|
|
|
|
|
40
|
|
|
def __init__(self, token=None, domain='https://api.anaconda.org', verify=True, **kwargs): |
|
41
|
|
|
|
|
42
|
|
|
self._session = requests.Session() |
|
43
|
|
|
self._session.headers['x-binstar-api-version'] = __version__ |
|
44
|
|
|
self.session.verify = verify |
|
45
|
|
|
self.session.auth = NullAuth() |
|
46
|
|
|
self.token = token |
|
47
|
|
|
|
|
48
|
|
|
user_agent = 'Anaconda-Client/{} (+https://anaconda.org)'.format(__version__) |
|
49
|
|
|
self._session.headers.update({ |
|
50
|
|
|
'User-Agent': user_agent, |
|
51
|
|
|
'Content-Type':'application/json', |
|
52
|
|
|
'Accept': 'application/json', |
|
53
|
|
|
}) |
|
54
|
|
|
|
|
55
|
|
|
if token: |
|
56
|
|
|
self._session.headers.update({'Authorization': 'token {}'.format(token)}) |
|
57
|
|
|
|
|
58
|
|
|
if domain.endswith('/'): |
|
59
|
|
|
domain = domain[:-1] |
|
60
|
|
|
self.domain = domain |
|
61
|
|
|
|
|
62
|
|
|
@property |
|
63
|
|
|
def session(self): |
|
64
|
|
|
return self._session |
|
65
|
|
|
|
|
66
|
|
|
def authentication_type(self): |
|
67
|
|
|
url = '%s/authentication-type' % self.domain |
|
68
|
|
|
res = self.session.get(url) |
|
69
|
|
|
try: |
|
70
|
|
|
self._check_response(res) |
|
71
|
|
|
res = res.json() |
|
72
|
|
|
return res['authentication_type'] |
|
73
|
|
|
except BinstarError: |
|
74
|
|
|
return 'password' |
|
75
|
|
|
|
|
76
|
|
|
def krb_authenticate(self, *args, **kwargs): |
|
77
|
|
|
try: |
|
78
|
|
|
from requests_kerberos import HTTPKerberosAuth |
|
79
|
|
|
return self._authenticate(HTTPKerberosAuth(), *args, **kwargs) |
|
80
|
|
|
except ImportError: |
|
81
|
|
|
raise BinstarError( |
|
82
|
|
|
'Kerberos authentication requires the requests-kerberos ' |
|
83
|
|
|
'package to be installed:\n' |
|
84
|
|
|
' conda install requests-kerberos\n' |
|
85
|
|
|
'or: \n' |
|
86
|
|
|
' pip install requests-kerberos' |
|
87
|
|
|
) |
|
88
|
|
|
|
|
89
|
|
|
def authenticate(self, username, password, *args, **kwargs): |
|
90
|
|
|
return self._authenticate((username, password), *args, **kwargs) |
|
91
|
|
|
|
|
92
|
|
|
def _authenticate(self, |
|
93
|
|
|
auth, |
|
94
|
|
|
application, |
|
95
|
|
|
application_url=None, |
|
96
|
|
|
for_user=None, |
|
97
|
|
|
scopes=None, |
|
98
|
|
|
created_with=None, |
|
99
|
|
|
max_age=None, |
|
100
|
|
|
strength='strong', |
|
101
|
|
|
fail_if_already_exists=False, |
|
102
|
|
|
hostname=platform.node()): |
|
103
|
|
|
''' |
|
104
|
|
|
Use basic authentication to create an authentication token using the interface below. |
|
105
|
|
|
With this technique, a username and password need not be stored permanently, and the user can |
|
106
|
|
|
revoke access at any time. |
|
107
|
|
|
|
|
108
|
|
|
:param username: The users name |
|
109
|
|
|
:param password: The users password |
|
110
|
|
|
:param application: The application that is requesting access |
|
111
|
|
|
:param application_url: The application's home page |
|
112
|
|
|
:param scopes: Scopes let you specify exactly what type of access you need. Scopes limit access for the tokens. |
|
113
|
|
|
''' |
|
114
|
|
|
|
|
115
|
|
|
url = '%s/authentications' % (self.domain) |
|
116
|
|
|
payload = {"scopes": scopes, "note": application, "note_url": application_url, |
|
117
|
|
|
'hostname': hostname, |
|
118
|
|
|
'user': for_user, |
|
119
|
|
|
'max-age': max_age, |
|
120
|
|
|
'created_with': None, |
|
121
|
|
|
'strength': strength, |
|
122
|
|
|
'fail-if-exists': fail_if_already_exists} |
|
123
|
|
|
|
|
124
|
|
|
data, headers = jencode(payload) |
|
125
|
|
|
res = self.session.post(url, auth=auth, data=data, headers=headers) |
|
126
|
|
|
self._check_response(res) |
|
127
|
|
|
res = res.json() |
|
128
|
|
|
token = res['token'] |
|
129
|
|
|
self.session.headers.update({'Authorization': 'token %s' % (token)}) |
|
130
|
|
|
return token |
|
131
|
|
|
|
|
132
|
|
|
def list_scopes(self): |
|
133
|
|
|
url = '%s/scopes' % (self.domain) |
|
134
|
|
|
res = requests.get(url) |
|
135
|
|
|
self._check_response(res) |
|
136
|
|
|
return res.json() |
|
137
|
|
|
|
|
138
|
|
|
def authentication(self): |
|
139
|
|
|
''' |
|
140
|
|
|
Retrieve information on the current authentication token |
|
141
|
|
|
''' |
|
142
|
|
|
url = '%s/authentication' % (self.domain) |
|
143
|
|
|
res = self.session.get(url) |
|
144
|
|
|
self._check_response(res) |
|
145
|
|
|
return res.json() |
|
146
|
|
|
|
|
147
|
|
|
def authentications(self): |
|
148
|
|
|
''' |
|
149
|
|
|
Get a list of the current authentication tokens |
|
150
|
|
|
''' |
|
151
|
|
|
|
|
152
|
|
|
url = '%s/authentications' % (self.domain) |
|
153
|
|
|
res = self.session.get(url) |
|
154
|
|
|
self._check_response(res) |
|
155
|
|
|
return res.json() |
|
156
|
|
|
|
|
157
|
|
|
def remove_authentication(self, auth_name=None, organization=None): |
|
158
|
|
|
""" |
|
159
|
|
|
Remove the current authentication or the one given by `auth_name` |
|
160
|
|
|
""" |
|
161
|
|
|
if auth_name: |
|
162
|
|
|
if organization: |
|
163
|
|
|
url = '%s/authentications/org/%s/name/%s' % (self.domain, organization, auth_name) |
|
164
|
|
|
else: |
|
165
|
|
|
url = '%s/authentications/name/%s' % (self.domain, auth_name) |
|
166
|
|
|
else: |
|
167
|
|
|
url = '%s/authentications' % (self.domain,) |
|
168
|
|
|
|
|
169
|
|
|
res = self.session.delete(url) |
|
170
|
|
|
self._check_response(res, [201]) |
|
171
|
|
|
|
|
172
|
|
|
def _check_response(self, res, allowed=[200]): |
|
173
|
|
|
api_version = res.headers.get('x-binstar-api-version', '0.2.1') |
|
174
|
|
|
if pv(api_version) > pv(__version__): |
|
175
|
|
|
msg = ('The api server is running the binstar-api version %s. you are using %s\n' % (api_version, __version__) |
|
176
|
|
|
+ 'Please update your client with pip install -U binstar or conda update binstar') |
|
177
|
|
|
warnings.warn(msg, stacklevel=4) |
|
178
|
|
|
|
|
179
|
|
|
|
|
180
|
|
|
if not res.status_code in allowed: |
|
181
|
|
|
short, long = STATUS_CODES.get(res.status_code, ('?', 'Undefined error')) |
|
182
|
|
|
msg = '%s: %s ([%s] %s -> %s)' % (short, long, res.request.method, res.request.url, res.status_code) |
|
183
|
|
|
try: |
|
184
|
|
|
data = res.json() |
|
185
|
|
|
except: |
|
186
|
|
|
pass |
|
187
|
|
|
else: |
|
188
|
|
|
msg = data.get('error', msg) |
|
189
|
|
|
|
|
190
|
|
|
ErrCls = errors.BinstarError |
|
191
|
|
|
if res.status_code == 401: |
|
192
|
|
|
ErrCls = errors.Unauthorized |
|
193
|
|
|
elif res.status_code == 404: |
|
194
|
|
|
ErrCls = errors.NotFound |
|
195
|
|
|
elif res.status_code == 409: |
|
196
|
|
|
ErrCls = errors.Conflict |
|
197
|
|
|
elif res.status_code >= 500: |
|
198
|
|
|
ErrCls = errors.ServerError |
|
199
|
|
|
|
|
200
|
|
|
raise ErrCls(msg, res.status_code) |
|
201
|
|
|
|
|
202
|
|
|
def user(self, login=None): |
|
203
|
|
|
''' |
|
204
|
|
|
Get user infomration. |
|
205
|
|
|
|
|
206
|
|
|
:param login: (optional) the login name of the user or None. If login is None |
|
207
|
|
|
this method will return the information of the authenticated user. |
|
208
|
|
|
''' |
|
209
|
|
|
if login: |
|
210
|
|
|
url = '%s/user/%s' % (self.domain, login) |
|
211
|
|
|
else: |
|
212
|
|
|
url = '%s/user' % (self.domain) |
|
213
|
|
|
|
|
214
|
|
|
res = self.session.get(url, verify=self.session.verify) |
|
215
|
|
|
self._check_response(res) |
|
216
|
|
|
|
|
217
|
|
|
return res.json() |
|
218
|
|
|
|
|
219
|
|
|
def user_packages( |
|
220
|
|
|
self, |
|
221
|
|
|
login=None, |
|
222
|
|
|
platform=None, |
|
223
|
|
|
package_type=None, |
|
224
|
|
|
type_=None, |
|
225
|
|
|
access=None): |
|
226
|
|
|
''' |
|
227
|
|
|
Returns a list of packages for a given user and optionally filter |
|
228
|
|
|
by `platform`, `package_type` and `type_`. |
|
229
|
|
|
|
|
230
|
|
|
:param login: (optional) the login name of the user or None. If login |
|
231
|
|
|
is None this method will return the packages for the |
|
232
|
|
|
authenticated user. |
|
233
|
|
|
:param platform: only find packages that include files for this platform. |
|
234
|
|
|
(e.g. 'linux-64', 'osx-64', 'win-32') |
|
235
|
|
|
:param package_type: only find packages that have this kind of file |
|
236
|
|
|
(e.g. 'env', 'conda', 'pypi') |
|
237
|
|
|
:param type_: only find packages that have this conda `type` |
|
238
|
|
|
(i.e. 'app') |
|
239
|
|
|
:param access: only find packages that have this access level |
|
240
|
|
|
(e.g. 'private', 'authenticated', 'public') |
|
241
|
|
|
''' |
|
242
|
|
|
if login: |
|
243
|
|
|
url = '{0}/packages/{1}'.format(self.domain, login) |
|
244
|
|
|
else: |
|
245
|
|
|
url = '{0}/packages'.format(self.domain) |
|
246
|
|
|
|
|
247
|
|
|
arguments = collections.OrderedDict() |
|
248
|
|
|
|
|
249
|
|
|
if platform: |
|
250
|
|
|
arguments['platform'] = platform |
|
251
|
|
|
if package_type: |
|
252
|
|
|
arguments['package_type'] = package_type |
|
253
|
|
|
if type_: |
|
254
|
|
|
arguments['type'] = type_ |
|
255
|
|
|
if access: |
|
256
|
|
|
arguments['access'] = access |
|
257
|
|
|
|
|
258
|
|
|
res = self.session.get(url, params=arguments) |
|
259
|
|
|
self._check_response(res) |
|
260
|
|
|
|
|
261
|
|
|
return res.json() |
|
262
|
|
|
|
|
263
|
|
|
def package(self, login, package_name): |
|
264
|
|
|
''' |
|
265
|
|
|
Get infomration about a specific package |
|
266
|
|
|
|
|
267
|
|
|
:param login: the login of the package owner |
|
268
|
|
|
:param package_name: the name of the package |
|
269
|
|
|
''' |
|
270
|
|
|
url = '%s/package/%s/%s' % (self.domain, login, package_name) |
|
271
|
|
|
res = self.session.get(url) |
|
272
|
|
|
self._check_response(res) |
|
273
|
|
|
return res.json() |
|
274
|
|
|
|
|
275
|
|
|
def package_add_collaborator(self, owner, package_name, collaborator): |
|
276
|
|
|
url = '%s/packages/%s/%s/collaborators/%s' % (self.domain, owner, package_name, collaborator) |
|
277
|
|
|
res = self.session.put(url) |
|
278
|
|
|
self._check_response(res, [201]) |
|
279
|
|
|
return |
|
280
|
|
|
|
|
281
|
|
|
def package_remove_collaborator(self, owner, package_name, collaborator): |
|
282
|
|
|
url = '%s/packages/%s/%s/collaborators/%s' % (self.domain, owner, package_name, collaborator) |
|
283
|
|
|
res = self.session.delete(url) |
|
284
|
|
|
self._check_response(res, [201]) |
|
285
|
|
|
return |
|
286
|
|
|
|
|
287
|
|
|
def package_collaborators(self, owner, package_name): |
|
288
|
|
|
|
|
289
|
|
|
url = '%s/packages/%s/%s/collaborators' % (self.domain, owner, package_name) |
|
290
|
|
|
res = self.session.get(url) |
|
291
|
|
|
self._check_response(res, [200]) |
|
292
|
|
|
return res.json() |
|
293
|
|
|
|
|
294
|
|
|
def all_packages(self, modified_after=None): |
|
295
|
|
|
''' |
|
296
|
|
|
''' |
|
297
|
|
|
url = '%s/package_listing' % (self.domain) |
|
298
|
|
|
data = {'modified_after':modified_after or ''} |
|
299
|
|
|
res = self.session.get(url, data=data) |
|
300
|
|
|
self._check_response(res) |
|
301
|
|
|
return res.json() |
|
302
|
|
|
|
|
303
|
|
|
|
|
304
|
|
|
def add_package(self, login, package_name, |
|
305
|
|
|
summary=None, |
|
306
|
|
|
license=None, |
|
307
|
|
|
public=True, |
|
308
|
|
|
license_url=None, |
|
309
|
|
|
attrs=None): |
|
310
|
|
|
''' |
|
311
|
|
|
Add a new package to a users account |
|
312
|
|
|
|
|
313
|
|
|
:param login: the login of the package owner |
|
314
|
|
|
:param package_name: the name of the package to be created |
|
315
|
|
|
:param package_type: A type identifyer for the package (eg. 'pypi' or 'conda', etc.) |
|
316
|
|
|
:param summary: A short summary about the package |
|
317
|
|
|
:param license: the name of the package license |
|
318
|
|
|
:param license_url: the url of the package license |
|
319
|
|
|
:param public: if true then the package will be hosted publicly |
|
320
|
|
|
:param attrs: A dictionary of extra attributes for this package |
|
321
|
|
|
''' |
|
322
|
|
|
url = '%s/package/%s/%s' % (self.domain, login, package_name) |
|
323
|
|
|
|
|
324
|
|
|
attrs = attrs or {} |
|
325
|
|
|
attrs['summary'] = summary |
|
326
|
|
|
attrs['license'] = {'name':license, 'url':license_url} |
|
327
|
|
|
|
|
328
|
|
|
payload = dict(public=bool(public), |
|
329
|
|
|
publish=False, |
|
330
|
|
|
public_attrs=dict(attrs or {}) |
|
331
|
|
|
) |
|
332
|
|
|
|
|
333
|
|
|
data, headers = jencode(payload) |
|
334
|
|
|
res = self.session.post(url, data=data, headers=headers) |
|
335
|
|
|
self._check_response(res) |
|
336
|
|
|
return res.json() |
|
337
|
|
|
|
|
338
|
|
|
def remove_package(self, username, package_name): |
|
339
|
|
|
|
|
340
|
|
|
url = '%s/package/%s/%s' % (self.domain, username, package_name) |
|
341
|
|
|
|
|
342
|
|
|
res = self.session.delete(url) |
|
343
|
|
|
self._check_response(res, [201]) |
|
344
|
|
|
return |
|
345
|
|
|
|
|
346
|
|
|
def release(self, login, package_name, version): |
|
347
|
|
|
''' |
|
348
|
|
|
Get information about a specific release |
|
349
|
|
|
|
|
350
|
|
|
:param login: the login of the package owner |
|
351
|
|
|
:param package_name: the name of the package |
|
352
|
|
|
:param version: the name of the package |
|
353
|
|
|
''' |
|
354
|
|
|
url = '%s/release/%s/%s/%s' % (self.domain, login, package_name, version) |
|
355
|
|
|
res = self.session.get(url) |
|
356
|
|
|
self._check_response(res) |
|
357
|
|
|
return res.json() |
|
358
|
|
|
|
|
359
|
|
|
def remove_release(self, username, package_name, version): |
|
360
|
|
|
''' |
|
361
|
|
|
remove a release and all files under it |
|
362
|
|
|
|
|
363
|
|
|
:param username: the login of the package owner |
|
364
|
|
|
:param package_name: the name of the package |
|
365
|
|
|
:param version: the name of the package |
|
366
|
|
|
''' |
|
367
|
|
|
url = '%s/release/%s/%s/%s' % (self.domain, username, package_name, version) |
|
368
|
|
|
res = self.session.delete(url) |
|
369
|
|
|
self._check_response(res, [201]) |
|
370
|
|
|
return |
|
371
|
|
|
|
|
372
|
|
|
def add_release(self, login, package_name, version, requirements, announce, |
|
373
|
|
|
description, icon=None): |
|
374
|
|
|
''' |
|
375
|
|
|
Add a new release to a package. |
|
376
|
|
|
|
|
377
|
|
|
:param login: the login of the package owner |
|
378
|
|
|
:param package_name: the name of the package |
|
379
|
|
|
:param version: the version string of the release |
|
380
|
|
|
:param requirements: A dict of requirements TODO: describe |
|
381
|
|
|
:param announce: An announcement that will be posted to all package watchers |
|
382
|
|
|
:param description: A long description about the package |
|
383
|
|
|
''' |
|
384
|
|
|
|
|
385
|
|
|
url = '%s/release/%s/%s/%s' % (self.domain, login, package_name, version) |
|
386
|
|
|
|
|
387
|
|
|
payload = {'requirements': requirements, |
|
388
|
|
|
'announce': announce, |
|
389
|
|
|
'description': description, |
|
390
|
|
|
'icon': icon, |
|
391
|
|
|
} |
|
392
|
|
|
data, headers = jencode(payload) |
|
393
|
|
|
res = self.session.post(url, data=data, headers=headers) |
|
394
|
|
|
self._check_response(res) |
|
395
|
|
|
return res.json() |
|
396
|
|
|
|
|
397
|
|
|
def distribution(self, login, package_name, release, basename=None): |
|
398
|
|
|
|
|
399
|
|
|
url = '%s/dist/%s/%s/%s/%s' % (self.domain, login, package_name, release, basename) |
|
400
|
|
|
|
|
401
|
|
|
res = self.session.get(url) |
|
402
|
|
|
self._check_response(res) |
|
403
|
|
|
return res.json() |
|
404
|
|
|
|
|
405
|
|
|
def remove_dist(self, login, package_name, release, basename=None, _id=None): |
|
406
|
|
|
|
|
407
|
|
|
if basename: |
|
408
|
|
|
url = '%s/dist/%s/%s/%s/%s' % (self.domain, login, package_name, release, basename) |
|
409
|
|
|
elif _id: |
|
410
|
|
|
url = '%s/dist/%s/%s/%s/-/%s' % (self.domain, login, package_name, release, _id) |
|
411
|
|
|
else: |
|
412
|
|
|
raise TypeError("method remove_dist expects either 'basename' or '_id' arguments") |
|
413
|
|
|
|
|
414
|
|
|
res = self.session.delete(url) |
|
415
|
|
|
self._check_response(res) |
|
416
|
|
|
return res.json() |
|
417
|
|
|
|
|
418
|
|
|
|
|
419
|
|
|
def download(self, login, package_name, release, basename, md5=None): |
|
420
|
|
|
''' |
|
421
|
|
|
Dowload a package distribution |
|
422
|
|
|
|
|
423
|
|
|
:param login: the login of the package owner |
|
424
|
|
|
:param package_name: the name of the package |
|
425
|
|
|
:param version: the version string of the release |
|
426
|
|
|
:param basename: the basename of the distribution to download |
|
427
|
|
|
:param md5: (optional) an md5 hash of the download if given and the package has not changed |
|
428
|
|
|
None will be returned |
|
429
|
|
|
|
|
430
|
|
|
:returns: a file like object or None |
|
431
|
|
|
''' |
|
432
|
|
|
|
|
433
|
|
|
url = '%s/download/%s/%s/%s/%s' % (self.domain, login, package_name, release, basename) |
|
434
|
|
|
if md5: |
|
435
|
|
|
headers = {'ETag':md5, } |
|
436
|
|
|
else: |
|
437
|
|
|
headers = {} |
|
438
|
|
|
|
|
439
|
|
|
res = self.session.get(url, headers=headers, allow_redirects=False) |
|
440
|
|
|
self._check_response(res, allowed=[200, 302, 304]) |
|
441
|
|
|
|
|
442
|
|
|
if res.status_code == 200: |
|
443
|
|
|
# We received the content directly from anaconda.org |
|
444
|
|
|
return res |
|
445
|
|
|
elif res.status_code == 304: |
|
446
|
|
|
# The content has not changed |
|
447
|
|
|
return None |
|
448
|
|
|
elif res.status_code == 302: |
|
449
|
|
|
# Download from s3: |
|
450
|
|
|
# We need to create a new request (without using session) to avoid |
|
451
|
|
|
# sending the custom headers set on our session to S3 (which causes |
|
452
|
|
|
# a failure). |
|
453
|
|
|
res2 = requests.get(res.headers['location'], stream=True) |
|
454
|
|
|
return res2 |
|
455
|
|
|
|
|
456
|
|
|
|
|
457
|
|
|
def upload(self, login, package_name, release, basename, fd, distribution_type, |
|
458
|
|
|
description='', md5=None, size=None, dependencies=None, attrs=None, channels=('main',), callback=None): |
|
459
|
|
|
''' |
|
460
|
|
|
Upload a new distribution to a package release. |
|
461
|
|
|
|
|
462
|
|
|
:param login: the login of the package owner |
|
463
|
|
|
:param package_name: the name of the package |
|
464
|
|
|
:param version: the version string of the release |
|
465
|
|
|
:param basename: the basename of the distribution to download |
|
466
|
|
|
:param fd: a file like object to upload |
|
467
|
|
|
:param distribution_type: pypi or conda or ipynb, etc |
|
468
|
|
|
:param description: (optional) a short description about the file |
|
469
|
|
|
:param attrs: any extra attributes about the file (eg. build=1, pyversion='2.7', os='osx') |
|
470
|
|
|
|
|
471
|
|
|
''' |
|
472
|
|
|
url = '%s/stage/%s/%s/%s/%s' % (self.domain, login, package_name, release, basename) |
|
473
|
|
|
if attrs is None: |
|
474
|
|
|
attrs = {} |
|
475
|
|
|
if not isinstance(attrs, dict): |
|
476
|
|
|
raise TypeError('argument attrs must be a dictionary') |
|
477
|
|
|
|
|
478
|
|
|
payload = dict(distribution_type=distribution_type, description=description, attrs=attrs, |
|
479
|
|
|
dependencies=dependencies, channels=channels) |
|
480
|
|
|
|
|
481
|
|
|
data, headers = jencode(payload) |
|
482
|
|
|
res = self.session.post(url, data=data, headers=headers) |
|
483
|
|
|
self._check_response(res) |
|
484
|
|
|
obj = res.json() |
|
485
|
|
|
|
|
486
|
|
|
s3url = obj['post_url'] |
|
487
|
|
|
s3data = obj['form_data'] |
|
488
|
|
|
|
|
489
|
|
|
if md5 is None: |
|
490
|
|
|
_hexmd5, b64md5, size = compute_hash(fd, size=size) |
|
491
|
|
|
elif size is None: |
|
492
|
|
|
spos = fd.tell() |
|
493
|
|
|
fd.seek(0, os.SEEK_END) |
|
494
|
|
|
size = fd.tell() - spos |
|
495
|
|
|
fd.seek(spos) |
|
496
|
|
|
|
|
497
|
|
|
s3data['Content-Length'] = size |
|
498
|
|
|
s3data['Content-MD5'] = b64md5 |
|
499
|
|
|
|
|
500
|
|
|
data_stream, headers = stream_multipart(s3data, files={'file':(basename, fd)}, |
|
501
|
|
|
callback=callback) |
|
502
|
|
|
|
|
503
|
|
|
s3res = requests.post(s3url, data=data_stream, verify=self.session.verify, timeout=10 * 60 * 60, headers=headers) |
|
504
|
|
|
|
|
505
|
|
|
if s3res.status_code != 201: |
|
506
|
|
|
log.info(s3res.text) |
|
507
|
|
|
log.info('') |
|
508
|
|
|
log.info('') |
|
509
|
|
|
raise errors.BinstarError('Error uploading package', s3res.status_code) |
|
510
|
|
|
|
|
511
|
|
|
url = '%s/commit/%s/%s/%s/%s' % (self.domain, login, package_name, release, basename) |
|
512
|
|
|
payload = dict(dist_id=obj['dist_id']) |
|
513
|
|
|
data, headers = jencode(payload) |
|
514
|
|
|
res = self.session.post(url, data=data, headers=headers) |
|
515
|
|
|
self._check_response(res) |
|
516
|
|
|
|
|
517
|
|
|
return res.json() |
|
518
|
|
|
|
|
519
|
|
|
def search(self, query, package_type=None): |
|
520
|
|
|
url = '%s/search' % self.domain |
|
521
|
|
|
res = self.session.get(url, params={'name':query, 'type':package_type}) |
|
522
|
|
|
self._check_response(res) |
|
523
|
|
|
return res.json() |
|
524
|
|
|
|
|
525
|
|
|
|
|
526
|
|
|
from ._version import get_versions |
|
527
|
|
|
__version__ = get_versions()['version'] |
|
528
|
|
|
del get_versions |
|
529
|
|
|
|