124 Commits

Author SHA1 Message Date
  LecygneNoir 36110432da Merge branch 'hotfix/v0.12.1' 2 months ago
  LecygneNoir 388f76b855 Fix a bug in when configuring log level 2 months ago
  LecygneNoir bcb0e267f3 Merge branch 'release/v0.12.0' 3 months ago
  LecygneNoir 8bc79853c8 Bump version to v0.12.0 and prepare poetry build for v0.12.0 3 months ago
  LecygneNoir 45a1cbccff Merge branch 'feature/remove_formatcheck' into develop 3 months ago
  LecygneNoir 0a1360d8e2 Prepare changelo for v0.12.0 3 months ago
  LecygneNoir f8ae2b1c5e Update libraries and dependencies for prismedia 3 months ago
  LecygneNoir 0a53e77bd6 Add auto search for thumbnail based on .png in addition to .jpg and .jpeg 3 months ago
  LecygneNoir 2f7629ef1e Remove format checks for videos and thumbnail as Youtube and Peertube have no limitation anymore 3 months ago
  LecygneNoir e0a63ed4b2 Revert README to use prismedia as it's indeed the binary script installed through pip 4 months ago
  LecygneNoir ba2a1ebb79 Merge branch 'feature/improve_genconfig' into develop 4 months ago
  LecygneNoir cf3d4c32c3 Better resilience for the genconfig function thanks to happy path and @Zykino suggestion! 4 months ago
  LecygneNoir 85f0fe9b6f Add ask_overwirte function to utils in order to be more general and usable in other modules 4 months ago
  LecygneNoir 1a006f3b6c Improve genconfig system and documentation for easier use, cf #55 4 months ago
  LecygneNoir cdef038323 Move logger initialization in __init__.py to be able to use it from any module 4 months ago
  LecygneNoir cbf3386bac Remove confusing warning when using genconfig, see #55 5 months ago
  LecygneNoir ca733e0dc3 Merge pull request 'hearthbeat (keepaliveΒ ?)' (#54) from Zykino/prismedia:hearthbeat into develop 5 months ago
  Zykino a4f162320d Fix some spacing formatting and documentation 5 months ago
  Zykino 29b1747c3e Visit all of Youtube’s playlists 5 months ago
  Zykino ea39fe9854 Fix some spacing 5 months ago
  Zykino a725e848ab Add an option to use some credits easiely 5 months ago
  LecygneNoir 9b6da1e3dc Merge tag 'v0.11.0' into develop 6 months ago
  LecygneNoir 194e2e4606 Merge branch 'release/v0.11.0' 6 months ago
  LecygneNoir 339caeb7f7 Bump files to v0.11.0, fix #50 6 months ago
  LecygneNoir e6375b5aa0 Merge pull request 'Add a progression bar for Peertube's upload' (#52) from Zykino/prismedia:feature/progression into develop 6 months ago
  Zykino 6add140732 Disable the progressbar when the user want a quiet output 6 months ago
  Zykino c4e3243131 Add clint as requirement 6 months ago
  Zykino 93f1205ab8 Make it possible to choose between multiples tipes of progress bar 6 months ago
  Zykino 09c2d84357 Add a progression bar to Peertube upload 7 months ago
  LecygneNoir 230ac545c4 Patch incorrect loading of NFO keys, breaking the match when the key contains -, and add example for auto-originalDate in NFO 7 months ago
  LecygneNoir 42ee7d761b Need to strip the getmtime timestamp as some OSes return timestamp with microsecond (XXXX.YYYY) 7 months ago
  LecygneNoir 1a937098d8 Merge branch 'feature/recording_date' into develop 7 months ago
  LecygneNoir 736582b495 Change the originalDate behaviour to not default, and add option to auto manage the original date if needed 7 months ago
  LecygneNoir 4a9fda5e77 Add one function to deal with date to avoid duplicate code 7 months ago
  LecygneNoir 8dc3a86aab Stripe the README from the full --help output to focus on some main features 7 months ago
  LecygneNoir 4b7c01a707 Add help for option originalDate and changelog about the feature 7 months ago
  LecygneNoir dc98f2e155 Add functions to manage Original Date of record for peertube 7 months ago
  LecygneNoir 60bf26418d Add the originalDate options to Youtube videos 7 months ago
  LecygneNoir 447310a17e Add options and bunch of functions to maange the originalDate fields in prismedia, cf #50 7 months ago
  LecygneNoir 9b597f461e Merge tag 'v0.10.3' into develop 8 months ago
  LecygneNoir 8b1470ab31 Merge branch 'hotfix/v0.10.3' 8 months ago
  LecygneNoir 6d15ad18ca Fix peertube pagination index for playlsit, as index begins at 0, not 1, shame on me! 😳 8 months ago
  LecygneNoir 5c991581e8 bump to v0.10.2 for poetry 8 months ago
  LecygneNoir 4956a19d0e bump to v0.10.2 for poetry 8 months ago
  LecygneNoir 2f8543b43c Merge tag 'v0.10.2' into develop 8 months ago
  LecygneNoir 5607c8ea06 Merge branch 'release/v0.10.2' 8 months ago
  LecygneNoir 25435453bd Bump to version v0.10.2 and fix typo in ticket number inside Changelog for v0.10.2 8 months ago
  LecygneNoir dab44244f3 Revert the workaround for Youtube playlist bug now the bug is fixed by Youtube (see #47) 8 months ago
  LecygneNoir 26476347d3 Renable Peertube upload after tests πŸ˜“ 8 months ago
  LecygneNoir 5160e9e68d Add a check to avoid uploading on Peertube with more than 5 tags (see #48) 8 months ago
  LecygneNoir e61a70460d Merge branch 'feature/41_pt_playlistrecreated' into develop 8 months ago
  LecygneNoir 0aae4da68f Add changelog for #41 and #47 8 months ago
  LecygneNoir cbb7c745de Add pagination to find Peertube existing playlists as default pagination shows 14 playlists max. See #41 8 months ago
  LecygneNoir c2db597388 Fix message about thumbnail where it missed a space, close #49 8 months ago
  LecygneNoir e33a4c91a0 Merge tag 'v0.10.1' into develop 10 months ago
  LecygneNoir bb451e108d Merge branch 'hotfix/v0.10.1' 10 months ago
  LecygneNoir 28a7541fa8 Bump version to v0.10.1 10 months ago
  LecygneNoir 320c3b1a0b Fix bug introduced by v0.10.0 breaking thumbnail on youtube 10 months ago
  LecygneNoir fbfe3356ec Merge tag 'v0.10.0' into develop 10 months ago
  LecygneNoir e7a4d1656a Merge branch 'release/v0.10.0' 10 months ago
  LecygneNoir 1e9b719e0c Bump version to 0.10.0 and edit Changelog en Readme for the 0.10.0 10 months ago
  LecygneNoir ee578e8e82 Add a workaround Youtube API bug regarding playlist to ignore incorrect 404 return, see #47 for more details 10 months ago
  LecygneNoir f8ca4b093a Merge branch 'feature/logs_improvement' into develop 10 months ago
  LecygneNoir 379aef1dd8 Following discussion in #29, rename print-url option and add a batch options 10 months ago
  LecygneNoir 1c441bf67a Add new options --quiet and --print-url for an easier scripting use of prismedia, cf #29 1 year ago
  LecygneNoir dc7dd5cb46 Full rewrite of logging system to introduce --log as an option, and prepare the work for #29 1 year ago
  LecygneNoir 25682af83a Merge branch 'feature/strict_options' into develop 1 year ago
  LecygneNoir 542bb6f1f9 Correction of some typos 1 year ago
  LecygneNoir 5022aface4 Prepare changelog and documentation for strict check option 1 year ago
  LecygneNoir 003830696f Add strict options to force and check the existence of specific option before uploading, following issue #36. Need upgrade of Schema to have Hooks. 1 year ago
  LecygneNoir 2e4e876169 Merge tag 'v0.9.1' into develop 1 year ago
  LecygneNoir e52e7f354d Merge branch 'release/v0.9.1' 1 year ago
  LecygneNoir 701e61413c Update prismedia, documentation and dependencies to v0.9.1 1 year ago
  LecygneNoir 4e20d9efc4 Merge branch 'bypass-mime-verification' of Zykino/prismedia into develop 1 year ago
  Zykino 802d70b8d5 Empower user to force using the file they selected 1 year ago
  LecygneNoir a1c472a5fa Merge tag 'v0.9.0' into develop 1 year ago
  LecygneNoir 57a4f3dfd0 Merge branch 'release/v0.9.0' 1 year ago
  LecygneNoir 4e5c2e1245 Update Changelog for the v0.9.0 1 year ago
  LecygneNoir 1b55340b34 Update poetry.lock to match new python-magic requirements 1 year ago
  LecygneNoir 64c5378e18 Merge branch 'feature/nfo-enhanced' into develop 1 year ago
  LecygneNoir 1169301f14 After test python-magic is required on Windows in addition to python-magic-bin, so remove the 'Linux only' marker 1 year ago
  LecygneNoir 03ae92d1af Load nfo.txt or NFO.txt regardless the letter case 1 year ago
  LecygneNoir 11a91af534 Add better description for Enhanced NFO based on @Zykino review 1 year ago
  LecygneNoir 881a01f862 Write documentation about the new NFO usage and possibilities 1 year ago
  LecygneNoir ef5d6b843a Add a full set of samples to understand better who NFO works now 1 year ago
  LecygneNoir 17017ae90c Modify the NFO functions to allow an enhanced use with priorities in options 1 year ago
  LecygneNoir e91ada951f gitignore .mp4 to avoid commiting test files 1 year ago
  LecygneNoir af65627fcf Fix some typo regarding poetry 1 year ago
  LecygneNoir cb39eef8e0 Merge branch 'feature/poetry' into develop 1 year ago
  LecygneNoir 8faae852ea Add documentation for poetry and adjust Changelog 1 year ago
  LecygneNoir 72b47b95ec Modify files and add poetry configuration files to use poetry for distributing and publishing 1 year ago
  LecygneNoir b1a5d244d4 Prepare poetry with more standard nomination and configuration and change gitignore to avoid committing file by error 1 year ago
  LecygneNoir 429ea2333e Add new feature to schedule different publication date depending of the platform. Target v0.9.0, Fix #43 1 year ago
  LecygneNoir 159ab00cc9 Merge tag '0.8.0' into develop 1 year ago
  LecygneNoir 1dd41f0c46 Merge branch 'release/0.8.0' 1 year ago
  LecygneNoir 04e5c326ee Update version to 0.8.0 1 year ago
  LecygneNoir 99eee2363b Test if using python3 at the beginning of the script, and change shebang to do not force python3, as it enforce default python3 value and thus the check failed even if python2 is used 1 year ago
  LecygneNoir 76e379ab97 Merge branch 'feature/python3' into develop 1 year ago
  LecygneNoir 1e72033846 Update changelog, help and readme to finish the python3 feature 1 year ago
  LecygneNoir 2a624e1d9b Uncomment upload lines commented by error 1 year ago
  LecygneNoir 2b00d65546 Merge branch 'feature/python3-windows' of Zykino/prismedia into feature/python3 1 year ago
  Zykino 77bcda7a79 Add a debug option. Upload to Peertube before Youtube. 1 year ago
  Zykino 591ed0ab80 Fix some the README: sync the dependency list with requirements.txt 1 year ago
  Zykino e94b48278a Force the NFO parsing to read as UTF-8 1 year ago
  Zykino 8c99747898 Remove the CLI input decoding 1 year ago
  Zykino aa7aeed688 Force the use of at least Python 3 1 year ago
  Zykino 4f2a69e025 Add the binaries for the lib needed on Windows 1 year ago
  LecygneNoir aafa71ce6d Update Changelog about python3 1 year ago
  LecygneNoir ee92ff3a6f Add a requirements.txt for an easier installation of dependencies 1 year ago
  LecygneNoir fa633ee5bb Update files, functions and code to work with python3 1 year ago
  LecygneNoir 8b26f0ee53 Merge tag 'v0.7.1' into develop 1 year ago
  LecygneNoir 7b0c543865 Merge branch 'hotfix/v0.7.1' 1 year ago
  LecygneNoir e5c8c4c9b9 Release hotfix v0.7.1 1 year ago
  LecygneNoir 5dc6c78211 Fix bug #42 , crash on Peertube when there is only one tag in video 1 year ago
  LecygneNoir 83a1d30c1c Merge tag 'v0.7.0' into develop 1 year ago
  LecygneNoir 1fc0577ce7 Merge branch 'release/v0.7.0' 1 year ago
  LecygneNoir ee2e11b788 Release prismedia v0.7.0, close #40 1 year ago
  LecygneNoir 9c72a563bd Merge branch 'feature/pt-channel' into develop 1 year ago
  LecygneNoir aa81f13973 Update nfo and readme for the new channel feature for peertube 1 year ago
  LecygneNoir 44875b3567 Add new feature to combine channel and playlist on peertube πŸŽ‰ 1 year ago
  LecygneNoir 322774a214 Rework README's feature list for more lisibility 1 year ago
  LecygneNoir b21317ec7e Merge branch 'hotfix/v0.6.4' into develop 1 year ago
  LecygneNoir 4ea1daf966 Merge branch 'hotfix/v0.6.4' 1 year ago
  LecygneNoir 6f67de0a3d Merge branch 'hotfix/0.6.3' into develop 2 years ago
24 changed files with 2343 additions and 898 deletions
Split View
  1. +5
    -1
      .gitignore
  2. +119
    -0
      CHANGELOG.md
  3. +181
    -113
      README.md
  4. +0
    -257
      lib/pt_upload.py
  5. +0
    -213
      lib/utils.py
  6. +0
    -25
      nfo_example.txt
  7. +567
    -0
      poetry.lock
  8. +12
    -0
      prismedia/__init__.py
  9. +2
    -0
      prismedia/__main__.py
  10. +0
    -0
      prismedia/config/peertube_secret.sample
  11. +0
    -0
      prismedia/config/youtube_secret.json.sample
  12. +24
    -0
      prismedia/genconfig.py
  13. +398
    -0
      prismedia/pt_upload.py
  14. +12
    -0
      prismedia/samples/cli_nfo.txt
  15. +26
    -0
      prismedia/samples/full_nfo_examples.txt
  16. +10
    -0
      prismedia/samples/nfo.txt
  17. +14
    -0
      prismedia/samples/samples.txt
  18. +14
    -0
      prismedia/samples/yourvideo.txt
  19. +422
    -0
      prismedia/upload.py
  20. +240
    -0
      prismedia/utils.py
  21. +110
    -59
      prismedia/yt_upload.py
  22. +0
    -230
      prismedia_upload.py
  23. +48
    -0
      pyproject.toml
  24. +139
    -0
      requirements.txt

+ 5
- 1
.gitignore View File

@ -61,4 +61,8 @@ target/
# Project
youtube_secret.json
peertube_secret
.youtube_credentials.json
.youtube_credentials.json
nfo_example.txt
peertube_secret.sample
youtube_secret.json.sample
*.mp4

+ 119
- 0
CHANGELOG.md View File

@ -1,5 +1,124 @@
# Changelog
## v0.12.1
### Fix
- Fix an error when setting log level in configuration
## v0.12.0
### Features
- Add `--heartbeat` option to send request to youtube API, avoiding youtube to disabling you API account if you do not upload video often (Thanks @Zykino see #54)
- Rework and improve genconfig process to avoid erasing existing configuration and make it more easy to use
- Add a `prismedia-init` script when installing prismedia to easily generate basic configuration (see #55)
- Update multiple dependencies used for prismedia as they were very old.
- Add auto search for thumbnail in `.png` in addition to `.jpg` and `.jepg`.
### Fixes
- Add pagination for youtube playlist to search for all user playlists (Thanks @Zykino)
- Remove file format check for both videos and thumbnail as Youtube and Peertube now accepts more than .mp4 and .jpg (see #60)
## v0.11.0
### Features
- Add the configuration of Original date of Record for Youtube and Peertube (see #50)
- Add a progress bar when uploading on Peertube (Thanks @Zykino, see #52)
## v0.10.3
### Fix
- Fix the pagination for Peertube playlist, as index begins at 0, not 1
## v0.10.2
### Fixes
- Fix a typo in log (missing space when displaying thumbnail) (see #49)
- Add pagination when searching playlist in Peertube as default pagination show only 14 playlists (see #41)
- Add a check to avoid uploading video on Peertube with more than 5 tags (see #48)
- Revert the workaround for Youtube playlist bug now the bug is fixed by Youtube (see #47)
## v0.10.1
### Fix
- Fix a bug introduced with v0.10.0 that broke thumbnail on youtube upload.
## v0.10.0
### Features
- Add the possibility to specify strict checks option to never forgot parameters when uploading (see #36)
- Improve logging system, add options for batch upload and print url-only in the stdout (see #29)
- --debug option is now deprecated in favor of --log=debug
### Fixes
- Workaround against the Youtube API breakdown while adding video in playlist. See #47 for details. Should be removed once Google fix their bugs.
## v0.9.1
### Features
- Possibility to bypass the MIME check for .mp4 when the user is sure of its video (#46 , thanks to @zykino)
- Now **available with pip** for installation! (See the README for doc)
## v0.9.0
### Upgrade from v0.8.0
Now using [poetry](https://python-poetry.org/) for packaging and installing! It's easier to maintain and publish package, but means changes when using prismedia from command line.
**Using poetry** (recommanded)
- [install poetry](https://python-poetry.org/docs/#installation)
- git pull the repo
- install prismedia:
```bash
poetry install
```
- use prismedia from the command line directly from your path:
```bash
prismedia -h
```
**From source**
Prismedia is now seen as a python module, so you need to use `python -m prismedia` instead of `./prismedia_upload.py`.
Once you have pulled the new v0.9.0, you may update by using:
```
pip install -r requirements.txt
# Then use prismedia through python command line:
python -m prismedia -h
```
### Features
- Prismedia now uses [poetry](https://python-poetry.org) to allow easier installation usage and build, see the README (fix #34)
- Add two new options to schedule video by platform. You may now use youtubeAt and peertubeAt to prepare previews (fix #43)
- Enhance the NFO system to allow a hierarchical loading of multiple NFO, with priorities. See README and [prismedia/samples](prismedia/samples) for details (fix #11)
## v0.8.0
### Breaking changes
Now work with python 3! Support of python 2 is no longer available.
You should now use python 3 in order to use prismedia
### Features
- Add a requirements.txt file to make installing requirement easier.
- Add a debug option to show some infos before uploading (thanks to @zykino)
- Now uploading to Peertube before Youtube (thanks to @zykino)
## v0.7.1
### Fixes
Fix bug #42 , crash on Peertube when video has only one tag
## v0.7.0
### Features
Support Peertube channel additionally with playlist for Peertube!
Peertube only as channel are Peertube's feature. See #40 for details.
### Fixes
- Best uses of special chars in videoname, channel name and playlist name
- Some fixes in logging message for better lisibility
- Readme features list improved for better lisibility
## v0.6.4
### Fixes

+ 181
- 113
README.md View File

@ -1,168 +1,236 @@
# Prismedia
A scripting way to upload videos to peertube and youtube written in python2
## Dependencies
Search in your package manager, otherwise use ``pip install --upgrade``
- google-auth
- google-auth-oauthlib
- google-auth-httplib2
- google-api-python-client
- docopt
- schema
- python-magic
- python-magic-bin
- requests-toolbelt
- tzlocal
- unidecode
Scripting your way to upload videos to peertube and youtube. Works with Python 3.5+.
[TOC]: #
## Table of Contents
- [Installation](#installation-and-upgrade)
- [From pip](#from-pip)
- [From source](#from-source)
- [Configuration](#configuration)
- [Peertube](#peertube)
- [Youtube](#youtube)
- [Usage](#usage)
- [Enhanced use of NFO](#enhanced-use-of-nfo)
- [Strict check options](#strict-check-options)
- [Features](#features)
- [Compatibility](#compatibility)
- [Inspirations](#inspirations)
- [Contributors](#contributors)
## Installation and upgrade
### From pip
Simply install with
```sh
pip install prismedia
```
Upgrade with
```sh
pip install --upgrade prismedia
```
### From source
Get the source:
```sh
git clone https://git.lecygnenoir.info/LecygneNoir/prismedia.git prismedia
```
You may use pip to install requirements: `pip install -r requirements.txt` if you want to use the script directly.
(**note:** requirements are generated via `poetry export -f requirements.txt`)
Otherwise, you can use [poetry](https://python-poetry.org), which create a virtualenv for the project directly
(Or use the existing virtualenv if one is activated)
```sh
poetry install
```
## Configuration
Edit peertube_secret and youtube_secret.json with your credentials.
Generate configuration files by running `prismedia-init`.
Then, edit them to fill your credential as explained below.
### 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)
Configuration is in **peertube_secret** file.
You need your usual credentials and Peertube instance URL, in addition with API client_id and client_secret.
You can get client_id and client_secret by logging in your peertube instance and reaching the URL:
https://domain.example/api/v1/oauth-clients/local
*Alternatively, you can set ``OAUTHLIB_INSECURE_TRANSPORT`` to 1 if you do not use https (not recommended)*
### Youtube
Configuration is in **youtube_secret.json** file.
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
The first time you connect, prismedia will open your browser to ask 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``.
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**:
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:
- Go to the [Google console](https://console.developers.google.com/).
- Create project.
- Side menu: APIs & auth -> APIs
- Top menu: Enabled API(s): Enable all Youtube APIs.
- Side menu: APIs & auth -> Credentials.
- Create a Client ID: Add credentials -> OAuth 2.0 Client ID -> Other -> Name: prismedia1 -> Create -> OK
- Download JSON: Under the section "OAuth 2.0 client IDs". Save the file to your local system.
- 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
Simply upload a video:
```
./prismedia_upload.py --file="yourvideo.mp4"
If you plan a larger usage, please consider creating your own youtube_secret file:
- Go to the [Google console](https://console.developers.google.com/).
- Create project.
- Side menu: APIs & auth -> APIs
- Top menu: Enabled API(s): Enable all Youtube APIs.
- Side menu: APIs & auth -> Credentials.
- Create a Client ID: Add credentials -> OAuth 2.0 Client ID -> Other -> Name: prismedia1 -> Create -> OK
- Download JSON: Under the section "OAuth 2.0 client IDs". Save the file to your local system.
- Save this JSON as your youtube_secret.json file.
## Usage
Support only mp4 for cross compatibility between Youtube and Peertube.
**Note that all options may be specified in a NFO file!** (see [Enhanced NFO](#enhanced-use-of-nfo))
Here are some demonstration of main usage:
Upload a video:
```sh
prismedia --file="yourvideo.mp4"
```
Specify description and tags:
```
./prismedia_upload.py --file="yourvideo.mp4" -d "My supa description" -t "tag1,tag2,foo"
```sh
prismedia --file="yourvideo.mp4" -d "My supa description" -t "tag1,tag2,foo"
```
Provide a thumbnail:
```sh
prismedia --file="yourvideo.mp4" -d "Video with thumbnail" --thumbnail="/path/to/your/thumbnail.jpg"
```
./prismedia_upload.py --file="yourvideo.mp4" -d "Video with thumbnail" --thumbnail="/path/to/your/thumbnail.jpg"
Publish on Peertube only, while using a channel and a playlist, creating them if they do not exist:
```sh
prismedia --file="yourvideo.mp4" --platform=peertube --channel="Cooking recipes" --playlist="Cake recipes" --channelCreate --playlistCreate
```
Use a NFO file to specify your video options:
(See [Enhanced NFO](#enhanced-use-of-nfo) for more precise example)
```sh
prismedia --file="yourvideo.mp4" --nfo /path/to/your/nfo.txt
```
Use a NFO file to specify your video options:
Use some credits to show some activity for you apikey so the platform know it is used and would not put your quota to 0 (only Youtube currently).
To prevent Youtube from inactivating your apikey after 90days of inactivity it is recommended to launch this command automatically from a script around once a month. It will mwke a call to use a few credits from your daily quota.
On Linux and MacOS, you can use cron, on Windows the "Task Scheduler".
```sh
prismedia --hearthbeat
```
./prismedia_upload.py --file="yourvideo.mp4" --nfo /path/to/your/nfo.txt
```
Use --help to get all available options:
Take a look at all available options with `--help`!
```sh
prismedia --help
```
## Enhanced use of NFO
Since Prismedia v0.9.0, the NFO system has been improved to allow hierarchical loading.
First, **if you already used nfo**, either with `--nfo` or by using `videoname.txt`, nothing changes :-)
But you are now able to use a more flexible NFO system, by using priorities. This allows you to set some defaults to avoid recreating a full nfo for each video
Basically, Prismedia will now load options in this order, using the last value found in case of conflict:
`nfo.txt < directory_name.txt < video_name.txt < command line NFO < command line argument`
You'll find a complete set of samples in the [prismedia/samples](prismedia/samples) directory so let's take it as an example:
```sh
$ tree Recipes/
Recipes/
β”œβ”€β”€ cli_nfo.txt
β”œβ”€β”€ nfo.txt
β”œβ”€β”€ samples.txt
β”œβ”€β”€ yourvideo1.mp4
β”œβ”€β”€ yourvideo1.txt
β”œβ”€β”€ yourvideo1.jpg
β”œβ”€β”€ yourvideo2.mp4
└── yourvideo2.txt
```
Options:
-f, --file=STRING Path to the video file to upload in mp4
--name=NAME Name of the video to upload. (default to video filename)
-d, --description=STRING Description of the video. (default: default description)
-t, --tags=STRING Tags for the video. comma separated.
WARN: tags with space and special characters (!, ', ", ?, ...)
are not supported by Mastodon to be published from Peertube
use mastodon compatibility below
--mt Force Mastodon compatibility for tags (drop every incompatible characters inside tags)
This option requires --tags
-c, --category=STRING Category for the videos, see below. (default: Films)
--cca License should be CreativeCommon Attribution (affects Youtube upload only)
-p, --privacy=STRING Choose between public, unlisted or private. (default: private)
--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 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)
--language=STRING Specify the default language for video. See below for supported language. (default is English)
--publishAt=DATE Publish the video at the given DATE using local server timezone.
DATE should be on the form YYYY-MM-DDThh:mm:ss eg: 2018-03-12T19:00:00
DATE should be in the future
For Peertube, requires the "atd" and "curl utilities installed on the system
--thumbnail=STRING Path to a file to use as a thumbnail for the video.
Supported types are jpg and jpeg.
By default, prismedia search for an image based on video name followed by .jpg or .jpeg
--playlist=STRING Set the playlist to use for the video. Also known as Channel for Peertube.
If the playlist is not found, spawn an error except if --playlist-create is set.
--playlistCreate Create the playlist if not exists. (default do not create)
Only relevant if --playlist is set.
-h --help Show this help.
--version Show version.
Categories:
Category is the type of video you upload. Default is films.
Here are available categories from Peertube and Youtube:
music, films, vehicles,
sports, travels, gaming, people,
comedy, entertainment, news,
how to, education, activism, science & technology,
science, technology, animals
Languages:
Language of the video (audio track), choose one. Default is English
Here are available languages from Peertube and Youtube:
Arabic, English, French, German, Hindi, Italian,
Japanese, Korean, Mandarin, Portuguese, Punjabi, Russian, Spanish
By using
```sh
prismedia --file=/path/to/Recipes/yourvideo1.mp4 --nfo=/path/to/Recipes/cli_nfo.txt --cca
```
Prismedia will:
- look for options in `nfo.txt`
- look for options in `samples.txt` (from directory name) and erase any previous conflicting options
- look for options in `yourvideo1.txt` (from video name) and erase any previous conflicting options
- look for options in `cli_nfo.txt` (from the `--nfo` in command line) and erase any previous conflicting options
- erase any previous option regarding CCA as it's specified in cli with `--cca`
- take `yourvideo1.jpg` as thumbnail if no other files has been specified in previous NFO
In other word, Prismedia will use option given in cli, then look for option in cli_nfo.txt, then complete with video_name.txt, then directory_name.txt, and finally complete with nfo.txt
It allows to specify more easily default options for an entire set of video, directory, playlist and so on.
## Strict check options
Since prismedia v0.10.0, a bunch of special options have been added to force the presence of parameters before uploading.
Strict options allow you to force some option to be present when uploading a video. It's useful to be sure you do not
forget something when uploading a video, for example if you use multiples NFO. You may force the presence of description,
tags, thumbnail, ...
All strict option are optionals and are provided only to avoid errors when uploading :-)
All strict options can be specified in NFO directly, the only strict option mandatory on cli is --withNFO
All strict options are off by default.
Available strict options:
- --withNFO Prevent the upload without a NFO, either specified via cli or found in the directory
- --withThumbnail Prevent the upload without a thumbnail
- --withName Prevent the upload if no name are found
- --withDescription Prevent the upload without description
- --withTags Prevent the upload without tags
- --withPlaylist Prevent the upload if no playlist
- --withPublishAt Prevent the upload if no schedule
- --withPlatform Prevent the upload if at least one platform is not specified
- --withCategory Prevent the upload if no category
- --withLanguage Prevent upload if no language
- --withChannel Prevent upload if no channel
## Features
- [x] Youtube upload
- [x] Peertube upload
- Support of all videos arguments (description, tags, category, licence, ...)
- Support of videos parameters (description, tags, category, licence, ...)
- [x] description
- [x] tags (no more than 30 characters per tag as Peertube does not support it)
- [x] Option to force tags to be compatible with Mastodon publication
- [x] categories
- [x] license: cca or not (Youtube only as Peertube uses Attribution by design)
- [x] privacy (between public, unlisted or private)
- [x] enabling/disabling comment (Peertube only as Youtube API does not support it)
- [x] nsfw (Peertube only as Youtube API does not support it)
- [x] set default language
- [x] thumbnail/preview
- [x] thumbnail
- [x] multiple lines description (see [issue 4](https://git.lecygnenoir.info/LecygneNoir/prismedia/issues/4))
- [x] add videos to playlist
- [x] create playlist
- [x] schedule your video with publishAt
- [x] combine channel and playlist (Peertube only as channel is Peertube feature). See [issue 40](https://git.lecygnenoir.info/LecygneNoir/prismedia/issues/40) for detailed usage.
- [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
- [ ] Record and forget: put the video in a directory, and the script uploads it for you
- [ ] Usable on Desktop (Linux and/or Windows and/or MacOS)
- [ ] Graphical User Interface
- [x] Allow choosing peertube or youtube upload (to retry a failed upload for example)
- [x] Usable on Desktop (Linux and/or Windows and/or MacOS)
- [x] Different schedules on platforms to prepare preview
- [x] Possibility to force the presence of upload options
- [ ] Copy and forget, eg possibility to copy video in a directory, and prismedia uploads itself: [Work in progress](https://git.lecygnenoir.info/Zykino/prismedia-autoupload) thanks to @Zykino πŸŽ‰ (Discussions in [issue 27](https://git.lecygnenoir.info/LecygneNoir/prismedia/issues/27))
- [ ] A usable graphical interface
## Compatibility
If your server uses peertube before 1.0.0-beta4, use the version inside tag 1.0.0-beta3!
- If you still use python2, use the version 0.7.1 (no more updated)
- If you use peertube before 1.0.0-beta4, use the version inside tag 1.0.0-beta3
## Inspirations
Inspired by [peeror](https://git.rigelk.eu/rigelk/peeror) and [youtube-upload](https://github.com/tokland/youtube-upload)
## Sources
inspired by [peeror](https://git.rigelk.eu/rigelk/peeror) and [youtube-upload](https://github.com/tokland/youtube-upload)
## Contributors
Thanks to: @LecygneNoir, @Zykino, @meewan, @rigelk 😘

+ 0
- 257
lib/pt_upload.py View File

@ -1,257 +0,0 @@
#!/usr/bin/env python2
# coding: utf-8
import os
import mimetypes
import json
import logging
import datetime
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
PEERTUBE_SECRETS_FILE = 'peertube_secret'
PEERTUBE_PRIVACY = {
"public": 1,
"unlisted": 2,
"private": 3
}
def get_authenticated_service(secret):
peertube_url = str(secret.get('peertube', 'peertube_url')).rstrip("/")
oauth_client = LegacyApplicationClient(
client_id=str(secret.get('peertube', 'client_id'))
)
try:
oauth = OAuth2Session(client=oauth_client)
oauth.fetch_token(
token_url=str(peertube_url + '/api/v1/users/token'),
# lower as peertube does not store uppercase for pseudo
username=str(secret.get('peertube', 'username').lower()),
password=str(secret.get('peertube', 'password')),
client_id=str(secret.get('peertube', 'client_id')),
client_secret=str(secret.get('peertube', 'client_secret'))
)
except Exception as e:
if hasattr(e, 'message'):
logging.error("Peertube: Error: " + str(e.message))
exit(1)
else:
logging.error("Peertube: Error: " + str(e))
exit(1)
return oauth
def get_default_channel(user_info):
return user_info['videoChannels'][0]['id']
def get_default_playlist(user_info):
return user_info['videoChannels'][0]['id']
def get_playlist_by_name(user_playlists, options):
for playlist in user_playlists["data"]:
if playlist['displayName'].encode('utf8') == str(options.get('--playlist')):
return playlist['id']
def create_playlist(oauth, url, options, default_channel):
template = ('Peertube: Playlist %s does not exist, creating it.')
logging.info(template % (str(options.get('--playlist'))))
# We use files for form-data Content
# see https://requests.readthedocs.io/en/latest/user/quickstart/#post-a-multipart-encoded-file
# None is used to mute "filename" field
files = {'displayName': (None, str(options.get('--playlist'))),
'privacy': (None, "1"),
'description': (None, "null"),
'videoChannelId': (None, str(default_channel)),
'thumbnailfile': (None, "null")}
try:
response = oauth.post(url + "/api/v1/video-playlists/",
files=files)
except Exception as e:
if hasattr(e, 'message'):
logging.error("Error: " + str(e.message))
else:
logging.error("Error: " + str(e))
if response is not None:
if response.status_code == 200:
jresponse = response.json()
jresponse = jresponse['videoPlaylist']
return jresponse['id']
else:
logging.error(('Peertube: Creating the playlist failed with an unexpected response: '
'%s') % response)
exit(1)
def set_playlist(oauth, url, video_id, playlist_id):
logging.info('Peertube: add video to playlist.')
data = '{"videoId":"' + str(video_id) + '"}'
headers = {
'Content-Type': "application/json"
}
try:
response = oauth.post(url + "/api/v1/video-playlists/"+str(playlist_id)+"/videos",
data=data,
headers=headers)
except Exception as e:
if hasattr(e, 'message'):
logging.error("Error: " + str(e.message))
else:
logging.error("Error: " + str(e))
if response is not None:
if response.status_code == 200:
logging.info('Peertube: Video is successfully added to the playlist.')
else:
logging.error(('Peertube: Configuring the playlist failed with an unexpected response: '
'%s') % response)
exit(1)
def upload_video(oauth, secret, options):
def get_userinfo():
return json.loads(oauth.get(url+"/api/v1/users/me").content)
def get_file(path):
mimetypes.init()
return (basename(path), open(abspath(path), 'rb'),
mimetypes.types_map[splitext(path)[1]])
def get_playlist(username):
return json.loads(oauth.get(url+"/api/v1/accounts/"+username+"/video-playlists").content)
path = options.get('--file')
url = str(secret.get('peertube', 'peertube_url')).rstrip('/')
user_info = get_userinfo()
user_playlists = get_playlist(str(secret.get('peertube', 'username').lower()))
# We need to transform fields into tuple to deal with tags as
# MultipartEncoder does not support list refer
# https://github.com/requests/toolbelt/issues/190 and
# https://github.com/requests/toolbelt/issues/205
fields = [
("name", options.get('--name') or splitext(basename(options.get('--file')))[0]),
("licence", "1"),
("description", options.get('--description') or "default description"),
("nsfw", str(int(options.get('--nsfw')) or "0")),
("videofile", get_file(path))
]
if options.get('--tags'):
tags = options.get('--tags').split(',')
for strtag in tags:
# Empty tag crashes Peertube, so skip them
if strtag == "":
continue
# Tag more than 30 chars crashes Peertube, so exit and check tags
if len(strtag) >= 30:
logging.warning("Peertube: Sorry, Peertube does not support tag with more than 30 characters, please reduce your tag size")
exit(1)
fields.append(("tags", strtag))
if options.get('--category'):
fields.append(("category", str(utils.getCategory(options.get('--category'), 'peertube'))))
else:
# if no category, set default to 2 (Films)
fields.append(("category", "2"))
if options.get('--language'):
fields.append(("language", str(utils.getLanguage(options.get('--language'), "peertube"))))
else:
# if no language, set default to 1 (English)
fields.append(("language", "en"))
if options.get('--disable-comments'):
fields.append(("commentsEnabled", "0"))
else:
fields.append(("commentsEnabled", "1"))
privacy = None
if options.get('--privacy'):
privacy = options.get('--privacy').lower()
if options.get('--publishAt'):
publishAt = options.get('--publishAt')
publishAt = datetime.datetime.strptime(publishAt, '%Y-%m-%dT%H:%M:%S')
tz = get_localzone()
tz = pytz.timezone(str(tz))
publishAt = tz.localize(publishAt).isoformat()
fields.append(("scheduleUpdate[updateAt]", publishAt))
fields.append(("scheduleUpdate[privacy]", str(PEERTUBE_PRIVACY["public"])))
fields.append(("privacy", str(PEERTUBE_PRIVACY["private"])))
else:
fields.append(("privacy", str(PEERTUBE_PRIVACY[privacy or "private"])))
if options.get('--thumbnail'):
fields.append(("thumbnailfile", get_file(options.get('--thumbnail'))))
fields.append(("previewfile", get_file(options.get('--thumbnail'))))
default_channel = get_default_channel(user_info)
fields.append(("channelId", str(default_channel)))
if options.get('--playlist'):
playlist_id = get_playlist_by_name(user_playlists, options)
if not playlist_id and options.get('--playlistCreate'):
playlist_id = create_playlist(oauth, url, options, default_channel)
elif not playlist_id:
logging.warning("Playlist `" + options.get('--playlist') + "` does not exist, please set --playlistCreate"
" if you want to create it")
exit(1)
multipart_data = MultipartEncoder(fields)
headers = {
'Content-Type': multipart_data.content_type
}
response = oauth.post(url + "/api/v1/videos/upload",
data=multipart_data,
headers=headers)
if response is not None:
if response.status_code == 200:
jresponse = response.json()
jresponse = jresponse['video']
uuid = jresponse['uuid']
video_id = str(jresponse['id'])
logging.info('Peertube : Video was successfully uploaded.')
template = 'Peertube: Watch it at %s/videos/watch/%s.'
logging.info(template % (url, uuid))
# Upload is successful we may set playlist
if options.get('--playlist'):
set_playlist(oauth, url, video_id, playlist_id)
else:
logging.error(('Peertube: The upload failed with an unexpected response: '
'%s') % response)
exit(1)
def run(options):
secret = RawConfigParser()
try:
secret.read(PEERTUBE_SECRETS_FILE)
except Exception as e:
logging.error("Peertube: Error loading " + str(PEERTUBE_SECRETS_FILE) + ": " + str(e))
exit(1)
insecure_transport = secret.get('peertube', 'OAUTHLIB_INSECURE_TRANSPORT')
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = insecure_transport
oauth = get_authenticated_service(secret)
try:
logging.info('Peertube: Uploading video...')
upload_video(oauth, secret, options)
except Exception as e:
if hasattr(e, 'message'):
logging.error("Peertube: Error: " + str(e.message))
else:
logging.error("Peertube: Error: " + str(e))

+ 0
- 213
lib/utils.py View File

@ -1,213 +0,0 @@
#!/usr/bin/python
# coding: utf-8
from ConfigParser import RawConfigParser, NoOptionError, NoSectionError
from os.path import dirname, splitext, basename, isfile
import re
from os import devnull
from subprocess import check_call, CalledProcessError, STDOUT
import unidecode
import logging
### CATEGORIES ###
YOUTUBE_CATEGORY = {
"music": 10,
"films": 1,
"vehicles": 2,
"sport": 17,
"travels": 19,
"gaming": 20,
"people": 22,
"comedy": 23,
"entertainment": 24,
"news": 25,
"how to": 26,
"education": 27,
"activism": 29,
"science & technology": 28,
"science": 28,
"technology": 28,
"animals": 15
}
PEERTUBE_CATEGORY = {
"music": 1,
"films": 2,
"vehicles": 3,
"sport": 5,
"travels": 6,
"gaming": 7,
"people": 8,
"comedy": 9,
"entertainment": 10,
"news": 11,
"how to": 12,
"education": 13,
"activism": 14,
"science & technology": 15,
"science": 15,
"technology": 15,
"animals": 16
}
### LANGUAGES ###
YOUTUBE_LANGUAGE = {
"arabic": 'ar',
"english": 'en',
"french": 'fr',
"german": 'de',
"hindi": 'hi',
"italian": 'it',
"japanese": 'ja',
"korean": 'ko',
"mandarin": 'zh-CN',
"portuguese": 'pt-PT',
"punjabi": 'pa',
"russian": 'ru',
"spanish": 'es'
}
PEERTUBE_LANGUAGE = {
"arabic": "ar",
"english": "en",
"french": "fr",
"german": "de",
"hindi": "hi",
"italian": "it",
"japanese": "ja",
"korean": "ko",
"mandarin": "zh",
"portuguese": "pt",
"punjabi": "pa",
"russian": "ru",
"spanish": "es"
}
######################
def getCategory(category, platform):
if platform == "youtube":
return YOUTUBE_CATEGORY[category.lower()]
else:
return PEERTUBE_CATEGORY[category.lower()]
def getLanguage(language, platform):
if platform == "youtube":
return YOUTUBE_LANGUAGE[language.lower()]
else:
return PEERTUBE_LANGUAGE[language.lower()]
def remove_empty_kwargs(**kwargs):
good_kwargs = {}
if kwargs is not None:
for key, value in kwargs.iteritems():
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
if options.get('--name'):
if isfile(video_directory + options.get('--name') + ".jpg"):
options['--thumbnail'] = video_directory + options.get('--name') + ".jpg"
elif isfile(video_directory + options.get('--name') + ".jpeg"):
options['--thumbnail'] = video_directory + options.get('--name') + ".jpeg"
# Then, if we still not have thumbnail, check for thumbnail based on videofile name
if not options.get('--thumbnail'):
video_file = splitext(basename(options.get('--file')))[0]
if isfile(video_directory + video_file + ".jpg"):
options['--thumbnail'] = video_directory + video_file + ".jpg"
elif isfile(video_directory + video_file + ".jpeg"):
options['--thumbnail'] = video_directory + video_file + ".jpeg"
return options
# return the nfo as a RawConfigParser 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.read(options.get('--nfo'))
return nfo
else:
logging.error("Given NFO file does not exist, please check your path.")
exit(1)
except Exception as e:
logging.error("Problem with NFO file: " + str(e))
exit(1)
else:
if options.get('--name'):
nfo_file = video_directory + options.get('--name') + ".txt"
if isfile(nfo_file):
try:
logging.info("Using " + nfo_file + " as NFO, loading...")
nfo = RawConfigParser()
nfo.read(nfo_file)
return nfo
except Exception as e:
logging.error("Problem with NFO file: " + str(e))
exit(1)
# if --nfo and --name does not exist, use --file as default
video_file = splitext(basename(options.get('--file')))[0]
nfo_file = video_directory + video_file + ".txt"
if isfile(nfo_file):
try:
logging.info("Using " + nfo_file + " as NFO, loading...")
nfo = RawConfigParser()
nfo.read(nfo_file)
return nfo
except Exception as e:
logging.error("Problem with nfo file: " + str(e))
exit(1)
logging.info("No suitable NFO found, skipping.")
return False
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():
key = key.replace("-", "")
try:
# get string options
if value is None and nfo.get('video', key):
options['--' + key] = nfo.get('video', key)
# get boolean options
elif value is False and nfo.getboolean('video', key):
options['--' + key] = nfo.getboolean('video', key)
except NoOptionError:
continue
except NoSectionError:
logging.error("Given NFO file miss section [video], please check syntax of your NFO.")
exit(1)
return options
def upcaseFirstLetter(s):
return s[0].upper() + s[1:]
def cleanString(toclean):
toclean = toclean.decode('utf-8')
toclean = unidecode.unidecode(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)

+ 0
- 25
nfo_example.txt View File

@ -1,25 +0,0 @@
### This NFO example show how to construct a NFO for your video ###
### All fields are optional, but you need at least one fields (otherwise NFO is useless :-p) ###
### See --help for options explanation
### Prismedia will search and use NFO in this order: ###
### 1. file passed in command line through --nfo ###
### 2. file inside video directory named after --name command line option append with .txt ###
### 3. file inside video directory named after --file command line option with .txt extension ###
[video]
name = videoname
description = Your complete video description
Multilines description
should be wrote with a blank space
at the beginning of the line :)
tags = list of tags, comma separated
category = Films
cca = True
privacy = private
disable-comments = True
thumbnail = /path/to/your/thumbnail.jpg # Set the absolute path to your thumbnail
playlist = My Test Playlist
playlistCreate = True
nsfw = True
platform = youtube, peertube
language = French
publishAt=2034-05-07T19:00:00

+ 567
- 0
poetry.lock View File

@ -0,0 +1,567 @@
[[package]]
category = "main"
description = "Command Arguments for Humans."
name = "args"
optional = false
python-versions = "*"
version = "0.1.0"
[[package]]
category = "main"
description = "Extensible memoizing collections and decorators"
name = "cachetools"
optional = false
python-versions = "*"
version = "3.1.1"
[[package]]
category = "main"
description = "Python package for providing Mozilla's CA Bundle."
name = "certifi"
optional = false
python-versions = "*"
version = "2020.12.5"
[[package]]
category = "main"
description = "Universal encoding detector for Python 2 and 3"
name = "chardet"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "4.0.0"
[[package]]
category = "main"
description = "Python Command Line Interface Tools"
name = "clint"
optional = false
python-versions = "*"
version = "0.5.1"
[package.dependencies]
args = "*"
[[package]]
category = "main"
description = "Updated configparser from Python 3.8 for Python 2.6+."
name = "configparser"
optional = false
python-versions = ">=3.6"
version = "5.0.2"
[package.extras]
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "pytest-black (>=0.3.7)", "pytest-mypy"]
[[package]]
category = "main"
description = "Backports and enhancements for the contextlib module"
name = "contextlib2"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "0.6.0.post1"
[[package]]
category = "main"
description = "Pythonic argument parser, that will make you smile"
name = "docopt"
optional = false
python-versions = "*"
version = "0.6.2"
[[package]]
category = "main"
description = "Clean single-source support for Python 3 and 2"
name = "future"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
version = "0.18.2"
[[package]]
category = "main"
description = "Google API client core library"
name = "google-api-core"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*"
version = "1.28.0"
[package.dependencies]
google-auth = ">=1.25.0,<2.0dev"
googleapis-common-protos = ">=1.6.0,<2.0dev"
packaging = ">=14.3"
protobuf = ">=3.12.0"
pytz = "*"
requests = ">=2.18.0,<3.0.0dev"
setuptools = ">=40.3.0"
six = ">=1.13.0"
[package.extras]
grpc = ["grpcio (>=1.29.0,<2.0dev)"]
grpcgcp = ["grpcio-gcp (>=0.2.2)"]
grpcio-gcp = ["grpcio-gcp (>=0.2.2)"]
[[package]]
category = "main"
description = "Google API Client Library for Python"
name = "google-api-python-client"
optional = false
python-versions = ">=3.6"
version = "2.5.0"
[package.dependencies]
google-api-core = ">=1.21.0,<2dev"
google-auth = ">=1.16.0,<2dev"
google-auth-httplib2 = ">=0.1.0"
httplib2 = ">=0.15.0,<1dev"
six = ">=1.13.0,<2dev"
uritemplate = ">=3.0.0,<4dev"
[[package]]
category = "main"
description = "Google Authentication Library"
name = "google-auth"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*"
version = "1.30.0"
[package.dependencies]
cachetools = ">=2.0.0,<5.0"
pyasn1-modules = ">=0.2.1"
setuptools = ">=40.3.0"
six = ">=1.9.0"
[package.dependencies.rsa]
python = ">=3.6"
version = ">=3.1.4,<5"
[package.extras]
aiohttp = ["aiohttp (>=3.6.2,<4.0.0dev)"]
pyopenssl = ["pyopenssl (>=20.0.0)"]
reauth = ["pyu2f (>=0.1.5)"]
[[package]]
category = "main"
description = "Google Authentication Library: httplib2 transport"
name = "google-auth-httplib2"
optional = false
python-versions = "*"
version = "0.1.0"
[package.dependencies]
google-auth = "*"
httplib2 = ">=0.15.0"
six = "*"
[[package]]
category = "main"
description = "Google Authentication Library"
name = "google-auth-oauthlib"
optional = false
python-versions = ">=3.6"
version = "0.4.4"
[package.dependencies]
google-auth = ">=1.0.0"
requests-oauthlib = ">=0.7.0"
[package.extras]
tool = ["click (>=6.0.0)"]
[[package]]
category = "main"
description = "Common protobufs used in Google APIs"
name = "googleapis-common-protos"
optional = false
python-versions = ">=3.6"
version = "1.53.0"
[package.dependencies]
protobuf = ">=3.12.0"
[package.extras]
grpc = ["grpcio (>=1.0.0)"]
[[package]]
category = "main"
description = "A comprehensive HTTP client library."
name = "httplib2"
optional = false
python-versions = "*"
version = "0.19.1"
[package.dependencies]
pyparsing = ">=2.4.2,<3"
[[package]]
category = "main"
description = "Internationalized Domain Names in Applications (IDNA)"
name = "idna"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "2.10"
[[package]]
category = "main"
description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic"
name = "oauthlib"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "3.1.0"
[package.extras]
rsa = ["cryptography"]
signals = ["blinker"]
signedtoken = ["cryptography", "pyjwt (>=1.0.0)"]
[[package]]
category = "main"
description = "Core utilities for Python packages"
name = "packaging"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "20.9"
[package.dependencies]
pyparsing = ">=2.0.2"
[[package]]
category = "main"
description = "Protocol Buffers"
name = "protobuf"
optional = false
python-versions = "*"
version = "3.17.0"
[package.dependencies]
six = ">=1.9"
[[package]]
category = "main"
description = "ASN.1 types and codecs"
name = "pyasn1"
optional = false
python-versions = "*"
version = "0.4.8"
[[package]]
category = "main"
description = "A collection of ASN.1-based protocols modules."
name = "pyasn1-modules"
optional = false
python-versions = "*"
version = "0.2.8"
[package.dependencies]
pyasn1 = ">=0.4.6,<0.5.0"
[[package]]
category = "main"
description = "Python parsing module"
name = "pyparsing"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
version = "2.4.7"
[[package]]
category = "main"
description = "World timezone definitions, modern and historical"
name = "pytz"
optional = false
python-versions = "*"
version = "2021.1"
[[package]]
category = "main"
description = "Python HTTP for Humans."
name = "requests"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "2.25.1"
[package.dependencies]
certifi = ">=2017.4.17"
chardet = ">=3.0.2,<5"
idna = ">=2.5,<3"
urllib3 = ">=1.21.1,<1.27"
[package.extras]
security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"]
[[package]]
category = "main"
description = "OAuthlib authentication support for Requests."
name = "requests-oauthlib"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.3.0"
[package.dependencies]
oauthlib = ">=3.0.0"
requests = ">=2.0.0"
[package.extras]
rsa = ["oauthlib (>=3.0.0)"]
[[package]]
category = "main"
description = "A utility belt for advanced users of python-requests"
name = "requests-toolbelt"
optional = false
python-versions = "*"
version = "0.9.1"
[package.dependencies]
requests = ">=2.0.1,<3.0.0"
[[package]]
category = "main"
description = "Pure-Python RSA implementation"
marker = "python_version >= \"3.6\""
name = "rsa"
optional = false
python-versions = "*"
version = "4.4"
[package.dependencies]
pyasn1 = ">=0.1.3"
[[package]]
category = "main"
description = "Simple data validation library"
name = "schema"
optional = false
python-versions = "*"
version = "0.7.4"
[package.dependencies]
contextlib2 = ">=0.5.5"
[[package]]
category = "main"
description = "Python 2 and 3 compatibility utilities"
name = "six"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
version = "1.16.0"
[[package]]
category = "main"
description = "tzinfo object for the local timezone"
name = "tzlocal"
optional = false
python-versions = "*"
version = "2.1"
[package.dependencies]
pytz = "*"
[[package]]
category = "main"
description = "ASCII transliterations of Unicode text"
name = "unidecode"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.2.0"
[[package]]
category = "main"
description = "URI templates"
name = "uritemplate"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "3.0.1"
[[package]]
category = "main"
description = "HTTP library with thread-safe connection pooling, file post, and more."
name = "urllib3"
optional = false
python-versions = "*"
version = "1.22"
[package.extras]
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"]
[metadata]
content-hash = "30e57c25d84e4981a4a70f5d30f4b8e2a6163f56b44a1ca170372bfbf91ea527"
python-versions = ">=3.6"
[metadata.files]
args = [
{file = "args-0.1.0.tar.gz", hash = "sha256:a785b8d837625e9b61c39108532d95b85274acd679693b71ebb5156848fcf814"},
]
cachetools = [
{file = "cachetools-3.1.1-py2.py3-none-any.whl", hash = "sha256:428266a1c0d36dc5aca63a2d7c5942e88c2c898d72139fca0e97fdd2380517ae"},
{file = "cachetools-3.1.1.tar.gz", hash = "sha256:8ea2d3ce97850f31e4a08b0e2b5e6c34997d7216a9d2c98e0f3978630d4da69a"},
]
certifi = [
{file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"},
{file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"},
]
chardet = [
{file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"},
{file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"},
]
clint = [
{file = "clint-0.5.1.tar.gz", hash = "sha256:05224c32b1075563d0b16d0015faaf9da43aa214e4a2140e51f08789e7a4c5aa"},
]
configparser = [
{file = "configparser-5.0.2-py3-none-any.whl", hash = "sha256:af59f2cdd7efbdd5d111c1976ecd0b82db9066653362f0962d7bf1d3ab89a1fa"},
{file = "configparser-5.0.2.tar.gz", hash = "sha256:85d5de102cfe6d14a5172676f09d19c465ce63d6019cf0a4ef13385fc535e828"},
]
contextlib2 = [
{file = "contextlib2-0.6.0.post1-py2.py3-none-any.whl", hash = "sha256:3355078a159fbb44ee60ea80abd0d87b80b78c248643b49aa6d94673b413609b"},
{file = "contextlib2-0.6.0.post1.tar.gz", hash = "sha256:01f490098c18b19d2bd5bb5dc445b2054d2fa97f09a4280ba2c5f3c394c8162e"},
]
docopt = [
{file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"},
]
future = [
{file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"},
]
google-api-core = [
{file = "google-api-core-1.28.0.tar.gz", hash = "sha256:02646803bd728e12dd1f45ee1dcc31c12614c9a1ac451b9a1ce26aa65df9b957"},
{file = "google_api_core-1.28.0-py2.py3-none-any.whl", hash = "sha256:a658a4e367511a444c7daf2c58855ff6fd7f7d138c154e5b17186c1f8154c8cb"},
]
google-api-python-client = [
{file = "google-api-python-client-2.5.0.tar.gz", hash = "sha256:656003c47cdcd1552238ed1265e95a2d14eea5e40a2a44e622880587664b3dd9"},
{file = "google_api_python_client-2.5.0-py2.py3-none-any.whl", hash = "sha256:1b8baad33d467dc6c50eed9305bffa3b592919aa895b0c2e8f5654ff0c27f0a1"},
]
google-auth = [
{file = "google-auth-1.30.0.tar.gz", hash = "sha256:9ad25fba07f46a628ad4d0ca09f38dcb262830df2ac95b217f9b0129c9e42206"},
{file = "google_auth-1.30.0-py2.py3-none-any.whl", hash = "sha256:588bdb03a41ecb4978472b847881e5518b5d9ec6153d3d679aa127a55e13b39f"},
]
google-auth-httplib2 = [
{file = "google-auth-httplib2-0.1.0.tar.gz", hash = "sha256:a07c39fd632becacd3f07718dfd6021bf396978f03ad3ce4321d060015cc30ac"},
{file = "google_auth_httplib2-0.1.0-py2.py3-none-any.whl", hash = "sha256:31e49c36c6b5643b57e82617cb3e021e3e1d2df9da63af67252c02fa9c1f4a10"},
]
google-auth-oauthlib = [
{file = "google-auth-oauthlib-0.4.4.tar.gz", hash = "sha256:09832c6e75032f93818edf1affe4746121d640c625a5bef9b5c96af676e98eee"},
{file = "google_auth_oauthlib-0.4.4-py2.py3-none-any.whl", hash = "sha256:0e92aacacfb94978de3b7972cf4b0f204c3cd206f74ddd0dc0b31e91164e6317"},
]
googleapis-common-protos = [
{file = "googleapis-common-protos-1.53.0.tar.gz", hash = "sha256:a88ee8903aa0a81f6c3cec2d5cf62d3c8aa67c06439b0496b49048fb1854ebf4"},
{file = "googleapis_common_protos-1.53.0-py2.py3-none-any.whl", hash = "sha256:f6d561ab8fb16b30020b940e2dd01cd80082f4762fa9f3ee670f4419b4b8dbd0"},
]
httplib2 = [
{file = "httplib2-0.19.1-py3-none-any.whl", hash = "sha256:2ad195faf9faf079723f6714926e9a9061f694d07724b846658ce08d40f522b4"},
{file = "httplib2-0.19.1.tar.gz", hash = "sha256:0b12617eeca7433d4c396a100eaecfa4b08ee99aa881e6df6e257a7aad5d533d"},
]
idna = [
{file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
{file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
]
oauthlib = [
{file = "oauthlib-3.1.0-py2.py3-none-any.whl", hash = "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"},
{file = "oauthlib-3.1.0.tar.gz", hash = "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889"},
]
packaging = [
{file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"},
{file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"},
]
protobuf = [
{file = "protobuf-3.17.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:15351df904347da2081a2eebc42b192c29724eb57dbe56dae440be843f1e4779"},
{file = "protobuf-3.17.0-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:5356981c1919782b8c2e3ea5c5d85ad5937b8178a025ac9edc2f2ca5b4a717ae"},
{file = "protobuf-3.17.0-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:eac0a2a7ea99e17175f6e7b53cdc9004ed786c072fbdf933def0e454e14fd323"},
{file = "protobuf-3.17.0-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4c8d0997fdc0a4cf9de7950d598ce6974b22e8618bbcf1d15e9842010cf8420a"},
{file = "protobuf-3.17.0-cp35-cp35m-win32.whl", hash = "sha256:9ae321459d4890c3939c536382f75e232c9e91ce506310353c8a15ad5c379e0d"},
{file = "protobuf-3.17.0-cp35-cp35m-win_amd64.whl", hash = "sha256:295944ef0772498d7bf75f6aa5d4dfcfd02f5ce70f735b406e52e43ac3914d38"},
{file = "protobuf-3.17.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:850f429bd2399525d339d05bc809f090f16d3d88737bed637d355a5ee8d3b81a"},
{file = "protobuf-3.17.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:809a96d5a1a74538728710f9104f43ae77f5e48bde274ee321b10a324ba52e4f"},
{file = "protobuf-3.17.0-cp36-cp36m-win32.whl", hash = "sha256:8a3ac375539055164f31a330770f137875307e6f04c21e2647f2e7139c501295"},
{file = "protobuf-3.17.0-cp36-cp36m-win_amd64.whl", hash = "sha256:3d338910b10b88b18581cf6877b3938b2e262e8fdc2c1057f5a291787de63183"},
{file = "protobuf-3.17.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1488f786bd1912f97796cf5def8cacf433735616896cf7ed9dc786cee693dfc8"},
{file = "protobuf-3.17.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:bcaff977db178f0bfde10bab0d23a5f5adf5964adba70c315e45922a1c55eb90"},
{file = "protobuf-3.17.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:939ce06846ddfec99c0bff510510b3ee45778e7a3aec6544d1f36526e5fecb67"},
{file = "protobuf-3.17.0-cp37-cp37m-win32.whl", hash = "sha256:3237acce5b666c7b0f45785cc2d0809796d4df3593bd68338aebf25408139188"},
{file = "protobuf-3.17.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2f77afe33bb86c7d34221a86193256d69aa10818620fe4a7513d98211d67d672"},
{file = "protobuf-3.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:acc9f2091ace3de429eee424ab7ba0bc52a6aa9ffc9909e5c4de259a3f71db46"},
{file = "protobuf-3.17.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a29631f4f8bcf79b12a59e83d238d888de5034871461d788c74c68218ad75049"},
{file = "protobuf-3.17.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:05c304396e309661c45e3a97bd2d8da1fc2bab743ed2ca880bcb757271c40c0e"},
{file = "protobuf-3.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:baea44967071e6a51e705e4e88aebf35f530a14004cc69f60a185e5d7e13de7e"},
{file = "protobuf-3.17.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:3b5c461af5a3cebd796c73370db929b7e24cbaba655eefdc044226bc8a843d6b"},
{file = "protobuf-3.17.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:44399393c3a8cc04a4cfbdc721dd7f2114497efda582e946a91b8c4290ae5ff5"},
{file = "protobuf-3.17.0-py2.py3-none-any.whl", hash = "sha256:e32ef0c9f4b548c80d94dfff8b4130ca2ff3d50caaf2455889e3f5b8a01e8038"},
{file = "protobuf-3.17.0.tar.gz", hash = "sha256:05dfe9319939a8473c21b469f34f6486646e54fb8542637cf7ed8e2fbfe21538"},
]
pyasn1 = [
{file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"},
{file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"},
{file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"},
{file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"},
{file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"},
{file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"},
{file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"},
{file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"},
{file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"},
{file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"},
{file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"},
{file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"},
{file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"},
]
pyasn1-modules = [
{file = "pyasn1-modules-0.2.8.tar.gz", hash = "sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e"},
{file = "pyasn1_modules-0.2.8-py2.4.egg", hash = "sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199"},
{file = "pyasn1_modules-0.2.8-py2.5.egg", hash = "sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405"},
{file = "pyasn1_modules-0.2.8-py2.6.egg", hash = "sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb"},
{file = "pyasn1_modules-0.2.8-py2.7.egg", hash = "sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8"},
{file = "pyasn1_modules-0.2.8-py2.py3-none-any.whl", hash = "sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"},
{file = "pyasn1_modules-0.2.8-py3.1.egg", hash = "sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d"},
{file = "pyasn1_modules-0.2.8-py3.2.egg", hash = "sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45"},
{file = "pyasn1_modules-0.2.8-py3.3.egg", hash = "sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4"},
{file = "pyasn1_modules-0.2.8-py3.4.egg", hash = "sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811"},
{file = "pyasn1_modules-0.2.8-py3.5.egg", hash = "sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed"},
{file = "pyasn1_modules-0.2.8-py3.6.egg", hash = "sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0"},
{file = "pyasn1_modules-0.2.8-py3.7.egg", hash = "sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd"},
]
pyparsing = [
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
]
pytz = [
{file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"},
{file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"},
]
requests = [
{file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"},
{file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"},
]
requests-oauthlib = [
{file = "requests-oauthlib-1.3.0.tar.gz", hash = "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"},
{file = "requests_oauthlib-1.3.0-py2.py3-none-any.whl", hash = "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d"},
{file = "requests_oauthlib-1.3.0-py3.7.egg", hash = "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"},
]
requests-toolbelt = [
{file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"},
{file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"},
]
rsa = [
{file = "rsa-4.4-py2.py3-none-any.whl", hash = "sha256:4afbaaecc3e9550c7351fdf0ab3fea1857ff616b85bab59215f00fb42e0e9582"},
{file = "rsa-4.4.tar.gz", hash = "sha256:5d95293bbd0fbee1dd9cb4b72d27b723942eb50584abc8c4f5f00e4bcfa55307"},</