зеркало из https://github.com/mozilla/pontoon.git
Bug 1244423: updated requirements.txt to support pip 8
This commit is contained in:
Родитель
2a0b7a7344
Коммит
cc76e939aa
|
@ -5,7 +5,8 @@ python:
|
|||
before_install:
|
||||
- git submodule update --init --recursive
|
||||
install:
|
||||
- pip install -r requirements.txt
|
||||
- pip install -U --force pip
|
||||
- pip install --require-hashes -r requirements.txt
|
||||
before_script:
|
||||
- psql -c 'create database pontoon;' -U postgres
|
||||
script:
|
||||
|
|
904
bin/peep.py
904
bin/peep.py
|
@ -1,904 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
"""peep ("prudently examine every package") verifies that packages conform to a
|
||||
trusted, locally stored hash and only then installs them::
|
||||
|
||||
peep install -r requirements.txt
|
||||
|
||||
This makes your deployments verifiably repeatable without having to maintain a
|
||||
local PyPI mirror or use a vendor lib. Just update the version numbers and
|
||||
hashes in requirements.txt, and you're all set.
|
||||
|
||||
"""
|
||||
# This is here so embedded copies of peep.py are MIT-compliant:
|
||||
# Copyright (c) 2013 Erik Rose
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to
|
||||
# deal in the Software without restriction, including without limitation the
|
||||
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
# sell copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
from __future__ import print_function
|
||||
try:
|
||||
xrange = xrange
|
||||
except NameError:
|
||||
xrange = range
|
||||
from base64 import urlsafe_b64encode
|
||||
import cgi
|
||||
from collections import defaultdict
|
||||
from functools import wraps
|
||||
from hashlib import sha256
|
||||
from itertools import chain
|
||||
from linecache import getline
|
||||
import mimetypes
|
||||
from optparse import OptionParser
|
||||
from os.path import join, basename, splitext, isdir
|
||||
from pickle import dumps, loads
|
||||
import re
|
||||
import sys
|
||||
from shutil import rmtree, copy
|
||||
from sys import argv, exit
|
||||
from tempfile import mkdtemp
|
||||
import traceback
|
||||
try:
|
||||
from urllib2 import build_opener, HTTPHandler, HTTPSHandler, HTTPError
|
||||
except ImportError:
|
||||
from urllib.request import build_opener, HTTPHandler, HTTPSHandler
|
||||
from urllib.error import HTTPError
|
||||
try:
|
||||
from urlparse import urlparse
|
||||
except ImportError:
|
||||
from urllib.parse import urlparse # 3.4
|
||||
# TODO: Probably use six to make urllib stuff work across 2/3.
|
||||
|
||||
from pkg_resources import require, VersionConflict, DistributionNotFound
|
||||
|
||||
# We don't admit our dependency on pip in setup.py, lest a naive user simply
|
||||
# say `pip install peep.tar.gz` and thus pull down an untrusted copy of pip
|
||||
# from PyPI. Instead, we make sure it's installed and new enough here and spit
|
||||
# out an error message if not:
|
||||
|
||||
|
||||
def activate(specifier):
|
||||
"""Make a compatible version of pip importable. Raise a RuntimeError if we
|
||||
couldn't."""
|
||||
try:
|
||||
for distro in require(specifier):
|
||||
distro.activate()
|
||||
except (VersionConflict, DistributionNotFound):
|
||||
raise RuntimeError('The installed version of pip is too old; peep '
|
||||
'requires ' + specifier)
|
||||
|
||||
# Before 0.6.2, the log module wasn't there, so some
|
||||
# of our monkeypatching fails. It probably wouldn't be
|
||||
# much work to support even earlier, though.
|
||||
activate('pip>=0.6.2')
|
||||
|
||||
import pip
|
||||
from pip.commands.install import InstallCommand
|
||||
try:
|
||||
from pip.download import url_to_path # 1.5.6
|
||||
except ImportError:
|
||||
try:
|
||||
from pip.util import url_to_path # 0.7.0
|
||||
except ImportError:
|
||||
from pip.util import url_to_filename as url_to_path # 0.6.2
|
||||
from pip.index import PackageFinder, Link
|
||||
try:
|
||||
from pip.log import logger
|
||||
except ImportError:
|
||||
from pip import logger # 6.0
|
||||
from pip.req import parse_requirements
|
||||
try:
|
||||
from pip.utils.ui import DownloadProgressBar, DownloadProgressSpinner
|
||||
except ImportError:
|
||||
class NullProgressBar(object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def iter(self, ret, *args, **kwargs):
|
||||
return ret
|
||||
|
||||
DownloadProgressBar = DownloadProgressSpinner = NullProgressBar
|
||||
|
||||
|
||||
__version__ = 2, 4, 1
|
||||
|
||||
|
||||
ITS_FINE_ITS_FINE = 0
|
||||
SOMETHING_WENT_WRONG = 1
|
||||
# "Traditional" for command-line errors according to optparse docs:
|
||||
COMMAND_LINE_ERROR = 2
|
||||
|
||||
ARCHIVE_EXTENSIONS = ('.tar.bz2', '.tar.gz', '.tgz', '.tar', '.zip')
|
||||
|
||||
MARKER = object()
|
||||
|
||||
|
||||
class PipException(Exception):
|
||||
"""When I delegated to pip, it exited with an error."""
|
||||
|
||||
def __init__(self, error_code):
|
||||
self.error_code = error_code
|
||||
|
||||
|
||||
class UnsupportedRequirementError(Exception):
|
||||
"""An unsupported line was encountered in a requirements file."""
|
||||
|
||||
|
||||
class DownloadError(Exception):
|
||||
def __init__(self, link, exc):
|
||||
self.link = link
|
||||
self.reason = str(exc)
|
||||
|
||||
def __str__(self):
|
||||
return 'Downloading %s failed: %s' % (self.link, self.reason)
|
||||
|
||||
|
||||
def encoded_hash(sha):
|
||||
"""Return a short, 7-bit-safe representation of a hash.
|
||||
|
||||
If you pass a sha256, this results in the hash algorithm that the Wheel
|
||||
format (PEP 427) uses, except here it's intended to be run across the
|
||||
downloaded archive before unpacking.
|
||||
|
||||
"""
|
||||
return urlsafe_b64encode(sha.digest()).decode('ascii').rstrip('=')
|
||||
|
||||
|
||||
def run_pip(initial_args):
|
||||
"""Delegate to pip the given args (starting with the subcommand), and raise
|
||||
``PipException`` if something goes wrong."""
|
||||
status_code = pip.main(initial_args)
|
||||
|
||||
# Clear out the registrations in the pip "logger" singleton. Otherwise,
|
||||
# loggers keep getting appended to it with every run. Pip assumes only one
|
||||
# command invocation will happen per interpreter lifetime.
|
||||
logger.consumers = []
|
||||
|
||||
if status_code:
|
||||
raise PipException(status_code)
|
||||
|
||||
|
||||
def hash_of_file(path):
|
||||
"""Return the hash of a downloaded file."""
|
||||
with open(path, 'rb') as archive:
|
||||
sha = sha256()
|
||||
while True:
|
||||
data = archive.read(2 ** 20)
|
||||
if not data:
|
||||
break
|
||||
sha.update(data)
|
||||
return encoded_hash(sha)
|
||||
|
||||
|
||||
def is_git_sha(text):
|
||||
"""Return whether this is probably a git sha"""
|
||||
# Handle both the full sha as well as the 7-character abbreviation
|
||||
if len(text) in (40, 7):
|
||||
try:
|
||||
int(text, 16)
|
||||
return True
|
||||
except ValueError:
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def filename_from_url(url):
|
||||
parsed = urlparse(url)
|
||||
path = parsed.path
|
||||
return path.split('/')[-1]
|
||||
|
||||
|
||||
def requirement_args(argv, want_paths=False, want_other=False):
|
||||
"""Return an iterable of filtered arguments.
|
||||
|
||||
:arg argv: Arguments, starting after the subcommand
|
||||
:arg want_paths: If True, the returned iterable includes the paths to any
|
||||
requirements files following a ``-r`` or ``--requirement`` option.
|
||||
:arg want_other: If True, the returned iterable includes the args that are
|
||||
not a requirement-file path or a ``-r`` or ``--requirement`` flag.
|
||||
|
||||
"""
|
||||
was_r = False
|
||||
for arg in argv:
|
||||
# Allow for requirements files named "-r", don't freak out if there's a
|
||||
# trailing "-r", etc.
|
||||
if was_r:
|
||||
if want_paths:
|
||||
yield arg
|
||||
was_r = False
|
||||
elif arg in ['-r', '--requirement']:
|
||||
was_r = True
|
||||
else:
|
||||
if want_other:
|
||||
yield arg
|
||||
|
||||
|
||||
HASH_COMMENT_RE = re.compile(
|
||||
r"""
|
||||
\s*\#\s+ # Lines that start with a '#'
|
||||
(?P<hash_type>sha256):\s+ # Hash type is hardcoded to be sha256 for now.
|
||||
(?P<hash>[^\s]+) # Hashes can be anything except '#' or spaces.
|
||||
\s* # Suck up whitespace before the comment or
|
||||
# just trailing whitespace if there is no
|
||||
# comment. Also strip trailing newlines.
|
||||
(?:\#(?P<comment>.*))? # Comments can be anything after a whitespace+#
|
||||
# and are optional.
|
||||
$""", re.X)
|
||||
|
||||
|
||||
def peep_hash(argv):
|
||||
"""Return the peep hash of one or more files, returning a shell status code
|
||||
or raising a PipException.
|
||||
|
||||
:arg argv: The commandline args, starting after the subcommand
|
||||
|
||||
"""
|
||||
parser = OptionParser(
|
||||
usage='usage: %prog hash file [file ...]',
|
||||
description='Print a peep hash line for one or more files: for '
|
||||
'example, "# sha256: '
|
||||
'oz42dZy6Gowxw8AelDtO4gRgTW_xPdooH484k7I5EOY".')
|
||||
_, paths = parser.parse_args(args=argv)
|
||||
if paths:
|
||||
for path in paths:
|
||||
print('# sha256:', hash_of_file(path))
|
||||
return ITS_FINE_ITS_FINE
|
||||
else:
|
||||
parser.print_usage()
|
||||
return COMMAND_LINE_ERROR
|
||||
|
||||
|
||||
class EmptyOptions(object):
|
||||
"""Fake optparse options for compatibility with pip<1.2
|
||||
|
||||
pip<1.2 had a bug in parse_requirements() in which the ``options`` kwarg
|
||||
was required. We work around that by passing it a mock object.
|
||||
|
||||
"""
|
||||
default_vcs = None
|
||||
skip_requirements_regex = None
|
||||
isolated_mode = False
|
||||
|
||||
|
||||
def memoize(func):
|
||||
"""Memoize a method that should return the same result every time on a
|
||||
given instance.
|
||||
|
||||
"""
|
||||
@wraps(func)
|
||||
def memoizer(self):
|
||||
if not hasattr(self, '_cache'):
|
||||
self._cache = {}
|
||||
if func.__name__ not in self._cache:
|
||||
self._cache[func.__name__] = func(self)
|
||||
return self._cache[func.__name__]
|
||||
return memoizer
|
||||
|
||||
|
||||
def package_finder(argv):
|
||||
"""Return a PackageFinder respecting command-line options.
|
||||
|
||||
:arg argv: Everything after the subcommand
|
||||
|
||||
"""
|
||||
# We instantiate an InstallCommand and then use some of its private
|
||||
# machinery--its arg parser--for our own purposes, like a virus. This
|
||||
# approach is portable across many pip versions, where more fine-grained
|
||||
# ones are not. Ignoring options that don't exist on the parser (for
|
||||
# instance, --use-wheel) gives us a straightforward method of backward
|
||||
# compatibility.
|
||||
try:
|
||||
command = InstallCommand()
|
||||
except TypeError:
|
||||
# This is likely pip 1.3.0's "__init__() takes exactly 2 arguments (1
|
||||
# given)" error. In that version, InstallCommand takes a top=level
|
||||
# parser passed in from outside.
|
||||
from pip.baseparser import create_main_parser
|
||||
command = InstallCommand(create_main_parser())
|
||||
# The downside is that it essentially ruins the InstallCommand class for
|
||||
# further use. Calling out to pip.main() within the same interpreter, for
|
||||
# example, would result in arguments parsed this time turning up there.
|
||||
# Thus, we deepcopy the arg parser so we don't trash its singletons. Of
|
||||
# course, deepcopy doesn't work on these objects, because they contain
|
||||
# uncopyable regex patterns, so we pickle and unpickle instead. Fun!
|
||||
options, _ = loads(dumps(command.parser)).parse_args(argv)
|
||||
|
||||
# Carry over PackageFinder kwargs that have [about] the same names as
|
||||
# options attr names:
|
||||
possible_options = [
|
||||
'find_links', 'use_wheel', 'allow_external', 'allow_unverified',
|
||||
'allow_all_external', ('allow_all_prereleases', 'pre'),
|
||||
'process_dependency_links']
|
||||
kwargs = {}
|
||||
for option in possible_options:
|
||||
kw, attr = option if isinstance(option, tuple) else (option, option)
|
||||
value = getattr(options, attr, MARKER)
|
||||
if value is not MARKER:
|
||||
kwargs[kw] = value
|
||||
|
||||
# Figure out index_urls:
|
||||
index_urls = [options.index_url] + options.extra_index_urls
|
||||
if options.no_index:
|
||||
index_urls = []
|
||||
index_urls += getattr(options, 'mirrors', [])
|
||||
|
||||
# If pip is new enough to have a PipSession, initialize one, since
|
||||
# PackageFinder requires it:
|
||||
if hasattr(command, '_build_session'):
|
||||
kwargs['session'] = command._build_session(options)
|
||||
|
||||
return PackageFinder(index_urls=index_urls, **kwargs)
|
||||
|
||||
|
||||
class DownloadedReq(object):
|
||||
"""A wrapper around InstallRequirement which offers additional information
|
||||
based on downloading and examining a corresponding package archive
|
||||
|
||||
These are conceptually immutable, so we can get away with memoizing
|
||||
expensive things.
|
||||
|
||||
"""
|
||||
def __init__(self, req, argv, finder):
|
||||
"""Download a requirement, compare its hashes, and return a subclass
|
||||
of DownloadedReq depending on its state.
|
||||
|
||||
:arg req: The InstallRequirement I am based on
|
||||
:arg argv: The args, starting after the subcommand
|
||||
|
||||
"""
|
||||
self._req = req
|
||||
self._argv = argv
|
||||
self._finder = finder
|
||||
|
||||
# We use a separate temp dir for each requirement so requirements
|
||||
# (from different indices) that happen to have the same archive names
|
||||
# don't overwrite each other, leading to a security hole in which the
|
||||
# latter is a hash mismatch, the former has already passed the
|
||||
# comparison, and the latter gets installed.
|
||||
self._temp_path = mkdtemp(prefix='peep-')
|
||||
# Think of DownloadedReq as a one-shot state machine. It's an abstract
|
||||
# class that ratchets forward to being one of its own subclasses,
|
||||
# depending on its package status. Then it doesn't move again.
|
||||
self.__class__ = self._class()
|
||||
|
||||
def dispose(self):
|
||||
"""Delete temp files and dirs I've made. Render myself useless.
|
||||
|
||||
Do not call further methods on me after calling dispose().
|
||||
|
||||
"""
|
||||
rmtree(self._temp_path)
|
||||
|
||||
def _version(self):
|
||||
"""Deduce the version number of the downloaded package from its filename."""
|
||||
# TODO: Can we delete this method and just print the line from the
|
||||
# reqs file verbatim instead?
|
||||
def version_of_archive(filename, package_name):
|
||||
# Since we know the project_name, we can strip that off the left, strip
|
||||
# any archive extensions off the right, and take the rest as the
|
||||
# version.
|
||||
for ext in ARCHIVE_EXTENSIONS:
|
||||
if filename.endswith(ext):
|
||||
filename = filename[:-len(ext)]
|
||||
break
|
||||
# Handle github sha tarball downloads.
|
||||
if is_git_sha(filename):
|
||||
filename = package_name + '-' + filename
|
||||
if not filename.lower().replace('_', '-').startswith(package_name.lower()):
|
||||
# TODO: Should we replace runs of [^a-zA-Z0-9.], not just _, with -?
|
||||
give_up(filename, package_name)
|
||||
return filename[len(package_name) + 1:] # Strip off '-' before version.
|
||||
|
||||
def version_of_wheel(filename, package_name):
|
||||
# For Wheel files (http://legacy.python.org/dev/peps/pep-0427/#file-
|
||||
# name-convention) we know the format bits are '-' separated.
|
||||
whl_package_name, version, _rest = filename.split('-', 2)
|
||||
# Do the alteration to package_name from PEP 427:
|
||||
our_package_name = re.sub(r'[^\w\d.]+', '_', package_name, re.UNICODE)
|
||||
if whl_package_name != our_package_name:
|
||||
give_up(filename, whl_package_name)
|
||||
return version
|
||||
|
||||
def give_up(filename, package_name):
|
||||
raise RuntimeError("The archive '%s' didn't start with the package name "
|
||||
"'%s', so I couldn't figure out the version number. "
|
||||
"My bad; improve me." %
|
||||
(filename, package_name))
|
||||
|
||||
get_version = (version_of_wheel
|
||||
if self._downloaded_filename().endswith('.whl')
|
||||
else version_of_archive)
|
||||
return get_version(self._downloaded_filename(), self._project_name())
|
||||
|
||||
def _is_always_unsatisfied(self):
|
||||
"""Returns whether this requirement is always unsatisfied
|
||||
|
||||
This would happen in cases where we can't determine the version
|
||||
from the filename.
|
||||
|
||||
"""
|
||||
# If this is a github sha tarball, then it is always unsatisfied
|
||||
# because the url has a commit sha in it and not the version
|
||||
# number.
|
||||
url = self._url()
|
||||
if url:
|
||||
filename = filename_from_url(url)
|
||||
if filename.endswith(ARCHIVE_EXTENSIONS):
|
||||
filename, ext = splitext(filename)
|
||||
if is_git_sha(filename):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _path_and_line(self):
|
||||
"""Return the path and line number of the file from which our
|
||||
InstallRequirement came.
|
||||
|
||||
"""
|
||||
path, line = (re.match(r'-r (.*) \(line (\d+)\)$',
|
||||
self._req.comes_from).groups())
|
||||
return path, int(line)
|
||||
|
||||
@memoize # Avoid hitting the file[cache] over and over.
|
||||
def _expected_hashes(self):
|
||||
"""Return a list of known-good hashes for this package."""
|
||||
|
||||
def hashes_above(path, line_number):
|
||||
"""Yield hashes from contiguous comment lines before line
|
||||
``line_number``.
|
||||
|
||||
"""
|
||||
for line_number in xrange(line_number - 1, 0, -1):
|
||||
line = getline(path, line_number)
|
||||
match = HASH_COMMENT_RE.match(line)
|
||||
if match:
|
||||
yield match.groupdict()['hash']
|
||||
elif not line.lstrip().startswith('#'):
|
||||
# If we hit a non-comment line, abort
|
||||
break
|
||||
|
||||
hashes = list(hashes_above(*self._path_and_line()))
|
||||
hashes.reverse() # because we read them backwards
|
||||
return hashes
|
||||
|
||||
def _download(self, link):
|
||||
"""Download a file, and return its name within my temp dir.
|
||||
|
||||
This does no verification of HTTPS certs, but our checking hashes
|
||||
makes that largely unimportant. It would be nice to be able to use the
|
||||
requests lib, which can verify certs, but it is guaranteed to be
|
||||
available only in pip >= 1.5.
|
||||
|
||||
This also drops support for proxies and basic auth, though those could
|
||||
be added back in.
|
||||
|
||||
"""
|
||||
# Based on pip 1.4.1's URLOpener but with cert verification removed
|
||||
def opener(is_https):
|
||||
if is_https:
|
||||
opener = build_opener(HTTPSHandler())
|
||||
# Strip out HTTPHandler to prevent MITM spoof:
|
||||
for handler in opener.handlers:
|
||||
if isinstance(handler, HTTPHandler):
|
||||
opener.handlers.remove(handler)
|
||||
else:
|
||||
opener = build_opener()
|
||||
return opener
|
||||
|
||||
# Descended from unpack_http_url() in pip 1.4.1
|
||||
def best_filename(link, response):
|
||||
"""Return the most informative possible filename for a download,
|
||||
ideally with a proper extension.
|
||||
|
||||
"""
|
||||
content_type = response.info().get('content-type', '')
|
||||
filename = link.filename # fallback
|
||||
# Have a look at the Content-Disposition header for a better guess:
|
||||
content_disposition = response.info().get('content-disposition')
|
||||
if content_disposition:
|
||||
type, params = cgi.parse_header(content_disposition)
|
||||
# We use ``or`` here because we don't want to use an "empty" value
|
||||
# from the filename param:
|
||||
filename = params.get('filename') or filename
|
||||
ext = splitext(filename)[1]
|
||||
if not ext:
|
||||
ext = mimetypes.guess_extension(content_type)
|
||||
if ext:
|
||||
filename += ext
|
||||
if not ext and link.url != response.geturl():
|
||||
ext = splitext(response.geturl())[1]
|
||||
if ext:
|
||||
filename += ext
|
||||
return filename
|
||||
|
||||
# Descended from _download_url() in pip 1.4.1
|
||||
def pipe_to_file(response, path, size=0):
|
||||
"""Pull the data off an HTTP response, shove it in a new file, and
|
||||
show progress.
|
||||
|
||||
:arg response: A file-like object to read from
|
||||
:arg path: The path of the new file
|
||||
:arg size: The expected size, in bytes, of the download. 0 for
|
||||
unknown or to suppress progress indication (as for cached
|
||||
downloads)
|
||||
|
||||
"""
|
||||
def response_chunks(chunk_size):
|
||||
while True:
|
||||
chunk = response.read(chunk_size)
|
||||
if not chunk:
|
||||
break
|
||||
yield chunk
|
||||
|
||||
print('Downloading %s%s...' % (
|
||||
self._req.req,
|
||||
(' (%sK)' % (size / 1000)) if size > 1000 else ''))
|
||||
progress_indicator = (DownloadProgressBar(max=size).iter if size
|
||||
else DownloadProgressSpinner().iter)
|
||||
with open(path, 'wb') as file:
|
||||
for chunk in progress_indicator(response_chunks(4096), 4096):
|
||||
file.write(chunk)
|
||||
|
||||
url = link.url.split('#', 1)[0]
|
||||
try:
|
||||
response = opener(urlparse(url).scheme != 'http').open(url)
|
||||
except (HTTPError, IOError) as exc:
|
||||
raise DownloadError(link, exc)
|
||||
filename = best_filename(link, response)
|
||||
try:
|
||||
size = int(response.headers['content-length'])
|
||||
except (ValueError, KeyError, TypeError):
|
||||
size = 0
|
||||
pipe_to_file(response, join(self._temp_path, filename), size=size)
|
||||
return filename
|
||||
|
||||
# Based on req_set.prepare_files() in pip bb2a8428d4aebc8d313d05d590f386fa3f0bbd0f
|
||||
@memoize # Avoid re-downloading.
|
||||
def _downloaded_filename(self):
|
||||
"""Download the package's archive if necessary, and return its
|
||||
filename.
|
||||
|
||||
--no-deps is implied, as we have reimplemented the bits that would
|
||||
ordinarily do dependency resolution.
|
||||
|
||||
"""
|
||||
# Peep doesn't support requirements that don't come down as a single
|
||||
# file, because it can't hash them. Thus, it doesn't support editable
|
||||
# requirements, because pip itself doesn't support editable
|
||||
# requirements except for "local projects or a VCS url". Nor does it
|
||||
# support VCS requirements yet, because we haven't yet come up with a
|
||||
# portable, deterministic way to hash them. In summary, all we support
|
||||
# is == requirements and tarballs/zips/etc.
|
||||
|
||||
# TODO: Stop on reqs that are editable or aren't ==.
|
||||
|
||||
# If the requirement isn't already specified as a URL, get a URL
|
||||
# from an index:
|
||||
link = self._link() or self._finder.find_requirement(self._req, upgrade=False)
|
||||
|
||||
if link:
|
||||
lower_scheme = link.scheme.lower() # pip lower()s it for some reason.
|
||||
if lower_scheme == 'http' or lower_scheme == 'https':
|
||||
file_path = self._download(link)
|
||||
return basename(file_path)
|
||||
elif lower_scheme == 'file':
|
||||
# The following is inspired by pip's unpack_file_url():
|
||||
link_path = url_to_path(link.url_without_fragment)
|
||||
if isdir(link_path):
|
||||
raise UnsupportedRequirementError(
|
||||
"%s: %s is a directory. So that it can compute "
|
||||
"a hash, peep supports only filesystem paths which "
|
||||
"point to files" %
|
||||
(self._req, link.url_without_fragment))
|
||||
else:
|
||||
copy(link_path, self._temp_path)
|
||||
return basename(link_path)
|
||||
else:
|
||||
raise UnsupportedRequirementError(
|
||||
"%s: The download link, %s, would not result in a file "
|
||||
"that can be hashed. Peep supports only == requirements, "
|
||||
"file:// URLs pointing to files (not folders), and "
|
||||
"http:// and https:// URLs pointing to tarballs, zips, "
|
||||
"etc." % (self._req, link.url))
|
||||
else:
|
||||
raise UnsupportedRequirementError(
|
||||
"%s: couldn't determine where to download this requirement from."
|
||||
% (self._req,))
|
||||
|
||||
def install(self):
|
||||
"""Install the package I represent, without dependencies.
|
||||
|
||||
Obey typical pip-install options passed in on the command line.
|
||||
|
||||
"""
|
||||
other_args = list(requirement_args(self._argv, want_other=True))
|
||||
archive_path = join(self._temp_path, self._downloaded_filename())
|
||||
# -U so it installs whether pip deems the requirement "satisfied" or
|
||||
# not. This is necessary for GitHub-sourced zips, which change without
|
||||
# their version numbers changing.
|
||||
run_pip(['install'] + other_args + ['--no-deps', '-U', archive_path])
|
||||
|
||||
@memoize
|
||||
def _actual_hash(self):
|
||||
"""Download the package's archive if necessary, and return its hash."""
|
||||
return hash_of_file(join(self._temp_path, self._downloaded_filename()))
|
||||
|
||||
def _project_name(self):
|
||||
"""Return the inner Requirement's "unsafe name".
|
||||
|
||||
Raise ValueError if there is no name.
|
||||
|
||||
"""
|
||||
name = getattr(self._req.req, 'project_name', '')
|
||||
if name:
|
||||
return name
|
||||
raise ValueError('Requirement has no project_name.')
|
||||
|
||||
def _name(self):
|
||||
return self._req.name
|
||||
|
||||
def _link(self):
|
||||
try:
|
||||
return self._req.link
|
||||
except AttributeError:
|
||||
# The link attribute isn't available prior to pip 6.1.0, so fall
|
||||
# back to the now deprecated 'url' attribute.
|
||||
return Link(self._req.url) if self._req.url else None
|
||||
|
||||
def _url(self):
|
||||
link = self._link()
|
||||
return link.url if link else None
|
||||
|
||||
@memoize # Avoid re-running expensive check_if_exists().
|
||||
def _is_satisfied(self):
|
||||
self._req.check_if_exists()
|
||||
return (self._req.satisfied_by and
|
||||
not self._is_always_unsatisfied())
|
||||
|
||||
def _class(self):
|
||||
"""Return the class I should be, spanning a continuum of goodness."""
|
||||
try:
|
||||
self._project_name()
|
||||
except ValueError:
|
||||
return MalformedReq
|
||||
if self._is_satisfied():
|
||||
return SatisfiedReq
|
||||
if not self._expected_hashes():
|
||||
return MissingReq
|
||||
if self._actual_hash() not in self._expected_hashes():
|
||||
return MismatchedReq
|
||||
return InstallableReq
|
||||
|
||||
@classmethod
|
||||
def foot(cls):
|
||||
"""Return the text to be printed once, after all of the errors from
|
||||
classes of my type are printed.
|
||||
|
||||
"""
|
||||
return ''
|
||||
|
||||
|
||||
class MalformedReq(DownloadedReq):
|
||||
"""A requirement whose package name could not be determined"""
|
||||
|
||||
@classmethod
|
||||
def head(cls):
|
||||
return 'The following requirements could not be processed:\n'
|
||||
|
||||
def error(self):
|
||||
return '* Unable to determine package name from URL %s; add #egg=' % self._url()
|
||||
|
||||
|
||||
class MissingReq(DownloadedReq):
|
||||
"""A requirement for which no hashes were specified in the requirements file"""
|
||||
|
||||
@classmethod
|
||||
def head(cls):
|
||||
return ('The following packages had no hashes specified in the requirements file, which\n'
|
||||
'leaves them open to tampering. Vet these packages to your satisfaction, then\n'
|
||||
'add these "sha256" lines like so:\n\n')
|
||||
|
||||
def error(self):
|
||||
if self._url():
|
||||
# _url() always contains an #egg= part, or this would be a
|
||||
# MalformedRequest.
|
||||
line = self._url()
|
||||
else:
|
||||
line = '%s==%s' % (self._name(), self._version())
|
||||
return '# sha256: %s\n%s\n' % (self._actual_hash(), line)
|
||||
|
||||
|
||||
class MismatchedReq(DownloadedReq):
|
||||
"""A requirement for which the downloaded file didn't match any of my hashes."""
|
||||
@classmethod
|
||||
def head(cls):
|
||||
return ("THE FOLLOWING PACKAGES DIDN'T MATCH THE HASHES SPECIFIED IN THE REQUIREMENTS\n"
|
||||
"FILE. If you have updated the package versions, update the hashes. If not,\n"
|
||||
"freak out, because someone has tampered with the packages.\n\n")
|
||||
|
||||
def error(self):
|
||||
preamble = ' %s: expected' % self._project_name()
|
||||
if len(self._expected_hashes()) > 1:
|
||||
preamble += ' one of'
|
||||
padding = '\n' + ' ' * (len(preamble) + 1)
|
||||
return '%s %s\n%s got %s' % (preamble,
|
||||
padding.join(self._expected_hashes()),
|
||||
' ' * (len(preamble) - 4),
|
||||
self._actual_hash())
|
||||
|
||||
@classmethod
|
||||
def foot(cls):
|
||||
return '\n'
|
||||
|
||||
|
||||
class SatisfiedReq(DownloadedReq):
|
||||
"""A requirement which turned out to be already installed"""
|
||||
|
||||
@classmethod
|
||||
def head(cls):
|
||||
return ("These packages were already installed, so we didn't need to download or build\n"
|
||||
"them again. If you installed them with peep in the first place, you should be\n"
|
||||
"safe. If not, uninstall them, then re-attempt your install with peep.\n")
|
||||
|
||||
def error(self):
|
||||
return ' %s' % (self._req,)
|
||||
|
||||
|
||||
class InstallableReq(DownloadedReq):
|
||||
"""A requirement whose hash matched and can be safely installed"""
|
||||
|
||||
|
||||
# DownloadedReq subclasses that indicate an error that should keep us from
|
||||
# going forward with installation, in the order in which their errors should
|
||||
# be reported:
|
||||
ERROR_CLASSES = [MismatchedReq, MissingReq, MalformedReq]
|
||||
|
||||
|
||||
def bucket(things, key):
|
||||
"""Return a map of key -> list of things."""
|
||||
ret = defaultdict(list)
|
||||
for thing in things:
|
||||
ret[key(thing)].append(thing)
|
||||
return ret
|
||||
|
||||
|
||||
def first_every_last(iterable, first, every, last):
|
||||
"""Execute something before the first item of iter, something else for each
|
||||
item, and a third thing after the last.
|
||||
|
||||
If there are no items in the iterable, don't execute anything.
|
||||
|
||||
"""
|
||||
did_first = False
|
||||
for item in iterable:
|
||||
if not did_first:
|
||||
did_first = True
|
||||
first(item)
|
||||
every(item)
|
||||
if did_first:
|
||||
last(item)
|
||||
|
||||
|
||||
def downloaded_reqs_from_path(path, argv):
|
||||
"""Return a list of DownloadedReqs representing the requirements parsed
|
||||
out of a given requirements file.
|
||||
|
||||
:arg path: The path to the requirements file
|
||||
:arg argv: The commandline args, starting after the subcommand
|
||||
|
||||
"""
|
||||
finder = package_finder(argv)
|
||||
|
||||
def downloaded_reqs(parsed_reqs):
|
||||
"""Just avoid repeating this list comp."""
|
||||
return [DownloadedReq(req, argv, finder) for req in parsed_reqs]
|
||||
|
||||
try:
|
||||
return downloaded_reqs(parse_requirements(
|
||||
path, options=EmptyOptions(), finder=finder))
|
||||
except TypeError:
|
||||
# session is a required kwarg as of pip 6.0 and will raise
|
||||
# a TypeError if missing. It needs to be a PipSession instance,
|
||||
# but in older versions we can't import it from pip.download
|
||||
# (nor do we need it at all) so we only import it in this except block
|
||||
from pip.download import PipSession
|
||||
return downloaded_reqs(parse_requirements(
|
||||
path, options=EmptyOptions(), session=PipSession(), finder=finder))
|
||||
|
||||
|
||||
def peep_install(argv):
|
||||
"""Perform the ``peep install`` subcommand, returning a shell status code
|
||||
or raising a PipException.
|
||||
|
||||
:arg argv: The commandline args, starting after the subcommand
|
||||
|
||||
"""
|
||||
output = []
|
||||
out = output.append
|
||||
reqs = []
|
||||
try:
|
||||
req_paths = list(requirement_args(argv, want_paths=True))
|
||||
if not req_paths:
|
||||
out("You have to specify one or more requirements files with the -r option, because\n"
|
||||
"otherwise there's nowhere for peep to look up the hashes.\n")
|
||||
return COMMAND_LINE_ERROR
|
||||
|
||||
# We're a "peep install" command, and we have some requirement paths.
|
||||
reqs = list(chain.from_iterable(
|
||||
downloaded_reqs_from_path(path, argv)
|
||||
for path in req_paths))
|
||||
buckets = bucket(reqs, lambda r: r.__class__)
|
||||
|
||||
# Skip a line after pip's "Cleaning up..." so the important stuff
|
||||
# stands out:
|
||||
if any(buckets[b] for b in ERROR_CLASSES):
|
||||
out('\n')
|
||||
|
||||
printers = (lambda r: out(r.head()),
|
||||
lambda r: out(r.error() + '\n'),
|
||||
lambda r: out(r.foot()))
|
||||
for c in ERROR_CLASSES:
|
||||
first_every_last(buckets[c], *printers)
|
||||
|
||||
if any(buckets[b] for b in ERROR_CLASSES):
|
||||
out('-------------------------------\n'
|
||||
'Not proceeding to installation.\n')
|
||||
return SOMETHING_WENT_WRONG
|
||||
else:
|
||||
for req in buckets[InstallableReq]:
|
||||
req.install()
|
||||
|
||||
first_every_last(buckets[SatisfiedReq], *printers)
|
||||
|
||||
return ITS_FINE_ITS_FINE
|
||||
except (UnsupportedRequirementError, DownloadError) as exc:
|
||||
out(str(exc))
|
||||
return SOMETHING_WENT_WRONG
|
||||
finally:
|
||||
for req in reqs:
|
||||
req.dispose()
|
||||
print(''.join(output))
|
||||
|
||||
|
||||
def main():
|
||||
"""Be the top-level entrypoint. Return a shell status code."""
|
||||
commands = {'hash': peep_hash,
|
||||
'install': peep_install}
|
||||
try:
|
||||
if len(argv) >= 2 and argv[1] in commands:
|
||||
return commands[argv[1]](argv[2:])
|
||||
else:
|
||||
# Fall through to top-level pip main() for everything else:
|
||||
return pip.main()
|
||||
except PipException as exc:
|
||||
return exc.error_code
|
||||
|
||||
|
||||
def exception_handler(exc_type, exc_value, exc_tb):
|
||||
print('Oh no! Peep had a problem while trying to do stuff. Please write up a bug report')
|
||||
print('with the specifics so we can fix it:')
|
||||
print()
|
||||
print('https://github.com/erikrose/peep/issues/new')
|
||||
print()
|
||||
print('Here are some particulars you can copy and paste into the bug report:')
|
||||
print()
|
||||
print('---')
|
||||
print('peep:', repr(__version__))
|
||||
print('python:', repr(sys.version))
|
||||
print('pip:', repr(getattr(pip, '__version__', 'no __version__ attr')))
|
||||
print('Command line: ', repr(sys.argv))
|
||||
print(
|
||||
''.join(traceback.format_exception(exc_type, exc_value, exc_tb)))
|
||||
print('---')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
exit(main())
|
||||
except Exception:
|
||||
exception_handler(*sys.exc_info())
|
||||
exit(SOMETHING_WENT_WRONG)
|
|
@ -46,29 +46,11 @@ Installation
|
|||
re-activate the virtualenv. Read the virtualenv_ documentation to learn
|
||||
more about how virtualenv works.
|
||||
|
||||
3. Install the dependencies using peep_, a wrapper around pip that is
|
||||
included with Pontoon:
|
||||
3. Install the dependencies using the latest version of pip_:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
./bin/peep.py install -r requirements.txt
|
||||
|
||||
.. note::
|
||||
|
||||
For Mac OS X users, You may encounter a value error while running the above command.
|
||||
The installation process would terminate flagging an error as follows.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
raise ValueError, 'unknown locale: %s' % localename
|
||||
ValueError: unknown locale: UTF-8
|
||||
|
||||
To fix this existing python issue_, Add the following lines to your `.bash_profile` or equivalent.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
export LC_ALL=en_US.UTF-8
|
||||
export LANG=en_US.UTF-8
|
||||
pip install --require-hashes -r requirements.txt
|
||||
|
||||
4. Create a ``.env`` file at the root of the repository to configure the
|
||||
settings for your development instance. It should look something like this:
|
||||
|
@ -134,7 +116,7 @@ running:
|
|||
|
||||
The site should be available at http://localhost:8000.
|
||||
|
||||
.. _peep: https://github.com/erikrose/peep/
|
||||
.. _pip: https://pip.pypa.io/en/stable/
|
||||
.. _fork: http://help.github.com/fork-a-repo/
|
||||
.. _issue: https://bugs.python.org/issue18378
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ steps, as they don't affect your setup if nothing has changed:
|
|||
git pull origin master
|
||||
|
||||
# Install new dependencies or update existing ones.
|
||||
./bin/peep.py install -r requirements.txt
|
||||
pip install -U --force --require-hashes -r requirements.txt
|
||||
|
||||
# Run database migrations.
|
||||
python manage.py migrate
|
||||
|
|
|
@ -1,11 +1,47 @@
|
|||
# sha256: _5spJogkCW_JETkEYF2D9qqBcQLA8XARW0s58K4GUfM
|
||||
# sha256: dPYz7Tph2h0dWcMYVIPIGp1zRr8Oe18prQdkpvFZtoo
|
||||
sphinx_rtd_theme==0.1.8
|
||||
|
||||
# sha256: Ld8Y2jsGIfpD_uS3KQ2grnibRvuJkniorMzaGVxJeac
|
||||
# sha256: Gm5RMMK0LS3jAWk8KZ94zEvTUB54thDAjkXvxw4rURQ
|
||||
Sphinx==1.3.1
|
||||
|
||||
# sha256: 1P-sFubvUyOIPEErAl2WlLJdhf6kheozhAQShoZHX_s
|
||||
# sha256: 4nho5XDaq_7msuubIPbmMZypyiWHrEGEey00caeJi20
|
||||
graphviz==0.4.7
|
||||
sphinx-rtd-theme==0.1.8 \
|
||||
--hash=sha256:ff9b29268824096fc9113904605d83f6aa817102c0f170115b4b39f0ae0651f3 \
|
||||
--hash=sha256:74f633ed3a61da1d1d59c3185483c81a9d7346bf0e7b5f29ad0764a6f159b68a
|
||||
Sphinx==1.3.1 \
|
||||
--hash=sha256:2ddf18da3b0621fa43fee4b7290da0ae789b46fb899278a8acccda195c4979a7 \
|
||||
--hash=sha256:1a6e5130c2b42d2de301693c299f78cc4bd3501e78b610c08e45efc70e2b5114
|
||||
graphviz==0.4.7 \
|
||||
--hash=sha256:d4ffac16e6ef5323883c412b025d9694b25d85fea485ea338404128686475ffb \
|
||||
--hash=sha256:e27868e570daabfee6b2eb9b20f6e6319ca9ca2587ac41847b2d3471a7898b6d
|
||||
alabaster==0.7.7 \
|
||||
--hash=sha256:d57602b3d730c2ecb978a213face0b7a16ceaa4a263575361bd4fd9e2669a544 \
|
||||
--hash=sha256:f416a84e0d0ddbc288f6b8f2c276d10b40ca1238562cd9ed5a751292ec647b71
|
||||
Babel==2.2.0 \
|
||||
--hash=sha256:fed07cbcdcb3de79b53a8220eebed21c93f8dbb3dbce1d9c6b1c4b09e8aecf2b \
|
||||
--hash=sha256:d8cb4c0e78148aee89560f9fe21587aa57739c975bb89ff66b1e842cc697428f
|
||||
Jinja2==2.8 \
|
||||
--hash=sha256:1cc03ef32b64be19e0a5b54578dd790906a34943fe9102cfdae0d4495bd536b4 \
|
||||
--hash=sha256:bc1ff2ff88dbfacefde4ddde471d1417d3b304e8df103a7a9437d47269201bf4
|
||||
docutils==0.12 \
|
||||
--hash=sha256:dcebd4928112631626f4c4d0df59787c748404e66dda952110030ea883d3b8cd \
|
||||
--hash=sha256:c7db717810ab6965f66c8cf0398a98c9d8df982da39b4cd7f162911eb89596fa
|
||||
snowballstemmer==1.2.1 \
|
||||
--hash=sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89 \
|
||||
--hash=sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128
|
||||
six==1.10.0 \
|
||||
--hash=sha256:0ff78c403d9bccf5a425a6d31a12aa6b47f1c21ca4dc2573a7e2f32a97335eb1 \
|
||||
--hash=sha256:105f8d68616f8248e24bf0e9372ef04d3cc10104f1980f54d57b2ce73a5ad56a
|
||||
Pygments==2.1 \
|
||||
--hash=sha256:3e723f70dc47b4ad5ca3ab4f1d1c3f76438b3dc74d6a843ded3e154e8af99838 \
|
||||
--hash=sha256:148e04b185d3541b8d702e8cde3ee5acd06b31cc0d474127baba9f4652b2aaf1 \
|
||||
--hash=sha256:13a0ef5fafd7b16cf995bc28fe7aab0780dab1b2fda0fc89e033709af8b8a47b
|
||||
MarkupSafe==0.23 \
|
||||
--hash=sha256:a4ec1aff59b95a14b45eb2e23761a0179e98319da5a7eb76b56ea8cdc7b871c3
|
||||
pytz==2015.7 \
|
||||
--hash=sha256:3abe6a6d3fc2fbbe4c60144211f45da2edbe3182a6f6511af6bbba0598b1f992 \
|
||||
--hash=sha256:939ef9c1e1224d980405689a97ffcf7828c56d1517b31d73464356c1f2b7769e \
|
||||
--hash=sha256:ead4aefa7007249e05e51b01095719d5a8dd95760089f5730aac5698b1932918 \
|
||||
--hash=sha256:3cca0df08bd0ed98432390494ce3ded003f5e661aa460be7a734bffe35983605 \
|
||||
--hash=sha256:3ede470d3d17ba3c07638dfa0d10452bc1b6e5ad326127a65ba77e6aaeb11bec \
|
||||
--hash=sha256:68c47964f7186eec306b13629627722b9079cd4447ed9e5ecaecd4eac84ca734 \
|
||||
--hash=sha256:dd5d3991950aae40a6c81de1578942e73d629808cefc51d12cd157980e6cfc18 \
|
||||
--hash=sha256:a77c52062c07eb7c7b30545dbc73e32995b7e117eea750317b5cb5c7a4618f14 \
|
||||
--hash=sha256:81af9aec4bc960a9a0127c488f18772dae4634689233f06f65443e7b11ebeb51 \
|
||||
--hash=sha256:e079b1dadc5c06246cc1bb6fe1b23a50b1d1173f2edd5104efd40bb73a28f406 \
|
||||
--hash=sha256:fbd26746772c24cb93c8b97cbdad5cb9e46c86bbdb1b9d8a743ee00e2fb1fc5d \
|
||||
--hash=sha256:99266ef30a37e43932deec2b7ca73e83c8dbc3b9ff703ec73eca6b1dae6befea \
|
||||
--hash=sha256:8b6ce1c993909783bc96e0b4f34ea223bff7a4df2c90bdb9c4e0f1ac928689e3
|
||||
|
|
|
@ -10,8 +10,5 @@ skip = 1
|
|||
[pylama:docs/conf.py]
|
||||
skip = 1
|
||||
|
||||
[pylama:bin/peep.py]
|
||||
skip = 1
|
||||
|
||||
[pylama:media/projects/*]
|
||||
skip = 1
|
||||
|
|
342
requirements.txt
342
requirements.txt
|
@ -1,208 +1,138 @@
|
|||
-r docs/requirements.txt
|
||||
|
||||
# sha256: 0Bl46ajFHqezTscfP87RMmQ3zTZrnC4ZJlrOBQkk4OI
|
||||
# sha256: s6eaI9N7WgL6pVC5LLu-vrSqHXfmScPrOcGav1Ji2gQ
|
||||
argparse==1.3.0
|
||||
|
||||
# sha256: HhvLrNY1fBUa43zwKQ3MgJch0yziH9a3M5Vo893vG2k
|
||||
certifi==14.05.14
|
||||
|
||||
# sha256: 5T44s6Sv5tETLeYrdACkrDY0Utxd_PjYjo4MzmY8aKo
|
||||
chardet==2.3.0
|
||||
|
||||
# sha256: p7AqX3bYmnn4YZJvs04CnMQ0PBOAJSXIGFQqOf54jM4
|
||||
commonware==0.4.3
|
||||
|
||||
# sha256: FoEBYP8oIz76xsHcDuqNTJuHBC-SEFQdq0-SqQp9hZc
|
||||
configparser==3.5.0b2
|
||||
|
||||
# sha256: nbpWEfvyeJM0c0n9UcwZEctANoKnFjNzrazFZdEeLkw
|
||||
diff-match-patch==20121119
|
||||
|
||||
# sha256: RORjnsBXprap3duGnU46SBj8knjsSQzOOa7whEBZKpM
|
||||
django-session-csrf==0.5
|
||||
|
||||
# sha256: sGqGRRqZ1sJr-mUIKZnb9ljgh-WW6SkszmiLU-q6qto
|
||||
django-sha2==0.4
|
||||
# sha256: 3OvUkoESYxYm9MTQ31l4fHSEBOZt2pUhEAMOqIPTuM0
|
||||
# sha256: x9txeBCraWX2bIzwOYqYydjfmC2jm0zX8WKRHriVlvo
|
||||
docutils==0.12
|
||||
|
||||
# sha256: P1c6GL6U3ohtEZHyfBaEJ-9pPo3Pzs-VsXBXey62nLs
|
||||
mock==1.3.0
|
||||
|
||||
# sha256: bQ9HwFDM7R2av8iak3rQaHCil0_aMewK38r8JgC5R84
|
||||
pbr==1.6.0
|
||||
|
||||
# sha256: _1rZ4vjZ5dHou_vPR3IqtSfPDVHK7u2dptD0B5k4P94
|
||||
funcsigs==0.4
|
||||
|
||||
# sha256: YGbJwKsHlT-YhwvJfSn23v0Ga2whXedLmXZmbz9sPUA
|
||||
oauthlib==0.1.3
|
||||
|
||||
# sha256: seoUHVjtXkiu0mdPfIlN-4P2OcMobXsysuGfoDKltAA
|
||||
# sha256: INKg1YmmksEd9Um9fNqDxmXu8qg-AXuEP-zflW7brXQ
|
||||
polib==1.0.6
|
||||
|
||||
# sha256: rxL5lFTolenkMMdXI8O5QZwLzNDqd4A2YtaprFb2Qls
|
||||
py-bcrypt==0.3
|
||||
|
||||
# sha256: 5PgdU8Uz9r2VJrBH8Ef3sQHCSrFzOcGnrY-YslwQHqs
|
||||
pyasn1==0.1.7
|
||||
|
||||
# sha256: -Shql53l7vyjxaNVruygX0ba58QwixjgBOTdKvfX7Sk
|
||||
https://github.com/l20n/python-l20n/archive/4e32a8d.zip#egg=python-l20n
|
||||
|
||||
# sha256: NYsFoMJgXBXPgzfRcBbrla_qrQfJAN-cLN524hlKklg
|
||||
# sha256: PhW0FsmiA5waUSCLLNO7T_15bNGeYBsdJlevy3fD3JA
|
||||
pytz==2015.2
|
||||
|
||||
# sha256: jw9WgT-C0MJ9lXgiEmismvSPB2xx7mlpMwXOymyjVb0
|
||||
# sha256: BXcknUtsSxH9l8KAN-mGZL-qBVkCL-57zva3UqEG5QU
|
||||
requests==2.6.2
|
||||
|
||||
# sha256: 4rCwWTbCdrHt0uFSVVMjO2Zt-eKbXDuiI-7XOCd8gqA
|
||||
rsa==3.1.4
|
||||
|
||||
# sha256: 885mT4MGch1f7eqUp8OT4c95tnWADOt_ZwndUcezuJA
|
||||
https://github.com/Osmose/silme/archive/v0.9.2.zip#egg=silme==0.9.2
|
||||
|
||||
# sha256: QYqTw5en7asj5ViNvAZ6x0pyPts9VBvUk295R252Rdo
|
||||
# sha256: 4kBSQR_E-9H2cmNVN8P8IzDZSBsYwDF2lbRiWVEskdU
|
||||
six==1.9.0
|
||||
|
||||
# sha256: 1VJK5SO7ngnFe829HvriwofSBgNojqMfYCDtGApImvA
|
||||
suds==0.4
|
||||
|
||||
# sha256: ih29HHrehSBq53DJzl9DCBoJjJX5lK8RP2Q06QBVmmc
|
||||
https://github.com/jbalogh/test-utils/archive/e42e031.zip#egg=test-utils
|
||||
|
||||
# sha256: 3Lv0kFjkGWoG6YjZ3B52IyGrDQV8S-A1uE48ETU_wvg
|
||||
translate-toolkit==1.13.0
|
||||
|
||||
# sha256: 8uJz7TSsu1YJYtXPEpF5NtjfAil98JvTCJuFRtRYQTg
|
||||
# sha256: ygF2j97N4TQwHzFwdDIm9g7f9cOTXxJDc3jr2RFQY1M
|
||||
dj-database-url==0.3.0
|
||||
|
||||
# sha256: 3-W4wr5as0NbPXHlocZrsU2oEE2kTvDdjMw-ipJ2Ftk
|
||||
# sha256: EgxGIdHk9a2r4KaDRj0b56WmuZLt1HZNMjxifSKSUeA
|
||||
django-dotenv==1.3.0
|
||||
|
||||
# sha256: j7aT7P5M1v-a4xNv8KHqpKyuAa8ie7geZG3CutMpXM8
|
||||
django==1.8.7
|
||||
|
||||
# sha256: 3w1cLeq8MzdfKIx6tUYuBsbs2CTs-ie_qLzV5Mz7Nyo
|
||||
django-jinja==1.3.3
|
||||
# sha256: SOccixeBUzre5kVhzKIMnxfUATYhHCsyUx5w_-f7DO0
|
||||
# sha256: dS-jXUDLZF5tOlTM3gFyZzLkQaLVTZBCGcaOQC944ZQ
|
||||
django-pipeline==1.5.3
|
||||
|
||||
# sha256: pRec3pItK04EXuWxHocyPud9NjrkApT8glLyXWoOrwY
|
||||
# sha256: i8g1CCiCrZoBLNeQxGABHk2WvzUS2YoE09q75FOToIk
|
||||
gunicorn==19.3.0
|
||||
|
||||
# sha256: wAr-yzAqmaT4PeybBVxNHMGWkm1iyNsBXWhDLfgRjKg
|
||||
psycopg2==2.6
|
||||
|
||||
# sha256: Pk2AGZlglZuCiVGqJMv6o2zr7RSdzXKMEehVeYsV-HA
|
||||
# sha256: 2slBnbPs4nu1PHQzJD_CLtQs9o3p1sQSk5Dh2a7-YxA
|
||||
whitenoise==1.0.6
|
||||
|
||||
# sha256: _d1HqNBI4MX0Lu5RGoqvNYkcmW1slZVTx77Is6x0s8s
|
||||
newrelic==2.50.0.39
|
||||
|
||||
# sha256: 56MxTtRt0kOTkYZwt6EVbkuvODGJ9kSz5hIbocv7wvk
|
||||
django-browserid==1.0.0
|
||||
|
||||
# sha256: ECyBQVEUQ98B01RhDTsmiSQQBlQxZwm0OsBGSLUL9wM
|
||||
# sha256: zYMG5kw6EV3soTZoXpRduI_-FxOCAS7JOO0kGiDdfro
|
||||
factory_boy==2.5.2
|
||||
|
||||
# sha256: R3KrUYkik5LRbHC6e4u2a108GAdmlMVTR6-5jJULKDw
|
||||
# sha256: 4ZtPiklWgcNnq1bDwE-L7zDd15B939m-5mOj8yhnYrY
|
||||
# sha256: 9h4JCadD7tN7Egfjio57Si_gqCGF428r4lLvGz-QF1g
|
||||
nose==1.3.6
|
||||
|
||||
# sha256: Js7zxvYt8u7pVaJRld5veTiBMXwPX9GhxvniLzUakxM
|
||||
# sha256: 7ZCm7YglpRbGU_yQmcBTvjNbZPoASpA6GBt-AUiOxdQ
|
||||
django-nose==1.4
|
||||
|
||||
# sha256: 7cVxMGHxCWYEi_a0DZpRSzgeC6hJxk4DTE72wYR9MAc
|
||||
blessings==1.6
|
||||
|
||||
# sha256: RL1BNEwcwd5DSnJ2TtR_27vbzwPHgBEUwEM81saWy1U
|
||||
# sha256: qsAfM8hEZAezxebyGF1bCfXz5st3Px2y35nvzlpwuBs
|
||||
nose-progressive==1.5.1
|
||||
|
||||
# Temporarily using a fork instead due to
|
||||
# sha256: 2pk9Q6BeOnBk_rEsexukn8UmtnBZFIR-zf8HLpvDEBs
|
||||
# raygun4py==3.0.1
|
||||
|
||||
# sha256: AVJNORYtkB786KvHT4kbosH7rpS3a4UyB-57T15Di4I
|
||||
https://github.com/Osmose/raygun4py/archive/510b309704705cd66086792edf2d40ada74e2c21.zip#egg=raygun4py
|
||||
|
||||
# sha256: F3sOgxHG7b_H0YQVen6A9otrh0xWRfcwDUH0dfBfIx4
|
||||
jsonpickle==0.7.0
|
||||
|
||||
# sha256: aBEBCAkmImHkGre5Lz9tI_Nc-Bb77CvAUHeZLuvsbi8
|
||||
blinker==1.3
|
||||
|
||||
# sha256: s9NiusRxFydHzaNRMjjxFcvWxfi45jGb9ql6eJJyQJk
|
||||
lxml==3.4.4
|
||||
|
||||
# sha256: eorPcym-2jjO6inGiSEldNmmv__iTPVlAV6gBm987j8
|
||||
mercurial==3.4.1
|
||||
|
||||
# sha256: 9r_SEEjH38lbOWWaPad3ddbbHB96dFgFzMbGE494O20
|
||||
django-sslify==0.2.7
|
||||
|
||||
# sha256: bVTzUOeg5IkDpOO2ssq9G0PiN2X7yXUGVAKJNpKVQZE
|
||||
snowballstemmer==1.2.0
|
||||
|
||||
# sha256: nwLQNXGE3h8JPBABK1LnRUoQCL5qXBhat6Mwes6x0S4
|
||||
babel==1.3
|
||||
|
||||
# sha256: IvlnXkLcZAxEadT30hC67LXmlYI6-VTIE9UckwI1Z10
|
||||
Jinja2==2.5.5
|
||||
|
||||
# sha256: CjoiZeHvsN76ci0fQpcw3J35da3DxgrH0rQyvfA8BB0
|
||||
# sha256: Xe0vqQlP19_rPakmNkCf1wKg0H1gYoNQTX7gRAHO5cs
|
||||
# sha256: cyCRkITm2sj0VAY4pGRHo71zD8oXKvwX0sA-7SLPT1E
|
||||
Pygments==2.0.2
|
||||
|
||||
# sha256: SlwIPaNyS1CauqpJQ2oyf9EwkL3Ah8fIuUTQ1DeudTM
|
||||
# sha256: znfi_bqrquOT_84qYlKgpmbjl3xsL6HEjE3tBWl4WVE
|
||||
alabaster==0.7.4
|
||||
# sha256: G-4U2FR4kpaYI3chlYnX6eTwalRrktmfLu-8KdBxHXE
|
||||
# sha256: bg5ENLg92FuXwBRx8TP9z-qofWwklk7kdRGzmfYGInc
|
||||
django-bulk-update==1.1.4
|
||||
# sha256: Qjri4WBhUEQYq3q_CnQOJqeB-bx2dKbPXi8R7bSugCk
|
||||
parsimonious==0.6.2
|
||||
# sha256: fn9zpnXFGHErrdeDJ54m0WQUDz_C7XoyECw9CKaipKc
|
||||
jsonfield==1.0.3
|
||||
# sha256: FiVVlWFqbXjNeGpVzGQx2lt6zPRlEt-FRxKgzbs6z6o
|
||||
pylibmc==1.5.0
|
||||
# sha256: VZxCRyjkBCDajJmLE8PE8IYbR1JpoKh2-xcY__J20QY
|
||||
# sha256: YhN9U7WsovukJHRLobsw6SoRXZqGgcvmpkEdskGx0a0
|
||||
django-pylibmc==0.6.0
|
||||
# sha256: U_5sxKvAXajw8DIgLVoQ298q3k1CvHD1ZVeykr8ycok
|
||||
django-dirtyfields==0.7
|
||||
# sha256: fvosoXFZxZBAjLYk3pqhDTYPFAl8tw3XVZ5jLyz0sEg
|
||||
py-dateutil==2.2
|
||||
# sha256: 2_WWGNWp7_Fy0lAh82YUvlrwUB5FJ5dcpQS5WGPxT-0
|
||||
# sha256: CST5QHDG_FfUCLFphIxbOIMmaP_-Bg5ItIA_sj4OPq8
|
||||
celery==3.1.18
|
||||
# sha256: aI-UZrHDrhQQY4Hm29MoEV51xSYMVC60jmxGkx9pKMw
|
||||
billiard==3.3.0.20
|
||||
# sha256: _qhlOuS4en6wmWWUCfCeaANqe4PwoVoLxq7629mKUdw
|
||||
# sha256: H1ZavUTEt9-qTdVD1S-YLS8AaroKKzgwVCtNJagB_gk
|
||||
kombu==3.0.26
|
||||
# sha256: 406XZaYSD0ZkwSD0GYp4bDmg-35KWb0ZotbjqIS2OIk
|
||||
# sha256: 68_IZ95aaPn1uhTRHbrYjmr_hDWo05M51c6w5bBt5kA
|
||||
amqp==1.4.6
|
||||
# sha256: N4Ethjya0-NcBzTELgvwMgzow77YLNIK1UyzTRWBV7o
|
||||
anyjson==0.3.3
|
||||
# sha256: 8Q40TOv5GQPKIK20kXYNt01sQ7bwHFfld9L5HavACqg
|
||||
pylama==6.4.0
|
||||
# sha256: yY7zECUyGAxtDrno1pd0u97Y2QtnGYjuRV1W_h9xGos
|
||||
django-guardian==1.3
|
||||
argparse==1.3.0 \
|
||||
--hash=sha256:d01978e9a8c51ea7b34ec71f3fced1326437cd366b9c2e19265ace050924e0e2 \
|
||||
--hash=sha256:b3a79a23d37b5a02faa550b92cbbbebeb4aa1d77e649c3eb39c19abf5262da04
|
||||
certifi==14.05.14 \
|
||||
--hash=sha256:1e1bcbacd6357c151ae37cf0290dcc809721d32ce21fd6b7339568f3ddef1b69
|
||||
chardet==2.3.0 \
|
||||
--hash=sha256:e53e38b3a4afe6d1132de62b7400a4ac363452dc5dfcf8d88e8e0cce663c68aa
|
||||
commonware==0.4.3 \
|
||||
--hash=sha256:a7b02a5f76d89a79f861926fb34e029cc4343c13802525c818542a39fe788cce
|
||||
configparser==3.5.0b2 \
|
||||
--hash=sha256:16810160ff28233efac6c1dc0eea8d4c9b87042f9210541dab4f92a90a7d8597
|
||||
diff-match-patch==20121119 \
|
||||
--hash=sha256:9dba5611fbf27893347349fd51cc1911cb403682a7163373adacc565d11e2e4c
|
||||
django-session-csrf==0.5 \
|
||||
--hash=sha256:44e4639ec057a6b6a9dddb869d4e3a4818fc9278ec490cce39aef08440592a93
|
||||
django-sha2==0.4 \
|
||||
--hash=sha256:b06a86451a99d6c26bfa65082999dbf658e087e596e9292cce688b53eabaaada
|
||||
mock==1.3.0 \
|
||||
--hash=sha256:3f573a18be94de886d1191f27c168427ef693e8dcfcecf95b170577b2eb69cbb
|
||||
pbr==1.6.0 \
|
||||
--hash=sha256:6d0f47c050cced1d9abfc89a937ad06870a2974fda31ec0adfcafc2600b947ce
|
||||
funcsigs==0.4 \
|
||||
--hash=sha256:ff5ad9e2f8d9e5d1e8bbfbcf47722ab527cf0d51caeeed9da6d0f40799383fde
|
||||
oauthlib==0.1.3 \
|
||||
--hash=sha256:6066c9c0ab07953f98870bc97d29f6defd066b6c215de74b9976666f3f6c3d40
|
||||
polib==1.0.6 \
|
||||
--hash=sha256:b1ea141d58ed5e48aed2674f7c894dfb83f639c3286d7b32b2e19fa032a5b400 \
|
||||
--hash=sha256:20d2a0d589a692c11df549bd7cda83c665eef2a83e017b843fecdf956edbad74
|
||||
py-bcrypt==0.3 \
|
||||
--hash=sha256:af12f99454e895e9e430c75723c3b9419c0bccd0ea77803662d6a9ac56f6425b
|
||||
pyasn1==0.1.7 \
|
||||
--hash=sha256:e4f81d53c533f6bd9526b047f047f7b101c24ab17339c1a7ad8f98b25c101eab
|
||||
https://github.com/l20n/python-l20n/archive/4e32a8d.zip#egg=python-l20n \
|
||||
--hash=sha256:f9286a979de5eefca3c5a355aeeca05f46dae7c4308b18e004e4dd2af7d7ed29
|
||||
requests==2.6.2 \
|
||||
--hash=sha256:8f0f56813f82d0c27d9578221268ac9af48f076c71ee69693305ceca6ca355bd \
|
||||
--hash=sha256:0577249d4b6c4b11fd97c28037e98664bfaa0559022fee7bcef6b752a106e505
|
||||
rsa==3.1.4 \
|
||||
--hash=sha256:e2b0b05936c276b1edd2e1525553233b666df9e29b5c3ba223eed738277c82a0
|
||||
https://github.com/Osmose/silme/archive/v0.9.2.zip#egg=silme==0.9.2 \
|
||||
--hash=sha256:f3ce664f8306721d5fedea94a7c393e1cf79b675800ceb7f6709dd51c7b3b890
|
||||
suds==0.4 \
|
||||
--hash=sha256:d5524ae523bb9e09c57bcdbd1efae2c287d20603688ea31f6020ed180a489af0
|
||||
https://github.com/jbalogh/test-utils/archive/e42e031.zip#egg=test-utils \
|
||||
--hash=sha256:8a1dbd1c7ade85206ae770c9ce5f43081a098c95f994af113f6434e900559a67
|
||||
translate-toolkit==1.13.0 \
|
||||
--hash=sha256:dcbbf49058e4196a06e988d9dc1e762321ab0d057c4be035b84e3c11353fc2f8
|
||||
dj-database-url==0.3.0 \
|
||||
--hash=sha256:f2e273ed34acbb560962d5cf12917936d8df02297df09bd3089b8546d4584138 \
|
||||
--hash=sha256:ca01768fdecde134301f3170743226f60edff5c3935f12437378ebd911506353
|
||||
django-dotenv==1.3.0 \
|
||||
--hash=sha256:dfe5b8c2be5ab3435b3d71e5a1c66bb14da8104da44ef0dd8ccc3e8a927616d9 \
|
||||
--hash=sha256:120c4621d1e4f5adabe0a683463d1be7a5a6b992edd4764d323c627d229251e0
|
||||
django==1.8.7 \
|
||||
--hash=sha256:8fb693ecfe4cd6ff9ae3136ff0a1eaa4acae01af227bb81e646dc2bad3295ccf
|
||||
django-jinja==1.3.3 \
|
||||
--hash=sha256:df0d5c2deabc33375f288c7ab5462e06c6ecd824ecfa27bfa8bcd5e4ccfb372a
|
||||
django-pipeline==1.5.3 \
|
||||
--hash=sha256:48e71c8b1781533adee64561cca20c9f17d40136211c2b32531e70ffe7fb0ced \
|
||||
--hash=sha256:752fa35d40cb645e6d3a54ccde01726732e441a2d54d904219c68e402f78e194
|
||||
gunicorn==19.3.0 \
|
||||
--hash=sha256:a5179cde922d2b4e045ee5b11e87323ee77d363ae40294fc8252f25d6a0eaf06 \
|
||||
--hash=sha256:8bc835082882ad9a012cd790c460011e4d96bf3512d98a04d3dabbe45393a089
|
||||
psycopg2==2.6 \
|
||||
--hash=sha256:c00afecb302a99a4f83dec9b055c4d1cc196926d62c8db015d68432df8118ca8
|
||||
whitenoise==1.0.6 \
|
||||
--hash=sha256:3e4d80199960959b828951aa24cbfaa36cebed149dcd728c11e855798b15f870 \
|
||||
--hash=sha256:dac9419db3ece27bb53c7433243fc22ed42cf68de9d6c4129390e1d9aefe6310
|
||||
newrelic==2.50.0.39 \
|
||||
--hash=sha256:fddd47a8d048e0c5f42eee511a8aaf35891c996d6c959553c7bec8b3ac74b3cb
|
||||
django-browserid==1.0.0 \
|
||||
--hash=sha256:e7a3314ed46dd24393918670b7a1156e4baf383189f644b3e6121ba1cbfbc2f9
|
||||
factory-boy==2.5.2 \
|
||||
--hash=sha256:102c8141511443df01d354610d3b268924100654316709b43ac04648b50bf703 \
|
||||
--hash=sha256:cd8306e64c3a115deca136685e945db88ffe171382012ec938ed241a20dd7eba
|
||||
nose==1.3.6 \
|
||||
--hash=sha256:4772ab5189229392d16c70ba7b8bb66b5d3c18076694c55347afb98c950b283c \
|
||||
--hash=sha256:e19b4f8a495681c367ab56c3c04f8bef30ddd7907ddfd9bee663a3f3286762b6 \
|
||||
--hash=sha256:f61e0909a743eed37b1207e38a8e7b4a2fe0a82185e36f2be252ef1b3f901758
|
||||
django-nose==1.4 \
|
||||
--hash=sha256:26cef3c6f62df2eee955a25195de6f793881317c0f5fd1a1c6f9e22f351a9313 \
|
||||
--hash=sha256:ed90a6ed8825a516c653fc9099c053be335b64fa004a903a181b7e01488ec5d4
|
||||
blessings==1.6 \
|
||||
--hash=sha256:edc5713061f10966048bf6b40d9a514b381e0ba849c64e034c4ef6c1847d3007
|
||||
nose-progressive==1.5.1 \
|
||||
--hash=sha256:44bd41344c1cc1de434a72764ed47fdbbbdbcf03c7801114c0433cd6c696cb55 \
|
||||
--hash=sha256:aac01f33c8446407b3c5e6f2185d5b09f5f3e6cb773f1db2df99efce5a70b81b
|
||||
https://github.com/Osmose/raygun4py/archive/510b309704705cd66086792edf2d40ada74e2c21.zip#egg=raygun4py \
|
||||
--hash=sha256:da993d43a05e3a7064feb12c7b1ba49fc526b6705914847ecdff072e9bc3101b \
|
||||
--hash=sha256:01524d39162d901efce8abc74f891ba2c1fbae94b76b853207ee7b4f5e438b82
|
||||
jsonpickle==0.7.0 \
|
||||
--hash=sha256:177b0e8311c6edbfc7d184157a7e80f68b6b874c5645f7300d41f475f05f231e
|
||||
blinker==1.3 \
|
||||
--hash=sha256:6811010809262261e41ab7b92f3f6d23f35cf816fbec2bc05077992eebec6e2f
|
||||
lxml==3.4.4 \
|
||||
--hash=sha256:b3d362bac471172747cda3513238f115cbd6c5f8b8e6319bf6a97a7892724099
|
||||
mercurial==3.4.1 \
|
||||
--hash=sha256:7a8acf7329beda38ceea29c689212574d9a6bfffe24cf565015ea0066f7cee3f
|
||||
django-sslify==0.2.7 \
|
||||
--hash=sha256:f6bfd21048c7dfc95b39659a3da77775d6db1c1f7a745805ccc6c6138f783b6d
|
||||
django-bulk-update==1.1.4 \
|
||||
--hash=sha256:1bee14d854789296982377219589d7e9e4f06a546b92d99f2eefbc29d0711d71 \
|
||||
--hash=sha256:6e0e4434b83dd85b97c01471f133fdcfeaa87d6c24964ee47511b399f6062277
|
||||
parsimonious==0.6.2 \
|
||||
--hash=sha256:423ae2e16061504418ab7abf0a740e26a781f9bc7674a6cf5e2f11edb4ae8029
|
||||
jsonfield==1.0.3 \
|
||||
--hash=sha256:7e7f73a675c518712badd783279e26d164140f3fc2ed7a32102c3d08a6a2a4a7
|
||||
pylibmc==1.5.0 \
|
||||
--hash=sha256:16255595616a6d78cd786a55cc6431da5b7accf46512df854712a0cdbb3acfaa
|
||||
django-pylibmc==0.6.0 \
|
||||
--hash=sha256:559c424728e40420da8c998b13c3c4f0861b475269a0a876fb1718fff276d106 \
|
||||
--hash=sha256:62137d53b5aca2fba424744ba1bb30e92a115d9a8681cbe6a6411db241b1d1ad
|
||||
django-dirtyfields==0.7 \
|
||||
--hash=sha256:53fe6cc4abc05da8f0f032202d5a10dbdf2ade4d42bc70f56557b292bf327289
|
||||
py-dateutil==2.2 \
|
||||
--hash=sha256:7efa2ca17159c590408cb624de9aa10d360f14097cb70dd7559e632f2cf4b048
|
||||
celery==3.1.18 \
|
||||
--hash=sha256:dbf59618d5a9eff172d25021f36614be5af0501e4527975ca504b95863f14fed \
|
||||
--hash=sha256:0924f94070c6fc57d408b169848c5b38832668fffe060e48b4803fb23e0e3eaf
|
||||
billiard==3.3.0.20 \
|
||||
--hash=sha256:688f9466b1c3ae14106381e6dbd328115e75c5260c542eb48e6c46931f6928cc
|
||||
kombu==3.0.26 \
|
||||
--hash=sha256:fea8653ae4b87a7eb099659409f09e68036a7b83f0a15a0bc6aefadbd98a51dc \
|
||||
--hash=sha256:1f565abd44c4b7dfaa4dd543d52f982d2f006aba0a2b3830542b4d25a801fe09
|
||||
amqp==1.4.6 \
|
||||
--hash=sha256:e34e9765a6120f4664c120f4198a786c39a0fb7e4a59bd19a2d6e3a884b63889 \
|
||||
--hash=sha256:ebcfc867de5a68f9f5ba14d11dbad88e6aff8435a8d39339d5ceb0e5b06de640
|
||||
anyjson==0.3.3 \
|
||||
--hash=sha256:37812d863c9ad3e35c0734c42e0bf0320ce8c3bed82cd20ad54cb34d158157ba
|
||||
pylama==6.4.0 \
|
||||
--hash=sha256:f10e344cebf91903ca20adb491760db74d6c43b6f01c57e577d2f91dabc00aa8
|
||||
django-guardian==1.3 \
|
||||
--hash=sha256:c98ef3102532180c6d0eb9e8d69774bbded8d90b671988ee455d56fe1f711a8b
|
||||
futures==3.0.4 \
|
||||
--hash=sha256:4e860d18d866ff6c5f2804ebcbb16415f4f29cf57efea919178b809cf99326b6 \
|
||||
--hash=sha256:19485d83f7bd2151c0aeaf88fbba3ee50dadfb222ffc3b66a344ef4952b782a3
|
||||
|
|
Загрузка…
Ссылка в новой задаче