Compare commits

..

No commits in common. "main" and "v0.3.7" have entirely different histories.
main ... v0.3.7

29 changed files with 1171 additions and 1246 deletions

1
.gitignore vendored
View File

@ -21,4 +21,3 @@ scss/bs5/*.css*
scss/bs4/*.css* scss/bs4/*.css*
mail/*.html mail/*.html
jellyfin_accounts/data/*.html jellyfin_accounts/data/*.html
jellyfin_accounts/data/*.txt

View File

@ -1,27 +0,0 @@
FROM python:3.8.2-buster AS build
COPY . /opt/build
RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash -
RUN cd /opt/build \
&& rm -rf dist \
&& apt install nodejs \
&& ~/.poetry/bin/poetry update \
&& pip install libsass \
&& python scss/get_node_deps.py \
&& python scss/compile.py -y \
&& python mail/generate.py -y \
&& ~/.poetry/bin/poetry build -f wheel
FROM python:3.8.2-buster
COPY --from=build /opt/build/dist /opt/dist
RUN pip install /opt/dist/*.whl
RUN sed -i 's#id="pwrJfPath" placeholder="Folder"#id="pwrJfPath" value="/jf" disabled#g' /usr/local/lib/python3.8/site-packages/jellyfin_accounts/data/templates/setup.html
CMD [ "python3.8", "/usr/local/bin/jf-accounts", "-d", "/data" ]

View File

@ -1,6 +1,90 @@
## 👀 ➡️: Try [jfa-go](https://github.com/hrfee/jfa-go), a rewrite in Go. It's faster and has more features. # ![jellyfin-accounts](https://raw.githubusercontent.com/hrfee/jellyfin-accounts/bs5/images/jellyfin-accounts-banner-wide.svg)
###### I won't be updating this version any more, and switching should be easy.
**please don't open any new issues.** A basic account management system for [Jellyfin](https://github.com/jellyfin/jellyfin).
* Provides a web interface for creating/sending invites
* Sends out emails when a user requests a password reset
* Uses a basic python jellyfin API client for communication with the server.
* Uses [Flask](https://github.com/pallets/flask), [HTTPAuth](https://github.com/miguelgrinberg/Flask-HTTPAuth), [itsdangerous](https://github.com/pallets/itsdangerous), and [Waitress](https://github.com/Pylons/waitress)
* Frontend uses [Bootstrap](https://v5.getbootstrap.com)
* Password resets are handled using smtplib, requests, and [jinja](https://github.com/pallets/jinja)
## Interface
<p align="center">
<img src="https://raw.githubusercontent.com/hrfee/jellyfin-accounts/main/images/jfa.gif" width="100%"></img>
</p>
You can find the old README [here](https://github.com/hrfee/jellyfin-accounts/blob/main/README.old.md). <p align="center">
<img src="https://raw.githubusercontent.com/hrfee/jellyfin-accounts/main/images/admin.png" width="48%" style="margin-right: 1.5%;" alt="Admin page"></img>
<img src="https://raw.githubusercontent.com/hrfee/jellyfin-accounts/main/images/create.png" width="48%" style="margin-left: 1.5%;" alt="Account creation page"></img>
</p>
## Get it
### Requirements
* This should work anywhere Python does, i've tried to not use anything OS-specific. Drop an issue if there's a problem, of course.
```
* python >= 3.6
* flask
* flask_httpauth
* jinja2
* requests
* itsdangerous
* passlib
* pyOpenSSL
* waitress
* pytz
* python-dateutil
* watchdog
* packaging
```
### Install
Usually as simple as:
```
pip install jellyfin-accounts
```
If not, or if you want to use docker, see [install](https://github.com/hrfee/jellyfin-accounts/wiki/Install).
## Usage
* Passing no arguments will run the server
```
usage: jf-accounts [-h] [-c CONFIG] [-d DATA] [--host HOST] [-p PORT] [-g]
jellyfin-accounts
optional arguments:
-h, --help show this help message and exit
-c CONFIG, --config CONFIG
specifies path to configuration file.
-d DATA, --data DATA specifies directory to store data in. defaults to
~/.jf-accounts.
--host HOST address to host web ui on.
-p PORT, --port PORT port to host web ui on.
-g, --get_defaults tool to grab a JF users policy (access, perms, etc.)
and homescreen layout and output it as json to be used
as a user template.
```
## Setup
#### New user template
* You may want to restrict a user from accessing certain libraries (e.g 4K Movies), display their account on the login screen by default, or set a default homecrseen layout. Jellyfin stores these settings in the user's policy, configuration and displayPreferences.
* Make a temporary account and configure it, then in the web UI, go into "Settings => Set new account defaults". Choose the account, and its configuration will be stored for future use.
#### Emails/Password Resets
* When someone initiates forget password on Jellyfin, a file named `passwordreset*.json` is created in its configuration directory. This directory is monitored and when created, the program reads the username, expiry time and PIN, puts it into a template and sends it to whatever address is specified in `emails.json`.
* **The default forget password popup references the `passwordreset*.json` file created. This is confusing for users, so a quick fix is to edit the `MessageForgotPasswordFileCreated` string in Jellyfin's language folder.**
* Currently, jellyfin-accounts supports generic SSL/TLS or STARTTLS secured SMTP, and the [mailgun](https://mailgun.com) REST API.
* Email html is created using [mjml](https://mjml.io), and [jinja](https://github.com/pallets/jinja) templating is used. If you wish to create your own, ensure you use the same jinja expressions (`{{ pin }}`, etc.) as used in `data/email.mjml` or `invite-email.mjml`, and also create plain text versions for legacy email clients.
### Configuration
* Note: Make sure to put this behind a reverse proxy with HTTPS.
On first run, access the setup wizard at `0.0.0.0:8056`. When finished, restart the program.
The configuration is stored at `~/.jf-accounts/config.ini`. Settings can be changed through the web UI, or by manually editing the file.
For detailed descriptions of each setting, see [setup](https://github.com/hrfee/jellyfin-accounts/wiki/Setup).
### Donations
I strongly suggest you send your money to [Jellyfin](https://opencollective.com/jellyfin) or a good charity, but for those who want to help me out, a Paypal link is below.
[Donate](https://www.paypal.me/hrfee)

View File

@ -1,91 +0,0 @@
# ![jellyfin-accounts](https://raw.githubusercontent.com/hrfee/jellyfin-accounts/bs5/images/jellyfin-accounts-banner-wide.svg)
A basic account management system for [Jellyfin](https://github.com/jellyfin/jellyfin).
* Provides a web interface for creating/sending invites
* Sends out emails when a user requests a password reset
* Uses a basic python jellyfin API client for communication with the server.
* Uses [Flask](https://github.com/pallets/flask), [HTTPAuth](https://github.com/miguelgrinberg/Flask-HTTPAuth), [itsdangerous](https://github.com/pallets/itsdangerous), and [Waitress](https://github.com/Pylons/waitress)
* Frontend uses [Bootstrap](https://v5.getbootstrap.com)
* Password resets are handled using smtplib, requests, and [jinja](https://github.com/pallets/jinja)
## Interface
<p align="center">
<img src="https://raw.githubusercontent.com/hrfee/jellyfin-accounts/main/images/jfa.gif" width="100%"></img>
</p>
<p align="center">
<img src="https://raw.githubusercontent.com/hrfee/jellyfin-accounts/main/images/admin.png" width="48%" style="margin-right: 1.5%;" alt="Admin page"></img>
<img src="https://raw.githubusercontent.com/hrfee/jellyfin-accounts/main/images/create.png" width="48%" style="margin-left: 1.5%;" alt="Account creation page"></img>
</p>
## Get it
### Requirements
* This should work anywhere Python does, i've tried to not use anything OS-specific. Drop an issue if there's a problem, of course.
```
* python >= 3.6
* flask
* flask_httpauth
* jinja2
* requests
* itsdangerous
* passlib
* pyOpenSSL
* waitress
* pytz
* python-dateutil
* watchdog
* packaging
```
### Install
Usually as simple as:
```
pip install jellyfin-accounts
```
If not, or if you want to use docker, see [install](https://github.com/hrfee/jellyfin-accounts/wiki/Install).
## Usage
* Passing no arguments will run the server
```
usage: jf-accounts [-h] [-c CONFIG] [-d DATA] [--host HOST] [-p PORT] [-g]
jellyfin-accounts
optional arguments:
-h, --help show this help message and exit
-c CONFIG, --config CONFIG
specifies path to configuration file.
-d DATA, --data DATA specifies directory to store data in. defaults to
~/.jf-accounts.
--host HOST address to host web ui on.
-p PORT, --port PORT port to host web ui on.
-g, --get_defaults tool to grab a JF users policy (access, perms, etc.)
and homescreen layout and output it as json to be used
as a user template.
```
## Setup
#### New user template
* You may want to restrict a user from accessing certain libraries (e.g 4K Movies), display their account on the login screen by default, or set a default homecrseen layout. Jellyfin stores these settings in the user's policy, configuration and displayPreferences.
* Make a temporary account and configure it, then in the web UI, go into "Settings => Set new account defaults". Choose the account, and its configuration will be stored for future use.
#### Emails/Password Resets
* When someone initiates forget password on Jellyfin, a file named `passwordreset*.json` is created in its configuration directory. This directory is monitored and when created, the program reads the username, expiry time and PIN, puts it into a template and sends it to whatever address is specified in `emails.json`.
* **The default forget password popup references the `passwordreset*.json` file created. This is confusing for users, so a quick fix is to edit the `MessageForgotPasswordFileCreated` string in Jellyfin's language folder.**
* Currently, jellyfin-accounts supports generic SSL/TLS or STARTTLS secured SMTP, and the [mailgun](https://mailgun.com) REST API.
* Email html is created using [mjml](https://mjml.io), and [jinja](https://github.com/pallets/jinja) templating is used. If you wish to create your own, ensure you use the same jinja expressions (`{{ pin }}`, etc.) as used in `data/email.mjml` or `invite-email.mjml`, and also create plain text versions for legacy email clients.
### Configuration
* Note: Make sure to put this behind a reverse proxy with HTTPS.
On first run, access the setup wizard at `0.0.0.0:8056`. When finished, restart the program.
The configuration is stored at `~/.jf-accounts/config.ini`. Settings can be changed through the web UI, or by manually editing the file.
For detailed descriptions of each setting, see [setup](https://github.com/hrfee/jellyfin-accounts/wiki/Setup).
### Donations
I strongly suggest you send your money to [Jellyfin](https://opencollective.com/jellyfin) or a good charity, but for those who want to help me out, a Paypal link is below.
[Donate](https://www.paypal.me/hrfee)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

After

Width:  |  Height:  |  Size: 6.7 MiB

View File

@ -1,7 +1,5 @@
# Runs it! # Runs it!
__version__ = "0.3.9" __version__ = "0.3.7"
print("Note: jellyfin-accounts has been deprecated. Try jfa-go, a rewrite thats fast, portable, and has more features. Find it at\nhttps://github.com/hrfee/jfa-go\n")
import secrets import secrets
import configparser import configparser
@ -74,7 +72,7 @@ else:
# Temp config so logger knows whether to use debug mode or not # Temp config so logger knows whether to use debug mode or not
temp_config = configparser.RawConfigParser() temp_config = configparser.RawConfigParser()
temp_config.read(str(config_path.resolve())) temp_config.read(config_path)
def create_log(name): def create_log(name):
@ -100,8 +98,7 @@ config = Config(config_path, secrets.token_urlsafe(16), data_dir, local_dir, log
web_log = create_log("waitress") web_log = create_log("waitress")
if not first_run: if not first_run:
email_log = create_log("email") email_log = create_log("emails")
pwr_log = create_log("pwr")
auth_log = create_log("auth") auth_log = create_log("auth")
if args.host is not None: if args.host is not None:
@ -188,7 +185,6 @@ def resp(success=True, code=500):
r.status_code = code r.status_code = code
return r return r
app = Flask(__name__, root_path=str(local_dir))
def main(): def main():
if args.install: if args.install:
@ -294,6 +290,8 @@ def main():
success = True success = True
else: else:
global app
app = Flask(__name__, root_path=str(local_dir))
app.config["DEBUG"] = config.getboolean("ui", "debug") app.config["DEBUG"] = config.getboolean("ui", "debug")
app.config["SECRET_KEY"] = secrets.token_urlsafe(16) app.config["SECRET_KEY"] = secrets.token_urlsafe(16)
app.config["JSON_SORT_KEYS"] = False app.config["JSON_SORT_KEYS"] = False
@ -330,7 +328,7 @@ def main():
jellyfin_accounts.pw_reset.start() jellyfin_accounts.pw_reset.start()
pwr = threading.Thread(target=start_pwr, daemon=True) pwr = threading.Thread(target=start_pwr, daemon=True)
log.info("Starting password reset thread") log.info("Starting email thread")
pwr.start() pwr.start()
def signal_handler(sig, frame): def signal_handler(sig, frame):

View File

@ -0,0 +1,7 @@
A user was created using code {{ code }}.
Name: {{ username }}
Address: {{ address }}
Time: {{ time }}
Note: Notification emails can be toggled on the admin dashboard.

View File

@ -0,0 +1,10 @@
Hi {{ username }},
Someone has recently requests a password reset on Jellyfin.
If this was you, enter the below pin into the prompt.
This code will expire on {{ expiry_date }}, at {{ expiry_time }} , which is in {{ expires_in }}.
If this wasn't you, please ignore this email.
PIN: {{ pin }}
{{ message }}

View File

@ -0,0 +1,5 @@
Invite expired.
Code {{ code }} expired at {{ expiry }}.
Note: Notification emails can be toggled on the admin dashboard.

View File

@ -0,0 +1,8 @@
Hi,
You've been invited to Jellyfin.
To join, follow the below link.
This invite will expire on {{ expiry_date }}, at {{ expiry_time }}, which is in {{ expires_in }}, so act quick.
{{ invite_link }}
{{ message }}

View File

@ -21,19 +21,16 @@ function checkEmailRadio() {
document.getElementById('emailCommonArea').style.display = ''; document.getElementById('emailCommonArea').style.display = '';
document.getElementById('emailSMTPArea').style.display = ''; document.getElementById('emailSMTPArea').style.display = '';
document.getElementById('emailMailgunArea').style.display = 'none'; document.getElementById('emailMailgunArea').style.display = 'none';
document.getElementById('notificationsEnabled').checked = true;
} else if (document.getElementById('emailMailgunRadio').checked) { } else if (document.getElementById('emailMailgunRadio').checked) {
document.getElementById('emailCommonArea').style.display = ''; document.getElementById('emailCommonArea').style.display = '';
document.getElementById('emailSMTPArea').style.display = 'none'; document.getElementById('emailSMTPArea').style.display = 'none';
document.getElementById('emailMailgunArea').style.display = ''; document.getElementById('emailMailgunArea').style.display = '';
document.getElementById('notificationsEnabled').checked = true;
} else if (document.getElementById('emailDisabledRadio').checked) { } else if (document.getElementById('emailDisabledRadio').checked) {
document.getElementById('emailCommonArea').style.display = 'none'; document.getElementById('emailCommonArea').style.display = 'none';
document.getElementById('emailSMTPArea').style.display = 'none'; document.getElementById('emailSMTPArea').style.display = 'none';
document.getElementById('emailMailgunArea').style.display = 'none'; document.getElementById('emailMailgunArea').style.display = 'none';
document.getElementById('emailNextButton').href = '#page-8'; document.getElementById('emailNextButton').href = '#page-8';
document.getElementById('valBackButton').href = '#page-4'; document.getElementById('valBackButton').href = '#page-4';
document.getElementById('notificationsEnabled').checked = false;
}; };
}; };
var emailRadios = ['emailDisabledRadio', 'emailSMTPRadio', 'emailMailgunRadio']; var emailRadios = ['emailDisabledRadio', 'emailSMTPRadio', 'emailMailgunRadio'];
@ -168,7 +165,6 @@ document.getElementById('submitButton').onclick = function() {
if (document.getElementById('emailDisabledRadio').checked) { if (document.getElementById('emailDisabledRadio').checked) {
config['password_resets']['enabled'] = 'false'; config['password_resets']['enabled'] = 'false';
config['invite_emails']['enabled'] = 'false'; config['invite_emails']['enabled'] = 'false';
config['notificatons']['enabled'] = 'false';
} else { } else {
if (document.getElementById('emailSMTPRadio').checked) { if (document.getElementById('emailSMTPRadio').checked) {
if (document.getElementById('emailSSL_TLS').checked) { if (document.getElementById('emailSSL_TLS').checked) {

View File

@ -214,31 +214,18 @@
</div> </div>
</div> </div>
</div> </div>
<div class="modal fade" id="restartModal" role="dialog" aria-labelledby="Restart Warning" aria-hidden="true"> <div class="modal fade" id="restartModal" role="dialog" aria-labelledby"Restart Warning" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document"> <div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">Warning</h5> <h5 class="modal-title">Warning</h5>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p>A restart is needed to apply some settings. Restart now, later, or cancel?</p> <p>A restart is needed to apply some settings. This must be done manually. Apply now?</p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-light" data-dismiss="modal">Cancel</button> <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-secondary" id="applyRestarts" data-dismiss="modal">Apply, Restart later</button> <button type="button" class="btn btn-primary" id="applyRestarts" data-dismiss="alert">Apply</button>
<button type="button" class="btn btn-primary" id="applyAndRestart" data-dismiss="modal">Apply &amp; Restart</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="refreshModal" role="dialog" aria-labelledby="Refresh page notice" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Settings applied.</h5>
</div>
<div class="modal-body">
<p>Refresh the page in a few seconds.</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -157,7 +157,6 @@ var settingsModal = createModal('settingsMenu');
var userDefaultsModal = createModal('userDefaults'); var userDefaultsModal = createModal('userDefaults');
var usersModal = createModal('users'); var usersModal = createModal('users');
var restartModal = createModal('restartModal'); var restartModal = createModal('restartModal');
var refreshModal = createModal('refreshModal');
// Parsed invite: [<code>, <expires in _>, <1: Empty invite (no delete/link), 0: Actual invite>, <email address>, <remaining uses>, [<used-by>], <date created>, <notify on expiry>, <notify on creation>] // Parsed invite: [<code>, <expires in _>, <1: Empty invite (no delete/link), 0: Actual invite>, <email address>, <remaining uses>, [<used-by>], <date created>, <notify on expiry>, <notify on creation>]
function parseInvite(invite, empty = false) { function parseInvite(invite, empty = false) {
@ -651,11 +650,11 @@ document.getElementById('openDefaultsWizard').onclick = function() {
for (user of users) { for (user of users) {
let radio = document.createElement('div'); let radio = document.createElement('div');
radio.classList.add('radio'); radio.classList.add('radio');
let checked = 'checked';
if (first) { if (first) {
const checked = 'checked';
first = false; first = false;
} else { } else {
checked = ''; const checked = '';
}; };
radio.innerHTML = radio.innerHTML =
`<label><input type="radio" name="defaultRadios" id="default_${user['name']}" style="margin-right: 1rem;" ${checked}>${user['name']}</label>`; `<label><input type="radio" name="defaultRadios" id="default_${user['name']}" style="margin-right: 1rem;" ${checked}>${user['name']}</label>`;
@ -962,12 +961,8 @@ document.getElementById('openSettings').onclick = function () {
triggerTooltips(); triggerTooltips();
function sendConfig(modalId, restart = false) { function sendConfig(modalId) {
let modal = document.getElementById(modalId); let modal = document.getElementById(modalId);
modifiedConfig['restart-program'] = false;
if (restart) {
modifiedConfig['restart-program'] = true;
}
let send = JSON.stringify(modifiedConfig); let send = JSON.stringify(modifiedConfig);
let req = new XMLHttpRequest(); let req = new XMLHttpRequest();
req.open("POST", "/modifyConfig", true); req.open("POST", "/modifyConfig", true);
@ -980,8 +975,6 @@ function sendConfig(modalId, restart = false) {
if (modalId != 'settingsMenu') { if (modalId != 'settingsMenu') {
settingsModal.hide(); settingsModal.hide();
} }
} else if (restart) {
refreshModal.show();
} }
} }
}; };
@ -1017,8 +1010,7 @@ document.getElementById('settingsSave').onclick = function() {
} }
} }
if (restart_setting_changed) { if (restart_setting_changed) {
document.getElementById('applyRestarts').onclick = function(){ sendConfig('restartModal'); }; document.getElementById('applyRestarts').onclick = function(){sendConfig('restartModal');};
document.getElementById('applyAndRestart').onclick = function(){ sendConfig('restartModal', restart=true); };
settingsModal.hide(); settingsModal.hide();
restartModal.show(); restartModal.show();
} else if (settings_changed) { } else if (settings_changed) {

View File

@ -355,7 +355,7 @@
<div class="card-body text-center"> <div class="card-body text-center">
<h5 class="card-title">Finished!</h5> <h5 class="card-title">Finished!</h5>
<p class="card-text"> <p class="card-text">
Press the button below to submit your settings. The program will restart. Once it's done, refresh this page. Press the button below to submit your settings. The program will quit, so run it again, then refresh this page.
</p> </p>
<button id="submitButton" class="btn btn-primary">Submit</button> <button id="submitButton" class="btn btn-primary">Submit</button>
</div> </div>

View File

@ -8,7 +8,7 @@ from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from pathlib import Path from pathlib import Path
from dateutil import parser as date_parser from dateutil import parser as date_parser
from jinja2 import Template from jinja2 import Environment, FileSystemLoader
from jellyfin_accounts import config from jellyfin_accounts import config
from jellyfin_accounts import email_log as log from jellyfin_accounts import email_log as log
@ -35,16 +35,9 @@ class Email:
+ f"({self.from_name})" + f"({self.from_name})"
) )
) )
# sp = Path(config["invite_emails"]["email_
# template_loader = FileSystemLoader(searchpath=sp)
# template_loader = PackageLoader("jellyfin_accounts", "data")
# self.template_env = Environment(loader=template_loader)
def pretty_time(self, expiry, tzaware=False): def pretty_time(self, expiry):
if tzaware: current_time = datetime.datetime.now()
current_time = datetime.datetime.utcnow().replace(tzinfo=pytz.utc)
else:
current_time = datetime.datetime.now()
date = expiry.strftime(config["email"]["date_format"]) date = expiry.strftime(config["email"]["date_format"])
if config.getboolean("email", "use_24h"): if config.getboolean("email", "use_24h"):
log.debug(f"{self.address}: Using 24h time") log.debug(f"{self.address}: Using 24h time")
@ -75,9 +68,12 @@ class Email:
invite_link = config["invite_emails"]["url_base"] invite_link = config["invite_emails"]["url_base"]
invite_link += "/" + invite["code"] invite_link += "/" + invite["code"]
for key in ["text", "html"]: for key in ["text", "html"]:
fpath = Path(config["invite_emails"]["email_" + key]) sp = Path(config["invite_emails"]["email_" + key]) / ".."
with open(fpath, 'r') as f: sp = str(sp.resolve()) + "/"
template = Template(f.read()) template_loader = FileSystemLoader(searchpath=sp)
template_env = Environment(loader=template_loader)
fname = Path(config["invite_emails"]["email_" + key]).name
template = template_env.get_template(fname)
c = template.render( c = template.render(
expiry_date=pretty["date"], expiry_date=pretty["date"],
expiry_time=pretty["time"], expiry_time=pretty["time"],
@ -93,9 +89,12 @@ class Email:
log.debug(f'Constructing expiry notification for {invite["code"]}') log.debug(f'Constructing expiry notification for {invite["code"]}')
expiry = format_datetime(invite["expiry"]) expiry = format_datetime(invite["expiry"])
for key in ["text", "html"]: for key in ["text", "html"]:
fpath = Path(config["notifications"]["expiry_" + key]) sp = Path(config["notifications"]["expiry_" + key]) / ".."
with open(fpath, 'r') as f: sp = str(sp.resolve()) + "/"
template = Template(f.read()) template_loader = FileSystemLoader(searchpath=sp)
template_env = Environment(loader=template_loader)
fname = Path(config["notifications"]["expiry_" + key]).name
template = template_env.get_template(fname)
c = template.render(code=invite["code"], expiry=expiry) c = template.render(code=invite["code"], expiry=expiry)
self.content[key] = c self.content[key] = c
log.info(f"{self.address}: {key} constructed") log.info(f"{self.address}: {key} constructed")
@ -103,16 +102,19 @@ class Email:
def construct_created(self, invite): def construct_created(self, invite):
self.subject = "Notice: User created" self.subject = "Notice: User created"
log.debug(f'Constructing user creation notification for {invite["code"]}') log.debug(f'Constructing expiry notification for {invite["code"]}')
created = format_datetime(invite["created"]) created = format_datetime(invite["created"])
if config.getboolean("email", "no_username"): if config.getboolean("email", "no_username"):
email = "n/a" email = "n/a"
else: else:
email = invite["address"] email = invite["address"]
for key in ["text", "html"]: for key in ["text", "html"]:
fpath = Path(config["notifications"]["created_" + key]) sp = Path(config["notifications"]["created_" + key]) / ".."
with open(fpath, 'r') as f: sp = str(sp.resolve()) + "/"
template = Template(f.read()) template_loader = FileSystemLoader(searchpath=sp)
template_env = Environment(loader=template_loader)
fname = Path(config["notifications"]["created_" + key]).name
template = template_env.get_template(fname)
c = template.render( c = template.render(
code=invite["code"], code=invite["code"],
username=invite["username"], username=invite["username"],
@ -129,18 +131,22 @@ class Email:
log.debug(f"{self.address}: Constructing email content") log.debug(f"{self.address}: Constructing email content")
try: try:
expiry = date_parser.parse(reset["ExpirationDate"]) expiry = date_parser.parse(reset["ExpirationDate"])
expiry = expiry.replace(tzinfo=None)
except: except:
log.error(f"{self.address}: Couldn't parse expiry time") log.error(f"{self.address}: Couldn't parse expiry time")
return False return False
current_time = datetime.datetime.utcnow().replace(tzinfo=pytz.utc) current_time = datetime.datetime.now()
if expiry >= current_time: if expiry >= current_time:
log.debug(f"{self.address}: Invite valid") log.debug(f"{self.address}: Invite valid")
pretty = self.pretty_time(expiry, tzaware=True) pretty = self.pretty_time(expiry)
email_message = config["email"]["message"] email_message = config["email"]["message"]
for key in ["text", "html"]: for key in ["text", "html"]:
fpath = Path(config["password_resets"]["email_" + key]) sp = Path(config["password_resets"]["email_" + key]) / ".."
with open(fpath, 'r') as f: sp = str(sp.resolve()) + "/"
template = Template(f.read()) template_loader = FileSystemLoader(searchpath=sp)
template_env = Environment(loader=template_loader)
fname = Path(config["password_resets"]["email_" + key]).name
template = template_env.get_template(fname)
c = template.render( c = template.render(
username=reset["UserName"], username=reset["UserName"],
expiry_date=pretty["date"], expiry_date=pretty["date"],
@ -163,11 +169,6 @@ class Email:
class Mailgun(Email): class Mailgun(Email):
errors = {
400: "Mailgun failed with 400: Bad request",
401: "Mailgun failed with 401: Invalid API key",
}
def __init__(self, address): def __init__(self, address):
super().__init__(address) super().__init__(address)
self.api_url = config["mailgun"]["api_url"] self.api_url = config["mailgun"]["api_url"]
@ -189,12 +190,7 @@ class Mailgun(Email):
if response.ok: if response.ok:
log.info(f"{self.address}: Sent via mailgun.") log.info(f"{self.address}: Sent via mailgun.")
return True return True
elif response.status_code in Mailgun.errors: log.debug(f"{self.address}: Mailgun: {response.status_code}")
log.error(f"{self.address}: {Mailgun.errors[response.status_code]}")
else:
log.error(
f"{self.address}: Mailgun failed with error {response.status_code}"
)
return response return response
@ -242,9 +238,9 @@ class Smtp(Email):
log.info(f"{self.address}: Sent via smtp (starttls)") log.info(f"{self.address}: Sent via smtp (starttls)")
return True return True
except Exception as e: except Exception as e:
log.error( err = f"{self.address}: Failed to send via smtp: "
f"{self.address}: Failed to send via smtp ({type(e).__name__}: {e.args})" err += type(e).__name__
) log.error(err)
try: try:
log.error(e.smtp_error) log.error(e.smtp_error)
except: except:

View File

@ -35,8 +35,7 @@ class Repeat:
def checkInvites(): def checkInvites():
invites = dict(data_store.invites) invites = dict(data_store.invites)
# checkInvite already loops over everything, no point running it multiple times. # checkInvite already loops over everything, no point running it multiple times.
if len(invites) != 0: checkInvite(list(invites.keys())[0])
checkInvite(list(invites.keys())[0])
if config.getboolean("notifications", "enabled"): if config.getboolean("notifications", "enabled"):

View File

@ -44,7 +44,7 @@ class Jellyfin:
pass pass
def __init__(self, server, client, version, device, deviceId, cacheMinutes=30): def __init__(self, server, client, version, device, deviceId):
""" """
Initializes the Jellyfin object. All parameters except server Initializes the Jellyfin object. All parameters except server
have no effect on the client's capability. have no effect on the client's capability.
@ -61,8 +61,7 @@ class Jellyfin:
self.version = version self.version = version
self.device = device self.device = device
self.deviceId = deviceId self.deviceId = deviceId
self.authenticated = False self.timeout = 30 * 60
self.timeout = cacheMinutes * 60
self.userCacheAge = time.time() - self.timeout - 1 self.userCacheAge = time.time() - self.timeout - 1
self.userCachePublicAge = self.userCacheAge self.userCachePublicAge = self.userCacheAge
self.useragent = f"{self.client}/{self.version}" self.useragent = f"{self.client}/{self.version}"
@ -81,20 +80,10 @@ class Jellyfin:
"X-Emby-Authorization": self.auth, "X-Emby-Authorization": self.auth,
} }
try: try:
self.info = requests.get(f"{self.server}/System/Info/Public").json() self.info = requests.get(self.server + "/System/Info/Public").json()
except: except:
pass pass
def reloadCache(self):
""" Forces a reload of the user caches """
self.userCachePublicAge = time.time() - self.timeout - 1
self.getUsers()
try:
self.userCacheAge = self.userCachePublicAge
self.getUsers(public=False)
except self.AuthenticationRequiredError:
pass
def getUsers(self, username: str = "all", userId: str = "all", public: bool = True): def getUsers(self, username: str = "all", userId: str = "all", public: bool = True):
""" """
Returns details on user(s), such as ID, Name, Policy. Returns details on user(s), such as ID, Name, Policy.
@ -108,7 +97,7 @@ class Jellyfin:
""" """
if public is True: if public is True:
if (time.time() - self.userCachePublicAge) >= self.timeout: if (time.time() - self.userCachePublicAge) >= self.timeout:
response = requests.get(f"{self.server}/Users/Public").json() response = requests.get(self.server + "/emby/Users/Public").json()
self.userCachePublic = response self.userCachePublic = response
self.userCachePublicAge = time.time() self.userCachePublicAge = time.time()
else: else:
@ -118,7 +107,7 @@ class Jellyfin:
): ):
if (time.time() - self.userCacheAge) >= self.timeout: if (time.time() - self.userCacheAge) >= self.timeout:
response = requests.get( response = requests.get(
f"{self.server}/Users", self.server + "/emby/Users",
headers=self.header, headers=self.header,
params={"Username": self.username, "Pw": self.password}, params={"Username": self.username, "Pw": self.password},
) )
@ -126,7 +115,7 @@ class Jellyfin:
response = response.json() response = response.json()
self.userCache = response self.userCache = response
self.userCacheAge = time.time() self.userCacheAge = time.time()
elif response.status_code == 401: else:
try: try:
self.authenticate(self.username, self.password) self.authenticate(self.username, self.password)
return self.getUsers(username, userId, public) return self.getUsers(username, userId, public)
@ -162,10 +151,12 @@ class Jellyfin:
:param username: Plaintext username. :param username: Plaintext username.
:param password: Plaintext password. :param password: Plaintext password.
""" """
self.username = username
self.password = password
response = requests.post( response = requests.post(
f"{self.server}/Users/AuthenticateByName", self.server + "/emby/Users/AuthenticateByName",
headers=self.header, headers=self.header,
params={"Username": username, "Pw": password}, params={"Username": self.username, "Pw": self.password},
) )
if response.status_code == 200: if response.status_code == 200:
json = response.json() json = response.json()
@ -179,11 +170,8 @@ class Jellyfin:
self.auth += f", Token={self.accessToken}" self.auth += f", Token={self.accessToken}"
self.header["X-Emby-Authorization"] = self.auth self.header["X-Emby-Authorization"] = self.auth
self.info = requests.get( self.info = requests.get(
f"{self.server}/System/Info", headers=self.header self.server + "/System/Info", headers=self.header
).json() ).json()
self.username = username
self.password = password
self.authenticated = True
return True return True
else: else:
raise self.AuthenticationError raise self.AuthenticationError
@ -196,7 +184,7 @@ class Jellyfin:
:param policy: User policy in dictionary form. :param policy: User policy in dictionary form.
""" """
return requests.post( return requests.post(
f"{self.server}/Users/" + userId + "/Policy", self.server + "/Users/" + userId + "/Policy",
headers=self.header, headers=self.header,
params=policy, params=policy,
) )
@ -206,7 +194,7 @@ class Jellyfin:
if user["Name"] == username: if user["Name"] == username:
raise self.UserExistsError raise self.UserExistsError
response = requests.post( response = requests.post(
f"{self.server}/Users/New", self.server + "/emby/Users/New",
headers=self.header, headers=self.header,
params={"Name": username, "Password": password}, params={"Name": username, "Password": password},
) )
@ -224,7 +212,7 @@ class Jellyfin:
else: else:
param = "" param = ""
views = requests.get( views = requests.get(
f"{self.server}/Users/" + userId + "/Views" + param, headers=self.header self.server + "/Users/" + userId + "/Views" + param, headers=self.header
).json()["Items"] ).json()["Items"]
orderedViews = [] orderedViews = []
for library in views: for library in views:
@ -238,7 +226,7 @@ class Jellyfin:
:param configuration: Configuration to write in dictionary form. :param configuration: Configuration to write in dictionary form.
""" """
resp = requests.post( resp = requests.post(
f"{self.server}/Users/" + userId + "/Configuration", self.server + "/Users/" + userId + "/Configuration",
headers=self.header, headers=self.header,
params=configuration, params=configuration,
) )

View File

@ -6,7 +6,7 @@ from watchdog.events import FileSystemEventHandler
from jellyfin_accounts.email import Mailgun, Smtp from jellyfin_accounts.email import Mailgun, Smtp
from jellyfin_accounts.web_api import jf from jellyfin_accounts.web_api import jf
from jellyfin_accounts import config, data_store from jellyfin_accounts import config, data_store
from jellyfin_accounts import pwr_log as log from jellyfin_accounts import email_log as log
class Watcher: class Watcher:
@ -19,8 +19,7 @@ class Watcher:
self.observer.schedule(event_handler, self.dir, recursive=True) self.observer.schedule(event_handler, self.dir, recursive=True)
try: try:
self.observer.start() self.observer.start()
except (NotADirectoryError, except NotADirectoryError:
FileNotFoundError):
log.error(f"Directory {self.dir} does not exist") log.error(f"Directory {self.dir} does not exist")
try: try:
while True: while True:

View File

@ -5,8 +5,6 @@ from jellyfin_accounts.jf_api import Jellyfin
from jellyfin_accounts import config, config_path, app, first_run, resp from jellyfin_accounts import config, config_path, app, first_run, resp
from jellyfin_accounts import web_log as log from jellyfin_accounts import web_log as log
import os import os
import psutil
import sys
if first_run: if first_run:
@ -53,16 +51,8 @@ if first_run:
with open(config_path, "w") as config_file: with open(config_path, "w") as config_file:
temp_config.write(config_file) temp_config.write(config_file)
log.debug("Config written") log.debug("Config written")
log.info('Restarting...') # ugly exit, sorry
try: os._exit(1)
p = psutil.Process(os.getpid())
for handler in p.open_files() + p.connections():
os.close(handler.fd)
except:
pass
python = sys.executable
os.execl(python, python, *sys.argv)
return resp() return resp()
@app.route("/testJF", methods=["GET", "POST"]) @app.route("/testJF", methods=["GET", "POST"])

View File

@ -28,6 +28,7 @@ def page_not_found(e):
@app.route("/", methods=["GET", "POST"]) @app.route("/", methods=["GET", "POST"])
def admin(): def admin():
# return app.send_static_file('admin.html')
return render_template( return render_template(
"admin.html", "admin.html",
bs5=config.getboolean("ui", "bs5"), bs5=config.getboolean("ui", "bs5"),

View File

@ -5,10 +5,6 @@ import json
import datetime import datetime
import secrets import secrets
import time import time
import threading
import os
import sys
import psutil
from jellyfin_accounts import ( from jellyfin_accounts import (
config, config,
config_path, config_path,
@ -65,7 +61,7 @@ def checkInvite(code, used=False, username=None):
if email.construct_expiry( if email.construct_expiry(
{"code": invite, "expiry": expiry} {"code": invite, "expiry": expiry}
): ):
threading.Thread(target=email.send).start() email.send()
del data_store.invites[invite] del data_store.invites[invite]
elif invite == code: elif invite == code:
match = True match = True
@ -226,7 +222,7 @@ def newUser():
"created": datetime.datetime.now(), "created": datetime.datetime.now(),
} }
): ):
threading.Thread(target=email.send).start() email.send()
if user.status_code == 200: if user.status_code == 200:
try: try:
policy = data_store.user_template policy = data_store.user_template
@ -441,9 +437,9 @@ def modifyConfig():
temp_config = configparser.RawConfigParser( temp_config = configparser.RawConfigParser(
comment_prefixes="/", allow_no_value=True comment_prefixes="/", allow_no_value=True
) )
temp_config.read(str(config_path.resolve())) temp_config.read(config_path)
for section in data: for section in data:
if section in temp_config and 'restart-program' not in section: if section in temp_config:
for item in data[section]: for item in data[section]:
temp_config[section][item] = data[section][item] temp_config[section][item] = data[section][item]
data[section][item] = True data[section][item] = True
@ -451,18 +447,7 @@ def modifyConfig():
with open(config_path, "w") as config_file: with open(config_path, "w") as config_file:
temp_config.write(config_file) temp_config.write(config_file)
config.trigger_reload() config.trigger_reload()
log.info("Config written.") log.info("Config written. Restart may be needed to load settings.")
if 'restart-program' in data:
if data['restart-program']:
log.info('Restarting...')
try:
proc = psutil.Process(os.getpid())
for handler in proc.open_files() + proc.connections():
os.close(handler.fd)
except Exception as e:
log.error(f'Failed restart: {type(e).__name__}')
python = sys.executable
os.execl(python, python, *sys.argv)
return resp() return resp()

View File

@ -23,7 +23,7 @@
<p>Hi {{ username }},</p> <p>Hi {{ username }},</p>
<p> Someone has recently requested a password reset on Jellyfin.</p> <p> Someone has recently requested a password reset on Jellyfin.</p>
<p>If this was you, enter the below pin into the prompt.</p> <p>If this was you, enter the below pin into the prompt.</p>
<p>The code will expire on {{ expiry_date }}, at {{ expiry_time }} UTC, which is in {{ expires_in }}.</p> <p>The code will expire on {{ expiry_date }}, at {{ expiry_time }}, which is in {{ expires_in }}.</p>
<p>If this wasn't you, please ignore this email.</p> <p>If this wasn't you, please ignore this email.</p>
</mj-text> </mj-text>
<mj-button mj-class="blue bold">{{ pin }}</mj-button> <mj-button mj-class="blue bold">{{ pin }}</mj-button>
@ -37,4 +37,4 @@
</mj-column> </mj-column>
</mj-section> </mj-section>
</body> </body>
</mjml> </mjml>

View File

@ -2,7 +2,7 @@ Hi {{ username }},
Someone has recently requests a password reset on Jellyfin. Someone has recently requests a password reset on Jellyfin.
If this was you, enter the below pin into the prompt. If this was you, enter the below pin into the prompt.
This code will expire on {{ expiry_date }}, at {{ expiry_time }} UTC, which is in {{ expires_in }}. This code will expire on {{ expiry_date }}, at {{ expiry_time }} , which is in {{ expires_in }}.
If this wasn't you, please ignore this email. If this wasn't you, please ignore this email.
PIN: {{ pin }} PIN: {{ pin }}

View File

@ -1,39 +1,13 @@
import subprocess import subprocess
import shutil import shutil
import os
import argparse
from pathlib import Path from pathlib import Path
parser = argparse.ArgumentParser()
parser.add_argument(
"-y", "--yes", help="use assumed node bin directory.", action="store_true"
)
def runcmd(cmd): def runcmd(cmd):
if os.name == "nt":
return subprocess.check_output(cmd, shell=True)
proc = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE) proc = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE)
return proc.communicate() return proc.communicate()
local_path = Path(__file__).resolve().parent local_path = Path(__file__).resolve().parent
out = runcmd("npm bin") node_bin = local_path.parent / 'node_modules' / '.bin'
try:
node_bin = Path(out[0].decode('utf-8').rstrip())
except:
node_bin = Path(out.decode('utf-8').rstrip())
args = parser.parse_args()
if not args.yes:
print(f"assuming npm bin directory \"{node_bin}\". Is this correct?")
if input("[yY/nN]: ").lower() == "n":
node_bin = local_path.parent / 'node_modules' / '.bin'
print(f"this? \"{node_bin}\"")
if input("[yY/nN]: ").lower() == "n":
node_bin = input("input bin directory: ")
for mjml in [f for f in local_path.iterdir() if f.is_file() and 'mjml' in f.suffix]: for mjml in [f for f in local_path.iterdir() if f.is_file() and 'mjml' in f.suffix]:
print(f'Compiling {mjml.name}') print(f'Compiling {mjml.name}')

1438
package-lock.json generated

File diff suppressed because it is too large Load Diff

436
poetry.lock generated
View File

@ -1,18 +1,18 @@
[[package]] [[package]]
name = "appdirs"
version = "1.4.4"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev" category = "dev"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
name = "appdirs"
optional = false optional = false
python-versions = "*" python-versions = "*"
version = "1.4.4"
[[package]] [[package]]
name = "attrs"
version = "19.3.0"
description = "Classes Without Boilerplate"
category = "dev" category = "dev"
description = "Classes Without Boilerplate"
name = "attrs"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "19.3.0"
[package.extras] [package.extras]
azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"]
@ -21,12 +21,12 @@ docs = ["sphinx", "zope.interface"]
tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
[[package]] [[package]]
name = "black"
version = "19.10b0"
description = "The uncompromising code formatter."
category = "dev" category = "dev"
description = "The uncompromising code formatter."
name = "black"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
version = "19.10b0"
[package.dependencies] [package.dependencies]
appdirs = "*" appdirs = "*"
@ -41,72 +41,72 @@ typed-ast = ">=1.4.0"
d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
[[package]] [[package]]
name = "certifi"
version = "2020.6.20"
description = "Python package for providing Mozilla's CA Bundle."
category = "main" category = "main"
description = "Python package for providing Mozilla's CA Bundle."
name = "certifi"
optional = false optional = false
python-versions = "*" python-versions = "*"
version = "2020.4.5.2"
[[package]] [[package]]
name = "cffi"
version = "1.14.1"
description = "Foreign Function Interface for Python calling C code."
category = "main" category = "main"
description = "Foreign Function Interface for Python calling C code."
name = "cffi"
optional = false optional = false
python-versions = "*" python-versions = "*"
version = "1.14.0"
[package.dependencies] [package.dependencies]
pycparser = "*" pycparser = "*"
[[package]] [[package]]
name = "chardet"
version = "3.0.4"
description = "Universal encoding detector for Python 2 and 3"
category = "main" category = "main"
description = "Universal encoding detector for Python 2 and 3"
name = "chardet"
optional = false optional = false
python-versions = "*" python-versions = "*"
version = "3.0.4"
[[package]] [[package]]
name = "click"
version = "7.1.2"
description = "Composable command line interface toolkit"
category = "main" category = "main"
description = "Composable command line interface toolkit"
name = "click"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "7.1.2"
[[package]] [[package]]
name = "cryptography"
version = "3.2"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
category = "main" category = "main"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
name = "cryptography"
optional = false optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
version = "2.9.2"
[package.dependencies] [package.dependencies]
cffi = ">=1.8,<1.11.3 || >1.11.3" cffi = ">=1.8,<1.11.3 || >1.11.3"
six = ">=1.4.1" six = ">=1.4.1"
[package.extras] [package.extras]
docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] docs = ["sphinx (>=1.6.5,<1.8.0 || >1.8.0)", "sphinx-rtd-theme"]
docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"]
pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] idna = ["idna (>=2.1)"]
ssh = ["bcrypt (>=3.1.5)"] pep8test = ["flake8", "flake8-import-order", "pep8-naming"]
test = ["pytest (>=3.6.0,!=3.9.0,!=3.9.1,!=3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] test = ["pytest (>=3.6.0,<3.9.0 || >3.9.0,<3.9.1 || >3.9.1,<3.9.2 || >3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,<3.79.2 || >3.79.2)"]
[[package]] [[package]]
name = "flask"
version = "1.1.2"
description = "A simple framework for building complex web applications."
category = "main" category = "main"
description = "A simple framework for building complex web applications."
name = "flask"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "1.1.2"
[package.dependencies] [package.dependencies]
click = ">=5.1"
itsdangerous = ">=0.24"
Jinja2 = ">=2.10.1" Jinja2 = ">=2.10.1"
Werkzeug = ">=0.15" Werkzeug = ">=0.15"
click = ">=5.1"
itsdangerous = ">=0.24"
[package.extras] [package.extras]
dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"] dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-issues"]
@ -114,47 +114,47 @@ docs = ["sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet", "sphinx-
dotenv = ["python-dotenv"] dotenv = ["python-dotenv"]
[[package]] [[package]]
name = "flask-httpauth"
version = "4.1.0"
description = "Basic and Digest HTTP authentication for Flask routes"
category = "main" category = "main"
description = "Basic and Digest HTTP authentication for Flask routes"
name = "flask-httpauth"
optional = false optional = false
python-versions = "*" python-versions = "*"
version = "3.3.0"
[package.dependencies] [package.dependencies]
Flask = "*" Flask = "*"
[[package]] [[package]]
name = "greenlet"
version = "0.4.16"
description = "Lightweight in-process concurrent programming"
category = "dev" category = "dev"
description = "Lightweight in-process concurrent programming"
name = "greenlet"
optional = false optional = false
python-versions = "*" python-versions = "*"
version = "0.4.16"
[[package]] [[package]]
name = "idna" category = "main"
version = "2.10"
description = "Internationalized Domain Names in Applications (IDNA)" description = "Internationalized Domain Names in Applications (IDNA)"
category = "main" name = "idna"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "2.9"
[[package]] [[package]]
name = "itsdangerous" category = "main"
version = "1.1.0"
description = "Various helpers to pass data to untrusted environments and back." description = "Various helpers to pass data to untrusted environments and back."
category = "main" name = "itsdangerous"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.1.0"
[[package]] [[package]]
name = "jinja2"
version = "2.11.2"
description = "A very fast and expressive template engine."
category = "main" category = "main"
description = "A very fast and expressive template engine."
name = "jinja2"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "2.11.2"
[package.dependencies] [package.dependencies]
MarkupSafe = ">=0.23" MarkupSafe = ">=0.23"
@ -163,62 +163,62 @@ MarkupSafe = ">=0.23"
i18n = ["Babel (>=0.8)"] i18n = ["Babel (>=0.8)"]
[[package]] [[package]]
name = "libsass"
version = "0.20.0"
description = "Sass for Python: A straightforward binding of libsass for Python."
category = "dev" category = "dev"
description = "Sass for Python: A straightforward binding of libsass for Python."
name = "libsass"
optional = false optional = false
python-versions = "*" python-versions = "*"
version = "0.20.0"
[package.dependencies] [package.dependencies]
six = "*" six = "*"
[[package]] [[package]]
name = "markupsafe"
version = "1.1.1"
description = "Safely add untrusted strings to HTML/XML markup."
category = "main" category = "main"
description = "Safely add untrusted strings to HTML/XML markup."
name = "markupsafe"
optional = false optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
version = "1.1.1"
[[package]] [[package]]
name = "msgpack" category = "dev"
version = "1.0.0"
description = "MessagePack (de)serializer." description = "MessagePack (de)serializer."
category = "dev" name = "msgpack"
optional = false optional = false
python-versions = "*" python-versions = "*"
version = "1.0.0"
[[package]] [[package]]
name = "neovim"
version = "0.3.1"
description = "Transition packgage for pynvim"
category = "dev" category = "dev"
description = "Transition packgage for pynvim"
name = "neovim"
optional = false optional = false
python-versions = "*" python-versions = "*"
version = "0.3.1"
[package.dependencies] [package.dependencies]
pynvim = ">=0.3.1" pynvim = ">=0.3.1"
[[package]] [[package]]
name = "packaging"
version = "20.4"
description = "Core utilities for Python packages"
category = "main" category = "main"
description = "Core utilities for Python packages"
name = "packaging"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "20.4"
[package.dependencies] [package.dependencies]
pyparsing = ">=2.0.2" pyparsing = ">=2.0.2"
six = "*" six = "*"
[[package]] [[package]]
name = "passlib"
version = "1.7.2"
description = "comprehensive password hashing framework supporting over 30 schemes"
category = "main" category = "main"
description = "comprehensive password hashing framework supporting over 30 schemes"
name = "passlib"
optional = false optional = false
python-versions = "*" python-versions = "*"
version = "1.7.2"
[package.extras] [package.extras]
argon2 = ["argon2-cffi (>=18.2.0)"] argon2 = ["argon2-cffi (>=18.2.0)"]
@ -227,47 +227,36 @@ build_docs = ["sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)", "cloud-spthem
totp = ["cryptography"] totp = ["cryptography"]
[[package]] [[package]]
name = "pathspec"
version = "0.8.0"
description = "Utility library for gitignore style pattern matching of file paths."
category = "dev" category = "dev"
description = "Utility library for gitignore style pattern matching of file paths."
name = "pathspec"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "0.8.0"
[[package]] [[package]]
name = "pathtools"
version = "0.1.2"
description = "File system general utilities"
category = "main" category = "main"
description = "File system general utilities"
name = "pathtools"
optional = false optional = false
python-versions = "*" python-versions = "*"
version = "0.1.2"
[[package]] [[package]]
name = "psutil"
version = "5.7.2"
description = "Cross-platform lib for process and system monitoring in Python."
category = "main" category = "main"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.extras]
test = ["ipaddress", "mock", "unittest2", "enum34", "pywin32", "wmi"]
[[package]]
name = "pycparser"
version = "2.20"
description = "C parser in Python" description = "C parser in Python"
category = "main" name = "pycparser"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "2.20"
[[package]] [[package]]
name = "pynvim"
version = "0.4.1"
description = "Python client to neovim"
category = "dev" category = "dev"
description = "Python client to neovim"
name = "pynvim"
optional = false optional = false
python-versions = "*" python-versions = "*"
version = "0.4.1"
[package.dependencies] [package.dependencies]
greenlet = "*" greenlet = "*"
@ -278,12 +267,12 @@ pyuv = ["pyuv (>=1.0.0)"]
test = ["pytest (>=3.4.0)"] test = ["pytest (>=3.4.0)"]
[[package]] [[package]]
name = "pyopenssl"
version = "19.1.0"
description = "Python wrapper module around the OpenSSL library"
category = "main" category = "main"
description = "Python wrapper module around the OpenSSL library"
name = "pyopenssl"
optional = false optional = false
python-versions = "*" python-versions = "*"
version = "19.1.0"
[package.dependencies] [package.dependencies]
cryptography = ">=2.8" cryptography = ">=2.8"
@ -294,47 +283,47 @@ docs = ["sphinx", "sphinx-rtd-theme"]
test = ["flaky", "pretend", "pytest (>=3.0.1)"] test = ["flaky", "pretend", "pytest (>=3.0.1)"]
[[package]] [[package]]
name = "pyparsing"
version = "2.4.7"
description = "Python parsing module"
category = "main" category = "main"
description = "Python parsing module"
name = "pyparsing"
optional = false optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
version = "2.4.7"
[[package]] [[package]]
name = "python-dateutil"
version = "2.8.1"
description = "Extensions to the standard Python datetime module"
category = "main" category = "main"
description = "Extensions to the standard Python datetime module"
name = "python-dateutil"
optional = false optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
version = "2.8.1"
[package.dependencies] [package.dependencies]
six = ">=1.5" six = ">=1.5"
[[package]] [[package]]
name = "pytz" category = "main"
version = "2020.1"
description = "World timezone definitions, modern and historical" description = "World timezone definitions, modern and historical"
category = "main" name = "pytz"
optional = false optional = false
python-versions = "*" python-versions = "*"
version = "2020.1"
[[package]] [[package]]
name = "regex"
version = "2020.7.14"
description = "Alternative regular expression module, to replace re."
category = "dev" category = "dev"
description = "Alternative regular expression module, to replace re."
name = "regex"
optional = false optional = false
python-versions = "*" python-versions = "*"
version = "2020.6.8"
[[package]] [[package]]
name = "requests"
version = "2.24.0"
description = "Python HTTP for Humans."
category = "main" category = "main"
description = "Python HTTP for Humans."
name = "requests"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "2.23.0"
[package.dependencies] [package.dependencies]
certifi = ">=2017.4.17" certifi = ">=2017.4.17"
@ -344,75 +333,75 @@ urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26"
[package.extras] [package.extras]
security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"]
[[package]] [[package]]
name = "six"
version = "1.15.0"
description = "Python 2 and 3 compatibility utilities"
category = "main" category = "main"
description = "Python 2 and 3 compatibility utilities"
name = "six"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
version = "1.15.0"
[[package]] [[package]]
name = "taskipy"
version = "1.2.1"
description = "tasks runner for python projects"
category = "dev" category = "dev"
description = "tasks runner for python projects"
name = "taskipy"
optional = false optional = false
python-versions = ">=3.6,<4.0" python-versions = ">=3.6,<4.0"
version = "1.2.1"
[package.dependencies] [package.dependencies]
toml = ">=0.10.0,<0.11.0" toml = ">=0.10.0,<0.11.0"
[[package]] [[package]]
name = "toml" category = "dev"
version = "0.10.1"
description = "Python Library for Tom's Obvious, Minimal Language" description = "Python Library for Tom's Obvious, Minimal Language"
category = "dev" name = "toml"
optional = false optional = false
python-versions = "*" python-versions = "*"
version = "0.10.1"
[[package]] [[package]]
name = "typed-ast" category = "dev"
version = "1.4.1"
description = "a fork of Python 2 and 3 ast modules with type comment support" description = "a fork of Python 2 and 3 ast modules with type comment support"
category = "dev" name = "typed-ast"
optional = false optional = false
python-versions = "*" python-versions = "*"
version = "1.4.1"
[[package]] [[package]]
name = "urllib3"
version = "1.25.10"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "main" category = "main"
description = "HTTP library with thread-safe connection pooling, file post, and more."
name = "urllib3"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
version = "1.25.9"
[package.extras] [package.extras]
brotli = ["brotlipy (>=0.6.0)"] brotli = ["brotlipy (>=0.6.0)"]
secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"] secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"]
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"]
[[package]] [[package]]
name = "waitress"
version = "1.4.4"
description = "Waitress WSGI server"
category = "main" category = "main"
description = "Waitress WSGI server"
name = "waitress"
optional = false optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
version = "1.4.4"
[package.extras] [package.extras]
docs = ["Sphinx (>=1.8.1)", "docutils", "pylons-sphinx-themes (>=1.0.9)"] docs = ["Sphinx (>=1.8.1)", "docutils", "pylons-sphinx-themes (>=1.0.9)"]
testing = ["pytest", "pytest-cover", "coverage (>=5.0)"] testing = ["pytest", "pytest-cover", "coverage (>=5.0)"]
[[package]] [[package]]
name = "watchdog"
version = "0.10.3"
description = "Filesystem events monitoring"
category = "main" category = "main"
description = "Filesystem events monitoring"
name = "watchdog"
optional = false optional = false
python-versions = "*" python-versions = "*"
version = "0.10.2"
[package.dependencies] [package.dependencies]
pathtools = ">=0.1.1" pathtools = ">=0.1.1"
@ -421,21 +410,20 @@ pathtools = ">=0.1.1"
watchmedo = ["PyYAML (>=3.10)", "argh (>=0.24.1)"] watchmedo = ["PyYAML (>=3.10)", "argh (>=0.24.1)"]
[[package]] [[package]]
name = "werkzeug"
version = "1.0.1"
description = "The comprehensive WSGI web application library."
category = "main" category = "main"
description = "The comprehensive WSGI web application library."
name = "werkzeug"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "1.0.1"
[package.extras] [package.extras]
dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"] dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"]
watchdog = ["watchdog"] watchdog = ["watchdog"]
[metadata] [metadata]
lock-version = "1.1" content-hash = "fa8b5fb1ded41b673b8062a2bfc6467e6a484ff62b578147bec001d7d9d8ca16"
python-versions = "^3.6" python-versions = "^3.6"
content-hash = "1c2741c9be187d9d0be662509fb4a87f5978e5f44420e5049a20504824c29a59"
[metadata.files] [metadata.files]
appdirs = [ appdirs = [
@ -451,38 +439,38 @@ black = [
{file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"}, {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"},
] ]
certifi = [ certifi = [
{file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"}, {file = "certifi-2020.4.5.2-py2.py3-none-any.whl", hash = "sha256:9cd41137dc19af6a5e03b630eefe7d1f458d964d406342dd3edf625839b944cc"},
{file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"}, {file = "certifi-2020.4.5.2.tar.gz", hash = "sha256:5ad7e9a056d25ffa5082862e36f119f7f7cec6457fa07ee2f8c339814b80c9b1"},
] ]
cffi = [ cffi = [
{file = "cffi-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:66dd45eb9530e3dde8f7c009f84568bc7cac489b93d04ac86e3111fb46e470c2"}, {file = "cffi-1.14.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384"},
{file = "cffi-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:4f53e4128c81ca3212ff4cf097c797ab44646a40b42ec02a891155cd7a2ba4d8"}, {file = "cffi-1.14.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30"},
{file = "cffi-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:833401b15de1bb92791d7b6fb353d4af60dc688eaa521bd97203dcd2d124a7c1"}, {file = "cffi-1.14.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c"},
{file = "cffi-1.14.1-cp27-cp27m-win32.whl", hash = "sha256:26f33e8f6a70c255767e3c3f957ccafc7f1f706b966e110b855bfe944511f1f9"}, {file = "cffi-1.14.0-cp27-cp27m-win32.whl", hash = "sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78"},
{file = "cffi-1.14.1-cp27-cp27m-win_amd64.whl", hash = "sha256:b87dfa9f10a470eee7f24234a37d1d5f51e5f5fa9eeffda7c282e2b8f5162eb1"}, {file = "cffi-1.14.0-cp27-cp27m-win_amd64.whl", hash = "sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793"},
{file = "cffi-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:effd2ba52cee4ceff1a77f20d2a9f9bf8d50353c854a282b8760ac15b9833168"}, {file = "cffi-1.14.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e"},
{file = "cffi-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:bac0d6f7728a9cc3c1e06d4fcbac12aaa70e9379b3025b27ec1226f0e2d404cf"}, {file = "cffi-1.14.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a"},
{file = "cffi-1.14.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d6033b4ffa34ef70f0b8086fd4c3df4bf801fee485a8a7d4519399818351aa8e"}, {file = "cffi-1.14.0-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff"},
{file = "cffi-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:8416ed88ddc057bab0526d4e4e9f3660f614ac2394b5e019a628cdfff3733849"}, {file = "cffi-1.14.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f"},
{file = "cffi-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:892daa86384994fdf4856cb43c93f40cbe80f7f95bb5da94971b39c7f54b3a9c"}, {file = "cffi-1.14.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa"},
{file = "cffi-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:c991112622baee0ae4d55c008380c32ecfd0ad417bcd0417ba432e6ba7328caa"}, {file = "cffi-1.14.0-cp35-cp35m-win32.whl", hash = "sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5"},
{file = "cffi-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:fcf32bf76dc25e30ed793145a57426064520890d7c02866eb93d3e4abe516948"}, {file = "cffi-1.14.0-cp35-cp35m-win_amd64.whl", hash = "sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4"},
{file = "cffi-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f960375e9823ae6a07072ff7f8a85954e5a6434f97869f50d0e41649a1c8144f"}, {file = "cffi-1.14.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d"},
{file = "cffi-1.14.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a6d28e7f14ecf3b2ad67c4f106841218c8ab12a0683b1528534a6c87d2307af3"}, {file = "cffi-1.14.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc"},
{file = "cffi-1.14.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:cda422d54ee7905bfc53ee6915ab68fe7b230cacf581110df4272ee10462aadc"}, {file = "cffi-1.14.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac"},
{file = "cffi-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:4a03416915b82b81af5502459a8a9dd62a3c299b295dcdf470877cb948d655f2"}, {file = "cffi-1.14.0-cp36-cp36m-win32.whl", hash = "sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f"},
{file = "cffi-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:4ce1e995aeecf7cc32380bc11598bfdfa017d592259d5da00fc7ded11e61d022"}, {file = "cffi-1.14.0-cp36-cp36m-win_amd64.whl", hash = "sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b"},
{file = "cffi-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e23cb7f1d8e0f93addf0cae3c5b6f00324cccb4a7949ee558d7b6ca973ab8ae9"}, {file = "cffi-1.14.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3"},
{file = "cffi-1.14.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ddff0b2bd7edcc8c82d1adde6dbbf5e60d57ce985402541cd2985c27f7bec2a0"}, {file = "cffi-1.14.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66"},
{file = "cffi-1.14.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f90c2267101010de42f7273c94a1f026e56cbc043f9330acd8a80e64300aba33"}, {file = "cffi-1.14.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0"},
{file = "cffi-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:3cd2c044517f38d1b577f05927fb9729d3396f1d44d0c659a445599e79519792"}, {file = "cffi-1.14.0-cp37-cp37m-win32.whl", hash = "sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f"},
{file = "cffi-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fa72a52a906425416f41738728268072d5acfd48cbe7796af07a923236bcf96"}, {file = "cffi-1.14.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26"},
{file = "cffi-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:267adcf6e68d77ba154334a3e4fc921b8e63cbb38ca00d33d40655d4228502bc"}, {file = "cffi-1.14.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd"},
{file = "cffi-1.14.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:d3148b6ba3923c5850ea197a91a42683f946dba7e8eb82dfa211ab7e708de939"}, {file = "cffi-1.14.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55"},
{file = "cffi-1.14.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:98be759efdb5e5fa161e46d404f4e0ce388e72fbf7d9baf010aff16689e22abe"}, {file = "cffi-1.14.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2"},
{file = "cffi-1.14.1-cp38-cp38-win32.whl", hash = "sha256:6923d077d9ae9e8bacbdb1c07ae78405a9306c8fd1af13bfa06ca891095eb995"}, {file = "cffi-1.14.0-cp38-cp38-win32.whl", hash = "sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8"},
{file = "cffi-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:b1d6ebc891607e71fd9da71688fcf332a6630b7f5b7f5549e6e631821c0e5d90"}, {file = "cffi-1.14.0-cp38-cp38-win_amd64.whl", hash = "sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b"},
{file = "cffi-1.14.1.tar.gz", hash = "sha256:b2a2b0d276a136146e012154baefaea2758ef1f56ae9f4e01c612b0831e0bd2f"}, {file = "cffi-1.14.0.tar.gz", hash = "sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6"},
] ]
chardet = [ chardet = [
{file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"},
@ -493,36 +481,33 @@ click = [
{file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},
] ]
cryptography = [ cryptography = [
{file = "cryptography-3.2-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:9a8580c9afcdcddabbd064c0a74f337af74ff4529cdf3a12fa2e9782d677a2e5"}, {file = "cryptography-2.9.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:daf54a4b07d67ad437ff239c8a4080cfd1cc7213df57d33c97de7b4738048d5e"},
{file = "cryptography-3.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:7ef41304bf978f33cfb6f43ca13bb0faac0c99cda33693aa20ad4f5e34e8cb8f"}, {file = "cryptography-2.9.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:3b3eba865ea2754738616f87292b7f29448aec342a7c720956f8083d252bf28b"},
{file = "cryptography-3.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:87c2fffd61e934bc0e2c927c3764c20b22d7f5f7f812ee1a477de4c89b044ca6"}, {file = "cryptography-2.9.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:c447cf087cf2dbddc1add6987bbe2f767ed5317adb2d08af940db517dd704365"},
{file = "cryptography-3.2-cp27-cp27m-win32.whl", hash = "sha256:6e8a3c7c45101a7eeee93102500e1b08f2307c717ff553fcb3c1127efc9b6917"}, {file = "cryptography-2.9.2-cp27-cp27m-win32.whl", hash = "sha256:f118a95c7480f5be0df8afeb9a11bd199aa20afab7a96bcf20409b411a3a85f0"},
{file = "cryptography-3.2-cp27-cp27m-win_amd64.whl", hash = "sha256:52a47e60953679eea0b4d490ca3c241fb1b166a7b161847ef4667dfd49e7699d"}, {file = "cryptography-2.9.2-cp27-cp27m-win_amd64.whl", hash = "sha256:c4fd17d92e9d55b84707f4fd09992081ba872d1a0c610c109c18e062e06a2e55"},
{file = "cryptography-3.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:6a8f64ed096d13f92d1f601a92d9fd1f1025dc73a2ca1ced46dcf5e0d4930943"}, {file = "cryptography-2.9.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d0d5aeaedd29be304848f1c5059074a740fa9f6f26b84c5b63e8b29e73dfc270"},
{file = "cryptography-3.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:3e17d02941c0f169c5b877597ca8be895fca0e5e3eb882526a74aa4804380a98"}, {file = "cryptography-2.9.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:1e4014639d3d73fbc5ceff206049c5a9a849cefd106a49fa7aaaa25cc0ce35cf"},
{file = "cryptography-3.2-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:d1cbc3426e6150583b22b517ef3720036d7e3152d428c864ff0f3fcad2b97591"}, {file = "cryptography-2.9.2-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:96c080ae7118c10fcbe6229ab43eb8b090fccd31a09ef55f83f690d1ef619a1d"},
{file = "cryptography-3.2-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:22f8251f68953553af4f9c11ec5f191198bc96cff9f0ac5dd5ff94daede0ee6d"}, {file = "cryptography-2.9.2-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:e993468c859d084d5579e2ebee101de8f5a27ce8e2159959b6673b418fd8c785"},
{file = "cryptography-3.2-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:f2aa3f8ba9e2e3fd49bd3de743b976ab192fbf0eb0348cebde5d2a9de0090a9f"}, {file = "cryptography-2.9.2-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:88c881dd5a147e08d1bdcf2315c04972381d026cdb803325c03fe2b4a8ed858b"},
{file = "cryptography-3.2-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:9a07e6d255053674506091d63ab4270a119e9fc83462c7ab1dbcb495b76307af"}, {file = "cryptography-2.9.2-cp35-cp35m-win32.whl", hash = "sha256:651448cd2e3a6bc2bb76c3663785133c40d5e1a8c1a9c5429e4354201c6024ae"},
{file = "cryptography-3.2-cp35-cp35m-win32.whl", hash = "sha256:f0e3986f6cce007216b23c490f093f35ce2068f3c244051e559f647f6731b7ae"}, {file = "cryptography-2.9.2-cp35-cp35m-win_amd64.whl", hash = "sha256:726086c17f94747cedbee6efa77e99ae170caebeb1116353c6cf0ab67ea6829b"},
{file = "cryptography-3.2-cp35-cp35m-win_amd64.whl", hash = "sha256:284e275e3c099a80831f9898fb5c9559120d27675c3521278faba54e584a7832"}, {file = "cryptography-2.9.2-cp36-cp36m-win32.whl", hash = "sha256:091d31c42f444c6f519485ed528d8b451d1a0c7bf30e8ca583a0cac44b8a0df6"},
{file = "cryptography-3.2-cp36-abi3-win32.whl", hash = "sha256:f01c9116bfb3ad2831e125a73dcd957d173d6ddca7701528eff1e7d97972872c"}, {file = "cryptography-2.9.2-cp36-cp36m-win_amd64.whl", hash = "sha256:bb1f0281887d89617b4c68e8db9a2c42b9efebf2702a3c5bf70599421a8623e3"},
{file = "cryptography-3.2-cp36-abi3-win_amd64.whl", hash = "sha256:bd80bc156d3729b38cb227a5a76532aef693b7ac9e395eea8063ee50ceed46a5"}, {file = "cryptography-2.9.2-cp37-cp37m-win32.whl", hash = "sha256:18452582a3c85b96014b45686af264563e3e5d99d226589f057ace56196ec78b"},
{file = "cryptography-3.2-cp36-cp36m-win32.whl", hash = "sha256:8f0fd8b0751d75c4483c534b209e39e918f0d14232c0d8a2a76e687f64ced831"}, {file = "cryptography-2.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:22e91636a51170df0ae4dcbd250d318fd28c9f491c4e50b625a49964b24fe46e"},
{file = "cryptography-3.2-cp36-cp36m-win_amd64.whl", hash = "sha256:8a0866891326d3badb17c5fd3e02c926b635e8923fa271b4813cd4d972a57ff3"}, {file = "cryptography-2.9.2-cp38-cp38-win32.whl", hash = "sha256:844a76bc04472e5135b909da6aed84360f522ff5dfa47f93e3dd2a0b84a89fa0"},
{file = "cryptography-3.2-cp37-cp37m-win32.whl", hash = "sha256:57b8c1ed13b8aa386cabbfde3be175d7b155682470b0e259fecfe53850967f8a"}, {file = "cryptography-2.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:1dfa985f62b137909496e7fc182dac687206d8d089dd03eaeb28ae16eec8e7d5"},
{file = "cryptography-3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:88069392cd9a1e68d2cfd5c3a2b0d72a44ef3b24b8977a4f7956e9e3c4c9477a"}, {file = "cryptography-2.9.2.tar.gz", hash = "sha256:a0c30272fb4ddda5f5ffc1089d7405b7a71b0b0f51993cb4e5dbb4590b2fc229"},
{file = "cryptography-3.2-cp38-cp38-win32.whl", hash = "sha256:fb70a4cedd69dc52396ee114416a3656e011fb0311fca55eb55c7be6ed9c8aef"},
{file = "cryptography-3.2-cp38-cp38-win_amd64.whl", hash = "sha256:e15ac84dcdb89f92424cbaca4b0b34e211e7ce3ee7b0ec0e4f3c55cee65fae5a"},
{file = "cryptography-3.2.tar.gz", hash = "sha256:e4789b84f8dedf190148441f7c5bfe7244782d9cbb194a36e17b91e7d3e1cca9"},
] ]
flask = [ flask = [
{file = "Flask-1.1.2-py2.py3-none-any.whl", hash = "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"}, {file = "Flask-1.1.2-py2.py3-none-any.whl", hash = "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557"},
{file = "Flask-1.1.2.tar.gz", hash = "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060"}, {file = "Flask-1.1.2.tar.gz", hash = "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060"},
] ]
flask-httpauth = [ flask-httpauth = [
{file = "Flask-HTTPAuth-4.1.0.tar.gz", hash = "sha256:9e028e4375039a49031eb9ecc40be4761f0540476040f6eff329a31dabd4d000"}, {file = "Flask-HTTPAuth-3.3.0.tar.gz", hash = "sha256:6ef8b761332e780f9ff74d5f9056c2616f52babc1998b01d9f361a1e439e61b9"},
{file = "Flask_HTTPAuth-4.1.0-py2.py3-none-any.whl", hash = "sha256:29e0288869a213c7387f0323b6bf2c7191584fb1da8aa024d9af118e5cd70de7"}, {file = "Flask_HTTPAuth-3.3.0-py2.py3-none-any.whl", hash = "sha256:0149953720489407e51ec24bc2f86273597b7973d71cd51f9443bd0e2a89bd72"},
] ]
greenlet = [ greenlet = [
{file = "greenlet-0.4.16-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:80cb0380838bf4e48da6adedb0c7cd060c187bb4a75f67a5aa9ec33689b84872"}, {file = "greenlet-0.4.16-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:80cb0380838bf4e48da6adedb0c7cd060c187bb4a75f67a5aa9ec33689b84872"},
@ -544,8 +529,8 @@ greenlet = [
{file = "greenlet-0.4.16.tar.gz", hash = "sha256:6e06eac722676797e8fce4adb8ad3dc57a1bb3adfb0dd3fdf8306c055a38456c"}, {file = "greenlet-0.4.16.tar.gz", hash = "sha256:6e06eac722676797e8fce4adb8ad3dc57a1bb3adfb0dd3fdf8306c055a38456c"},
] ]
idna = [ idna = [
{file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, {file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"},
{file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, {file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"},
] ]
itsdangerous = [ itsdangerous = [
{file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"}, {file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"},
@ -643,19 +628,6 @@ pathspec = [
pathtools = [ pathtools = [
{file = "pathtools-0.1.2.tar.gz", hash = "sha256:7c35c5421a39bb82e58018febd90e3b6e5db34c5443aaaf742b3f33d4655f1c0"}, {file = "pathtools-0.1.2.tar.gz", hash = "sha256:7c35c5421a39bb82e58018febd90e3b6e5db34c5443aaaf742b3f33d4655f1c0"},
] ]
psutil = [
{file = "psutil-5.7.2-cp27-none-win32.whl", hash = "sha256:f2018461733b23f308c298653c8903d32aaad7873d25e1d228765e91ae42c3f2"},
{file = "psutil-5.7.2-cp27-none-win_amd64.whl", hash = "sha256:66c18ca7680a31bf16ee22b1d21b6397869dda8059dbdb57d9f27efa6615f195"},
{file = "psutil-5.7.2-cp35-cp35m-win32.whl", hash = "sha256:5e9d0f26d4194479a13d5f4b3798260c20cecf9ac9a461e718eb59ea520a360c"},
{file = "psutil-5.7.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4080869ed93cce662905b029a1770fe89c98787e543fa7347f075ade761b19d6"},
{file = "psutil-5.7.2-cp36-cp36m-win32.whl", hash = "sha256:d8a82162f23c53b8525cf5f14a355f5d1eea86fa8edde27287dd3a98399e4fdf"},
{file = "psutil-5.7.2-cp36-cp36m-win_amd64.whl", hash = "sha256:0ee3c36428f160d2d8fce3c583a0353e848abb7de9732c50cf3356dd49ad63f8"},
{file = "psutil-5.7.2-cp37-cp37m-win32.whl", hash = "sha256:ff1977ba1a5f71f89166d5145c3da1cea89a0fdb044075a12c720ee9123ec818"},
{file = "psutil-5.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:a5b120bb3c0c71dfe27551f9da2f3209a8257a178ed6c628a819037a8df487f1"},
{file = "psutil-5.7.2-cp38-cp38-win32.whl", hash = "sha256:10512b46c95b02842c225f58fa00385c08fa00c68bac7da2d9a58ebe2c517498"},
{file = "psutil-5.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:68d36986ded5dac7c2dcd42f2682af1db80d4bce3faa126a6145c1637e1b559f"},
{file = "psutil-5.7.2.tar.gz", hash = "sha256:90990af1c3c67195c44c9a889184f84f5b2320dce3ee3acbd054e3ba0b4a7beb"},
]
pycparser = [ pycparser = [
{file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"},
{file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"},
@ -680,31 +652,31 @@ pytz = [
{file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"}, {file = "pytz-2020.1.tar.gz", hash = "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"},
] ]
regex = [ regex = [
{file = "regex-2020.7.14-cp27-cp27m-win32.whl", hash = "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"}, {file = "regex-2020.6.8-cp27-cp27m-win32.whl", hash = "sha256:fbff901c54c22425a5b809b914a3bfaf4b9570eee0e5ce8186ac71eb2025191c"},
{file = "regex-2020.7.14-cp27-cp27m-win_amd64.whl", hash = "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644"}, {file = "regex-2020.6.8-cp27-cp27m-win_amd64.whl", hash = "sha256:112e34adf95e45158c597feea65d06a8124898bdeac975c9087fe71b572bd938"},
{file = "regex-2020.7.14-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc"}, {file = "regex-2020.6.8-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:92d8a043a4241a710c1cf7593f5577fbb832cf6c3a00ff3fc1ff2052aff5dd89"},
{file = "regex-2020.7.14-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067"}, {file = "regex-2020.6.8-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bae83f2a56ab30d5353b47f9b2a33e4aac4de9401fb582b55c42b132a8ac3868"},
{file = "regex-2020.7.14-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd"}, {file = "regex-2020.6.8-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:b2ba0f78b3ef375114856cbdaa30559914d081c416b431f2437f83ce4f8b7f2f"},
{file = "regex-2020.7.14-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88"}, {file = "regex-2020.6.8-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:95fa7726d073c87141f7bbfb04c284901f8328e2d430eeb71b8ffdd5742a5ded"},
{file = "regex-2020.7.14-cp36-cp36m-win32.whl", hash = "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4"}, {file = "regex-2020.6.8-cp36-cp36m-win32.whl", hash = "sha256:e3cdc9423808f7e1bb9c2e0bdb1c9dc37b0607b30d646ff6faf0d4e41ee8fee3"},
{file = "regex-2020.7.14-cp36-cp36m-win_amd64.whl", hash = "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f"}, {file = "regex-2020.6.8-cp36-cp36m-win_amd64.whl", hash = "sha256:c78e66a922de1c95a208e4ec02e2e5cf0bb83a36ceececc10a72841e53fbf2bd"},
{file = "regex-2020.7.14-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162"}, {file = "regex-2020.6.8-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:08997a37b221a3e27d68ffb601e45abfb0093d39ee770e4257bd2f5115e8cb0a"},
{file = "regex-2020.7.14-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf"}, {file = "regex-2020.6.8-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2f6f211633ee8d3f7706953e9d3edc7ce63a1d6aad0be5dcee1ece127eea13ae"},
{file = "regex-2020.7.14-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7"}, {file = "regex-2020.6.8-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:55b4c25cbb3b29f8d5e63aeed27b49fa0f8476b0d4e1b3171d85db891938cc3a"},
{file = "regex-2020.7.14-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89"}, {file = "regex-2020.6.8-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:89cda1a5d3e33ec9e231ece7307afc101b5217523d55ef4dc7fb2abd6de71ba3"},
{file = "regex-2020.7.14-cp37-cp37m-win32.whl", hash = "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6"}, {file = "regex-2020.6.8-cp37-cp37m-win32.whl", hash = "sha256:690f858d9a94d903cf5cada62ce069b5d93b313d7d05456dbcd99420856562d9"},
{file = "regex-2020.7.14-cp37-cp37m-win_amd64.whl", hash = "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204"}, {file = "regex-2020.6.8-cp37-cp37m-win_amd64.whl", hash = "sha256:1700419d8a18c26ff396b3b06ace315b5f2a6e780dad387e4c48717a12a22c29"},
{file = "regex-2020.7.14-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99"}, {file = "regex-2020.6.8-cp38-cp38-manylinux1_i686.whl", hash = "sha256:654cb773b2792e50151f0e22be0f2b6e1c3a04c5328ff1d9d59c0398d37ef610"},
{file = "regex-2020.7.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e"}, {file = "regex-2020.6.8-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:52e1b4bef02f4040b2fd547357a170fc1146e60ab310cdbdd098db86e929b387"},
{file = "regex-2020.7.14-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e"}, {file = "regex-2020.6.8-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:cf59bbf282b627130f5ba68b7fa3abdb96372b24b66bdf72a4920e8153fc7910"},
{file = "regex-2020.7.14-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a"}, {file = "regex-2020.6.8-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:5aaa5928b039ae440d775acea11d01e42ff26e1561c0ffcd3d805750973c6baf"},
{file = "regex-2020.7.14-cp38-cp38-win32.whl", hash = "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341"}, {file = "regex-2020.6.8-cp38-cp38-win32.whl", hash = "sha256:97712e0d0af05febd8ab63d2ef0ab2d0cd9deddf4476f7aa153f76feef4b2754"},
{file = "regex-2020.7.14-cp38-cp38-win_amd64.whl", hash = "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840"}, {file = "regex-2020.6.8-cp38-cp38-win_amd64.whl", hash = "sha256:6ad8663c17db4c5ef438141f99e291c4d4edfeaacc0ce28b5bba2b0bf273d9b5"},
{file = "regex-2020.7.14.tar.gz", hash = "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb"}, {file = "regex-2020.6.8.tar.gz", hash = "sha256:e9b64e609d37438f7d6e68c2546d2cb8062f3adb27e6336bc129b51be20773ac"},
] ]
requests = [ requests = [
{file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"}, {file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"},
{file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"}, {file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"},
] ]
six = [ six = [
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
@ -739,24 +711,18 @@ typed-ast = [
{file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"},
{file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"},
{file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"},
{file = "typed_ast-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c"},
{file = "typed_ast-1.4.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072"},
{file = "typed_ast-1.4.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91"},
{file = "typed_ast-1.4.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d"},
{file = "typed_ast-1.4.1-cp39-cp39-win32.whl", hash = "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395"},
{file = "typed_ast-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c"},
{file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"},
] ]
urllib3 = [ urllib3 = [
{file = "urllib3-1.25.10-py2.py3-none-any.whl", hash = "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"}, {file = "urllib3-1.25.9-py2.py3-none-any.whl", hash = "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"},
{file = "urllib3-1.25.10.tar.gz", hash = "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a"}, {file = "urllib3-1.25.9.tar.gz", hash = "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527"},
] ]
waitress = [ waitress = [
{file = "waitress-1.4.4-py2.py3-none-any.whl", hash = "sha256:3d633e78149eb83b60a07dfabb35579c29aac2d24bb803c18b26fb2ab1a584db"}, {file = "waitress-1.4.4-py2.py3-none-any.whl", hash = "sha256:3d633e78149eb83b60a07dfabb35579c29aac2d24bb803c18b26fb2ab1a584db"},
{file = "waitress-1.4.4.tar.gz", hash = "sha256:1bb436508a7487ac6cb097ae7a7fe5413aefca610550baf58f0940e51ecfb261"}, {file = "waitress-1.4.4.tar.gz", hash = "sha256:1bb436508a7487ac6cb097ae7a7fe5413aefca610550baf58f0940e51ecfb261"},
] ]
watchdog = [ watchdog = [
{file = "watchdog-0.10.3.tar.gz", hash = "sha256:4214e1379d128b0588021880ccaf40317ee156d4603ac388b9adcf29165e0c04"}, {file = "watchdog-0.10.2.tar.gz", hash = "sha256:c560efb643faed5ef28784b2245cf8874f939569717a4a12826a173ac644456b"},
] ]
werkzeug = [ werkzeug = [
{file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"}, {file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"},

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "jellyfin-accounts" name = "jellyfin-accounts"
version = "0.3.9" version = "0.3.7"
readme = "README.md" readme = "README.md"
description = "A simple account management system for Jellyfin" description = "A simple account management system for Jellyfin"
authors = ["Harvey Tindall <harveyltindall@gmail.com>"] authors = ["Harvey Tindall <harveyltindall@gmail.com>"]
@ -29,7 +29,6 @@ python-dateutil = "^2.8.1"
watchdog = "^0.10.2" watchdog = "^0.10.2"
waitress = "^1.4.3" waitress = "^1.4.3"
packaging = "^20.4" packaging = "^20.4"
psutil = "^5.7.2"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
neovim = "^0.3.1" neovim = "^0.3.1"

View File

@ -2,40 +2,14 @@
import sass import sass
import subprocess import subprocess
import shutil import shutil
import os
import argparse
from pathlib import Path from pathlib import Path
parser = argparse.ArgumentParser()
parser.add_argument(
"-y", "--yes", help="use assumed node bin directory.", action="store_true"
)
def runcmd(cmd): def runcmd(cmd):
if os.name == "nt":
return subprocess.check_output(cmd, shell=True)
proc = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE) proc = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE)
return proc.communicate() return proc.communicate()
local_path = Path(__file__).resolve().parent local_path = Path(__file__).resolve().parent
out = runcmd("npm bin") node_bin = local_path.parent / 'node_modules' / '.bin'
try:
node_bin = Path(out[0].decode('utf-8').rstrip())
except:
node_bin = Path(out.decode('utf-8').rstrip())
args = parser.parse_args()
if not args.yes:
print(f"assuming npm bin directory \"{node_bin}\". Is this correct?")
if input("[yY/nN]: ").lower() == "n":
node_bin = local_path.parent / 'node_modules' / '.bin'
print(f"this? \"{node_bin}\"")
if input("[yY/nN]: ").lower() == "n":
node_bin = input("input bin directory: ")
for bsv in [d for d in local_path.iterdir() if 'bs' in d.name]: for bsv in [d for d in local_path.iterdir() if 'bs' in d.name]:
scss = bsv / f'{bsv.name}-jf.scss' scss = bsv / f'{bsv.name}-jf.scss'
@ -47,11 +21,7 @@ for bsv in [d for d in local_path.iterdir() if 'bs' in d.name]:
precision=6)) precision=6))
if css.exists(): if css.exists():
print(f'{bsv.name}: Compiled.') print(f'{bsv.name}: Compiled.')
# postcss only excepts forwards slashes? weird. runcmd(f'{str((node_bin / "postcss").resolve())} {str(css.resolve())} --replace --use autoprefixer')
cssPath = str(css.resolve())
if os.name == 'nt':
cssPath = cssPath.replace('\\', '/')
runcmd(f'{str((node_bin / "postcss").resolve())} {cssPath} --replace --use autoprefixer')
print(f'{bsv.name}: Prefixed.') print(f'{bsv.name}: Prefixed.')
runcmd(f'{str((node_bin / "cleancss").resolve())} --level 1 --format breakWith=lf --output {str(min_css.resolve())} {str(css.resolve())}') runcmd(f'{str((node_bin / "cleancss").resolve())} --level 1 --format breakWith=lf --output {str(min_css.resolve())} {str(css.resolve())}')
if min_css.exists(): if min_css.exists():

View File

@ -1,25 +1,17 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import subprocess import subprocess
import os
from pathlib import Path from pathlib import Path
def runcmd(cmd): def runcmd(cmd):
if os.name == "nt":
return subprocess.check_output(cmd, shell=True)
proc = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE) proc = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE)
return proc.communicate() return proc.communicate()
print('Installing npm packages') print('Installing npm packages')
root_path = Path(__file__).parents[1] root_path = Path(__file__).parents[1]
if os.name == 'nt': runcmd(f'npm install --prefix {root_path}')
root_path /= 'node_modules'
runcmd(f'npm install')
if (root_path / 'node_modules' / 'cleancss').exists(): if (root_path / 'node_modules' / 'cleancss').exists():
print(f'Installed successfully in {str((root_path / "node_modules").resolve())}.') print(f'Installed successfully in {str((root_path / "node_modules").resolve())}.')