Compare commits

...

52 commits
v0.2.0 ... main

Author SHA1 Message Date
semantic-release
8846414940 0.7.2
Automatically generated by python-semantic-release
2021-10-22 10:59:19 -04:00
ada7c07eac fix: add exception for socket connection refused 2021-10-22 10:58:45 -04:00
semantic-release
9183302a1f 0.7.0
Automatically generated by python-semantic-release
2021-10-22 10:57:29 -04:00
bbfe16ed28 fix: add exception for socket connection refused 2021-10-22 10:57:18 -04:00
15c165074b refactor: correct type hinting for certs 2021-10-08 09:59:57 -04:00
semantic-release
036692ba35 0.7.1
Automatically generated by python-semantic-release
2021-10-07 15:27:11 -04:00
529a90773f Merge branch 'develop' 2021-10-07 15:26:52 -04:00
21076b20c7 refactor: add hostinfo type for type validation 2021-10-07 15:26:46 -04:00
d4eb075911 Merge branch 'develop' 2021-10-07 15:19:13 -04:00
1a2fab6135 refactor: move output to a print function rather than directly in main logic 2021-10-07 15:18:42 -04:00
593b221137 0.7.0 2021-10-07 09:49:55 -04:00
semantic-release
252f74b59e 0.7.0
Automatically generated by python-semantic-release
2021-10-07 09:49:08 -04:00
5f35baebb9 docs: add intro readme for docs building 2021-10-07 09:48:19 -04:00
cc23ddc7c7 docs: add shell completion information 2021-10-04 16:53:36 -04:00
457b36e1da feat(completion): add completion scripts 2021-10-04 16:48:25 -04:00
c22f282a61 docs: update full doc url in readme 2021-10-04 14:51:53 -04:00
c6d657952d test: fix separator testing issues
test: add coverage for cert-not-in-san case
2021-10-04 14:47:45 -04:00
18a59afead docs: remove personal "you/I/me" 2021-10-04 14:17:40 -04:00
0ab0fb8000 docs: add full documentation site 2021-10-04 14:08:12 -04:00
6092237594 docs: update changelog for 0.6.0 2021-10-04 14:02:45 -04:00
semantic-release
5455503445 0.6.0
Automatically generated by python-semantic-release
2021-10-04 14:01:49 -04:00
ddf0614712 docs: update docs to reflect all current options 2021-10-04 14:00:55 -04:00
d3ce964ded fix: default valid output text to "false", override with "--valid" 2021-10-04 13:59:55 -04:00
978b42ae57 build: add docs to make 2021-10-04 12:37:58 -04:00
185915df34 docs: add installation section 2021-10-04 12:29:40 -04:00
b1e0864117 Merge branch 'develop' 2021-10-04 12:18:48 -04:00
7326894353 docs: enable rtd theme 2021-10-04 12:18:43 -04:00
294b2f0ad2 Merge branch 'develop' 2021-10-04 12:09:45 -04:00
667e1c174e docs: add rough intro to test rtd integration 2021-10-04 12:09:35 -04:00
f21cf8b100 docs: add docs dir for handling sphinx-based documentation 2021-10-04 12:01:57 -04:00
a32e116b2a feat: add separator for --san-only
feat: add option to prefix the separator in the output
2021-10-04 10:51:16 -04:00
semantic-release
1cdd2746c6 0.5.0
Automatically generated by python-semantic-release
2021-10-04 10:03:57 -04:00
157da703b7 feat: add ability to output just the sans in a space separated list
tests: add test for --san-only
2021-10-04 09:58:43 -04:00
61a92c5c63 refactor: use Any as a workaround for specific types that haven't been imported yet 2021-10-01 17:00:32 -04:00
96146be161 refactor: add type hints to all functions 2021-10-01 15:07:06 -04:00
5c4171758c refactor: cleanup main logic to reduce branches
fix: undo linting cheat
2021-10-01 14:59:38 -04:00
6c5c368e68 cheating pylint by bumping up max-branches by 3 2021-10-01 12:43:09 -04:00
03cadb90cf update changelog 2021-10-01 12:30:53 -04:00
semantic-release
840044ab28 0.4.0
Automatically generated by python-semantic-release
2021-10-01 12:30:11 -04:00
498a581626 feat: add text output for cert validity in addition to color 2021-10-01 12:29:03 -04:00
ca17b3a871 chore: add ci directory for test input data 2021-10-01 12:21:11 -04:00
971bf6aec7 refactor: align output data rather than heading for output 2021-10-01 12:20:07 -04:00
ef5756bd95 test: add test for file input 2021-10-01 12:19:47 -04:00
310a9f7ffe chore: correct lint error on file encoding 2021-10-01 10:22:05 -04:00
cee20e2c7c docs: update readme with more details 2021-10-01 10:14:03 -04:00
semantic-release
386a983b79 0.3.0
Automatically generated by python-semantic-release
2021-10-01 10:05:28 -04:00
dd39b24277 feat: add option to get host names from an external file 2021-10-01 10:05:12 -04:00
5b59455bef chore: fix spelling 2021-09-30 16:34:22 -04:00
0f43276109 test: refactor test code for pep8 2021-09-30 16:31:19 -04:00
26638e7e73 test: update tests to handle all branches
chore: ignore the except clauses for coverage
2021-09-30 16:23:00 -04:00
9a1a960641 refactor: clean-up for pylint 2021-09-30 16:14:22 -04:00
d15740d3e9 docs: update docs with a _little_ more detail 2021-09-30 15:21:17 -04:00
25 changed files with 1192 additions and 132 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@
db.sqlite3 db.sqlite3
dist dist
.coverage .coverage
docs/build

View file

@ -45,7 +45,7 @@ limit-inference-results=100
# List of plugins (as comma separated values of python module names) to load, # List of plugins (as comma separated values of python module names) to load,
# usually to register additional checkers. # usually to register additional checkers.
load-plugins=pylint_django load-plugins=
# Pickle collected data for later comparisons. # Pickle collected data for later comparisons.
persistent=yes persistent=yes
@ -548,7 +548,7 @@ valid-metaclass-classmethod-first-arg=cls
ignored-parents= ignored-parents=
# Maximum number of arguments for function / method. # Maximum number of arguments for function / method.
max-args=5 max-args=50
# Maximum number of attributes for a class (see R0902). # Maximum number of attributes for a class (see R0902).
max-attributes=7 max-attributes=7

View file

@ -1,7 +1,106 @@
# Changelog # Changelog
## Unreleased (2021-09-28) ## v0.7.0 (2021-10-07)
#### New Features #### New Features
* (completion): add completion scripts
#### Docs
* add intro readme for docs building
* add shell completion information
* update full doc url in readme
* remove personal "you/I/me"
* add full documentation site
* update changelog for 0.6.0
#### Others
* fix separator testing issues
Full set of changes: [`v0.6.0...v0.7.0`](https://git.admin.franklin.edu/tins/checkcert/compare/v0.6.0...v0.7.0)
## v0.6.0 (2021-10-04)
#### New Features
* add separator for --san-only
#### Fixes
* default valid output text to "false", override with "--valid"
#### Docs
* update docs to reflect all current options
* add installation section
* enable rtd theme
* add rough intro to test rtd integration
* add docs dir for handling sphinx-based documentation
#### Others
* add docs to make
Full set of changes: [`v0.5.0...v0.6.0`](https://git.admin.franklin.edu/tins/checkcert/compare/v0.5.0...v0.6.0)
## v0.5.0 (2021-10-04)
#### New Features
* add ability to output just the sans in a space separated list
#### Refactorings
* use Any as a workaround for specific types that haven't been imported yet
* add type hints to all functions
* cleanup main logic to reduce branches
Full set of changes: [`v0.4.0...v0.5.0`](https://git.admin.franklin.edu/tins/checkcert/compare/v0.4.0...v0.5.0)
## v0.4.0 (2021-10-01)
#### New Features
* add text output for cert validity in addition to color
#### Refactorings
* align output data rather than heading for output
#### Docs
* update readme with more details
#### Others
* add ci directory for test input data
* correct lint error on file encoding
* add test for file input
Full set of changes: [`v0.3.0...v0.4.0`](https://git.admin.franklin.edu/tins/checkcert/compare/v0.3.0...v0.4.0)
## v0.3.0 (2021-10-01)
#### New Features
* add option to get host names from an external file
#### Refactorings
* clean-up for pylint
#### Docs
* update docs with a _little_ more detail
#### Others
* fix spelling
* refactor test code for pep8
* update tests to handle all branches
Full set of changes: [`v0.2.0...v0.3.0`](https://git.admin.franklin.edu/tins/checkcert/compare/v0.2.0...v0.3.0)
## v0.2.0 (2021-09-30)
#### New Features
* add color output for validity check of cert
* add option to display the text version of the x509 cert * add option to display the text version of the x509 cert
#### Fixes
* remove duplication of output
#### Others
* add base coverage for all functions
* add initial test cases

View file

@ -18,6 +18,10 @@ dir:
clean: clean:
rm -rf $(BUILDDIR) rm -rf $(BUILDDIR)
find . -name __pycache__|xargs rm -rf find . -name __pycache__|xargs rm -rf
rm -rf docs/build
docs:
cd docs && make html
poetry-release: build poetry-release: build
poetry publish poetry publish
@ -49,4 +53,4 @@ tea-release: build
release: poetry-release release: poetry-release
.PHONY: dir clean release gh-release poetry-release coverage tea-release .PHONY: dir clean release gh-release poetry-release coverage tea-release docs

View file

@ -1 +1,51 @@
get information about certificates, by default will output the expiration date # checkcert
This utility was based off of [this
gist](https://gist.github.com/gdamjan/55a8b9eec6cf7b771f92021d93b87b2c).
checkcert has the logic of that gist wrapped in a click-based CLI and added command-line options
(checkcert --help to see them)
Full documentation is available at
[https://checkcert.readthedocs.io](https://checkcert.readthedocs.io)
# Installation
## from PyPi
pip install checkert
# Usage
When you run `pip install checkcert`, you will get a `checkcert` command. To
show all the options, simply run `checkcert --help` to get the most-current list
of commands and options.
### Basic Usage
The basic usage is `checkcert example.com`
### Check cert with an alternate port
Anywhere you specify the host, you may use the format `host:port` to specify an
alternate port. If no port is specified, 443 will be used. To check something
running on port 8081 for example, execute `checkcert example.com:8081`
### Multiple domains
checkcert will take all domains specified on the command line. Multiple values
may be specified as `checkcert example.com www.example.com alt.example.com:444`
### Domain list from a file
checkcert can be instructed to pull the list of domains from a file instead with
the --filename option. The file contents will just be a domain per line
(specified in host:port format, or just host to default to port 443)
create a file named domains.txt with contents like the following
```
example.com
www.example.com
alt.example.com:444
```
Then execute `checkcert --filename domains.txt`

View file

@ -1,8 +0,0 @@
#!/usr/bin/env python
import re
import sys
from checkcert.checkcert import main
if __name__ == "__main__":
sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0])
sys.exit(main())

View file

@ -1,91 +1,132 @@
# heavily modified version of https://gist.githubusercontent.com/gdamjan/55a8b9eec6cf7b771f92021d93b87b2c/raw/d8dc194ec4d0187f985a57138019d04e3a59b51f/ssl-check.py """ CLI to get basic information about certificates and determine validity"""
# to give a CLI for passing the hosts to check and other optional output import sys
from collections import namedtuple
import concurrent.futures
from socket import socket
from typing import List, Tuple, Any
import click import click
from OpenSSL import SSL from OpenSSL import SSL
from OpenSSL import crypto from OpenSSL import crypto
from OpenSSL.crypto import X509
from cryptography import x509 from cryptography import x509
from cryptography.x509.oid import NameOID from cryptography.x509.oid import NameOID
import idna import idna
import sys
from socket import socket
from collections import namedtuple
__version__ = "0.2.0"
HostInfo = namedtuple(
field_names="cert hostname peername is_valid", typename="HostInfo"
)
def get_certificate(hostname, port): __version__ = "0.7.2"
HostInfo = namedtuple("HostInfo", ["cert", "hostname", "peername", "is_valid"])
def get_certificate(hostname: str, port: int) -> HostInfo:
"""retrieve certificate details and return HostInfo tuple of values"""
hostname_idna = idna.encode(hostname) hostname_idna = idna.encode(hostname)
sock = socket() sock = socket()
try: try:
sock.connect((hostname, port)) sock.connect((hostname, port))
peername = sock.getpeername()
ctx = SSL.Context(SSL.SSLv23_METHOD) # most compatible
ctx.check_hostname = False
ctx.verify_mode = SSL.VERIFY_NONE
sock_ssl = SSL.Connection(ctx, sock)
sock_ssl.set_connect_state()
sock_ssl.set_tlsext_host_name(hostname_idna)
sock_ssl.do_handshake()
cert = sock_ssl.get_peer_certificate()
crypto_cert = cert.to_cryptography()
sock_ssl.close()
sock.close()
except ConnectionRefusedError: except ConnectionRefusedError:
pass print("Error: Connection Refused")
sys.exit(3)
peername = sock.getpeername()
ctx = SSL.Context(SSL.SSLv23_METHOD) # most compatible
sock_ssl = SSL.Connection(ctx, sock)
sock_ssl.set_connect_state()
sock_ssl.set_tlsext_host_name(hostname_idna)
sock_ssl.do_handshake()
cert = sock_ssl.get_peer_certificate()
crypto_cert = cert.to_cryptography()
sock_ssl.close()
sock.close()
return HostInfo( return HostInfo(
cert=crypto_cert, cert=crypto_cert,
peername=peername, peername=peername,
hostname=hostname, hostname=hostname,
is_valid=not cert.has_expired(), is_valid=not cert.has_expired(), # is_valid is the inverse of has_expired
) )
def get_alt_names(cert): def get_alt_names(cert: x509.Certificate) -> Any:
"""retrieve the SAN values for given cert"""
try: try:
ext = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName) ext = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName)
return ext.value.get_values_for_type(x509.DNSName) return ext.value.get_values_for_type(x509.DNSName)
except x509.ExtensionNotFound: except x509.ExtensionNotFound: # pragma: no cover
return None return None
def get_x509_text(cert): def get_x509_text(cert: X509) -> bytes:
"""return the human-readable text version of the certificate"""
return crypto.dump_certificate(crypto.FILETYPE_TEXT, cert) return crypto.dump_certificate(crypto.FILETYPE_TEXT, cert)
def get_common_name(cert): def get_common_name(cert: x509.Certificate) -> Any:
"""Return the common name from the certificate"""
try: try:
names = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME) names = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
return names[0].value return names[0].value
except x509.ExtensionNotFound: except x509.ExtensionNotFound: # pragma: no cover
return None return None
def get_issuer(cert): def get_issuer(cert: x509.Certificate) -> Any:
"""Return the name of the CA/Issuer of the certificate"""
try: try:
names = cert.issuer.get_attributes_for_oid(NameOID.COMMON_NAME) names = cert.issuer.get_attributes_for_oid(NameOID.COMMON_NAME)
return names[0].value return names[0].value
except x509.ExtensionNotFound: except x509.ExtensionNotFound: # pragma: no cover
return None return None
def print_basic_info(hostinfo): def get_host_list_tuple(hosts: list) -> List[Tuple[str, int]]:
print( """create a tuple of host and port based on hosts given to us in the form
f""" host:port
{hostinfo.hostname} ({hostinfo.peername[0]}:{hostinfo.peername[1]}) """
\tcommonName: {get_common_name(hostinfo.cert)} all_hosts = []
\tSAN: {get_alt_names(hostinfo.cert)} for host in hosts:
\tissuer: {get_issuer(hostinfo.cert)} # if a host has a : in it, split on the :, first field will be host
\tnotBefore: {hostinfo.cert.not_valid_before} # second field will be the port
\tnotAfter: {hostinfo.cert.not_valid_after} if ":" in host:
""" host_info = host.split(":")
all_hosts.append((host_info[0], int(host_info[1])))
else:
all_hosts.append((host, 443))
return all_hosts
def print_output(hostinfo: HostInfo, settings: dict) -> None:
"""print the output of hostinfo conditionally based on items in settings"""
output_string: str = ""
if settings["san_only"]:
if settings["pre"]:
output_string += f"{settings['sep']}".lstrip()
alt_names = get_alt_names(hostinfo.cert)
if hostinfo.hostname not in alt_names:
alt_names.insert(0, hostinfo.hostname)
output_string += f"{settings['sep']}".join(alt_names)
print(output_string)
# in the san-only branch, once we get to this point the output is
# already complete, so just return to end the function processing
return None
output_string += (
f"\n{hostinfo.hostname} " f"({hostinfo.peername[0]}:{hostinfo.peername[1]})\n"
) )
output_string += f" commonName: {get_common_name(hostinfo.cert)}\n"
output_string += f" issuer: {get_issuer(hostinfo.cert)}\n"
output_string += f" notBefore: {hostinfo.cert.not_valid_before}\n"
output_string += f" notAfter: {hostinfo.cert.not_valid_after}\n"
if settings["valid"]:
output_string += f" Valid: {hostinfo.is_valid}\n"
if settings["san"]:
output_string += f" SAN: {get_alt_names(hostinfo.cert)}\n"
if hostinfo.is_valid and settings["color"]:
click.echo(click.style(output_string, fg="green"))
elif not hostinfo.is_valid and settings["color"]:
click.echo(click.style(output_string, fg="red"))
else:
click.echo(click.style(output_string))
return None
@click.command() @click.command()
@ -94,43 +135,58 @@ def print_basic_info(hostinfo):
@click.option( @click.option(
"--dump", is_flag=True, help="Dump the full text version of the x509 certificate" "--dump", is_flag=True, help="Dump the full text version of the x509 certificate"
) )
@click.option("--color/--no-color", default=True) @click.option(
"--color/--no-color",
default=True,
help="Enable/disable ANSI color output to show cert validity",
)
@click.option(
"--filename",
"-f",
type=click.Path(),
help="Read a list of hosts to check from a file",
)
@click.option(
"--valid/--no-valid", default=False, help="Show True/False for cert validity"
)
@click.option(
"--san-only",
"-o",
is_flag=True,
help="Output only the SAN names to use in passing to certbot for example",
)
@click.option(
"--pre/--no-pre", default=False, help="prefix the --san-only with the separator"
)
@click.option("--sep", "-s", default=" ", help="Separator to use in --san-only output")
@click.argument("hosts", nargs=-1) @click.argument("hosts", nargs=-1)
def main(san, dump, color, hosts): def main(san, dump, color, filename, valid, san_only, sep, pre, hosts):
"""Return information about certificates given including their validity"""
# setup the list of tuples # setup the list of tuples
HOSTS = []
# handle a domain given with a : in it to specify the port # handle a domain given with a : in it to specify the port
for host in hosts: if filename:
# if a host has a : in it, split on the :, first field will be host hosts = []
# second field will be the port with open(filename, "r", encoding="utf-8") as infile:
if ":" in host: for line in infile:
host_info = host.split(":") line = line.strip()
HOSTS.append((host_info[0], int(host_info[1]))) hosts.append(line)
else: all_hosts = get_host_list_tuple(hosts)
HOSTS.append((host, 443)) with concurrent.futures.ThreadPoolExecutor(max_workers=4) as epool:
for hostinfo in map(lambda x: get_certificate(x[0], x[1]), HOSTS): for hostinfo in epool.map(lambda x: get_certificate(x[0], x[1]), all_hosts):
output_string = "" if dump:
if dump: print(get_x509_text(hostinfo.cert).decode())
print(get_x509_text(hostinfo.cert).decode())
else:
output_string += (
f"{hostinfo.hostname} ({hostinfo.peername[0]}:{hostinfo.peername[1]})\n"
)
output_string += f"\tcommonName: {get_common_name(hostinfo.cert)}\n"
if san:
output_string += f"\tSAN: {get_alt_names(hostinfo.cert)}\n"
output_string += f"\tissuer: {get_issuer(hostinfo.cert)}\n"
output_string += f"\tnotBefore: {hostinfo.cert.not_valid_before}\n"
output_string += f"\tnotAfter: {hostinfo.cert.not_valid_after}\n\n"
if hostinfo.is_valid and color:
click.echo(click.style(output_string, fg="green"))
elif not hostinfo.is_valid and color:
click.echo(click.style(output_string, fg="red"))
else: else:
click.echo(click.style(output_string)) settings_dict = {
"san": san,
# print(f"Certificate for {domain}\nexpires after: {x509.get_not_after()}") "sep": sep,
"dump": dump,
"color": color,
"valid": valid,
"san_only": san_only,
"pre": pre,
}
print_output(hostinfo, settings_dict)
if __name__ == "__main__": if __name__ == "__main__":
main() # pragma: no cover main() # pylint: disable=no-value-for-parameter # pragma: no cover

3
ci/test_domains.txt Normal file
View file

@ -0,0 +1,3 @@
example.com
www.example.com
example.net

View file

@ -0,0 +1,29 @@
_checkcert_completion() {
local IFS=$'\n'
local response
response=$(env COMP_WORDS="${COMP_WORDS[*]}" COMP_CWORD=$COMP_CWORD _CHECKCERT_COMPLETE=bash_complete $1)
for completion in $response; do
IFS=',' read type value <<< "$completion"
if [[ $type == 'dir' ]]; then
COMREPLY=()
compopt -o dirnames
elif [[ $type == 'file' ]]; then
COMREPLY=()
compopt -o default
elif [[ $type == 'plain' ]]; then
COMPREPLY+=($value)
fi
done
return 0
}
_checkcert_completion_setup() {
complete -o nosort -F _checkcert_completion checkcert
}
_checkcert_completion_setup;

View file

@ -0,0 +1,35 @@
#compdef checkcert
_checkcert_completion() {
local -a completions
local -a completions_with_descriptions
local -a response
(( ! $+commands[checkcert] )) && return 1
response=("${(@f)$(env COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) _CHECKCERT_COMPLETE=zsh_complete checkcert)}")
for type key descr in ${response}; do
if [[ "$type" == "plain" ]]; then
if [[ "$descr" == "_" ]]; then
completions+=("$key")
else
completions_with_descriptions+=("$key":"$descr")
fi
elif [[ "$type" == "dir" ]]; then
_path_files -/
elif [[ "$type" == "file" ]]; then
_path_files -f
fi
done
if [ -n "$completions_with_descriptions" ]; then
_describe -V unsorted completions_with_descriptions -U
fi
if [ -n "$completions" ]; then
compadd -U -V unsorted -a completions
fi
}
compdef _checkcert_completion checkcert;

20
docs/Makefile Normal file
View file

@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

4
docs/README.md Normal file
View file

@ -0,0 +1,4 @@
# build pdf docs
If you want to build the pdf documentation via ``make latexpdf`` be warned that it requires a lot of packages. For fedora, the following should get all the dependencies to make it work:
``dnf -y install texlive-gsftop texlive-makeindexk texlive-parskip texlive-tabulary texlive-needspace texlive-upquote texlive-fancyvrb texlive-framed texlive-capt-of texlive-wrapfig texlive-float texlive-fncychap texlive-tex-gyre texlive-babel texlive-amsfonts texlive-amsmath texlive-ec texlive-cmap latexmk``

57
docs/source/conf.py Normal file
View file

@ -0,0 +1,57 @@
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- Project information -----------------------------------------------------
import sphinx_rtd_theme
project = "checkcert"
copyright = "2021, Alex Kelly"
author = "Alex Kelly"
# The full version, including alpha/beta/rc tags
release = "0.5.0"
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
"sphinx_rtd_theme",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = []
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = "sphinx_rtd_theme"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

BIN
docs/source/images/san.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

16
docs/source/index.rst Normal file
View file

@ -0,0 +1,16 @@
.. checkcert documentation master file, created by
sphinx-quickstart on Mon Oct 4 12:00:40 2021.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to checkcert's documentation!
=====================================
.. toctree::
:maxdepth: 2
:caption: Contents:
introduction
install
running
shell_completion

26
docs/source/install.rst Normal file
View file

@ -0,0 +1,26 @@
Installation
============
From Pypi
---------
The easiest way is to install via pypi via:
``pip install checkcert``
If you already have checkcert installed and wish to update it, run:
``pip install -U checkcert``
This will upgrade the currently-installed version to the latest.
From Git
--------
There are many methods for running within a virtual environment isolated for a particular package. Certcheck has been built with poetry, so this document details that method, but there are certainly other ways to accomplish the same thing
1. clone the repo ``git clone https://github.com/kellya/checkcert.git``
2. cd to the directory you just cloned ``cd checkcert``
3. install the required packages ``poetry install``
4. activate the poetry-built venv ``poetry shell``
5. run with ``python certcheck/certcheck.py``

View file

@ -0,0 +1,4 @@
Introduction
============
checkcert is a CLI to check a remote site secured with a certificate. It will allow you to get basic information about the certificate, if it is currently valid, any subjectAlternateNames that are present, and output content for ingestion into other utilities

214
docs/source/running.rst Normal file
View file

@ -0,0 +1,214 @@
Running checkcert
=================
This section assumes that checkcert is installed via pypi, and the ``certcheck`` entry-point in the path (which ``pip install checkcert`` should do automatically). If running from source, the full path to python and the checkcert.py script itself would need to be specified.
Basic query
-----------
To do a simple validation, run something like ``checkcert www.example.com``. This should yield output like:
.. figure:: images/basic.png
:alt: basic output example
Basic command output
The green text is showing that the certificate is valid. If the certificate had expired, the text would be displayed in Red. The ``--valid`` option may be specified to display a text represention of the cert validity in addtion to just coloring the output to show validity. When run with ``--valid``, the output will look like:
.. figure:: images/valid.png
:alt: output with valid option
Display cert validity in output text
.. note:: Output color is generated via Click's styling which should be aware of how the text is being handled and appropriately strip out the color sequences if output is being piped to another command, for example. The plain text output may be enforced with the ``--no-color`` option
Querying multiple domains
-------------------------
Multiple domains can be checked from a single command. Simply separate them on the command line with a space. So to check `www.example.com` and `www.example.org`, this can be run as:
``checkcert www.example.com www.example.org``
Which will generate:
.. figure:: images/multi.png
:alt: multi-domain output
Mutliple domains specified
Specify alternate ports
-----------------------
By default, the standard port `443` will be used. If a service is running on an alternate port, simply append `:portnum` to the domain. So if example.com was listening on 444 instead of 443, anywhere the domain is specified, the format ``example.com:444`` may be used instead.
Domains from a file
-------------------
While specifying a few domains on the commanline is fine, if there are many domains to check, an input file may be specified instead. Each domain should be on its own line. Alternate ports may be specified in this file too. For example, if a file named `domains.txt` is created with the following content:
.. code-block:: text
www.example.com
example.org
example.com:444
Then checkcert may be run as follows:
``checkcert --filename domains.txt``
Each line in the file will be parsed and checked the same as if it were run on the command line.
SubjectAlternateName handling
-----------------------------
checkcert has a `--san` option that will optionally show the subjectAlternateNames for a domain. This will output the same as previous iterations, but will add a list of SANs associated with the certificate retrieved.
.. figure:: images/san.png
:alt: subjectalternatename example
output with the SAN line
Just the SANs
^^^^^^^^^^^^^
It may be useful to output just the SANs for a given domain. This may be done by specifying the ``--san-only`` option. By default this will output all of SANs, separated by a space.
Running ``checkcert --san-only www.example.com`` will yield::
www.example.org example.com example.edu example.net example.org www.example.com www.example.edu www.example.net
If an alternate separator is desired, add the ``--sep <separator>`` option, so for comma separated, run ``checkcert --san-only --sep , www.example.com`` which will yield::
www.example.org,example.com,example.edu,example.net,example.org,www.example.com,www.example.edu,www.example.net
If it is desired to output that list to another command as commandline options (for example when migrating from a managed PKI front-end to using ACME/certbot). Certbot wants the domains specified as -d. checkcert may run as: ``checkcert --san-only --sep " -d "- www.example.com`` which will return::
www.example.org -d example.com -d example.edu -d example.net -d example.org -d www.example.com -d www.example.edu -d www.example.net
.. note:: the documentation for certbot specifies multiple domains should be specified each with their own '-d' option. certbot in particular seems to work with a comma-separated list too, even though the docs don't specify that it works that way.
Notice that the " -d " is used as a **separator** and therefore the first entry does not have a -d before it. To make -d show up before every entry (as would be required in this case) The ``--pre`` option must be added to "prefix" the entry with the separator; thus, running ``checkcert --san-only --sep " -d " --pre www.example.com`` will yield::
-d www.example.org -d example.com -d example.edu -d example.net -d example.org -d www.example.com -d www.example.edu -d www.example.net
.. note:: Since this is utilizing separator in a slightly different way, the space before the option must be specified, otherwise everything would run together. checkcert is aware of this and will strip off the space at the beginning if ``--pre`` is used.
This output could be directly included into certbot as ``certbot $(checkcert --san-only --sep " -d " --pre www.example.com")``
.. note:: Certbot was the specific reason this was added. As such, it will prepend the name queried to the list. The domain itself is not always included in the SAN list, checkcert will verify if the domain is in the list and will always prefix it if not. If example.com was not in the list of SANS, but ``checkcert --san --pre --sep " -d " example.com`` was used, '-d example.com' will be the first entry even if it was not part of the SANs.
Dumping the text version of a cert
----------------------------------
``--dump`` will output the textual version of the certificate presented. Running ``checkcert --dump www.example.com`` will return::
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
0f:be:08:b0:85:4d:05:73:8a:b0:cc:e1:c9:af:ee:c9
Signature Algorithm: sha256WithRSAEncryption
Issuer: C=US, O=DigiCert Inc, CN=DigiCert TLS RSA SHA256 2020 CA1
Validity
Not Before: Nov 24 00:00:00 2020 GMT
Not After : Dec 25 23:59:59 2021 GMT
Subject: C=US, ST=California, L=Los Angeles, O=Internet Corporation for Assigned Names and Numbers, CN=www.example.org
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (2048 bit)
Modulus:
00:ba:fc:ee:cc:ca:0a:08:ff:0e:93:1d:b3:be:0b:
9c:03:96:22:9e:b1:4f:10:ae:51:40:fd:53:5f:b3:
c4:61:40:28:04:ee:a2:e6:12:00:b0:82:85:98:5c:
6d:5b:6b:20:84:44:62:95:4e:6b:76:7c:50:70:5d:
df:13:1d:ec:63:83:ad:63:a5:52:04:f2:cf:84:ba:
db:2a:8c:c7:2e:b4:3c:64:df:eb:61:36:fe:86:03:
54:79:3e:cd:03:59:8f:ef:c2:04:93:10:23:e2:a1:
b9:b6:58:b8:26:ae:35:68:26:d4:94:2b:7b:7a:ab:
86:5e:89:08:9a:10:be:51:8e:48:a5:01:19:4b:4b:
4a:0f:8b:ee:da:4b:19:d3:84:1e:b6:9d:24:f2:35:
9d:02:f3:00:db:b5:b7:13:08:07:1c:d7:95:19:66:
c9:3c:2d:03:9f:b4:6a:3f:0d:77:af:b8:45:c9:2e:
53:a6:57:b2:c2:37:58:d6:70:7b:69:de:a4:71:95:
d9:6c:47:1a:15:9e:d9:b9:ea:c0:e9:19:0f:18:4f:
8f:b2:76:51:6f:5a:05:26:46:28:5e:29:ac:ba:f9:
15:16:15:9e:1d:05:c2:18:2d:5c:b8:35:92:ac:cd:
dc:a5:0b:ce:cc:a1:f6:bd:2e:dd:d7:9f:b3:1a:5b:
38:23
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Authority Key Identifier:
keyid:B7:6B:A2:EA:A8:AA:84:8C:79:EA:B4:DA:0F:98:B2:C5:95:76:B9:F4
X509v3 Subject Key Identifier:
26:1A:F8:E4:B1:B0:72:84:CE:DA:81:06:D2:27:98:FB:ED:3A:3D:17
X509v3 Subject Alternative Name:
DNS:www.example.org, DNS:example.com, DNS:example.edu, DNS:example.net, DNS:example.org, DNS:www.example.com, DNS:www.example.edu, DNS:www.example.net
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Server Authentication, TLS Web Client Authentication
X509v3 CRL Distribution Points:
Full Name:
URI:http://crl3.digicert.com/DigiCertTLSRSASHA2562020CA1.crl
Full Name:
URI:http://crl4.digicert.com/DigiCertTLSRSASHA2562020CA1.crl
X509v3 Certificate Policies:
Policy: 2.16.840.1.114412.1.1
CPS: https://www.digicert.com/CPS
Policy: 2.23.140.1.2.2
Authority Information Access:
OCSP - URI:http://ocsp.digicert.com
CA Issuers - URI:http://cacerts.digicert.com/DigiCertTLSRSASHA2562020CA1.crt
X509v3 Basic Constraints: critical
CA:FALSE
CT Precertificate SCTs:
Signed Certificate Timestamp:
Version : v1 (0x0)
Log ID : F6:5C:94:2F:D1:77:30:22:14:54:18:08:30:94:56:8E:
E3:4D:13:19:33:BF:DF:0C:2F:20:0B:CC:4E:F1:64:E3
Timestamp : Nov 24 19:32:04.334 2020 GMT
Extensions: none
Signature : ecdsa-with-SHA256
30:46:02:21:00:A4:6B:A8:D0:43:A4:F1:07:32:2D:ED:
9C:39:7D:77:E8:73:C1:9F:ED:22:4A:00:C5:BE:9A:C9:
B5:B6:12:DC:B1:02:21:00:8D:E8:5F:8A:C7:52:CD:0D:
A1:23:D5:B5:BB:DB:DB:62:13:88:22:D6:70:EC:83:5E:
3F:C9:AC:94:4C:8C:58:3A
Signed Certificate Timestamp:
Version : v1 (0x0)
Log ID : 5C:DC:43:92:FE:E6:AB:45:44:B1:5E:9A:D4:56:E6:10:
37:FB:D5:FA:47:DC:A1:73:94:B2:5E:E6:F6:C7:0E:CA
Timestamp : Nov 24 19:32:04.429 2020 GMT
Extensions: none
Signature : ecdsa-with-SHA256
30:45:02:20:6A:AC:11:FA:05:09:12:FF:9B:8E:89:30:
DF:0E:05:6E:CA:8E:59:CC:ED:B5:C2:0A:3C:33:34:A8:
B0:33:DA:AC:02:21:00:DA:D8:5C:51:6D:64:0A:A6:AA:
3D:8B:35:20:13:3A:6A:97:4F:76:B9:67:CB:BE:FC:CC:
A4:57:67:B4:3F:1B:BD
Signature Algorithm: sha256WithRSAEncryption
a7:2a:10:30:5c:b8:6b:7a:1b:f8:66:38:f6:e9:a0:0a:d5:13:
82:82:f8:65:89:57:a5:b8:eb:13:29:1d:84:6c:ec:fb:e3:05:
11:d7:1e:31:5e:0e:e2:c0:00:e5:6d:06:48:be:3d:55:6f:ba:
b7:11:35:b6:ea:c4:cf:84:f1:30:4c:bb:33:9e:11:17:2b:c9:
d2:19:4b:2c:d0:ad:5f:17:23:84:e1:df:17:a2:3b:a8:7f:69:
29:7c:48:a6:61:5f:26:3f:75:e2:3b:5b:a3:36:b3:1c:cd:e3:
04:57:30:1f:fc:c9:fa:4b:8e:48:80:58:27:9c:a2:c7:c3:26:
dc:17:02:fa:e6:6c:ea:81:01:5c:92:8f:d3:18:08:17:70:7a:
c2:a3:4b:6c:3a:fa:e3:cf:f6:fe:7e:c9:56:e5:a5:4e:1b:14:
4f:a9:98:9d:79:b1:1e:c3:ab:b1:0d:15:85:a9:46:b6:e5:c2:
58:e8:5a:fe:c8:14:28:68:90:c6:b8:c8:94:7f:e1:0f:89:fa:
a7:d6:09:37:a1:62:b7:00:27:b5:be:f1:b1:5e:45:28:06:b3:
54:15:e6:c3:c8:ac:82:01:ce:86:e2:2b:e1:7a:e4:bd:4c:cb:
9c:5e:d0:62:c2:61:bd:8b:5a:62:b6:76:30:bc:46:0f:e3:45:
23:c0:64:5f

View file

@ -0,0 +1,60 @@
Shell Completion
================
In order to use shell completion, checkcert must first be installed via ``pip install checkcert``. That generates the `checkcert` entry point that ultimately is used for the command completion. These instructions will not work if using a git cloned version.
Completion scripts are provided in the source's "completion" directory. You may copy those to source in your shell's rc files.
These steps are basically copied from Click's Documentation, since that is what is generating the completions. Check `Click's completion documentation <https://click.palletsprojects.com/en/8.0.x/shell-completion/>`_ for more details.
Obtaining from github
---------------------
Download the needed completion script from github. Using curl, you could download the zsh completion with
``curl -LO https://raw.githubusercontent.com/kellya/checkcert/main/completions/checkcert-complete.zsh``
Then just append ``source /path/to/checkcert-complete.zsh`` to your .zshrc.
Bash will be pretty much the same, just use:
``curl -LO https://raw.githubusercontent.com/kellya/checkcert/main/completions/checkcert-complete.sh`` instead
Generating Completion script
----------------------------
If you do not have access to the files from github, but have checkcert installed, you may generate the completion scripts for inclusion in your shell rc file.
zsh
^^^
``_CHECKCERT_COMPLETE=zsh_source checkcert > ~/.checkcert-complete.zsh``
Then in .zshrc, add a ``source ~/.checkcert-complete.zsh``
.. note:: There are various plugin directories that could be used to automatically install a completion. There are many options, so this doc just highlights a way that will work.
bash
^^^^
The bash method is pretty much the same as zsh.
``_CHECKCERT_COMPLETE=bash_source checkcert > ~/.checkcert-complete.bash``
Generating Completion via eval
------------------------------
Instead of generating a script to execute, you may use eval to generate the completions. This is a little quicker to implement; however there is a speed trade-off as the shell has to run this each time.
zsh
^^^
Execute the following: ``eval "$(_CHECKCERT_COMPLETE=zsh_source checkcert)"``. You may put this in your ~/.zshrc to persist the setting.
bash
^^^^
Execute the following: ``eval "$(_CHECKCERT_COMPLETE=bash_source checkcert)"``

400
poetry.lock generated
View file

@ -1,6 +1,14 @@
[[package]]
name = "alabaster"
version = "0.7.12"
description = "A configurable sidebar-enabled Sphinx theme"
category = "dev"
optional = false
python-versions = "*"
[[package]] [[package]]
name = "astroid" name = "astroid"
version = "2.8.0" version = "2.8.2"
description = "An abstract syntax tree for Python with inference support." description = "An abstract syntax tree for Python with inference support."
category = "dev" category = "dev"
optional = false optional = false
@ -33,6 +41,17 @@ docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"]
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"]
[[package]]
name = "babel"
version = "2.9.1"
description = "Internationalization utilities"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.dependencies]
pytz = ">=2015.7"
[[package]] [[package]]
name = "bleach" name = "bleach"
version = "4.1.0" version = "4.1.0"
@ -119,7 +138,7 @@ toml = ["toml"]
[[package]] [[package]]
name = "cryptography" name = "cryptography"
version = "3.4.8" version = "35.0.0"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
category = "main" category = "main"
optional = false optional = false
@ -132,9 +151,9 @@ cffi = ">=1.12"
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,!=3.1.0,!=3.1.1)", "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"] pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"]
sdist = ["setuptools-rust (>=0.11.4)"] sdist = ["setuptools_rust (>=0.11.4)"]
ssh = ["bcrypt (>=3.1.5)"] ssh = ["bcrypt (>=3.1.5)"]
test = ["pytest (>=6.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] test = ["pytest (>=6.2.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"]
[[package]] [[package]]
name = "docutils" name = "docutils"
@ -186,6 +205,14 @@ category = "dev"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
[[package]]
name = "imagesize"
version = "1.2.0"
description = "Getting image size from png/jpeg/jpeg2000/gif file"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]] [[package]]
name = "importlib-metadata" name = "importlib-metadata"
version = "4.8.1" version = "4.8.1"
@ -244,6 +271,20 @@ python-versions = ">=3.6"
test = ["pytest", "pytest-trio", "pytest-asyncio", "testpath", "trio", "async-timeout"] test = ["pytest", "pytest-trio", "pytest-asyncio", "testpath", "trio", "async-timeout"]
trio = ["trio", "async-generator"] trio = ["trio", "async-generator"]
[[package]]
name = "jinja2"
version = "3.0.2"
description = "A very fast and expressive template engine."
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
[[package]] [[package]]
name = "keyring" name = "keyring"
version = "23.2.1" version = "23.2.1"
@ -270,6 +311,14 @@ category = "dev"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
[[package]]
name = "markupsafe"
version = "2.0.1"
description = "Safely add untrusted strings to HTML/XML markup."
category = "dev"
optional = false
python-versions = ">=3.6"
[[package]] [[package]]
name = "mccabe" name = "mccabe"
version = "0.6.1" version = "0.6.1"
@ -278,6 +327,31 @@ category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
[[package]]
name = "mypy"
version = "0.910"
description = "Optional static typing for Python"
category = "dev"
optional = false
python-versions = ">=3.5"
[package.dependencies]
mypy-extensions = ">=0.4.3,<0.5.0"
toml = "*"
typing-extensions = ">=3.7.4"
[package.extras]
dmypy = ["psutil (>=4.0)"]
python2 = ["typed-ast (>=1.4.0,<1.5.0)"]
[[package]]
name = "mypy-extensions"
version = "0.4.3"
description = "Experimental type system extensions for programs checked with the mypy typechecker."
category = "dev"
optional = false
python-versions = "*"
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "21.0" version = "21.0"
@ -342,7 +416,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]] [[package]]
name = "pyflakes" name = "pyflakes"
version = "2.3.1" version = "2.4.0"
description = "passive checker of Python programs" description = "passive checker of Python programs"
category = "dev" category = "dev"
optional = false optional = false
@ -460,6 +534,14 @@ docs = ["Sphinx (==1.3.6)"]
mypy = ["mypy", "types-requests"] mypy = ["mypy", "types-requests"]
test = ["coverage (>=5,<6)", "pytest (>=5,<6)", "pytest-xdist (>=1,<2)", "pytest-mock (>=2,<3)", "responses (==0.13.3)", "mock (==1.3.0)"] test = ["coverage (>=5,<6)", "pytest (>=5,<6)", "pytest-xdist (>=1,<2)", "pytest-mock (>=2,<3)", "responses (==0.13.3)", "mock (==1.3.0)"]
[[package]]
name = "pytz"
version = "2021.3"
description = "World timezone definitions, modern and historical"
category = "dev"
optional = false
python-versions = "*"
[[package]] [[package]]
name = "pywin32-ctypes" name = "pywin32-ctypes"
version = "0.2.0" version = "0.2.0"
@ -470,7 +552,7 @@ python-versions = "*"
[[package]] [[package]]
name = "readme-renderer" name = "readme-renderer"
version = "29.0" version = "30.0"
description = "readme_renderer is a library for rendering \"readme\" descriptions for Warehouse" description = "readme_renderer is a library for rendering \"readme\" descriptions for Warehouse"
category = "dev" category = "dev"
optional = false optional = false
@ -480,10 +562,9 @@ python-versions = "*"
bleach = ">=2.1.0" bleach = ">=2.1.0"
docutils = ">=0.13.1" docutils = ">=0.13.1"
Pygments = ">=2.5.1" Pygments = ">=2.5.1"
six = "*"
[package.extras] [package.extras]
md = ["cmarkgfm (>=0.5.0,<0.6.0)"] md = ["cmarkgfm (>=0.5.0,<0.7.0)"]
[[package]] [[package]]
name = "requests" name = "requests"
@ -576,6 +657,131 @@ category = "dev"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
[[package]]
name = "snowballstemmer"
version = "2.1.0"
description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms."
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "sphinx"
version = "4.2.0"
description = "Python documentation generator"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
alabaster = ">=0.7,<0.8"
babel = ">=1.3"
colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""}
docutils = ">=0.14,<0.18"
imagesize = "*"
Jinja2 = ">=2.3"
packaging = "*"
Pygments = ">=2.0"
requests = ">=2.5.0"
snowballstemmer = ">=1.1"
sphinxcontrib-applehelp = "*"
sphinxcontrib-devhelp = "*"
sphinxcontrib-htmlhelp = ">=2.0.0"
sphinxcontrib-jsmath = "*"
sphinxcontrib-qthelp = "*"
sphinxcontrib-serializinghtml = ">=1.1.5"
[package.extras]
docs = ["sphinxcontrib-websupport"]
lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.900)", "docutils-stubs", "types-typed-ast", "types-pkg-resources", "types-requests"]
test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"]
[[package]]
name = "sphinx-rtd-theme"
version = "1.0.0"
description = "Read the Docs theme for Sphinx"
category = "dev"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
[package.dependencies]
docutils = "<0.18"
sphinx = ">=1.6"
[package.extras]
dev = ["transifex-client", "sphinxcontrib-httpdomain", "bump2version"]
[[package]]
name = "sphinxcontrib-applehelp"
version = "1.0.2"
description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books"
category = "dev"
optional = false
python-versions = ">=3.5"
[package.extras]
lint = ["flake8", "mypy", "docutils-stubs"]
test = ["pytest"]
[[package]]
name = "sphinxcontrib-devhelp"
version = "1.0.2"
description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document."
category = "dev"
optional = false
python-versions = ">=3.5"
[package.extras]
lint = ["flake8", "mypy", "docutils-stubs"]
test = ["pytest"]
[[package]]
name = "sphinxcontrib-htmlhelp"
version = "2.0.0"
description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.extras]
lint = ["flake8", "mypy", "docutils-stubs"]
test = ["pytest", "html5lib"]
[[package]]
name = "sphinxcontrib-jsmath"
version = "1.0.1"
description = "A sphinx extension which renders display math in HTML via JavaScript"
category = "dev"
optional = false
python-versions = ">=3.5"
[package.extras]
test = ["pytest", "flake8", "mypy"]
[[package]]
name = "sphinxcontrib-qthelp"
version = "1.0.3"
description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document."
category = "dev"
optional = false
python-versions = ">=3.5"
[package.extras]
lint = ["flake8", "mypy", "docutils-stubs"]
test = ["pytest"]
[[package]]
name = "sphinxcontrib-serializinghtml"
version = "1.1.5"
description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)."
category = "dev"
optional = false
python-versions = ">=3.5"
[package.extras]
lint = ["flake8", "mypy", "docutils-stubs"]
test = ["pytest"]
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.10.2" version = "0.10.2"
@ -674,7 +880,7 @@ python-versions = "*"
[[package]] [[package]]
name = "zipp" name = "zipp"
version = "3.5.1" version = "3.6.0"
description = "Backport of pathlib-compatible object wrapper for zip files" description = "Backport of pathlib-compatible object wrapper for zip files"
category = "dev" category = "dev"
optional = false optional = false
@ -687,12 +893,16 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.9" python-versions = "^3.9"
content-hash = "f9e3c5502f1aa3c91b8c2a76f7f4c14d39cb84281143d987cf9e8ed043efa175" content-hash = "e6d761e045c3a735d1e875f5384d01840d28850284d11b5252823516c6d9c55f"
[metadata.files] [metadata.files]
alabaster = [
{file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"},
{file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"},
]
astroid = [ astroid = [
{file = "astroid-2.8.0-py3-none-any.whl", hash = "sha256:dcc06f6165f415220013801642bd6c9808a02967070919c4b746c6864c205471"}, {file = "astroid-2.8.2-py3-none-any.whl", hash = "sha256:9eaeaf92b3e21b70bec1a262e7eb118d2e96294892a5de595c92a12adc80dfc2"},
{file = "astroid-2.8.0.tar.gz", hash = "sha256:fe81f80c0b35264acb5653302ffbd935d394f1775c5e4487df745bf9c2442708"}, {file = "astroid-2.8.2.tar.gz", hash = "sha256:304e99c129794f2cfda584a12b71fde85205da950e2f330f4be09150525ae949"},
] ]
atomicwrites = [ atomicwrites = [
{file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
@ -702,6 +912,10 @@ attrs = [
{file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"},
{file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
] ]
babel = [
{file = "Babel-2.9.1-py2.py3-none-any.whl", hash = "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9"},
{file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"},
]
bleach = [ bleach = [
{file = "bleach-4.1.0-py2.py3-none-any.whl", hash = "sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994"}, {file = "bleach-4.1.0-py2.py3-none-any.whl", hash = "sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994"},
{file = "bleach-4.1.0.tar.gz", hash = "sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da"}, {file = "bleach-4.1.0.tar.gz", hash = "sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da"},
@ -828,23 +1042,26 @@ coverage = [
{file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"},
] ]
cryptography = [ cryptography = [
{file = "cryptography-3.4.8-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:a00cf305f07b26c351d8d4e1af84ad7501eca8a342dedf24a7acb0e7b7406e14"}, {file = "cryptography-35.0.0-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:d57e0cdc1b44b6cdf8af1d01807db06886f10177469312fbde8f44ccbb284bc9"},
{file = "cryptography-3.4.8-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:f44d141b8c4ea5eb4dbc9b3ad992d45580c1d22bf5e24363f2fbf50c2d7ae8a7"}, {file = "cryptography-35.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:ced40344e811d6abba00295ced98c01aecf0c2de39481792d87af4fa58b7b4d6"},
{file = "cryptography-3.4.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0a7dcbcd3f1913f664aca35d47c1331fce738d44ec34b7be8b9d332151b0b01e"}, {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:54b2605e5475944e2213258e0ab8696f4f357a31371e538ef21e8d61c843c28d"},
{file = "cryptography-3.4.8-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34dae04a0dce5730d8eb7894eab617d8a70d0c97da76b905de9efb7128ad7085"}, {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:7b7ceeff114c31f285528ba8b390d3e9cfa2da17b56f11d366769a807f17cbaa"},
{file = "cryptography-3.4.8-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1eb7bb0df6f6f583dd8e054689def236255161ebbcf62b226454ab9ec663746b"}, {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d69645f535f4b2c722cfb07a8eab916265545b3475fdb34e0be2f4ee8b0b15e"},
{file = "cryptography-3.4.8-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:9965c46c674ba8cc572bc09a03f4c649292ee73e1b683adb1ce81e82e9a6a0fb"}, {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2d0e0acc20ede0f06ef7aa58546eee96d2592c00f450c9acb89c5879b61992"},
{file = "cryptography-3.4.8-cp36-abi3-win32.whl", hash = "sha256:21ca464b3a4b8d8e86ba0ee5045e103a1fcfac3b39319727bc0fc58c09c6aff7"}, {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:07bb7fbfb5de0980590ddfc7f13081520def06dc9ed214000ad4372fb4e3c7f6"},
{file = "cryptography-3.4.8-cp36-abi3-win_amd64.whl", hash = "sha256:3520667fda779eb788ea00080124875be18f2d8f0848ec00733c0ec3bb8219fc"}, {file = "cryptography-35.0.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7eba2cebca600a7806b893cb1d541a6e910afa87e97acf2021a22b32da1df52d"},
{file = "cryptography-3.4.8-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d2a6e5ef66503da51d2110edf6c403dc6b494cc0082f85db12f54e9c5d4c3ec5"}, {file = "cryptography-35.0.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:18d90f4711bf63e2fb21e8c8e51ed8189438e6b35a6d996201ebd98a26abbbe6"},
{file = "cryptography-3.4.8-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a305600e7a6b7b855cd798e00278161b681ad6e9b7eca94c721d5f588ab212af"}, {file = "cryptography-35.0.0-cp36-abi3-win32.whl", hash = "sha256:c10c797ac89c746e488d2ee92bd4abd593615694ee17b2500578b63cad6b93a8"},
{file = "cryptography-3.4.8-pp36-pypy36_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:3fa3a7ccf96e826affdf1a0a9432be74dc73423125c8f96a909e3835a5ef194a"}, {file = "cryptography-35.0.0-cp36-abi3-win_amd64.whl", hash = "sha256:7075b304cd567694dc692ffc9747f3e9cb393cc4aa4fb7b9f3abd6f5c4e43588"},
{file = "cryptography-3.4.8-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:d9ec0e67a14f9d1d48dd87a2531009a9b251c02ea42851c060b25c782516ff06"}, {file = "cryptography-35.0.0-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a688ebcd08250eab5bb5bca318cc05a8c66de5e4171a65ca51db6bd753ff8953"},
{file = "cryptography-3.4.8-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5b0fbfae7ff7febdb74b574055c7466da334a5371f253732d7e2e7525d570498"}, {file = "cryptography-35.0.0-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d99915d6ab265c22873f1b4d6ea5ef462ef797b4140be4c9d8b179915e0985c6"},
{file = "cryptography-3.4.8-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94fff993ee9bc1b2440d3b7243d488c6a3d9724cc2b09cdb297f6a886d040ef7"}, {file = "cryptography-35.0.0-pp36-pypy36_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:928185a6d1ccdb816e883f56ebe92e975a262d31cc536429041921f8cb5a62fd"},
{file = "cryptography-3.4.8-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:8695456444f277af73a4877db9fc979849cd3ee74c198d04fc0776ebc3db52b9"}, {file = "cryptography-35.0.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:ebeddd119f526bcf323a89f853afb12e225902a24d29b55fe18dd6fcb2838a76"},
{file = "cryptography-3.4.8-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:cd65b60cfe004790c795cc35f272e41a3df4631e2fb6b35aa7ac6ef2859d554e"}, {file = "cryptography-35.0.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:22a38e96118a4ce3b97509443feace1d1011d0571fae81fc3ad35f25ba3ea999"},
{file = "cryptography-3.4.8.tar.gz", hash = "sha256:94cc5ed4ceaefcbe5bf38c8fba6a21fc1d365bb8fb826ea1688e3370b2e24a1c"}, {file = "cryptography-35.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb80e8a1f91e4b7ef8b33041591e6d89b2b8e122d787e87eeb2b08da71bb16ad"},
{file = "cryptography-35.0.0-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:abb5a361d2585bb95012a19ed9b2c8f412c5d723a9836418fab7aaa0243e67d2"},
{file = "cryptography-35.0.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:1ed82abf16df40a60942a8c211251ae72858b25b7421ce2497c2eb7a1cee817c"},
{file = "cryptography-35.0.0.tar.gz", hash = "sha256:9933f28f70d0517686bd7de36166dda42094eac49415459d9bdf5e7df3e0086d"},
] ]
docutils = [ docutils = [
{file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"},
@ -865,6 +1082,10 @@ idna = [
{file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"}, {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"},
{file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"}, {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"},
] ]
imagesize = [
{file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"},
{file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"},
]
importlib-metadata = [ importlib-metadata = [
{file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"}, {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"},
{file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"}, {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"},
@ -886,6 +1107,10 @@ jeepney = [
{file = "jeepney-0.7.1-py3-none-any.whl", hash = "sha256:1b5a0ea5c0e7b166b2f5895b91a08c14de8915afda4407fb5022a195224958ac"}, {file = "jeepney-0.7.1-py3-none-any.whl", hash = "sha256:1b5a0ea5c0e7b166b2f5895b91a08c14de8915afda4407fb5022a195224958ac"},
{file = "jeepney-0.7.1.tar.gz", hash = "sha256:fa9e232dfa0c498bd0b8a3a73b8d8a31978304dcef0515adc859d4e096f96f4f"}, {file = "jeepney-0.7.1.tar.gz", hash = "sha256:fa9e232dfa0c498bd0b8a3a73b8d8a31978304dcef0515adc859d4e096f96f4f"},
] ]
jinja2 = [
{file = "Jinja2-3.0.2-py3-none-any.whl", hash = "sha256:8569982d3f0889eed11dd620c706d39b60c36d6d25843961f33f77fb6bc6b20c"},
{file = "Jinja2-3.0.2.tar.gz", hash = "sha256:827a0e32839ab1600d4eb1c4c33ec5a8edfbc5cb42dafa13b81f182f97784b45"},
]
keyring = [ keyring = [
{file = "keyring-23.2.1-py3-none-any.whl", hash = "sha256:bd2145a237ed70c8ce72978b497619ddfcae640b6dcf494402d5143e37755c6e"}, {file = "keyring-23.2.1-py3-none-any.whl", hash = "sha256:bd2145a237ed70c8ce72978b497619ddfcae640b6dcf494402d5143e37755c6e"},
{file = "keyring-23.2.1.tar.gz", hash = "sha256:6334aee6073db2fb1f30892697b1730105b5e9a77ce7e61fca6b435225493efe"}, {file = "keyring-23.2.1.tar.gz", hash = "sha256:6334aee6073db2fb1f30892697b1730105b5e9a77ce7e61fca6b435225493efe"},
@ -914,10 +1139,75 @@ lazy-object-proxy = [
{file = "lazy_object_proxy-1.6.0-cp39-cp39-win32.whl", hash = "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61"}, {file = "lazy_object_proxy-1.6.0-cp39-cp39-win32.whl", hash = "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61"},
{file = "lazy_object_proxy-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b"}, {file = "lazy_object_proxy-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b"},
] ]
markupsafe = [
{file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"},
{file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"},
{file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"},
{file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"},
{file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"},
{file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"},
{file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"},
{file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"},
{file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"},
]
mccabe = [ mccabe = [
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
] ]
mypy = [
{file = "mypy-0.910-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457"},
{file = "mypy-0.910-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb"},
{file = "mypy-0.910-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9"},
{file = "mypy-0.910-cp35-cp35m-win_amd64.whl", hash = "sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e"},
{file = "mypy-0.910-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921"},
{file = "mypy-0.910-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6"},
{file = "mypy-0.910-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212"},
{file = "mypy-0.910-cp36-cp36m-win_amd64.whl", hash = "sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885"},
{file = "mypy-0.910-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0"},
{file = "mypy-0.910-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de"},
{file = "mypy-0.910-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703"},
{file = "mypy-0.910-cp37-cp37m-win_amd64.whl", hash = "sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a"},
{file = "mypy-0.910-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504"},
{file = "mypy-0.910-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9"},
{file = "mypy-0.910-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072"},
{file = "mypy-0.910-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811"},
{file = "mypy-0.910-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e"},
{file = "mypy-0.910-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b"},
{file = "mypy-0.910-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2"},
{file = "mypy-0.910-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97"},
{file = "mypy-0.910-cp39-cp39-win_amd64.whl", hash = "sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8"},
{file = "mypy-0.910-py3-none-any.whl", hash = "sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d"},
{file = "mypy-0.910.tar.gz", hash = "sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150"},
]
mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
packaging = [ packaging = [
{file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"},
{file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"},
@ -943,8 +1233,8 @@ pycparser = [
{file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"},
] ]
pyflakes = [ pyflakes = [
{file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"},
{file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"},
] ]
pygments = [ pygments = [
{file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"}, {file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"},
@ -974,13 +1264,17 @@ python-semantic-release = [
{file = "python-semantic-release-7.19.2.tar.gz", hash = "sha256:8ca0e5f72d31e7b0603b95caad6fb2d5315483ac1fadd86648771966d9ec6f2c"}, {file = "python-semantic-release-7.19.2.tar.gz", hash = "sha256:8ca0e5f72d31e7b0603b95caad6fb2d5315483ac1fadd86648771966d9ec6f2c"},
{file = "python_semantic_release-7.19.2-py3-none-any.whl", hash = "sha256:b2c8bb16a643fee0831be4d06138bc1440ebd4f252c3397d41abde179ea56852"}, {file = "python_semantic_release-7.19.2-py3-none-any.whl", hash = "sha256:b2c8bb16a643fee0831be4d06138bc1440ebd4f252c3397d41abde179ea56852"},
] ]
pytz = [
{file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"},
{file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"},
]
pywin32-ctypes = [ pywin32-ctypes = [
{file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"},
{file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"},
] ]
readme-renderer = [ readme-renderer = [
{file = "readme_renderer-29.0-py2.py3-none-any.whl", hash = "sha256:63b4075c6698fcfa78e584930f07f39e05d46f3ec97f65006e430b595ca6348c"}, {file = "readme_renderer-30.0-py2.py3-none-any.whl", hash = "sha256:3286806450d9961d6e3b5f8a59f77e61503799aca5155c8d8d40359b4e1e1adc"},
{file = "readme_renderer-29.0.tar.gz", hash = "sha256:92fd5ac2bf8677f310f3303aa4bce5b9d5f9f2094ab98c29f13791d7b805a3db"}, {file = "readme_renderer-30.0.tar.gz", hash = "sha256:8299700d7a910c304072a7601eafada6712a5b011a20139417e1b1e9f04645d8"},
] ]
requests = [ requests = [
{file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"},
@ -1014,6 +1308,42 @@ smmap = [
{file = "smmap-4.0.0-py2.py3-none-any.whl", hash = "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2"}, {file = "smmap-4.0.0-py2.py3-none-any.whl", hash = "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2"},
{file = "smmap-4.0.0.tar.gz", hash = "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182"}, {file = "smmap-4.0.0.tar.gz", hash = "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182"},
] ]
snowballstemmer = [
{file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"},
{file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"},
]
sphinx = [
{file = "Sphinx-4.2.0-py3-none-any.whl", hash = "sha256:98a535c62a4fcfcc362528592f69b26f7caec587d32cd55688db580be0287ae0"},
{file = "Sphinx-4.2.0.tar.gz", hash = "sha256:94078db9184491e15bce0a56d9186e0aec95f16ac20b12d00e06d4e36f1058a6"},
]
sphinx-rtd-theme = [
{file = "sphinx_rtd_theme-1.0.0-py2.py3-none-any.whl", hash = "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8"},
{file = "sphinx_rtd_theme-1.0.0.tar.gz", hash = "sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c"},
]
sphinxcontrib-applehelp = [
{file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"},
{file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"},
]
sphinxcontrib-devhelp = [
{file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"},
{file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"},
]
sphinxcontrib-htmlhelp = [
{file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"},
{file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"},
]
sphinxcontrib-jsmath = [
{file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"},
{file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"},
]
sphinxcontrib-qthelp = [
{file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"},
{file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"},
]
sphinxcontrib-serializinghtml = [
{file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"},
{file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"},
]
toml = [ toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
@ -1051,6 +1381,6 @@ wrapt = [
{file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"}, {file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"},
] ]
zipp = [ zipp = [
{file = "zipp-3.5.1-py3-none-any.whl", hash = "sha256:8dc6c4d5a809d659067cc713f76bcf42fae8ae641db12fddfa93694a15abc96b"}, {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"},
{file = "zipp-3.5.1.tar.gz", hash = "sha256:1fc9641b26f3bd81069b7738b039f2819cab6e3fc3399a953e19d92cc81eff4d"}, {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"},
] ]

View file

@ -1,10 +1,11 @@
[tool.poetry] [tool.poetry]
name = "checkcert" name = "checkcert"
version = "0.2.0" version = "0.7.2"
description = "CLI to check tls cert information and determine validity" description = "CLI to check tls cert information and determine validity"
authors = ["Alex Kelly <kellya@arachnitech.com>"] authors = ["Alex Kelly <kellya@arachnitech.com>"]
readme = "README.md" readme = "README.md"
homepage = "https://github.com/kellya/checkcert" homepage = "https://github.com/kellya/checkcert"
documentation = "https://checkcert.readthedocs.io"
license = "MIT" license = "MIT"
[tool.poetry.dependencies] [tool.poetry.dependencies]
@ -18,6 +19,9 @@ coverage = "^5.5"
pylint = "^2.11.1" pylint = "^2.11.1"
pyflakes = "^2.3.1" pyflakes = "^2.3.1"
pytest = "^6.2.5" pytest = "^6.2.5"
Sphinx = "^4.2.0"
sphinx-rtd-theme = "^1.0.0"
mypy = "^0.910"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]

View file

@ -1,29 +1,85 @@
""" Tests to validate checkcert"""
from click.testing import CliRunner
from checkcert.checkcert import main as cert_main from checkcert.checkcert import main as cert_main
from checkcert.checkcert import __version__ as cert_version from checkcert.checkcert import __version__ as cert_version
from click.testing import CliRunner
runner = CliRunner() runner = CliRunner()
def test_main(): def test_main():
if cert_main: """validate the core function returns correctly for:
response = runner.invoke(cert_main, ["www.franklin.edu"]) a domain
assert response.exit_code == 0 a domain and a port
response = runner.invoke(cert_main, ["www.franklin.edu:443"]) no names specified
assert response.exit_code == 0 a list of domains
a list of domains with a port specified on one
"""
response = runner.invoke(cert_main, ["www.franklin.edu"])
assert response.exit_code == 0
response = runner.invoke(cert_main, ["www.franklin.edu:443"])
assert response.exit_code == 0
response = runner.invoke(cert_main, ["www.franklin.edu", "--no-color"])
assert response.exit_code == 0
response = runner.invoke(cert_main, ["www.franklin.edu", "library.franklin.edu"])
assert response.exit_code == 0
response = runner.invoke(cert_main, ["www.franklin.edu", "--valid"])
assert response.exit_code == 0
response = runner.invoke(
cert_main, ["www.franklin.edu:443", "library.franklin.edu"]
)
assert response.exit_code == 0
def test_version(): def test_version():
"""get the version output and ensure it matches the version var"""
response = runner.invoke(cert_main, ["--version"]) response = runner.invoke(cert_main, ["--version"])
assert response.exit_code == 0 assert response.exit_code == 0
assert cert_version in response.output assert cert_version in response.output
def test_dump(): def test_dump():
"""verify that --dump outputs data"""
response = runner.invoke(cert_main, ["www.franklin.edu", "--dump"]) response = runner.invoke(cert_main, ["www.franklin.edu", "--dump"])
assert response.exit_code == 0 assert response.exit_code == 0
assert "www.franklin.edu" in response.output
def test_san(): def test_san():
"""verify --san outputs correctly"""
response = runner.invoke(cert_main, ["www.franklin.edu", "--san"]) response = runner.invoke(cert_main, ["www.franklin.edu", "--san"])
assert response.exit_code == 0 assert response.exit_code == 0
def test_san_only():
"""verify --san outputs correctly"""
check_domain = "example.com"
response = runner.invoke(cert_main, ["--san-only", check_domain])
assert response.exit_code == 0
sep_string = ","
response = runner.invoke(
cert_main, ["--san-only", "--sep", sep_string, check_domain]
)
assert response.exit_code == 0
assert sep_string in response.output
response = runner.invoke(
cert_main, ["--san-only", "--sep", sep_string, "--pre", check_domain]
)
assert response.exit_code == 0
assert response.output.startswith(sep_string)
response = runner.invoke(
cert_main, ["--san-only", "--sep", sep_string, "--pre", "cs.franklin.edu"]
)
assert response.exit_code == 0
assert response.output.startswith(sep_string)
def test_bad_cert():
"""verify an expired certificate works"""
response = runner.invoke(cert_main, ["support.bluequill.com", "--san"])
assert response.exit_code == 0
def test_from_file():
"""Verify loading domains from file"""
response = runner.invoke(cert_main, ["--filename", "ci/test_domains.txt"])
assert response.exit_code == 0