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
|
|
|
|