#34 Use poetry, requirements, put in a module ready for publication, bump to 0.6.2

Closed
rigelk wants to merge 13 commits from rigelk/prismedia:requirements into develop
  1. +49
    -26
      README.md
  2. +302
    -0
      poetry.lock
  3. +5
    -0
      prismedia/__init__.py
  4. +2
    -0
      prismedia/__main__.py
  5. +2
    -2
      prismedia/config/nfo_example.txt
  6. +0
    -0
      prismedia/config/peertube_secret.sample
  7. +0
    -0
      prismedia/config/youtube_secret.json.sample
  8. +15
    -0
      prismedia/genconfig.py
  9. +6
    -4
      prismedia/pt_upload.py
  10. +20
    -16
      prismedia/upload.py
  11. +32
    -11
      prismedia/utils.py
  12. +10
    -11
      prismedia/yt_upload.py
  13. +39
    -0
      pyproject.toml
  14. +98
    -0
      requirements.txt

+ 49
- 26
README.md View File

@ -1,9 +1,10 @@
# Prismedia
A scripting way to upload videos to peertube and youtube written in python2
Scripting your way to upload videos to peertube and youtube. Works with Python 2.7 and 3.3+.
## Dependencies
Search in your package manager, otherwise use ``pip install --upgrade``
Search in your system package manager, otherwise use ``pip install --upgrade`` for the following packages:
- google-auth
- google-auth-oauthlib
- google-auth-httplib2
@ -14,29 +15,44 @@ Search in your package manager, otherwise use ``pip install --upgrade``
- python-magic-bin
- requests-toolbelt
- tzlocal
- configparser
- future
Otherwise, you can use the requirements file with `pip install -r requirements.txt`. (*note:* requirements are generated via `poetry export -f requirements.txt`)
Otherwise, you can use [poetry](https://poetry.eustace.io/):
```
poetry install # installs the dependency in the current virtualenv, or creates one specific to the project if no virtualenv is currently active
```
## Configuration
Edit peertube_secret and youtube_secret.json with your credentials.
Generate sample files with `python -m prismedia.genconfig`. Edit
`peertube_secret` and `youtube_secret.json` with your credentials.
### Peertube
Set your credentials, peertube server URL.
You can get client_id and client_secret by logging in your peertube website and reaching the URL: https://domain.example/api/v1/oauth-clients/local
You can set ``OAUTHLIB_INSECURE_TRANSPORT`` to 1 if you do not use https (not recommended)
### Youtube
Youtube uses combination of oauth and API access to identify.
**Credentials**
The first time you connect, prismedia will open your browser to as you to authenticate to
Youtube and allow the app to use your Youtube channel.
**It is here you choose which channel you will upload to**.
Once authenticated, the token is stored inside the file ``.youtube_credentials.json``.
Prismedia will try to use this file at each launch, and re-ask for authentication if it does not exist.
Youtube uses OAuth 2.0 to restrict its API access to identified users. Registering a client is documented [here](https://developers.google.com/youtube/v3/guides/uploading_a_video).
**Credentials:** the first time you connect, prismedia will open your browser
to as you to authenticate to Youtube and allow the app to use your Youtube
channel.
**Oauth**:
The default youtube_secret.json should allow you to upload some videos.
If you plan an larger usage, please consider creating your own youtube_secret file:
**It is here you choose which channel you will upload to:** once authenticated,
the token is stored inside the file `.youtube_credentials.json`. Prismedia will
try to use this file at each launch, and re-ask for authentication if it does
not exist.
**OAuth 2.0**: the default `youtube_secret.json` should allow you to upload
some videos. If you plan a more frequent usage, please consider creating your
own `youtube_secret` file:
- Go to the [Google console](https://console.developers.google.com/).
- Create project.
@ -48,37 +64,36 @@ If you plan an larger usage, please consider creating your own youtube_secret fi
- Save this JSON as your youtube_secret.json file.
## How To
Currently in heavy development
Support only mp4 for cross compatibility between Youtube and Peertube
>> Currently in heavy development
Supports only mp4 for cross compatibility between Youtube and Peertube.
Simply upload a video:
```
./prismedia_upload.py --file="yourvideo.mp4"
python -m prismedia --file="yourvideo.mp4"
```
Specify description and tags:
```
./prismedia_upload.py --file="yourvideo.mp4" -d "My supa description" -t "tag1,tag2,foo"
python -m prismedia --file="yourvideo.mp4" -d "My supa description" -t "tag1,tag2,foo"
```
Provide a thumbnail:
```
./prismedia_upload.py --file="yourvideo.mp4" -d "Video with thumbnail" --thumbnail="/path/to/your/thumbnail.jpg"
python -m prismedia --file="yourvideo.mp4" -d "Video with thumbnail" --thumbnail="/path/to/your/thumbnail.jpg"
```
Use a NFO file to specify your video options:
```
./prismedia_upload.py --file="yourvideo.mp4" --nfo /path/to/your/nfo.txt
python -m prismedia --file="yourvideo.mp4" --nfo /path/to/your/nfo.txt
```
Use --help to get all available options:
```
@ -98,7 +113,8 @@ Options:
--disable-comments Disable comments (Peertube only as YT API does not support) (default: comments are enabled)
--nsfw Set the video as No Safe For Work (Peertube only as YT API does not support) (default: video is safe)
--nfo=STRING Configure a specific nfo file to set options for the video.
By default Prismedia search a .txt based on video name
By default Prismedia search a .txt based on the video name and will
decode the file as UTF-8 (so make sure your nfo file is UTF-8 encoded)
See nfo_example.txt for more details
--platform=STRING List of platform(s) to upload to, comma separated.
Supported platforms are youtube and peertube (default is both)
@ -149,8 +165,8 @@ Languages:
- [x] set default language
- [x] thumbnail/preview
- [x] multiple lines description (see [issue 4](https://git.lecygnenoir.info/LecygneNoir/prismedia/issues/4))
- [x] add videos to playlist for Peertube
- [x] add videos to playlist for Youtube
- [x] add videos to playlist
- [x] create playlist
- [x] Use a config file (NFO) file to retrieve videos arguments
- [x] Allow to choose peertube or youtube upload (to resume failed upload for example)
- [x] Add publishAt option to plan your videos
@ -160,7 +176,14 @@ Languages:
## Compatibility
If your server uses peertube before 1.0.0-beta4, use the version inside tag 1.0.0-beta3!
### Compatibility with PeerTube
If your server uses PeerTube before `1.0.0-beta4`, use the version inside tag `1.0.0-beta3` of prismedia!
### Compatibility with Operating Systems
The script has been tested on Linux and Windows platforms.
## Sources
inspired by [peeror](https://git.rigelk.eu/rigelk/peeror) and [youtube-upload](https://github.com/tokland/youtube-upload)
Prismedia has been inspired by [peeror](https://git.rigelk.eu/rigelk/peeror) and [youtube-upload](https://github.com/tokland/youtube-upload).

+ 302
- 0
poetry.lock
File diff suppressed because it is too large
View File


+ 5
- 0
prismedia/__init__.py View File

@ -0,0 +1,5 @@
from future import standard_library
standard_library.install_aliases()
from . import upload
from . import genconfig

+ 2
- 0
prismedia/__main__.py View File

@ -0,0 +1,2 @@
from .upload import main
main()

nfo_example.txt → prismedia/config/nfo_example.txt View File

@ -9,7 +9,7 @@
name = videoname
description = Your complete video description
Multilines description
should be wrote with a blank space
should be written with a blank space
at the beginning of the line :)
tags = list of tags, comma separated
category = Films
@ -22,4 +22,4 @@ playlistCreate = True
nsfw = True
platform = youtube, peertube
language = French
publishAt=2034-05-07T19:00:00
publishAt=2034-05-07T19:00:00

peertube_secret.sample → prismedia/config/peertube_secret.sample View File


youtube_secret.json.sample → prismedia/config/youtube_secret.json.sample View File


+ 15
- 0
prismedia/genconfig.py View File

@ -0,0 +1,15 @@
from os.path import join, abspath, isfile, dirname
from os import listdir
from shutil import copyfile
def genconfig():
path = join(dirname(__file__), 'config')
files = [f for f in listdir(path) if isfile(join(path, f))]
for f in files:
copyfile(join(path, f), f)
if __name__ == '__main__':
genconfig()

lib/pt_upload.py → prismedia/pt_upload.py View File

@ -1,4 +1,3 @@
#!/usr/bin/env python2
# coding: utf-8
import os
@ -10,12 +9,15 @@ import pytz
from os.path import splitext, basename, abspath
from tzlocal import get_localzone
LecygneNoir commented 5 years ago
Review

I notice that at the beginning of the libs we still use #!/usr/bin/env python2 as shebang.

It does nothing as files are never direclty calles as script, but to avoid any future (and hard to debug :D) problems between python2 and python3, it should be better to totally remove them?

Sheband are not needed when loading python files as libraries?

I notice that at the beginning of the libs we still use `#!/usr/bin/env python2` as shebang. It does nothing as files are never direclty calles as script, but to avoid any future (and hard to debug :D) problems between python2 and python3, it should be better to totally remove them? Sheband are not needed when loading python files as libraries?
rigelk commented 5 years ago
Review

Yes, we should remove them, libraries don't use them.

Yes, we should remove them, libraries don't use them.
from ConfigParser import RawConfigParser
from requests_oauthlib import OAuth2Session
from oauthlib.oauth2 import LegacyApplicationClient
from requests_toolbelt.multipart.encoder import MultipartEncoder
import utils
from . import utils
from six.moves import configparser
ConfigParser = configparser.RawConfigParser
PEERTUBE_SECRETS_FILE = 'peertube_secret'
PEERTUBE_PRIVACY = {
@ -209,7 +211,7 @@ def upload_video(oauth, secret, options):
def run(options):
secret = RawConfigParser()
secret = ConfigParser()
try:
secret.read(PEERTUBE_SECRETS_FILE)
except Exception as e:

prismedia_upload.py → prismedia/upload.py View File

@ -2,7 +2,7 @@
# coding: utf-8
"""
prismedia_upload - tool to upload videos to Peertube and Youtube
prismedia - tool to upload videos to Peertube and Youtube
Usage:
prismedia_upload.py --file=<FILE> [options]
@ -59,21 +59,16 @@ Languages:
Japanese, Korean, Mandarin, Portuguese, Punjabi, Russian, Spanish
"""
from os.path import dirname, realpath
import sys
import datetime
import locale
import logging
logging.basicConfig(format='%(asctime)s %(message)s', level=logging.INFO)
from docopt import docopt
# Allows a relative import from the parent folder
sys.path.insert(0, dirname(realpath(__file__)) + "/lib")
import yt_upload
import pt_upload
import utils
from . import yt_upload
from . import pt_upload
from . import utils
try:
# noinspection PyUnresolvedReferences
@ -92,7 +87,7 @@ except ImportError:
'see https://github.com/ahupp/python-magic\n')
exit(1)
VERSION = "prismedia v0.6.1-1"
VERSION = "prismedia v0.6.2"
VALID_PRIVACY_STATUSES = ('public', 'private', 'unlisted')
VALID_CATEGORIES = (
@ -157,6 +152,7 @@ def validatePublish(publish):
return False
return True
def validateThumbnail(thumbnail):
supported_types = ['image/jpg', 'image/jpeg']
if magic.from_file(thumbnail, mime=True) in supported_types:
@ -164,24 +160,24 @@ def validateThumbnail(thumbnail):
else:
return False
if __name__ == '__main__':
def main():
options = docopt(__doc__, version=VERSION)
schema = Schema({
'--file': And(str, validateVideo, error='file is not supported, please use mp4'),
Optional('--name'): Or(None, And(
str,
basestring,
lambda x: not x.isdigit(),
error="The video name should be a string")
),
Optional('--description'): Or(None, And(
str,
basestring,
lambda x: not x.isdigit(),
error="The video name should be a string")
error="The video description should be a string")
),
Optional('--tags'): Or(None, And(
str,
basestring,
lambda x: not x.isdigit(),
error="Tags should be a string")
),
@ -219,6 +215,7 @@ if __name__ == '__main__':
'--version': bool
})
utils.decodeArgumentStrings(options, locale.getpreferredencoding())
options = utils.parseNFO(options)
if not options.get('--thumbnail'):
@ -233,3 +230,10 @@ if __name__ == '__main__':
yt_upload.run(options)
if options.get('--platform') is None or "peertube" in options.get('--platform'):
pt_upload.run(options)
if __name__ == '__main__':
import warnings
warnings.warn("use 'python -m prismedia', not 'python -m prismedia.upload'", DeprecationWarning)
main()

lib/utils.py → prismedia/utils.py View File

@ -8,8 +8,11 @@ from os import devnull
from subprocess import check_call, CalledProcessError, STDOUT
import unidecode
import logging
from six.moves import configparser
### CATEGORIES ###
ConfigParser = configparser.RawConfigParser
# CATEGORIES #
YOUTUBE_CATEGORY = {
"music": 10,
"films": 1,
@ -102,11 +105,12 @@ def getLanguage(language, platform):
def remove_empty_kwargs(**kwargs):
good_kwargs = {}
if kwargs is not None:
for key, value in kwargs.iteritems():
for key, value in viewitems(kwargs):
if value:
good_kwargs[key] = value
return good_kwargs
def searchThumbnail(options):
video_directory = dirname(options.get('--file')) + "/"
# First, check for thumbnail based on videoname
@ -125,14 +129,14 @@ def searchThumbnail(options):
return options
# return the nfo as a RawConfigParser object
# return the nfo as a ConfigParser object
def loadNFO(options):
video_directory = dirname(options.get('--file')) + "/"
if options.get('--nfo'):
try:
logging.info("Using " + options.get('--nfo') + " as NFO, loading...")
if isfile(options.get('--nfo')):
nfo = RawConfigParser()
nfo = ConfigParser()
nfo.read(options.get('--nfo'))
return nfo
else:
@ -147,7 +151,7 @@ def loadNFO(options):
if isfile(nfo_file):
try:
logging.info("Using " + nfo_file + " as NFO, loading...")
nfo = RawConfigParser()
nfo = ConfigParser()
nfo.read(nfo_file)
return nfo
except Exception as e:
@ -160,7 +164,7 @@ def loadNFO(options):
if isfile(nfo_file):
try:
logging.info("Using " + nfo_file + " as NFO, loading...")
nfo = RawConfigParser()
nfo = ConfigParser()
nfo.read(nfo_file)
return nfo
except Exception as e:
@ -173,8 +177,9 @@ def loadNFO(options):
def parseNFO(options):
nfo = loadNFO(options)
if nfo:
# We need to check all options and replace it with the nfo value if not defined (None or False)
for key, value in options.iteritems():
# We need to check all options and replace it with the nfo value if not
# defined (None or False)
for key, value in viewitems(options):
key = key.replace("-", "")
try:
# get string options
@ -183,10 +188,10 @@ def parseNFO(options):
# get boolean options
elif value is False and nfo.getboolean('video', key):
options['--' + key] = nfo.getboolean('video', key)
except NoOptionError:
except configparser.NoOptionError:
continue
except NoSectionError:
logging.error("Given NFO file miss section [video], please check syntax of your NFO.")
except configparser.NoSectionError:
logging.error("Given NFO file misses section [video], please check the syntax of your NFO.")
exit(1)
return options
@ -201,3 +206,19 @@ def cleanString(toclean):
cleaned = re.sub('[^A-Za-z0-9]+', '', toclean)
return cleaned
LecygneNoir commented 5 years ago
Review

This line does not work with python3

Unfortunately, in python3 they have changed the unicode type that does not longer exist, in favor of str : https://docs.python.org/dev/howto/pyporting.html#text-versus-binary-data:

# python 3.7.2
2019-02-23 10:49:04,384 Peertube: Error: name 'unicode' is not defined

However, we cannot use str direclty in python2 as this is not the same:

#python 2.7.15
2019-02-23 10:58:52,130 Peertube: Error: str() takes at most 1 argument (2 given)

I am looking for a way to have something working in both case, I'll let you know if I found something, does not hesitate to tell if you have any idea!

This line does not work with python3 Unfortunately, in python3 they have changed the `unicode` type that does not longer exist, in favor of `str` : [https://docs.python.org/dev/howto/pyporting.html#text-versus-binary-data](https://docs.python.org/dev/howto/pyporting.html#text-versus-binary-data): ``` # python 3.7.2 2019-02-23 10:49:04,384 Peertube: Error: name 'unicode' is not defined ``` However, we cannot use `str` direclty in python2 as this is not the same: ``` #python 2.7.15 2019-02-23 10:58:52,130 Peertube: Error: str() takes at most 1 argument (2 given) ``` I am looking for a way to have something working in both case, I'll let you know if I found something, does not hesitate to tell if you have any idea!
LecygneNoir commented 5 years ago
Review

To reproduce you does not need a working account on Peertube instance:

± python -m prismedia --file="path/file.mp4" --nfo nfo_example.txt
2019-02-23 10:48:53,012 Using nfo_example.txt as NFO, loading...
2019-02-23 10:49:04,142 Peertube: Uploading video...
2019-02-23 10:49:04,384 Peertube: Error: name 'unicode' is not defined
To reproduce you does not need a working account on Peertube instance: ``` ± python -m prismedia --file="path/file.mp4" --nfo nfo_example.txt 2019-02-23 10:48:53,012 Using nfo_example.txt as NFO, loading... 2019-02-23 10:49:04,142 Peertube: Uploading video... 2019-02-23 10:49:04,384 Peertube: Error: name 'unicode' is not defined ```
LecygneNoir commented 5 years ago
Review

The simple way should be to do something like that:

        try:
            strtoclean = unicodedata.normalize('NFKD', unicode(s, 'utf-8')).encode('ASCII', 'ignore')
        except:
            strtoclean = unicodedata.normalize('NFKD', str(s, 'utf-8')).encode('ASCII', 'ignore')

But as we did more treatment than juste returning the string, this broke other things in the code :-/

Error: decoding str is not supported

Apparently it's the strtoclean = unicodedata.normalize('NFKD', str(s, 'utf-8')).encode('ASCII', 'ignore') that does not work, even if we never call to decode directly, all these method should call it implicitely, I do not find a workaround at the moment.

The simple way should be to do something like that: ``` try: strtoclean = unicodedata.normalize('NFKD', unicode(s, 'utf-8')).encode('ASCII', 'ignore') except: strtoclean = unicodedata.normalize('NFKD', str(s, 'utf-8')).encode('ASCII', 'ignore') ``` But as we did more treatment than juste returning the string, this broke other things in the code :-/ ``` Error: decoding str is not supported ``` Apparently it's the `strtoclean = unicodedata.normalize('NFKD', str(s, 'utf-8')).encode('ASCII', 'ignore')` that does not work, even if we never call to `decode` directly, all these method should call it implicitely, I do not find a workaround at the moment.
LecygneNoir commented 5 years ago
Review

This point should now be easiest thanks to v0.6.1-1

I have rewritten this code to use less python2 specific code based on unicode/str differences between python2 and 3, with the help of the SIX library you use in other places in the code, we should be able make this part compatible python3!

This point should now be easiest thanks to v0.6.1-1 I have rewritten this code to use less python2 specific code based on unicode/str differences between python2 and 3, with the help of the `SIX` library you use in other places in the code, we should be able make this part compatible python3!
def decodeArgumentStrings(options, encoding):
# Python crash when decoding from UTF-8 to UTF-8, so we prevent this
if "utf-8" == encoding.lower():
return
if options["--name"] is not None:
options["--name"] = options["--name"].decode(encoding)
if options["--description"] is not None:
options["--description"] = options["--description"].decode(encoding)
if options["--tags"] is not None:
options["--tags"] = options["--tags"].decode(encoding)

lib/yt_upload.py → prismedia/yt_upload.py View File

@ -1,8 +1,7 @@
#!/usr/bin/env python2
# coding: utf-8
# From Youtube samples : https://raw.githubusercontent.com/youtube/api-samples/master/python/upload_video.py # noqa
import httplib
import http.client
import httplib2
import random
import time
@ -22,7 +21,7 @@ from googleapiclient.http import MediaFileUpload
from google_auth_oauthlib.flow import InstalledAppFlow
import utils
from . import utils
logging.basicConfig(format='%(asctime)s %(message)s', level=logging.INFO)
@ -38,13 +37,13 @@ MAX_RETRIES = 10
RETRIABLE_EXCEPTIONS = (
IOError,
httplib2.HttpLib2Error,
httplib.NotConnected,
httplib.IncompleteRead,
httplib.ImproperConnectionState,
httplib.CannotSendRequest,
httplib.CannotSendHeader,
httplib.ResponseNotReady,
httplib.BadStatusLine,
http.client.NotConnected,
http.client.IncompleteRead,
http.client.ImproperConnectionState,
http.client.CannotSendRequest,
http.client.CannotSendHeader,
http.client.ResponseNotReady,
http.client.BadStatusLine,
)
RETRIABLE_STATUS_CODES = [500, 502, 503, 504]
@ -254,7 +253,7 @@ def set_playlist(youtube, playlist_id, video_id):
logging.error("Youtube: Error: " + str(e.message))
else:
logging.error("Youtube: Error: " + str(e))
logging.info('Youtube: Video is correclty added to the playlist.')
logging.info('Youtube: Video is correctly added to the playlist.')
# This method implements an exponential backoff strategy to resume a

+ 39
- 0
pyproject.toml View File

@ -0,0 +1,39 @@
[tool.poetry]
name = "prismedia"
version = "0.6.2"
description = "scripting your way to upload videos on peertube and youtube"
authors = [
"LecygneNoir <git@lecygnenoir.info>",
"Rigel Kent <sendmemail@rigelk.eu>"
]
license = "AGPL-3.0-only"
readme = 'README.md'
repository = "https://git.lecygnenoir.info/LecygneNoir/prismedia"
homepage = "https://git.lecygnenoir.info/LecygneNoir/prismedia"
keywords = ['peertube', 'youtube']
[tool.poetry.dependencies]
python = "~2.7 || ^3.3"
google-auth-oauthlib = "^0.2.0"
requests-toolbelt = "^0.9.1"
docopt = "^0.6.2"
google-auth = "^1.6"
google-auth-httplib2 = "^0.0.3"
tzlocal = "^1.5"
python-magic = "^0.4.15"
schema = "^0.6.8"
google-api-python-client = "^1.7"
configparser = "^3.7"
future = "^0.17.1"
[tool.poetry.dev-dependencies]
[tool.poetry.scripts]
prismedia = 'prismedia.upload:main'
[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"

+ 98
- 0
requirements.txt View File

@ -0,0 +1,98 @@
cachetools==3.1.0 \
--hash=sha256:219b7dc6024195b6f2bc3d3f884d1fef458745cd323b04165378622dcc823852 \
--hash=sha256:9efcc9fab3b49ab833475702b55edd5ae07af1af7a4c627678980b45e459c460
certifi==2018.11.29 \
--hash=sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7 \
--hash=sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033
chardet==3.0.4 \
--hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \
--hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691
configparser==3.7.1 \
--hash=sha256:5bd5fa2a491dc3cfe920a3f2a107510d65eceae10e9c6e547b90261a4710df32 \
--hash=sha256:c114ff90ee2e762db972fa205f02491b1f5cf3ff950decd8542c62970c9bedac \
--hash=sha256:df28e045fbff307a28795b18df6ac8662be3219435560ddb068c283afab1ea7a
docopt==0.6.2 \
--hash=sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491
future==0.17.1 \
--hash=sha256:67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8
google-api-python-client==1.7.6 \
--hash=sha256:51dc9139aa06fd0b1339a22b6b1c9fe74cb891b3dd803e8c464c5a18a8de23dc \
--hash=sha256:bf98b066fb6e4e6da1f2f11d6cb0bb947de156aef8562a32b0692e7073d38593
google-auth==1.6.1 \
--hash=sha256:494e747bdc2cdeb0fa6ef85118de2ea1a563f160294cce05048c6ff563fda1bb \
--hash=sha256:b08a27888e9d1c17a891b3688aacc9c6f2019d7f6c5a2e73588e6bb9a2c0fa98
google-auth-httplib2==0.0.3 \
--hash=sha256:098fade613c25b4527b2c08fa42d11f3c2037dda8995d86de0745228e965d445 \
--hash=sha256:f1c437842155680cf9918df9bc51c1182fda41feef88c34004bd1978c8157e08
google-auth-oauthlib==0.2.0 \
--hash=sha256:226d1d0960f86ba5d9efd426a70b291eaba96f47d071657e0254ea969025728a \
--hash=sha256:81ba22acada4d13b1d83f9371ab19fd61f1250a542d21cf49e4dcf0637a7344a
httplib2==0.12.1 \
--hash=sha256:4ba6b8fd77d0038769bf3c33c9a96a6f752bc4cdf739701fdcaf210121f399d4
idna==2.6 \
--hash=sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f \
--hash=sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4
oauthlib==2.1.0 \
--hash=sha256:ac35665a61c1685c56336bda97d5eefa246f1202618a1d6f34fccb1bdd404162 \
--hash=sha256:d883b36b21a6ad813953803edfa563b1b579d79ca758fe950d1bc9e8b326025b
pyasn1==0.4.5 \
--hash=sha256:061442c60842f6d11051d4fdae9bc197b64bd41573a12234a753a0cb80b4f30b \
--hash=sha256:0ee2449bf4c4e535823acc25624c45a8b454f328d59d3f3eeb82d3567100b9bd \
--hash=sha256:5f9fb05c33e53b9a6ee3b1ed1d292043f83df465852bec876e93b47fd2df7eed \
--hash=sha256:65201d28e081f690a32401e6253cca4449ccacc8f3988e811fae66bd822910ee \
--hash=sha256:79b336b073a52fa3c3d8728e78fa56b7d03138ef59f44084de5f39650265b5ff \
--hash=sha256:8ec20f61483764de281e0b4aba7d12716189700debcfa9e7935780850bf527f3 \
--hash=sha256:9458d0273f95d035de4c0d5e0643f25daba330582cc71bb554fe6969c015042a \
--hash=sha256:98d97a1833a29ca61cd04a60414def8f02f406d732f9f0bcb49f769faff1b699 \
--hash=sha256:b00d7bfb6603517e189d1ad76967c7e805139f63e43096e5f871d1277f50aea5 \
--hash=sha256:b06c0cfd708b806ea025426aace45551f91ea7f557e0c2d4fbd9a4b346873ce0 \
--hash=sha256:d14d05984581770333731690f5453efd4b82e1e5d824a1d7976b868a2e5c38e8 \
--hash=sha256:da2420fe13a9452d8ae97a0e478adde1dee153b11ba832a95b223a2ba01c10f7 \
--hash=sha256:da6b43a8c9ae93bc80e2739efb38cc776ba74a886e3e9318d65fe81a8b8a2c6e
pyasn1-modules==0.2.4 \
--hash=sha256:136020f884635942239b33abdb63b1e0fdfb3c4bc8693f769ff1ab0908133a5b \
--hash=sha256:1c2ce0717e099620d7d425d2bb55e68f8126d77c8ba93112f0448a212048fe76 \
--hash=sha256:39da883a45dfc71314c48bba772be63a13946d0dd6abde326df163656a7b13e1 \
--hash=sha256:4160b0caedf8f1675ca7b94a65900d0219c715ac745cbc0c93557a9864b19748 \
--hash=sha256:50c5f454c29bc8a7b8bfffc0fd00fed1f9012160b4532807a33c27af91747337 \
--hash=sha256:52c46ecb2c1e7a03fe54dc8e11d6460ec7ebdcaedba3b0fe4ba2a811521df05f \
--hash=sha256:6db7a0510e55212b42a1f3e3553559eb214c8c8495e1018b4135d2bfb5a9169a \
--hash=sha256:79580acf813e3b7d6e69783884e6e83ac94bf4617b36a135b85c599d8a818a7b \
--hash=sha256:98e80b5ae1ed0d92694927a3e34df016c3b69b7bf439b32fc0a0dc516ec3653d \
--hash=sha256:9e879981cbf4c868a2267385a56837e0d384eab2d1690e6e0c8bba28d102509e \
--hash=sha256:a52090e8c5841ebbf08ae455146792d9ef3e8445b21055d3a3b7ed9c712b7c7c \
--hash=sha256:c00dad1d69d8592bbbc978f5beb3e992d3bf996e6b97eeec1c8608f81221d922 \
--hash=sha256:c226b5c17683d98498e157d6ac0098b93f9c475da5bc50072f64bf3f3f6b828f
python-magic==0.4.15 \
--hash=sha256:f2674dcfad52ae6c49d4803fa027809540b130db1dec928cfbb9240316831375 \
--hash=sha256:f3765c0f582d2dfc72c15f3b5a82aecfae9498bd29ca840d72f37d7bd38bfcd5
pytz==2018.9 \
--hash=sha256:32b0891edff07e28efe91284ed9c31e123d84bea3fd98e1f72be2508f43ef8d9 \
--hash=sha256:d5f05e487007e29e03409f9398d074e158d920d36eb82eaf66fb1136b0c5374c
requests==2.18.4 \
--hash=sha256:6a1b267aa90cac58ac3a765d067950e7dbbf75b1da07e895d1f594193a40a38b \
--hash=sha256:9c443e7324ba5b85070c4a818ade28bfabedf16ea10206da1132edaa6dda237e
requests-oauthlib==0.8.0 \
--hash=sha256:50a8ae2ce8273e384895972b56193c7409601a66d4975774c60c2aed869639ca \
--hash=sha256:883ac416757eada6d3d07054ec7092ac21c7f35cb1d2cf82faf205637081f468
requests-toolbelt==0.9.1 \
--hash=sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f \
--hash=sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0
rsa==4.0 \
--hash=sha256:14ba45700ff1ec9eeb206a2ce76b32814958a98e372006c8fb76ba820211be66 \
--hash=sha256:1a836406405730121ae9823e19c6e806c62bbad73f890574fff50efa4122c487
schema==0.6.8 \
--hash=sha256:d994b0dc4966000037b26898df638e3e2a694cc73636cb2050e652614a350687 \
--hash=sha256:fa1a53fe5f3b6929725a4e81688c250f46838e25d8c1885a10a590c8c01a7b74
six==1.12.0 \
--hash=sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c \
--hash=sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73
tzlocal==1.5.1 \
--hash=sha256:4ebeb848845ac898da6519b9b31879cf13b6626f7184c496037b818e238f2c4e
uritemplate==3.0.0 \
--hash=sha256:01c69f4fe8ed503b2951bef85d996a9d22434d2431584b5b107b2981ff416fbd \
--hash=sha256:1b9c467a940ce9fb9f50df819e8ddd14696f89b9a8cc87ac77952ba416e0a8fd \
--hash=sha256:c02643cebe23fc8adb5e6becffe201185bf06c40bda5c0b4028a93f1527d011d
urllib3==1.22 \
--hash=sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b \
--hash=sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f

Loading…
Cancel
Save