Compare commits

...

13 Commits

14 changed files with 580 additions and 70 deletions
Split View
  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
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
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