PK@G dxf/exceptions.py""" Module containing exceptions thrown by :mod:`dxf`. """ class DXFError(Exception): """ Base exception class for all dxf errors """ pass class DXFUnexpectedError(DXFError): """ Unexpected value error """ def __init__(self, got, expected): """ :param got: Actual value received :param expected: Value that was expected """ super(DXFUnexpectedError, self).__init__() self.got = got self.expected = expected class DXFUnexpectedStatusCodeError(DXFUnexpectedError): """ Unexpected HTTP status code """ def __str__(self): return 'expected status code %d, got %d' % (self.expected, self.got) class DXFDigestMismatchError(DXFUnexpectedError): """ Digest didn't match expected value """ def __str__(self): return 'expected digest %s, got %s' % (self.expected, self.got) class DXFUnexpectedKeyTypeError(DXFUnexpectedError): """ Cryptographic key type not supported """ def __str__(self): return 'expected key type %s, got %s' % (self.expected, self.got) class DXFUnexpectedDigestMethodError(DXFUnexpectedError): """ Digest method not supported """ def __str__(self): return 'expected digest method %s, got %s' % (self.expected, self.got) class DXFDisallowedSignatureAlgorithmError(DXFError): """ Signature algorithm forbidden """ def __init__(self, alg): """ :param alg: Forbidden signature algorithm :type alg: str """ super(DXFDisallowedSignatureAlgorithmError, self).__init__() self.alg = alg def __str__(self): return 'disallowed signature algorithm: %s' % self.alg class DXFSignatureChainNotImplementedError(DXFError): """ Signature chains not supported """ def __str__(self): return 'verification with a cert chain is not implemented' class DXFUnauthorizedError(DXFError): """ Registry returned authorized error """ def __str__(self): return 'unauthorized' class DXFAuthInsecureError(DXFError): """ Can't authenticate over insecure (non-HTTPS) connection """ def __str__(self): return 'Auth requires HTTPS' PKd4H&j0 W Wdxf/__init__.py""" Module for accessing a Docker v2 Registry """ import base64 import hashlib import json import sys try: import urllib.parse as urlparse from urllib.parse import urlencode except ImportError: # pylint: disable=import-error,no-name-in-module,wrong-import-order from urllib import urlencode import urlparse import requests import www_authenticate import ecdsa import jws # pylint: disable=wildcard-import from dxf import exceptions if sys.version_info < (3, 0): _binary_type = str else: _binary_type = bytes # pylint: disable=redefined-builtin long = int def _to_bytes_2and3(s): return s if isinstance(s, _binary_type) else s.encode('utf-8') jws.utils.to_bytes_2and3 = _to_bytes_2and3 jws.algos.to_bytes_2and3 = _to_bytes_2and3 def _urlsafe_b64encode(s): return base64.urlsafe_b64encode(_to_bytes_2and3(s)).rstrip(b'=').decode('utf-8') def _pad64(s): return s + b'=' * (-len(s) % 4) def _urlsafe_b64decode(s): return base64.urlsafe_b64decode(_pad64(_to_bytes_2and3(s))) def _num_to_base64(n): b = bytearray() while n: b.insert(0, n & 0xFF) n >>= 8 # need to pad to 32 bytes while len(b) < 32: b.insert(0, 0) return base64.urlsafe_b64encode(b).rstrip(b'=').decode('utf-8') def _base64_to_num(s): b = bytearray(_urlsafe_b64decode(s)) m = len(b) - 1 return sum((1 << ((m - bi)*8)) * bb for (bi, bb) in enumerate(b)) def _jwk_to_key(jwk): if jwk['kty'] != 'EC': raise exceptions.DXFUnexpectedKeyTypeError(jwk['kty'], 'EC') if jwk['crv'] != 'P-256': raise exceptions.DXFUnexpectedKeyTypeError(jwk['crv'], 'P-256') # pylint: disable=bad-continuation return ecdsa.VerifyingKey.from_public_point( ecdsa.ellipticcurve.Point(ecdsa.NIST256p.curve, _base64_to_num(jwk['x']), _base64_to_num(jwk['y'])), ecdsa.NIST256p) def hash_bytes(buf): """ Hash bytes using the same method the registry uses (currently SHA-256). :param filename: Bytes to hash :type filename: str :rtype: str :returns: Hex-encoded hash of file's content """ sha256 = hashlib.sha256() sha256.update(buf) return sha256.hexdigest() def hash_file(filename): """ Hash a file using the same method the registry uses (currently SHA-256). :param filename: Name of file to hash :type filename: str :rtype: str :returns: Hex-encoded hash of file's content """ sha256 = hashlib.sha256() with open(filename, 'rb') as f: for chunk in iter(lambda: f.read(8192), b''): sha256.update(chunk) return sha256.hexdigest() def _verify_manifest(content, content_digest=None, verify=True): # pylint: disable=too-many-locals,too-many-branches # Adapted from https://github.com/joyent/node-docker-registry-client manifest = json.loads(content) if verify or ('signatures' in manifest): signatures = [] for sig in manifest['signatures']: protected64 = sig['protected'] protected = _urlsafe_b64decode(protected64).decode('utf-8') protected_header = json.loads(protected) format_length = protected_header['formatLength'] format_tail64 = protected_header['formatTail'] format_tail = _urlsafe_b64decode(format_tail64).decode('utf-8') alg = sig['header']['alg'] if alg.lower() == 'none': raise exceptions.DXFDisallowedSignatureAlgorithmError('none') if sig['header'].get('chain'): raise exceptions.DXFSignatureChainNotImplementedError() signatures.append({ 'alg': alg, 'signature': sig['signature'], 'protected64': protected64, 'key': _jwk_to_key(sig['header']['jwk']), 'format_length': format_length, 'format_tail': format_tail }) payload = content[:signatures[0]['format_length']] + \ signatures[0]['format_tail'] payload64 = _urlsafe_b64encode(payload) else: payload = content if content_digest: method, expected_dgst = content_digest.split(':') if method != 'sha256': raise exceptions.DXFUnexpectedDigestMethodError(method, 'sha256') hasher = hashlib.new(method) hasher.update(payload.encode('utf-8')) dgst = hasher.hexdigest() if dgst != expected_dgst: raise exceptions.DXFDigestMismatchError(dgst, expected_dgst) if verify: for sig in signatures: data = { 'key': sig['key'], 'header': { 'alg': sig['alg'] } } jws.header.process(data, 'verify') sig64 = sig['signature'] data['verifier']("%s.%s" % (sig['protected64'], payload64), _urlsafe_b64decode(sig64), sig['key']) dgsts = [] for layer in manifest['fsLayers']: method, dgst = layer['blobSum'].split(':') if method != 'sha256': raise exceptions.DXFUnexpectedDigestMethodError(method, 'sha256') dgsts.append(dgst) return dgsts def _raise_for_status(r): # pylint: disable=no-member if r.status_code == requests.codes.unauthorized: raise exceptions.DXFUnauthorizedError() r.raise_for_status() class _ReportingFile(object): def __init__(self, dgst, f, cb): self._dgst = dgst self._f = f self._cb = cb self._size = requests.utils.super_len(f) cb(dgst, b'', self._size) # define __iter__ so requests thinks we're a stream # (models.py, PreparedRequest.prepare_body) def __iter__(self): assert not "called" # define fileno, tell and mode so requests can find length # (utils.py, super_len) def fileno(self): return self._f.fileno() def tell(self): return self._f.tell() @property def mode(self): return self._f.mode def read(self, n): chunk = self._f.read(n) if len(chunk) > 0: self._cb(self._dgst, chunk, self._size) return chunk class DXFBase(object): # pylint: disable=too-many-instance-attributes """ Class for communicating with a Docker v2 registry. Contains only operations which aren't related to repositories. Can act as a context manager. For each context entered, a new `requests.Session `_ is obtained. Connections to the same host are shared by the session. When the context exits, all the session's connections are closed. If you don't use :class:`DXFBase` as a context manager, each request uses an ephemeral session. If you don't read all the data from an iterator returned by :meth:`DXF.pull_blob` then the underlying connection won't be closed until Python garbage collects the iterator. """ def __init__(self, host, auth=None, insecure=False, auth_host=None): """ :param host: Host name of registry. Can contain port numbers. e.g. ``registry-1.docker.io``, ``localhost:5000``. :type host: str :param auth: Authentication function to be called whenever authentication to the registry is required. Receives the :class:`DXFBase` object and a HTTP response object. It should call :meth:`authenticate` with a username, password and ``response`` before it returns. :type auth: function(dxf_obj, response) :param insecure: Use HTTP instead of HTTPS (which is the default) when connecting to the registry. :type insecure: bool :param auth_host: Host to use for token authentication. If set, overrides host returned by then registry. :type auth_host: str """ self._base_url = ('http' if insecure else 'https') + '://' + host + '/v2/' self._auth = auth self._insecure = insecure self._auth_host = auth_host self._token = None self._headers = {} self._repo = None self._sessions = [requests] @property def token(self): """ str: Authentication token. This will be obtained automatically when you call :meth:`authenticate`. If you've obtained a token previously, you can also set it but be aware tokens expire quickly. """ return self._token @token.setter def token(self, value): self._token = value self._headers = { 'Authorization': 'Bearer ' + value } def _base_request(self, method, path, **kwargs): url = urlparse.urljoin(self._base_url, path) r = getattr(self._sessions[0], method)(url, headers=self._headers, **kwargs) # pylint: disable=no-member if r.status_code == requests.codes.unauthorized and self._auth: headers = self._headers self._auth(self, r) if self._headers != headers: r = getattr(self._sessions[0], method)(url, headers=self._headers, **kwargs) _raise_for_status(r) return r def authenticate(self, username=None, password=None, actions=None, response=None): """ Authenticate to the registry, using a username and password if supplied, otherwise as the anonymous user. :param username: User name to authenticate as. :type username: str :param password: User's password. :type password: str :param actions: If you know which types of operation you need to make on the registry, specify them here. Valid actions are ``pull``, ``push`` and ``*``. :type actions: list :param response: When the ``auth`` function you passed to :class:`DXFBase`'s constructor is called, it is passed a HTTP response object. Pass it back to :meth:`authenticate` to have it automatically detect which actions are required. :type response: requests.Response :rtype: str :returns: Authentication token, if the registry supports bearer tokens. Otherwise ``None``, and HTTP Basic auth is used. """ if self._insecure: raise exceptions.DXFAuthInsecureError() if response is None: response = self._sessions[0].get(self._base_url) # pylint: disable=no-member if response.status_code != requests.codes.unauthorized: raise exceptions.DXFUnexpectedStatusCodeError(response.status_code, requests.codes.unauthorized) parsed = www_authenticate.parse(response.headers['www-authenticate']) if username is not None and password is not None: headers = { 'Authorization': 'Basic ' + base64.b64encode(_to_bytes_2and3(username + ':' + password)).decode('utf-8') } else: headers = {} if 'bearer' in parsed: info = parsed['bearer'] if actions and self._repo: scope = 'repository:' + self._repo + ':' + ','.join(actions) else: scope = info['scope'] url_parts = list(urlparse.urlparse(info['realm'])) query = urlparse.parse_qs(url_parts[4]) query.update({ 'service': info['service'], 'scope': scope }) url_parts[4] = urlencode(query, True) url_parts[0] = 'https' if self._auth_host: url_parts[1] = self._auth_host auth_url = urlparse.urlunparse(url_parts) r = self._sessions[0].get(auth_url, headers=headers) _raise_for_status(r) self.token = r.json()['token'] return self._token else: self._headers = headers def list_repos(self): """ List all repositories in the registry. :rtype: list :returns: List of repository names. """ return self._base_request('get', '_catalog').json()['repositories'] def __enter__(self): assert len(self._sessions) > 0 session = requests.Session() session.__enter__() self._sessions.insert(0, session) return self def __exit__(self, *args): assert len(self._sessions) > 1 session = self._sessions.pop(0) return session.__exit__(*args) class DXF(DXFBase): """ Class for operating on a Docker v2 repositories. """ # pylint: disable=too-many-arguments def __init__(self, host, repo, auth=None, insecure=False, auth_host=None): """ :param host: Host name of registry. Can contain port numbers. e.g. ``registry-1.docker.io``, ``localhost:5000``. :type host: str :param repo: Name of the repository to access on the registry. Typically this is of the form ``username/reponame`` but for your own registries you don't actually have to stick to that. :type repo: str :param auth: Authentication function to be called whenever authentication to the registry is required. Receives the :class:`DXF` object and a HTTP response object. It should call :meth:`DXFBase.authenticate` with a username, password and ``response`` before it returns. :type auth: function(dxf_obj, response) :param insecure: Use HTTP instead of HTTPS (which is the default) when connecting to the registry. :type insecure: bool :param auth_host: Host to use for token authentication. If set, overrides host returned by then registry. :type auth_host: str """ super(DXF, self).__init__(host, auth, insecure, auth_host) self._repo = repo def _request(self, method, path, **kwargs): return super(DXF, self)._base_request(method, self._repo + '/' + path, **kwargs) def push_blob(self, filename, progress=None): """ Upload a file to the registry and return its (SHA-256) hash. The registry is content-addressable so the file's content (aka blob) can be retrieved later by passing the hash to :meth:`pull_blob`. :param filename: File to upload. :type filename: str :param progress: Optional function to call as the upload progresses. The function will be called with the hash of the file's content, the blob just read from the file and the total size of the file. :type progress: function(dgst, chunk, total) :rtype: str :returns: Hash of file's content. """ dgst = hash_file(filename) try: self._request('head', 'blobs/sha256:' + dgst) return dgst except requests.exceptions.HTTPError as ex: # pylint: disable=no-member if ex.response.status_code != requests.codes.not_found: raise r = self._request('post', 'blobs/uploads/') upload_url = r.headers['Location'] url_parts = list(urlparse.urlparse(upload_url)) query = urlparse.parse_qs(url_parts[4]) query.update({'digest': 'sha256:' + dgst}) url_parts[4] = urlencode(query, True) url_parts[0] = 'http' if self._insecure else 'https' upload_url = urlparse.urlunparse(url_parts) with open(filename, 'rb') as f: self._base_request('put', upload_url, data=_ReportingFile(dgst, f, progress) if progress else f) return dgst # pylint: disable=no-self-use def pull_blob(self, digest, size=False): """ Download a blob from the registry given the hash of its content. :param digest: Hash of the blob's content. :type digest: str :param size: Whether to return the size of the blob too. :type size: bool :rtype: iterator :returns: If ``size`` is falsey, a byte string iterator over the blob's content. If ``size`` is truthy, a tuple containing the iterator and the blob's size. """ r = self._request('get', 'blobs/sha256:' + digest, stream=True) # pylint: disable=too-few-public-methods class Chunks(object): def __iter__(self): sha256 = hashlib.sha256() for chunk in r.iter_content(8192): sha256.update(chunk) yield chunk dgst = sha256.hexdigest() if dgst != digest: raise exceptions.DXFDigestMismatchError(dgst, digest) return (Chunks(), long(r.headers['content-length'])) if size else Chunks() def blob_size(self, digest): """ Return the size of a blob in the registry given the hash of its content. :param digest: Hash of the blob's content. :type digest: str :rtype: long :returns: Whether the blob exists. """ r = self._request('head', 'blobs/sha256:' + digest) return long(r.headers['content-length']) def del_blob(self, digest): """ Delete a blob from the registry given the hash of its content. Note that the registry doesn't support deletes yet so expect an error. :param digest: Hash of the blob's content. :type digest: str """ self._request('delete', 'blobs/sha256:' + digest) # For dtuf; highly unlikely anyone else will want this def make_unsigned_manifest(self, alias, *digests): return json.dumps({ 'name': self._repo, 'tag': alias, 'fsLayers': [{'blobSum': 'sha256:' + dgst} for dgst in digests], 'history': [{'v1Compatibility': '{}'} for dgst in digests] }, sort_keys=True) def set_alias(self, alias, *digests): # pylint: disable=too-many-locals """ Give a name (alias) to a set of blobs. Each blob is specified by the hash of its content. :param alias: Alias name :type alias: str :param digests: List of blob hashes (strings). :type digests: list :rtype: str :returns: The registry manifest used to define the alias. You almost definitely won't need this. """ manifest_json = self.make_unsigned_manifest(alias, *digests) manifest64 = _urlsafe_b64encode(manifest_json) format_length = manifest_json.rfind('}') format_tail = manifest_json[format_length:] protected_json = json.dumps({ 'formatLength': format_length, 'formatTail': _urlsafe_b64encode(format_tail) }) protected64 = _urlsafe_b64encode(protected_json) key = ecdsa.SigningKey.generate(curve=ecdsa.NIST256p) point = key.privkey.public_key.point data = { 'key': key, 'header': { 'alg': 'ES256' } } jws.header.process(data, 'sign') sig = data['signer']("%s.%s" % (protected64, manifest64), key) signatures = [{ 'header': { 'jwk': { 'kty': 'EC', 'crv': 'P-256', 'x': _num_to_base64(point.x()), 'y': _num_to_base64(point.y()) }, 'alg': 'ES256' }, 'signature': _urlsafe_b64encode(sig), 'protected': protected64 }] signed_json = manifest_json[:format_length] + \ ', "signatures": ' + json.dumps(signatures) + \ format_tail #print _verify_manifest(signed_json) self._request('put', 'manifests/' + alias, data=signed_json) return signed_json def get_alias(self, alias=None, manifest=None, verify=True, sizes=False): """ Get the blob hashes assigned to an alias. :param alias: Alias name. You almost definitely will only need to pass this argument. :type alias: str :param manifest: If you previously obtained a manifest, specify it here instead of ``alias``. You almost definitely won't need to do this. :type manifest: str :param verify: Whether to verify the integrity of the alias definition in the registry itself. You almost definitely won't need to change this from the default (``True``). :type verify: bool :param sizes: Whether to return sizes of the blobs along with their hashes :type sizes: bool :rtype: list :returns: If ``sizes`` is falsey, a list of blob hashes (strings) which are assigned to the alias. If ``sizes`` is truthy, a list of (hash,size) tuples for each blob. """ if alias: r = self._request('get', 'manifests/' + alias) manifest = r.content.decode('utf-8') dcd = r.headers['docker-content-digest'] else: dcd = None dgsts = _verify_manifest(manifest, dcd, verify) if not sizes: return dgsts # V2 Schema 2 will put the size in the manifest, so we wouldn't need # to make separate requests to get the size of each blob. # Instead, we could get _verify_manifest to return them. return [(dgst, self.blob_size(dgst)) for dgst in dgsts] def del_alias(self, alias): """ Delete an alias from the registry. The blobs it points to won't be deleted. Use :meth:`del_blob` for that. Note that the registry doesn't support deletes yet so expect an error. :param alias: Alias name. :type alias: str :rtype: list :returns: A list of blob hashes (strings) which were assigned to the alias. """ dgsts = self.get_alias(alias) self._request('delete', 'manifests/' + alias) return dgsts def list_aliases(self): """ List all aliases defined in the repository. :rtype: list :returns: List of alias names (strings). """ return self._request('get', 'tags/list').json()['tags'] PKG|; dxf/__main__.pyimport dxf.main dxf.main.main() PK/7H` dxf/main.py#pylint: disable=wrong-import-position,wrong-import-order,superfluous-parens import os import argparse import sys import tqdm import dxf import dxf.exceptions _choices = ['auth', 'push-blob', 'pull-blob', 'blob-size', 'del-blob', 'set-alias', 'get-alias', 'del-alias', 'list-aliases', 'list-repos'] _parser = argparse.ArgumentParser() _subparsers = _parser.add_subparsers(dest='op') for c in _choices: sp = _subparsers.add_parser(c) if c != 'list-repos': sp.add_argument("repo") sp.add_argument('args', nargs='*') def _flatten(l): return [item for sublist in l for item in sublist] # pylint: disable=too-many-statements def doit(args, environ): dxf_progress = environ.get('DXF_PROGRESS') if dxf_progress == '1' or (dxf_progress != '0' and sys.stderr.isatty()): bars = {} def progress(dgst, chunk, size): if dgst not in bars: bars[dgst] = tqdm.tqdm(desc=dgst[0:8], total=size, leave=True) if len(chunk) > 0: bars[dgst].update(len(chunk)) if bars[dgst].n >= bars[dgst].total: bars[dgst].close() del bars[dgst] else: progress = None def auth(dxf_obj, response): # pylint: disable=redefined-outer-name username = environ.get('DXF_USERNAME') password = environ.get('DXF_PASSWORD') dxf_obj.authenticate(username, password, response=response) # pylint: disable=redefined-variable-type args = _parser.parse_args(args) if args.op != 'list-repos': dxf_obj = dxf.DXF(environ['DXF_HOST'], args.repo, auth, environ.get('DXF_INSECURE') == '1', environ.get('DXF_AUTH_HOST')) else: dxf_obj = dxf.DXFBase(environ['DXF_HOST'], auth, environ.get('DXF_INSECURE') == '1', environ.get('DXF_AUTH_HOST')) def _doit(): # pylint: disable=too-many-branches if args.op == "auth": token = dxf_obj.authenticate(environ['DXF_USERNAME'], environ['DXF_PASSWORD'], actions=args.args) if token: print(token) return token = environ.get('DXF_TOKEN') if token: dxf_obj.token = token if args.op == "push-blob": if len(args.args) < 1: _parser.error('too few arguments') if len(args.args) > 2: _parser.error('too many arguments') if len(args.args) == 2 and not args.args[1].startswith('@'): _parser.error('invalid alias') dgst = dxf_obj.push_blob(args.args[0], progress) if len(args.args) == 2: dxf_obj.set_alias(args.args[1][1:], dgst) print(dgst) elif args.op == "pull-blob": _stdout = getattr(sys.stdout, 'buffer', sys.stdout) if len(args.args) == 0: dgsts = dxf_obj.get_alias(manifest=sys.stdin.read()) else: dgsts = _flatten([dxf_obj.get_alias(name[1:]) if name.startswith('@') else [name] for name in args.args]) for dgst in dgsts: it, size = dxf_obj.pull_blob(dgst, size=True) if environ.get('DXF_BLOB_INFO') == '1': print(dgst + ' ' + str(size)) if progress: progress(dgst, b'', size) for chunk in it: if progress: progress(dgst, chunk, size) _stdout.write(chunk) elif args.op == 'blob-size': if len(args.args) == 0: sizes = [dxf_obj.get_alias(manifest=sys.stdin.read(), sizes=True)] else: sizes = [dxf_obj.get_alias(name[1:], sizes=True) if name.startswith('@') else [(name, dxf_obj.blob_size(name))] for name in args.args] for tuples in sizes: print(sum([size for _, size in tuples])) elif args.op == 'del-blob': if len(args.args) == 0: dgsts = dxf_obj.get_alias(manifest=sys.stdin.read()) else: dgsts = _flatten([dxf_obj.del_alias(name[1:]) if name.startswith('@') else [name] for name in args.args]) for dgst in dgsts: dxf_obj.del_blob(dgst) elif args.op == "set-alias": if len(args.args) < 2: _parser.error('too few arguments') dgsts = [dxf.hash_file(dgst) if os.sep in dgst else dgst for dgst in args.args[1:]] sys.stdout.write(dxf_obj.set_alias(args.args[0], *dgsts)) elif args.op == "get-alias": if len(args.args) == 0: dgsts = dxf_obj.get_alias(manifest=sys.stdin.read()) else: dgsts = _flatten([dxf_obj.get_alias(name) for name in args.args]) for dgst in dgsts: print(dgst) elif args.op == "del-alias": for name in args.args: for dgst in dxf_obj.del_alias(name): print(dgst) elif args.op == 'list-aliases': if len(args.args) > 0: _parser.error('too many arguments') for name in dxf_obj.list_aliases(): print(name) elif args.op == 'list-repos': for name in dxf_obj.list_repos(): print(name) try: _doit() return 0 except dxf.exceptions.DXFUnauthorizedError: import traceback traceback.print_exc() import errno return errno.EACCES def main(): exit(doit(sys.argv[1:], os.environ)) PK8H?T*python_dxf-2.0.3.dist-info/DESCRIPTION.rst\ |Build Status| |Coverage Status| |PyPI version| Python module and command-line tool for storing and retrieving data in a Docker registry. - Store arbitrary data (blob-store) - Content addressable - Set up named aliases to blobs - Supports Docker registry version 2 - Works on Python 2.7 and 3.4 Command-line example: .. code:: shell dxf push-blob fred/datalogger logger.dat @may15-readings dxf pull-blob fred/datalogger @may15-readings which is the same as: .. code:: shell dxf set-alias fred/datalogger may15-readings $(dxf push-blob fred/datalogger logger.dat) dxf pull-blob fred/datalogger $(dxf get-alias fred/datalogger may15-readings) Module example: .. code:: python from dxf import DXF def auth(dxf, response): dxf.authenticate('fred', 'somepassword', response=response) dxf = DXF('registry-1.docker.io', 'fred/datalogger', auth) dgst = dxf.push_blob('logger.dat') dxf.set_alias('may15-readings', dgst) assert dxf.get_alias('may15-readings') == [dgst] for chunk in dxf.pull_blob(dgst): sys.stdout.write(chunk) Usage ----- The module API is described `here `__. The ``dxf`` command-line tool uses the following environment variables: - ``DXF_HOST`` - Host where Docker registry is running - ``DXF_INSECURE`` - Set this to ``1`` if you want to connect to the registry using ``http`` rather than ``https`` (which is the default). - ``DXF_USERNAME`` - Name of user to authenticate as - ``DXF_PASSWORD`` - User's password - ``DXF_AUTH_HOST`` - If set, always perform token authentication to this host, overriding the value returned by the registry. - ``DXF_PROGRESS`` - If this is set to ``1``, a progress bar is displayed (on standard error) during ``push-blob`` and ``pull-blob``. If this is set to ``0``, a progress bar is not displayed. If this is set to any other value, a progress bar is only displayed if standard error is a terminal. - ``DXF_BLOB_INFO`` - Set this to ``1`` if you want ``pull-blob`` to prepend each blob with its digest and size (printed in plain text, separated by a space and followed by a newline). You can use the following options with ``dxf``. Supply the name of the repository you wish to work with in each case as the second argument. - ``dxf push-blob [@alias]`` Upload a file to the registry and optionally give it a name (alias). The blob's hash is printed to standard output. The hash or the alias can be used to fetch the blob later using ``pull-blob``. - ``dxf pull-blob |<@alias>...`` Download blobs from the registry to standard output. For each blob you can specify its hash (remember the registry is content-addressable) or an alias you've given it (using ``push-blob`` or ``set-alias``). - ``dxf blob-size |<@alias>...`` Print the size of blobs in the registry. If you specify an alias, the sum of all the blobs it points to will be printed. - ``dxf del-blob |<@alias>...`` Delete blobs from the registry. If you specify an alias the blobs it points to will be deleted, not the alias itself. Use ``del-alias`` for that. Note that the Docker registry doesn't support deletes yet so expect an error. - ``dxf set-alias |...`` Give a name (alias) to a set of blobs. For each blob you can either specify its hash (as printed by ``get-blob``) or, if you have the blob's contents on disk, its filename (including a path separator to distinguish it from a hash). - ``dxf get-alias ...`` For each alias you specify, print the hashes of all the blobs it points to. - ``dxf del-alias ...`` Delete each specified alias. The blobs they point to won't be deleted (use ``del-blob`` for that), but their hashes will be printed. Note that the Docker registry doesn't support deletes yet so expect an error. - ``dxf list-aliases `` Print all the aliases defined in the repository. - ``dxf list-repos`` Print the names of all the repositories in the registry. Not all versions of the registry support this. Authentication tokens --------------------- ``dxf`` automatically obtains Docker registry authentication tokens using your ``DXF_USERNAME`` and ``DXF_PASSWORD`` environment variables as necessary. However, if you wish to override this then you can use the following command: - ``dxf auth ...`` Authenticate to the registry using ``DXF_USERNAME`` and ``DXF_PASSWORD``, and print the resulting token. ``action`` can be ``pull``, ``push`` or ``*``. If you assign the token to the ``DXF_TOKEN`` environment variable, for example: ``DXF_TOKEN=$(dxf auth fred/datalogger pull)`` then subsequent ``dxf`` commands will use the token without needing ``DXF_USERNAME`` and ``DXF_PASSWORD`` to be set. Note however that the token expires after a few minutes, after which ``dxf`` will exit with ``EACCES``. Installation ------------ .. code:: shell pip install python_dxf Licence ------- `MIT `__ Tests ----- .. code:: shell make test Lint ---- .. code:: shell make lint Code Coverage ------------- .. code:: shell make coverage `coverage.py `__ results are available `here `__. Coveralls page is `here `__. .. |Build Status| image:: https://travis-ci.org/davedoesdev/dxf.png :target: https://travis-ci.org/davedoesdev/dxf .. |Coverage Status| image:: https://coveralls.io/repos/davedoesdev/dxf/badge.png?branch=master :target: https://coveralls.io/r/davedoesdev/dxf?branch=master .. |PyPI version| image:: https://badge.fury.io/py/python_dxf.png :target: http://badge.fury.io/py/python_dxf PK8H;g''+python_dxf-2.0.3.dist-info/entry_points.txt[console_scripts] dxf = dxf.main:main PK8H2=l(python_dxf-2.0.3.dist-info/metadata.json{"extensions": {"python.commands": {"wrap_console": {"dxf": "dxf.main:main"}}, "python.details": {"contacts": [{"email": "dave@davedoesdev.com", "name": "David Halls", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "https://github.com/davedoesdev/dxf"}}, "python.exports": {"console_scripts": {"dxf": "dxf.main:main"}}}, "extras": [], "generator": "bdist_wheel (0.26.0)", "keywords": ["docker", "registry"], "license": "MIT", "metadata_version": "2.0", "name": "python-dxf", "run_requires": [{"requires": ["ecdsa (>=0.13)", "jws (>=0.1.3)", "requests (>=2.9.0)", "tqdm (>=3.1.4)", "www-authenticate (>=0.9.2)"]}], "summary": "Package for accessing a Docker v2 registry", "version": "2.0.3"}PK8H(python_dxf-2.0.3.dist-info/top_level.txtdxf PK8Hndnn python_dxf-2.0.3.dist-info/WHEELWheel-Version: 1.0 Generator: bdist_wheel (0.26.0) Root-Is-Purelib: true Tag: py2-none-any Tag: py3-none-any PK8HAH{#python_dxf-2.0.3.dist-info/METADATAMetadata-Version: 2.0 Name: python-dxf Version: 2.0.3 Summary: Package for accessing a Docker v2 registry Home-page: https://github.com/davedoesdev/dxf Author: David Halls Author-email: dave@davedoesdev.com License: MIT Keywords: docker registry Platform: UNKNOWN Requires-Dist: ecdsa (>=0.13) Requires-Dist: jws (>=0.1.3) Requires-Dist: requests (>=2.9.0) Requires-Dist: tqdm (>=3.1.4) Requires-Dist: www-authenticate (>=0.9.2) \ |Build Status| |Coverage Status| |PyPI version| Python module and command-line tool for storing and retrieving data in a Docker registry. - Store arbitrary data (blob-store) - Content addressable - Set up named aliases to blobs - Supports Docker registry version 2 - Works on Python 2.7 and 3.4 Command-line example: .. code:: shell dxf push-blob fred/datalogger logger.dat @may15-readings dxf pull-blob fred/datalogger @may15-readings which is the same as: .. code:: shell dxf set-alias fred/datalogger may15-readings $(dxf push-blob fred/datalogger logger.dat) dxf pull-blob fred/datalogger $(dxf get-alias fred/datalogger may15-readings) Module example: .. code:: python from dxf import DXF def auth(dxf, response): dxf.authenticate('fred', 'somepassword', response=response) dxf = DXF('registry-1.docker.io', 'fred/datalogger', auth) dgst = dxf.push_blob('logger.dat') dxf.set_alias('may15-readings', dgst) assert dxf.get_alias('may15-readings') == [dgst] for chunk in dxf.pull_blob(dgst): sys.stdout.write(chunk) Usage ----- The module API is described `here `__. The ``dxf`` command-line tool uses the following environment variables: - ``DXF_HOST`` - Host where Docker registry is running - ``DXF_INSECURE`` - Set this to ``1`` if you want to connect to the registry using ``http`` rather than ``https`` (which is the default). - ``DXF_USERNAME`` - Name of user to authenticate as - ``DXF_PASSWORD`` - User's password - ``DXF_AUTH_HOST`` - If set, always perform token authentication to this host, overriding the value returned by the registry. - ``DXF_PROGRESS`` - If this is set to ``1``, a progress bar is displayed (on standard error) during ``push-blob`` and ``pull-blob``. If this is set to ``0``, a progress bar is not displayed. If this is set to any other value, a progress bar is only displayed if standard error is a terminal. - ``DXF_BLOB_INFO`` - Set this to ``1`` if you want ``pull-blob`` to prepend each blob with its digest and size (printed in plain text, separated by a space and followed by a newline). You can use the following options with ``dxf``. Supply the name of the repository you wish to work with in each case as the second argument. - ``dxf push-blob [@alias]`` Upload a file to the registry and optionally give it a name (alias). The blob's hash is printed to standard output. The hash or the alias can be used to fetch the blob later using ``pull-blob``. - ``dxf pull-blob |<@alias>...`` Download blobs from the registry to standard output. For each blob you can specify its hash (remember the registry is content-addressable) or an alias you've given it (using ``push-blob`` or ``set-alias``). - ``dxf blob-size |<@alias>...`` Print the size of blobs in the registry. If you specify an alias, the sum of all the blobs it points to will be printed. - ``dxf del-blob |<@alias>...`` Delete blobs from the registry. If you specify an alias the blobs it points to will be deleted, not the alias itself. Use ``del-alias`` for that. Note that the Docker registry doesn't support deletes yet so expect an error. - ``dxf set-alias |...`` Give a name (alias) to a set of blobs. For each blob you can either specify its hash (as printed by ``get-blob``) or, if you have the blob's contents on disk, its filename (including a path separator to distinguish it from a hash). - ``dxf get-alias ...`` For each alias you specify, print the hashes of all the blobs it points to. - ``dxf del-alias ...`` Delete each specified alias. The blobs they point to won't be deleted (use ``del-blob`` for that), but their hashes will be printed. Note that the Docker registry doesn't support deletes yet so expect an error. - ``dxf list-aliases `` Print all the aliases defined in the repository. - ``dxf list-repos`` Print the names of all the repositories in the registry. Not all versions of the registry support this. Authentication tokens --------------------- ``dxf`` automatically obtains Docker registry authentication tokens using your ``DXF_USERNAME`` and ``DXF_PASSWORD`` environment variables as necessary. However, if you wish to override this then you can use the following command: - ``dxf auth ...`` Authenticate to the registry using ``DXF_USERNAME`` and ``DXF_PASSWORD``, and print the resulting token. ``action`` can be ``pull``, ``push`` or ``*``. If you assign the token to the ``DXF_TOKEN`` environment variable, for example: ``DXF_TOKEN=$(dxf auth fred/datalogger pull)`` then subsequent ``dxf`` commands will use the token without needing ``DXF_USERNAME`` and ``DXF_PASSWORD`` to be set. Note however that the token expires after a few minutes, after which ``dxf`` will exit with ``EACCES``. Installation ------------ .. code:: shell pip install python_dxf Licence ------- `MIT `__ Tests ----- .. code:: shell make test Lint ---- .. code:: shell make lint Code Coverage ------------- .. code:: shell make coverage `coverage.py `__ results are available `here `__. Coveralls page is `here `__. .. |Build Status| image:: https://travis-ci.org/davedoesdev/dxf.png :target: https://travis-ci.org/davedoesdev/dxf .. |Coverage Status| image:: https://coveralls.io/repos/davedoesdev/dxf/badge.png?branch=master :target: https://coveralls.io/r/davedoesdev/dxf?branch=master .. |PyPI version| image:: https://badge.fury.io/py/python_dxf.png :target: http://badge.fury.io/py/python_dxf PK8Hļg>!python_dxf-2.0.3.dist-info/RECORDdxf/__init__.py,sha256=YMP2jblf4nijprnlvw649sYfMpxLa6l8KNSOp-5C0QY,22304 dxf/__main__.py,sha256=zZ6YKnPfKwClNkEbaA1TefND9XNVv7Mn6fRtQZmnOCg,32 dxf/exceptions.py,sha256=kiJ4FUR-W5ZtNXbHMdEeK9Loc7_uDoLCz6jGvf_vmPY,2248 dxf/main.py,sha256=zGrFWUNOlpMxX8Dd93kxJOzB5amY2wEcTnITt80OQKI,6294 python_dxf-2.0.3.dist-info/DESCRIPTION.rst,sha256=PV_tvFVCPetkA21z5u_nAUIgeBarPTNurIzp8vyTSdc,6161 python_dxf-2.0.3.dist-info/METADATA,sha256=uv2vOWaYdkJ8eRUd-wP0dgp_mmxYUVs48HGjv99wIh0,6591 python_dxf-2.0.3.dist-info/RECORD,, python_dxf-2.0.3.dist-info/WHEEL,sha256=GrqQvamwgBV4nLoJe0vhYRSWzWsx7xjlt74FT0SWYfE,110 python_dxf-2.0.3.dist-info/entry_points.txt,sha256=bJZzdOwQgK5zG411LTVOUD3NivLLmc90cbnDIe_3MlQ,39 python_dxf-2.0.3.dist-info/metadata.json,sha256=5ctToqXCscWSKD0_Ub5HZ9saSsgGr7PFKj-W7u_s3YA,742 python_dxf-2.0.3.dist-info/top_level.txt,sha256=KIorSkG8NEY_3rZSrWwFTW3B7KcbCpQvklGPcthVgYQ,4 PK@G dxf/exceptions.pyPKd4H&j0 W Wdxf/__init__.pyPKG|; D`dxf/__main__.pyPK/7H` `dxf/main.pyPK8H?T*Pypython_dxf-2.0.3.dist-info/DESCRIPTION.rstPK8H;g''+python_dxf-2.0.3.dist-info/entry_points.txtPK8H2=l(python_dxf-2.0.3.dist-info/metadata.jsonPK8H(Epython_dxf-2.0.3.dist-info/top_level.txtPK8Hndnn python_dxf-2.0.3.dist-info/WHEELPK8HAH{#;python_dxf-2.0.3.dist-info/METADATAPK8Hļg>!;python_dxf-2.0.3.dist-info/RECORDPK =