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' PK5*HaUaUdxf/__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:`auth_by_password` 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:`auth_by_password`. 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 auth_by_password(self, username, password, actions=None, response=None): """ Authenticate to the registry using a username and password. :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:`auth_by_password` 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']) headers = { 'Authorization': 'Basic ' + base64.b64encode(_to_bytes_2and3(username + ':' + password)).decode('utf-8') } 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.auth_by_password` 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 :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 file'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 *HZُ 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') if username and password: dxf_obj.auth_by_password(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.auth_by_password(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": 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) sys.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)) PK*Hy*python_dxf-1.9.0.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.auth_by_password('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): std: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 PK*H;g''+python_dxf-1.9.0.dist-info/entry_points.txt[console_scripts] dxf = dxf.main:main PK*HaR(python_dxf-1.9.0.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": "1.9.0"}PK*H(python_dxf-1.9.0.dist-info/top_level.txtdxf PK*H''\\ python_dxf-1.9.0.dist-info/WHEELWheel-Version: 1.0 Generator: bdist_wheel (0.26.0) Root-Is-Purelib: true Tag: py2-none-any PK*H{#python_dxf-1.9.0.dist-info/METADATAMetadata-Version: 2.0 Name: python-dxf Version: 1.9.0 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.auth_by_password('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): std: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 PK*H.w!python_dxf-1.9.0.dist-info/RECORDdxf/__init__.py,sha256=9TDp1V0UzMaEBVYuxtBssV2cFGrMi6zMJ-uV-nCy7-M,21857 dxf/__main__.py,sha256=zZ6YKnPfKwClNkEbaA1TefND9XNVv7Mn6fRtQZmnOCg,32 dxf/exceptions.py,sha256=kiJ4FUR-W5ZtNXbHMdEeK9Loc7_uDoLCz6jGvf_vmPY,2248 dxf/main.py,sha256=6t4lgc1PUW8sWZnyOcaqPAuuk5ECkhF5XiaM11ydrc4,6287 python_dxf-1.9.0.dist-info/DESCRIPTION.rst,sha256=E4yfzAoh3AgvfBK2vmcfm6B9mIuyzqszehWAY8AR64k,6165 python_dxf-1.9.0.dist-info/METADATA,sha256=1sd2KWin-cKlaFjEGgui8MsOfBmAVLTvvYCS0SJPLHw,6595 python_dxf-1.9.0.dist-info/RECORD,, python_dxf-1.9.0.dist-info/WHEEL,sha256=JTb7YztR8fkPg6aSjc571Q4eiVHCwmUDlX8PhuuqIIE,92 python_dxf-1.9.0.dist-info/entry_points.txt,sha256=bJZzdOwQgK5zG411LTVOUD3NivLLmc90cbnDIe_3MlQ,39 python_dxf-1.9.0.dist-info/metadata.json,sha256=mKTnaaHneShiKqPJ5cvtbJ0B9wvG4Bn7FaZQz42CaXM,742 python_dxf-1.9.0.dist-info/top_level.txt,sha256=KIorSkG8NEY_3rZSrWwFTW3B7KcbCpQvklGPcthVgYQ,4 PK@G dxf/exceptions.pyPK5*HaUaUdxf/__init__.pyPKG|; ^dxf/__main__.pyPK *HZُ ^dxf/main.pyPK*Hy*wpython_dxf-1.9.0.dist-info/DESCRIPTION.rstPK*H;g''+python_dxf-1.9.0.dist-info/entry_points.txtPK*HaR(Wpython_dxf-1.9.0.dist-info/metadata.jsonPK*H(python_dxf-1.9.0.dist-info/top_level.txtPK*H''\\ ͓python_dxf-1.9.0.dist-info/WHEELPK*H{#gpython_dxf-1.9.0.dist-info/METADATAPK*H.w!kpython_dxf-1.9.0.dist-info/RECORDPK =,