diff --git a/.gitignore b/.gitignore
index 5d381cc..f820ebd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -160,3 +160,4 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
+*creds.json
diff --git a/LICENSE b/LICENSE
index d41c0bd..7fc4901 100644
--- a/LICENSE
+++ b/LICENSE
@@ -230,3 +230,4 @@ The hypothetical commands `show w' and `show c' should show the appropriate part
You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see .
The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read .
+
diff --git a/README.md b/README.md
index fa54931..e3aa989 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,6 @@
-# twitter-wiki-bot
+# wiki-postbot
+
+Bot to add tweets using an extended wikilink syntax to the wiki
+
+Starting with twitter, but then will add masto
-Bot to add tweets using an extended wikilink syntax to the wiki!
\ No newline at end of file
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..ea5351c
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,838 @@
+[[package]]
+name = "atomicwrites"
+version = "1.4.0"
+description = "Atomic file writes."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "attrs"
+version = "21.4.0"
+description = "Classes Without Boilerplate"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[package.extras]
+dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
+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", "cloudpickle"]
+tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"]
+
+[[package]]
+name = "brotli"
+version = "1.0.9"
+description = "Python bindings for the Brotli compression library"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "brotlicffi"
+version = "1.0.9.2"
+description = "Python CFFI bindings to the Brotli library"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+cffi = ">=1.0.0"
+
+[[package]]
+name = "certifi"
+version = "2022.6.15"
+description = "Python package for providing Mozilla's CA Bundle."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "cffi"
+version = "1.15.0"
+description = "Foreign Function Interface for Python calling C code."
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+pycparser = "*"
+
+[[package]]
+name = "charset-normalizer"
+version = "2.0.12"
+description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
+category = "main"
+optional = false
+python-versions = ">=3.5.0"
+
+[package.extras]
+unicode_backport = ["unicodedata2"]
+
+[[package]]
+name = "colorama"
+version = "0.4.5"
+description = "Cross-platform colored terminal text."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[[package]]
+name = "commonmark"
+version = "0.9.1"
+description = "Python parser for the CommonMark Markdown spec"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.extras]
+test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"]
+
+[[package]]
+name = "cssselect2"
+version = "0.6.0"
+description = "CSS selectors for Python ElementTree"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+tinycss2 = "*"
+webencodings = "*"
+
+[package.extras]
+doc = ["sphinx", "sphinx-rtd-theme"]
+test = ["pytest", "pytest-cov", "pytest-flake8", "pytest-isort", "coverage"]
+
+[[package]]
+name = "fonttools"
+version = "4.33.3"
+description = "Tools to manipulate font files"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+brotli = {version = ">=1.0.1", optional = true, markers = "platform_python_implementation == \"CPython\" and extra == \"woff\""}
+brotlicffi = {version = ">=0.8.0", optional = true, markers = "platform_python_implementation != \"CPython\" and extra == \"woff\""}
+zopfli = {version = ">=0.1.4", optional = true, markers = "extra == \"woff\""}
+
+[package.extras]
+all = ["fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "zopfli (>=0.1.4)", "lz4 (>=1.7.4.2)", "matplotlib", "sympy", "skia-pathops (>=0.5.0)", "uharfbuzz (>=0.23.0)", "brotlicffi (>=0.8.0)", "scipy", "brotli (>=1.0.1)", "munkres", "unicodedata2 (>=14.0.0)", "xattr"]
+graphite = ["lz4 (>=1.7.4.2)"]
+interpolatable = ["scipy", "munkres"]
+lxml = ["lxml (>=4.0,<5)"]
+pathops = ["skia-pathops (>=0.5.0)"]
+plot = ["matplotlib"]
+repacker = ["uharfbuzz (>=0.23.0)"]
+symfont = ["sympy"]
+type1 = ["xattr"]
+ufo = ["fs (>=2.2.0,<3)"]
+unicode = ["unicodedata2 (>=14.0.0)"]
+woff = ["zopfli (>=0.1.4)", "brotlicffi (>=0.8.0)", "brotli (>=1.0.1)"]
+
+[[package]]
+name = "html5lib"
+version = "1.1"
+description = "HTML parser based on the WHATWG HTML specification"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[package.dependencies]
+six = ">=1.9"
+webencodings = "*"
+
+[package.extras]
+all = ["genshi", "chardet (>=2.2)", "lxml"]
+chardet = ["chardet (>=2.2)"]
+genshi = ["genshi"]
+lxml = ["lxml"]
+
+[[package]]
+name = "idna"
+version = "3.3"
+description = "Internationalized Domain Names in Applications (IDNA)"
+category = "main"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
+name = "iniconfig"
+version = "1.1.1"
+description = "iniconfig: brain-dead simple config-ini parsing"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "oauthlib"
+version = "3.2.0"
+description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+rsa = ["cryptography (>=3.0.0)"]
+signals = ["blinker (>=1.4.0)"]
+signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
+
+[[package]]
+name = "packaging"
+version = "21.3"
+description = "Core utilities for Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
+
+[[package]]
+name = "parse"
+version = "1.19.0"
+description = "parse() is the opposite of format()"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "pillow"
+version = "9.1.1"
+description = "Python Imaging Library (Fork)"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+docs = ["olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinx-rtd-theme (>=1.0)", "sphinxext-opengraph"]
+tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
+
+[[package]]
+name = "pluggy"
+version = "1.0.0"
+description = "plugin and hook calling mechanisms for python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+testing = ["pytest", "pytest-benchmark"]
+
+[[package]]
+name = "py"
+version = "1.11.0"
+description = "library with cross-python path, ini-parsing, io, code, log facilities"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[[package]]
+name = "pycparser"
+version = "2.21"
+description = "C parser in Python"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "pydyf"
+version = "0.2.0"
+description = "A low-level PDF generator."
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+doc = ["sphinx", "sphinx-rtd-theme"]
+test = ["pytest", "pytest-xdist", "pytest-flake8", "pytest-isort", "pytest-cov", "coverage", "pillow"]
+
+[[package]]
+name = "pygments"
+version = "2.12.0"
+description = "Pygments is a syntax highlighting package written in Python."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "pypandoc"
+version = "1.8.1"
+description = "Thin wrapper for pandoc."
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "pyparsing"
+version = "3.0.9"
+description = "pyparsing module - Classes and methods to define and execute parsing grammars"
+category = "dev"
+optional = false
+python-versions = ">=3.6.8"
+
+[package.extras]
+diagrams = ["railroad-diagrams", "jinja2"]
+
+[[package]]
+name = "pyphen"
+version = "0.12.0"
+description = "Pure Python module to hyphenate text"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+doc = ["sphinx", "sphinx-rtd-theme"]
+test = ["pytest", "pytest-cov", "pytest-flake8", "pytest-isort", "coverage"]
+
+[[package]]
+name = "pytest"
+version = "7.1.2"
+description = "pytest: simple powerful testing with Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
+attrs = ">=19.2.0"
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+iniconfig = "*"
+packaging = "*"
+pluggy = ">=0.12,<2.0"
+py = ">=1.8.2"
+tomli = ">=1.0.0"
+
+[package.extras]
+testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
+
+[[package]]
+name = "requests"
+version = "2.28.0"
+description = "Python HTTP for Humans."
+category = "main"
+optional = false
+python-versions = ">=3.7, <4"
+
+[package.dependencies]
+certifi = ">=2017.4.17"
+charset-normalizer = ">=2.0.0,<2.1.0"
+idna = ">=2.5,<4"
+urllib3 = ">=1.21.1,<1.27"
+
+[package.extras]
+socks = ["PySocks (>=1.5.6,!=1.5.7)"]
+use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
+
+[[package]]
+name = "requests-oauthlib"
+version = "1.3.1"
+description = "OAuthlib authentication support for Requests."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[package.dependencies]
+oauthlib = ">=3.0.0"
+requests = ">=2.0.0"
+
+[package.extras]
+rsa = ["oauthlib[signedtoken] (>=3.0.0)"]
+
+[[package]]
+name = "rich"
+version = "12.4.4"
+description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
+category = "main"
+optional = false
+python-versions = ">=3.6.3,<4.0.0"
+
+[package.dependencies]
+commonmark = ">=0.9.0,<0.10.0"
+pygments = ">=2.6.0,<3.0.0"
+
+[package.extras]
+jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"]
+
+[[package]]
+name = "six"
+version = "1.16.0"
+description = "Python 2 and 3 compatibility utilities"
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "tinycss2"
+version = "1.1.1"
+description = "A tiny CSS parser"
+category = "main"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+webencodings = ">=0.4"
+
+[package.extras]
+doc = ["sphinx", "sphinx-rtd-theme"]
+test = ["pytest", "pytest-cov", "pytest-flake8", "pytest-isort", "coverage"]
+
+[[package]]
+name = "tomli"
+version = "2.0.1"
+description = "A lil' TOML parser"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[[package]]
+name = "tweepy"
+version = "4.10.0"
+description = "Twitter library for Python"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+oauthlib = ">=3.2.0,<4"
+requests = ">=2.27.0,<3"
+requests-oauthlib = ">=1.2.0,<2"
+
+[package.extras]
+async = ["aiohttp (>=3.7.3,<4)", "async-lru (>=1.0.3,<2)"]
+dev = ["coverage (>=4.4.2)", "coveralls (>=2.1.0)", "tox (>=3.21.0)"]
+socks = ["requests[socks] (>=2.27.0,<3)"]
+test = ["vcrpy (>=1.10.3)"]
+
+[[package]]
+name = "urllib3"
+version = "1.26.9"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
+
+[package.extras]
+brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"]
+secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
+socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
+
+[[package]]
+name = "weasyprint"
+version = "55.0"
+description = "The Awesome Document Factory"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+cffi = ">=0.6"
+cssselect2 = ">=0.1"
+fonttools = {version = ">=4.0.0", extras = ["woff"]}
+html5lib = ">=1.1"
+Pillow = ">=4.0.0"
+pydyf = ">=0.0.3"
+Pyphen = ">=0.9.1"
+tinycss2 = ">=1.0.0"
+
+[package.extras]
+doc = ["sphinx", "sphinx-rtd-theme"]
+test = ["pytest", "pytest-cov", "pytest-flake8", "pytest-isort", "coverage"]
+
+[[package]]
+name = "webencodings"
+version = "0.5.1"
+description = "Character encoding aliases for legacy web content"
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "zopfli"
+version = "0.2.1"
+description = "Zopfli module for python"
+category = "main"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+test = ["pytest"]
+
+[metadata]
+lock-version = "1.1"
+python-versions = "^3.9"
+content-hash = "423cdca5ac87cd46ad86f1a5ac17ae17330ad736b226edfa90b6723e5b230674"
+
+[metadata.files]
+atomicwrites = [
+ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
+ {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
+]
+attrs = [
+ {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
+ {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
+]
+brotli = [
+ {file = "Brotli-1.0.9-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:268fe94547ba25b58ebc724680609c8ee3e5a843202e9a381f6f9c5e8bdb5c70"},
+ {file = "Brotli-1.0.9-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:c2415d9d082152460f2bd4e382a1e85aed233abc92db5a3880da2257dc7daf7b"},
+ {file = "Brotli-1.0.9-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5913a1177fc36e30fcf6dc868ce23b0453952c78c04c266d3149b3d39e1410d6"},
+ {file = "Brotli-1.0.9-cp27-cp27m-win32.whl", hash = "sha256:afde17ae04d90fbe53afb628f7f2d4ca022797aa093e809de5c3cf276f61bbfa"},
+ {file = "Brotli-1.0.9-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7cb81373984cc0e4682f31bc3d6be9026006d96eecd07ea49aafb06897746452"},
+ {file = "Brotli-1.0.9-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:db844eb158a87ccab83e868a762ea8024ae27337fc7ddcbfcddd157f841fdfe7"},
+ {file = "Brotli-1.0.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9744a863b489c79a73aba014df554b0e7a0fc44ef3f8a0ef2a52919c7d155031"},
+ {file = "Brotli-1.0.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a72661af47119a80d82fa583b554095308d6a4c356b2a554fdc2799bc19f2a43"},
+ {file = "Brotli-1.0.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ee83d3e3a024a9618e5be64648d6d11c37047ac48adff25f12fa4226cf23d1c"},
+ {file = "Brotli-1.0.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:19598ecddd8a212aedb1ffa15763dd52a388518c4550e615aed88dc3753c0f0c"},
+ {file = "Brotli-1.0.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:44bb8ff420c1d19d91d79d8c3574b8954288bdff0273bf788954064d260d7ab0"},
+ {file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e23281b9a08ec338469268f98f194658abfb13658ee98e2b7f85ee9dd06caa91"},
+ {file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3496fc835370da351d37cada4cf744039616a6db7d13c430035e901443a34daa"},
+ {file = "Brotli-1.0.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b83bb06a0192cccf1eb8d0a28672a1b79c74c3a8a5f2619625aeb6f28b3a82bb"},
+ {file = "Brotli-1.0.9-cp310-cp310-win32.whl", hash = "sha256:26d168aac4aaec9a4394221240e8a5436b5634adc3cd1cdf637f6645cecbf181"},
+ {file = "Brotli-1.0.9-cp310-cp310-win_amd64.whl", hash = "sha256:622a231b08899c864eb87e85f81c75e7b9ce05b001e59bbfbf43d4a71f5f32b2"},
+ {file = "Brotli-1.0.9-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:c83aa123d56f2e060644427a882a36b3c12db93727ad7a7b9efd7d7f3e9cc2c4"},
+ {file = "Brotli-1.0.9-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:6b2ae9f5f67f89aade1fab0f7fd8f2832501311c363a21579d02defa844d9296"},
+ {file = "Brotli-1.0.9-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:68715970f16b6e92c574c30747c95cf8cf62804569647386ff032195dc89a430"},
+ {file = "Brotli-1.0.9-cp35-cp35m-win32.whl", hash = "sha256:defed7ea5f218a9f2336301e6fd379f55c655bea65ba2476346340a0ce6f74a1"},
+ {file = "Brotli-1.0.9-cp35-cp35m-win_amd64.whl", hash = "sha256:88c63a1b55f352b02c6ffd24b15ead9fc0e8bf781dbe070213039324922a2eea"},
+ {file = "Brotli-1.0.9-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:503fa6af7da9f4b5780bb7e4cbe0c639b010f12be85d02c99452825dd0feef3f"},
+ {file = "Brotli-1.0.9-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:40d15c79f42e0a2c72892bf407979febd9cf91f36f495ffb333d1d04cebb34e4"},
+ {file = "Brotli-1.0.9-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:93130612b837103e15ac3f9cbacb4613f9e348b58b3aad53721d92e57f96d46a"},
+ {file = "Brotli-1.0.9-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87fdccbb6bb589095f413b1e05734ba492c962b4a45a13ff3408fa44ffe6479b"},
+ {file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:6d847b14f7ea89f6ad3c9e3901d1bc4835f6b390a9c71df999b0162d9bb1e20f"},
+ {file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:495ba7e49c2db22b046a53b469bbecea802efce200dffb69b93dd47397edc9b6"},
+ {file = "Brotli-1.0.9-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:4688c1e42968ba52e57d8670ad2306fe92e0169c6f3af0089be75bbac0c64a3b"},
+ {file = "Brotli-1.0.9-cp36-cp36m-win32.whl", hash = "sha256:61a7ee1f13ab913897dac7da44a73c6d44d48a4adff42a5701e3239791c96e14"},
+ {file = "Brotli-1.0.9-cp36-cp36m-win_amd64.whl", hash = "sha256:1c48472a6ba3b113452355b9af0a60da5c2ae60477f8feda8346f8fd48e3e87c"},
+ {file = "Brotli-1.0.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3b78a24b5fd13c03ee2b7b86290ed20efdc95da75a3557cc06811764d5ad1126"},
+ {file = "Brotli-1.0.9-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:9d12cf2851759b8de8ca5fde36a59c08210a97ffca0eb94c532ce7b17c6a3d1d"},
+ {file = "Brotli-1.0.9-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6c772d6c0a79ac0f414a9f8947cc407e119b8598de7621f39cacadae3cf57d12"},
+ {file = "Brotli-1.0.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29d1d350178e5225397e28ea1b7aca3648fcbab546d20e7475805437bfb0a130"},
+ {file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7bbff90b63328013e1e8cb50650ae0b9bac54ffb4be6104378490193cd60f85a"},
+ {file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ec1947eabbaf8e0531e8e899fc1d9876c179fc518989461f5d24e2223395a9e3"},
+ {file = "Brotli-1.0.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12effe280b8ebfd389022aa65114e30407540ccb89b177d3fbc9a4f177c4bd5d"},
+ {file = "Brotli-1.0.9-cp37-cp37m-win32.whl", hash = "sha256:f909bbbc433048b499cb9db9e713b5d8d949e8c109a2a548502fb9aa8630f0b1"},
+ {file = "Brotli-1.0.9-cp37-cp37m-win_amd64.whl", hash = "sha256:97f715cf371b16ac88b8c19da00029804e20e25f30d80203417255d239f228b5"},
+ {file = "Brotli-1.0.9-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e16eb9541f3dd1a3e92b89005e37b1257b157b7256df0e36bd7b33b50be73bcb"},
+ {file = "Brotli-1.0.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:160c78292e98d21e73a4cc7f76a234390e516afcd982fa17e1422f7c6a9ce9c8"},
+ {file = "Brotli-1.0.9-cp38-cp38-manylinux1_i686.whl", hash = "sha256:b663f1e02de5d0573610756398e44c130add0eb9a3fc912a09665332942a2efb"},
+ {file = "Brotli-1.0.9-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:5b6ef7d9f9c38292df3690fe3e302b5b530999fa90014853dcd0d6902fb59f26"},
+ {file = "Brotli-1.0.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a674ac10e0a87b683f4fa2b6fa41090edfd686a6524bd8dedbd6138b309175c"},
+ {file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e2d9e1cbc1b25e22000328702b014227737756f4b5bf5c485ac1d8091ada078b"},
+ {file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b336c5e9cf03c7be40c47b5fd694c43c9f1358a80ba384a21969e0b4e66a9b17"},
+ {file = "Brotli-1.0.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:85f7912459c67eaab2fb854ed2bc1cc25772b300545fe7ed2dc03954da638649"},
+ {file = "Brotli-1.0.9-cp38-cp38-win32.whl", hash = "sha256:35a3edbe18e876e596553c4007a087f8bcfd538f19bc116917b3c7522fca0429"},
+ {file = "Brotli-1.0.9-cp38-cp38-win_amd64.whl", hash = "sha256:269a5743a393c65db46a7bb982644c67ecba4b8d91b392403ad8a861ba6f495f"},
+ {file = "Brotli-1.0.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2aad0e0baa04517741c9bb5b07586c642302e5fb3e75319cb62087bd0995ab19"},
+ {file = "Brotli-1.0.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5cb1e18167792d7d21e21365d7650b72d5081ed476123ff7b8cac7f45189c0c7"},
+ {file = "Brotli-1.0.9-cp39-cp39-manylinux1_i686.whl", hash = "sha256:16d528a45c2e1909c2798f27f7bf0a3feec1dc9e50948e738b961618e38b6a7b"},
+ {file = "Brotli-1.0.9-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:56d027eace784738457437df7331965473f2c0da2c70e1a1f6fdbae5402e0389"},
+ {file = "Brotli-1.0.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bf919756d25e4114ace16a8ce91eb340eb57a08e2c6950c3cebcbe3dff2a5e7"},
+ {file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e4c4e92c14a57c9bd4cb4be678c25369bf7a092d55fd0866f759e425b9660806"},
+ {file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e48f4234f2469ed012a98f4b7874e7f7e173c167bed4934912a29e03167cf6b1"},
+ {file = "Brotli-1.0.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9ed4c92a0665002ff8ea852353aeb60d9141eb04109e88928026d3c8a9e5433c"},
+ {file = "Brotli-1.0.9-cp39-cp39-win32.whl", hash = "sha256:cfc391f4429ee0a9370aa93d812a52e1fee0f37a81861f4fdd1f4fb28e8547c3"},
+ {file = "Brotli-1.0.9-cp39-cp39-win_amd64.whl", hash = "sha256:854c33dad5ba0fbd6ab69185fec8dab89e13cda6b7d191ba111987df74f38761"},
+ {file = "Brotli-1.0.9-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9749a124280a0ada4187a6cfd1ffd35c350fb3af79c706589d98e088c5044267"},
+ {file = "Brotli-1.0.9-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:76ffebb907bec09ff511bb3acc077695e2c32bc2142819491579a695f77ffd4d"},
+ {file = "Brotli-1.0.9.zip", hash = "sha256:4d1b810aa0ed773f81dceda2cc7b403d01057458730e309856356d4ef4188438"},
+]
+brotlicffi = [
+ {file = "brotlicffi-1.0.9.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:408ec4359f9763280d5c4e0ad29c51d1240b25fdd18719067e972163b4125b98"},
+ {file = "brotlicffi-1.0.9.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:2e4629f7690ded66c8818715c6d4dd6a7ff6a4f10fad6186fe99850f781ce210"},
+ {file = "brotlicffi-1.0.9.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:137c4635edcdf593de5ce9d0daa596bf499591b16b8fca5fd72a490deb54b2ee"},
+ {file = "brotlicffi-1.0.9.2-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:af8a1b7bcfccf9c41a3c8654994d6a81821fdfe4caddcfe5045bfda936546ca3"},
+ {file = "brotlicffi-1.0.9.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9078432af4785f35ab3840587eed7fb131e3fc77eb2a739282b649b343c584dd"},
+ {file = "brotlicffi-1.0.9.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7bb913d5bf3b4ce2ec59872711dc9faaff5f320c3c3827cada2d8a7b793a7753"},
+ {file = "brotlicffi-1.0.9.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:16a0c9392a1059e2e62839fbd037d2e7e03c8ae5da65e9746f582464f7fab1bb"},
+ {file = "brotlicffi-1.0.9.2-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:94d2810efc5723f1447b332223b197466190518a3eeca93b9f357efb5b22c6dc"},
+ {file = "brotlicffi-1.0.9.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:9e70f3e20f317d70912b10dbec48b29114d3dbd0e9d88475cb328e6c086f0546"},
+ {file = "brotlicffi-1.0.9.2-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:586f0ea3c2eed455d5f2330b9ab4a591514c8de0ee53d445645efcfbf053c69f"},
+ {file = "brotlicffi-1.0.9.2-cp35-abi3-manylinux1_i686.whl", hash = "sha256:4454c3baedc277fd6e65f983e3eb8e77f4bc15060f69370a0201746e2edeca81"},
+ {file = "brotlicffi-1.0.9.2-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:52c1c12dad6eb1d44213a0a76acf5f18f64653bd801300bef5e2f983405bdde5"},
+ {file = "brotlicffi-1.0.9.2-cp35-abi3-manylinux2010_i686.whl", hash = "sha256:21cd400d24b344c218d8e32b394849e31b7c15784667575dbda9f65c46a64b0a"},
+ {file = "brotlicffi-1.0.9.2-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:71061f8bc86335b652e442260c4367b782a92c6e295cf5a10eff84c7d19d8cf5"},
+ {file = "brotlicffi-1.0.9.2-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:15e0db52c56056be6310fc116b3d7c6f34185594e261f23790b2fb6489998363"},
+ {file = "brotlicffi-1.0.9.2-cp35-abi3-win32.whl", hash = "sha256:551305703d12a2dd1ae43d3dde35dee20b1cb49b5796279d4d34e2c6aec6be4d"},
+ {file = "brotlicffi-1.0.9.2-cp35-abi3-win_amd64.whl", hash = "sha256:2be4fb8a7cb482f226af686cd06d2a2cab164ccdf99e460f8e3a5ec9a5337da2"},
+ {file = "brotlicffi-1.0.9.2-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:8e7221d8a084d32d15c7b58e0ce0573972375c5038423dbe83f217cfe512e680"},
+ {file = "brotlicffi-1.0.9.2-pp27-pypy_73-manylinux1_x86_64.whl", hash = "sha256:75a46bc5ed2753e1648cc211dcb2c1ac66116038766822dc104023f67ff4dfd8"},
+ {file = "brotlicffi-1.0.9.2-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:1e27c43ef72a278f9739b12b2df80ee72048cd4cbe498f8bbe08aaaa67a5d5c8"},
+ {file = "brotlicffi-1.0.9.2-pp27-pypy_73-win32.whl", hash = "sha256:feb942814285bdc5e97efc77a04e48283c17dfab9ea082d79c0a7b9e53ef1eab"},
+ {file = "brotlicffi-1.0.9.2-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a6208d82c3172eeeb3be83ed4efd5831552c7cd47576468e50fcf0fb23fcf97f"},
+ {file = "brotlicffi-1.0.9.2-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:408c810c599786fb806556ff17e844a903884e6370ca400bcec7fa286149f39c"},
+ {file = "brotlicffi-1.0.9.2-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:a73099858ee343e8801710a08be8d194f47715ff21e98d92a19ac461058f52d1"},
+ {file = "brotlicffi-1.0.9.2-pp36-pypy36_pp73-win32.whl", hash = "sha256:916b790f967a18a595e61f218c252f83718ac91f24157d622cf0fa710cd26ab7"},
+ {file = "brotlicffi-1.0.9.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ba4a00263af40e875ec3d6c7f623cbf8c795b55705da18c64ec36b6bf0848bc5"},
+ {file = "brotlicffi-1.0.9.2-pp37-pypy37_pp73-manylinux1_x86_64.whl", hash = "sha256:df78aa47741122b0d5463f1208b7bb18bc9706dee5152d9f56e0ead4865015cd"},
+ {file = "brotlicffi-1.0.9.2-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:9030cd5099252d16bfa4e22659c84a89c102e94f8e81d30764788b72e2d7cfb7"},
+ {file = "brotlicffi-1.0.9.2-pp37-pypy37_pp73-win32.whl", hash = "sha256:7e72978f4090a161885b114f87b784f538dcb77dafc6602592c1cf39ae8d243d"},
+ {file = "brotlicffi-1.0.9.2.tar.gz", hash = "sha256:0c248a68129d8fc6a217767406c731e498c3e19a7be05ea0a90c3c86637b7d96"},
+]
+certifi = [
+ {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"},
+ {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"},
+]
+cffi = [
+ {file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"},
+ {file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"},
+ {file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"},
+ {file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"},
+ {file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"},
+ {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"},
+ {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"},
+ {file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"},
+ {file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"},
+ {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"},
+ {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"},
+ {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"},
+ {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"},
+ {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"},
+ {file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"},
+ {file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"},
+ {file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"},
+ {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"},
+ {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"},
+ {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"},
+ {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"},
+ {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"},
+ {file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"},
+ {file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"},
+ {file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"},
+ {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"},
+ {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"},
+ {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"},
+ {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"},
+ {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"},
+ {file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"},
+ {file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"},
+ {file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"},
+ {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"},
+ {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"},
+ {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"},
+ {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"},
+ {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"},
+ {file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"},
+ {file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"},
+ {file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"},
+ {file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"},
+ {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"},
+ {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"},
+ {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"},
+ {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"},
+ {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"},
+ {file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"},
+ {file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"},
+ {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"},
+]
+charset-normalizer = [
+ {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"},
+ {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"},
+]
+colorama = [
+ {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
+ {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
+]
+commonmark = [
+ {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"},
+ {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"},
+]
+cssselect2 = [
+ {file = "cssselect2-0.6.0-py3-none-any.whl", hash = "sha256:3a83b2a68370c69c9cd3fcb88bbfaebe9d22edeef2c22d1ff3e1ed9c7fa45ed8"},
+ {file = "cssselect2-0.6.0.tar.gz", hash = "sha256:5b5d6dea81a5eb0c9ca39f116c8578dd413778060c94c1f51196371618909325"},
+]
+fonttools = [
+ {file = "fonttools-4.33.3-py3-none-any.whl", hash = "sha256:f829c579a8678fa939a1d9e9894d01941db869de44390adb49ce67055a06cc2a"},
+ {file = "fonttools-4.33.3.zip", hash = "sha256:c0fdcfa8ceebd7c1b2021240bd46ef77aa8e7408cf10434be55df52384865f8e"},
+]
+html5lib = [
+ {file = "html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d"},
+ {file = "html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"},
+]
+idna = [
+ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
+ {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
+]
+iniconfig = [
+ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
+ {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
+]
+oauthlib = [
+ {file = "oauthlib-3.2.0-py3-none-any.whl", hash = "sha256:6db33440354787f9b7f3a6dbd4febf5d0f93758354060e802f6c06cb493022fe"},
+ {file = "oauthlib-3.2.0.tar.gz", hash = "sha256:23a8208d75b902797ea29fd31fa80a15ed9dc2c6c16fe73f5d346f83f6fa27a2"},
+]
+packaging = [
+ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
+ {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
+]
+parse = [
+ {file = "parse-1.19.0.tar.gz", hash = "sha256:9ff82852bcb65d139813e2a5197627a94966245c897796760a3a2a8eb66f020b"},
+]
+pillow = [
+ {file = "Pillow-9.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:42dfefbef90eb67c10c45a73a9bc1599d4dac920f7dfcbf4ec6b80cb620757fe"},
+ {file = "Pillow-9.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffde4c6fabb52891d81606411cbfaf77756e3b561b566efd270b3ed3791fde4e"},
+ {file = "Pillow-9.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c857532c719fb30fafabd2371ce9b7031812ff3889d75273827633bca0c4602"},
+ {file = "Pillow-9.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59789a7d06c742e9d13b883d5e3569188c16acb02eeed2510fd3bfdbc1bd1530"},
+ {file = "Pillow-9.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d45dbe4b21a9679c3e8b3f7f4f42a45a7d3ddff8a4a16109dff0e1da30a35b2"},
+ {file = "Pillow-9.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e9ed59d1b6ee837f4515b9584f3d26cf0388b742a11ecdae0d9237a94505d03a"},
+ {file = "Pillow-9.1.1-cp310-cp310-win32.whl", hash = "sha256:b3fe2ff1e1715d4475d7e2c3e8dabd7c025f4410f79513b4ff2de3d51ce0fa9c"},
+ {file = "Pillow-9.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5b650dbbc0969a4e226d98a0b440c2f07a850896aed9266b6fedc0f7e7834108"},
+ {file = "Pillow-9.1.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:0b4d5ad2cd3a1f0d1df882d926b37dbb2ab6c823ae21d041b46910c8f8cd844b"},
+ {file = "Pillow-9.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9370d6744d379f2de5d7fa95cdbd3a4d92f0b0ef29609b4b1687f16bc197063d"},
+ {file = "Pillow-9.1.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b761727ed7d593e49671d1827044b942dd2f4caae6e51bab144d4accf8244a84"},
+ {file = "Pillow-9.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a66fe50386162df2da701b3722781cbe90ce043e7d53c1fd6bd801bca6b48d4"},
+ {file = "Pillow-9.1.1-cp37-cp37m-win32.whl", hash = "sha256:2b291cab8a888658d72b575a03e340509b6b050b62db1f5539dd5cd18fd50578"},
+ {file = "Pillow-9.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:1d4331aeb12f6b3791911a6da82de72257a99ad99726ed6b63f481c0184b6fb9"},
+ {file = "Pillow-9.1.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8844217cdf66eabe39567118f229e275f0727e9195635a15e0e4b9227458daaf"},
+ {file = "Pillow-9.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b6617221ff08fbd3b7a811950b5c3f9367f6e941b86259843eab77c8e3d2b56b"},
+ {file = "Pillow-9.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20d514c989fa28e73a5adbddd7a171afa5824710d0ab06d4e1234195d2a2e546"},
+ {file = "Pillow-9.1.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:088df396b047477dd1bbc7de6e22f58400dae2f21310d9e2ec2933b2ef7dfa4f"},
+ {file = "Pillow-9.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53c27bd452e0f1bc4bfed07ceb235663a1df7c74df08e37fd6b03eb89454946a"},
+ {file = "Pillow-9.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3f6c1716c473ebd1649663bf3b42702d0d53e27af8b64642be0dd3598c761fb1"},
+ {file = "Pillow-9.1.1-cp38-cp38-win32.whl", hash = "sha256:c67db410508b9de9c4694c57ed754b65a460e4812126e87f5052ecf23a011a54"},
+ {file = "Pillow-9.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:f054b020c4d7e9786ae0404278ea318768eb123403b18453e28e47cdb7a0a4bf"},
+ {file = "Pillow-9.1.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:c17770a62a71718a74b7548098a74cd6880be16bcfff5f937f900ead90ca8e92"},
+ {file = "Pillow-9.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3f6a6034140e9e17e9abc175fc7a266a6e63652028e157750bd98e804a8ed9a"},
+ {file = "Pillow-9.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f372d0f08eff1475ef426344efe42493f71f377ec52237bf153c5713de987251"},
+ {file = "Pillow-9.1.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09e67ef6e430f90caa093528bd758b0616f8165e57ed8d8ce014ae32df6a831d"},
+ {file = "Pillow-9.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66daa16952d5bf0c9d5389c5e9df562922a59bd16d77e2a276e575d32e38afd1"},
+ {file = "Pillow-9.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d78ca526a559fb84faaaf84da2dd4addef5edb109db8b81677c0bb1aad342601"},
+ {file = "Pillow-9.1.1-cp39-cp39-win32.whl", hash = "sha256:55e74faf8359ddda43fee01bffbc5bd99d96ea508d8a08c527099e84eb708f45"},
+ {file = "Pillow-9.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:7c150dbbb4a94ea4825d1e5f2c5501af7141ea95825fadd7829f9b11c97aaf6c"},
+ {file = "Pillow-9.1.1-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:769a7f131a2f43752455cc72f9f7a093c3ff3856bf976c5fb53a59d0ccc704f6"},
+ {file = "Pillow-9.1.1-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:488f3383cf5159907d48d32957ac6f9ea85ccdcc296c14eca1a4e396ecc32098"},
+ {file = "Pillow-9.1.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b525a356680022b0af53385944026d3486fc8c013638cf9900eb87c866afb4c"},
+ {file = "Pillow-9.1.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6e760cf01259a1c0a50f3c845f9cad1af30577fd8b670339b1659c6d0e7a41dd"},
+ {file = "Pillow-9.1.1-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4165205a13b16a29e1ac57efeee6be2dfd5b5408122d59ef2145bc3239fa340"},
+ {file = "Pillow-9.1.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937a54e5694684f74dcbf6e24cc453bfc5b33940216ddd8f4cd8f0f79167f765"},
+ {file = "Pillow-9.1.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:baf3be0b9446a4083cc0c5bb9f9c964034be5374b5bc09757be89f5d2fa247b8"},
+ {file = "Pillow-9.1.1.tar.gz", hash = "sha256:7502539939b53d7565f3d11d87c78e7ec900d3c72945d4ee0e2f250d598309a0"},
+]
+pluggy = [
+ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
+ {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
+]
+py = [
+ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
+ {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
+]
+pycparser = [
+ {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
+ {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
+]
+pydyf = [
+ {file = "pydyf-0.2.0-py3-none-any.whl", hash = "sha256:f0468bc644d3a5b0a7072d0d92bd5c024cf4beede0df56100d9919a59f15e1f0"},
+ {file = "pydyf-0.2.0.tar.gz", hash = "sha256:06ebc18b4de29fc1450ae49dd142ecd26bd7ba09d0b1919e365fbc3d8af8a622"},
+]
+pygments = [
+ {file = "Pygments-2.12.0-py3-none-any.whl", hash = "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"},
+ {file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"},
+]
+pypandoc = [
+ {file = "pypandoc-1.8.1-py3-none-any.whl", hash = "sha256:3d7eda399f9169f16106362c55a8f12f30ab0575cfd2cdc6e1856b214cc4c38c"},
+ {file = "pypandoc-1.8.1.tar.gz", hash = "sha256:8c1b651d338e8441843b991835f59d561a8473cfe63f0126d330fdb3cb518809"},
+]
+pyparsing = [
+ {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
+ {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
+]
+pyphen = [
+ {file = "pyphen-0.12.0-py3-none-any.whl", hash = "sha256:459020cd320eb200c0c5ba46b98b2278fd34c5546f520fdcd2ce5f8d733eb994"},
+ {file = "pyphen-0.12.0.tar.gz", hash = "sha256:b7d3dfc24b6f2178cdb2b1757ace0bd5d222de3e62c28d22ac578c5f22a13e9b"},
+]
+pytest = [
+ {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"},
+ {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"},
+]
+requests = [
+ {file = "requests-2.28.0-py3-none-any.whl", hash = "sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f"},
+ {file = "requests-2.28.0.tar.gz", hash = "sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b"},
+]
+requests-oauthlib = [
+ {file = "requests-oauthlib-1.3.1.tar.gz", hash = "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a"},
+ {file = "requests_oauthlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5"},
+]
+rich = [
+ {file = "rich-12.4.4-py3-none-any.whl", hash = "sha256:d2bbd99c320a2532ac71ff6a3164867884357da3e3301f0240090c5d2fdac7ec"},
+ {file = "rich-12.4.4.tar.gz", hash = "sha256:4c586de507202505346f3e32d1363eb9ed6932f0c2f63184dea88983ff4971e2"},
+]
+six = [
+ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
+ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
+]
+tinycss2 = [
+ {file = "tinycss2-1.1.1-py3-none-any.whl", hash = "sha256:fe794ceaadfe3cf3e686b22155d0da5780dd0e273471a51846d0a02bc204fec8"},
+ {file = "tinycss2-1.1.1.tar.gz", hash = "sha256:b2e44dd8883c360c35dd0d1b5aad0b610e5156c2cb3b33434634e539ead9d8bf"},
+]
+tomli = [
+ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
+ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
+]
+tweepy = [
+ {file = "tweepy-4.10.0-py3-none-any.whl", hash = "sha256:f0abbd234a588e572f880f99a094ac321217ff3eade6c0eca118ed6db8e2cf0a"},
+ {file = "tweepy-4.10.0.tar.gz", hash = "sha256:7f92574920c2f233663fff154745fc2bb0d10aedc23617379a912d8e4fefa399"},
+]
+urllib3 = [
+ {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"},
+ {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"},
+]
+weasyprint = [
+ {file = "weasyprint-55.0-py3-none-any.whl", hash = "sha256:6a5008b3e1152498f206b2d0791b3e161f507607e31ca42b307cb31b49795462"},
+ {file = "weasyprint-55.0.tar.gz", hash = "sha256:ea5d5f2f159262e38b6e85939d8510e9735a47751a9647c9eaa93c22ced86230"},
+]
+webencodings = [
+ {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"},
+ {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"},
+]
+zopfli = [
+ {file = "zopfli-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2f61ecd57bc47684c44a60e8cecb8e67f633cf238f30cc255627e172119ad72d"},
+ {file = "zopfli-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a8ca5b541544a7b959fdf5a8f614c52a31002e4be489663d835aadeef3473cb0"},
+ {file = "zopfli-0.2.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bbcdbfe93dad34f0e30f166092ffdf95e564e415b29732a6f6a52def7bb1c4d3"},
+ {file = "zopfli-0.2.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7c829c577f976b05e4ec583da4f48f31448db97b9f7b65c438d45ba7893aa2a7"},
+ {file = "zopfli-0.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28a103434694ce35cbe4f2380e18077052a78f1a0de061b3c8f1bd35b54c2822"},
+ {file = "zopfli-0.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8a40d9d113902aea0de370dce115051cf9cae4767b50af4fdde66d931b9bcae5"},
+ {file = "zopfli-0.2.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:d8457452a9151b56f17bbb9af57a4764fb41958ab84bf808e3296aefb6e61bca"},
+ {file = "zopfli-0.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b882f85b13c47eb19e7f07bccac7564701424ed5ec6d7ba8886ad6de04110e21"},
+ {file = "zopfli-0.2.1-cp310-cp310-win32.whl", hash = "sha256:e9091778e9e0dbbded72c389eace553153102acc9da560870d9d0845c8547a8d"},
+ {file = "zopfli-0.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:7e2b47662632809d7035f4fc16edbe141c4538158ad74eb3c47532b8bedb8277"},
+ {file = "zopfli-0.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ba4001a8c798a9bb2a59bb30284acd604e0c702477dce69b7fde35a50e55a95b"},
+ {file = "zopfli-0.2.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fa38b9bb6636fd11b3348dfd6aee4839e71145c3aebc76de4ba44886ec9fd6e"},
+ {file = "zopfli-0.2.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:58b2bd497273e1344098370d959f837ec1d18bae9bfafad8f4e4a2802cbbf049"},
+ {file = "zopfli-0.2.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:51537999c2114a68b1c0fac8f7ab2a05e4251d778b568213f1666f04feb79e1c"},
+ {file = "zopfli-0.2.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:38b1928a5fc5c706ec90aa833ffb5f4512bf886fe41c1b30a95edf7fb09544d6"},
+ {file = "zopfli-0.2.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7b189d80aeedb986d226e966df9e13fe8ccc28352d9d5c6c1bd3ac208aa79769"},
+ {file = "zopfli-0.2.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:f04a2cf50797dfa6fa954dd24de533f471542aa1923b89381a046a936bcbdcc5"},
+ {file = "zopfli-0.2.1-cp37-cp37m-win32.whl", hash = "sha256:3663d25419476b9e999e12ab8f6f5f8d39079bf536947da2cb5f3e45491db6f4"},
+ {file = "zopfli-0.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:fe9c9276edafb8746c01be15e5d702dcdab41062e2f7fb8af1dd8fa51a18b717"},
+ {file = "zopfli-0.2.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:338c7b2bba06ff60a73f724b7e1c8a16a5aebe9155edf58cf69b4ff7cb0b46f6"},
+ {file = "zopfli-0.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:dbf30730f169bbe77b2b6e000bf8b486559e8a36e6ab82f2471d75be4661e6bc"},
+ {file = "zopfli-0.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d067f39d72f364ae94a118df3b4cf2db8a9f53625f497ad0f04bf91ea8720db0"},
+ {file = "zopfli-0.2.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0190633ca26568f6fa810af9b7c108279d5f565a8aca1224333ff732f565086d"},
+ {file = "zopfli-0.2.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:faf26674e52a957b8fd76e955b6d89215265f4b8c7e13abf834c84a8e23def9d"},
+ {file = "zopfli-0.2.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:683c2d521553aa8ca4526c911d419d37223db298f76048ce920aded18ae060cb"},
+ {file = "zopfli-0.2.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0f45111b31f32aef070be180433fec5de548dc87caf92c5c304d1e3642b12815"},
+ {file = "zopfli-0.2.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b25540fee3d54e0fb627f911f7e1bed79611ea09aa048708777a34f6f1ac9b70"},
+ {file = "zopfli-0.2.1-cp38-cp38-win32.whl", hash = "sha256:cfffa5ad327585754f811fd49518d7170d200ba7888c49ae8629ae94c6c4a77d"},
+ {file = "zopfli-0.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:95a7ca4571797375b1fa924858cda344767621ef70a43b1b2c0b116e275f42b2"},
+ {file = "zopfli-0.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5ffcdc5ee695da73990f7806f8f1dd9a6d99d338bbe54d08fb3aaa55c9603e27"},
+ {file = "zopfli-0.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2442b99067229d786aa9f9b86976db1ba9602c1b204f361d2901d1bbcaca3441"},
+ {file = "zopfli-0.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:890a83502bb5dff27b1e2b829f8b879d9b7c1383d20a68dc13f0de71da4ff604"},
+ {file = "zopfli-0.2.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ee224ec851b4b53042ff6ad6e81712c8c9aa18396396acb024fe5a65c6bb8f1"},
+ {file = "zopfli-0.2.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a9def54c90edd112f785b07a812005ab374e7d0aaf50ecac900ca0c51adab3e7"},
+ {file = "zopfli-0.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9e123bfb16cf86ef5abda375012a97c7d00d989b3519d785ca298326f671a81b"},
+ {file = "zopfli-0.2.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c5d722b2bfde6dbfbda548e7f6b5b50b57ee06f334b055be24d9bbcdbee60e84"},
+ {file = "zopfli-0.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:29357c34b8de35c05dd04d8706393bfa09b2dd8bb7a331cc5e98e2b8d39d2efd"},
+ {file = "zopfli-0.2.1-cp39-cp39-win32.whl", hash = "sha256:fd917247bb0489c924d74186075cf0a2d7d06b3b1413197f9f6ee8fcbd0263a0"},
+ {file = "zopfli-0.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:2cd55bf9952884f3c0a3a32f95e5ba5fb0e1f371c9b1a99ed41351c1e00f4700"},
+ {file = "zopfli-0.2.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0f052a07a6fb6b7bf87ed30a099187597eb594a8a573a4426c16a6a852a05d86"},
+ {file = "zopfli-0.2.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c993919cd01ea5c4b0134908bbd59321d9bdbb9085aa30c538fa1289745fe52f"},
+ {file = "zopfli-0.2.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:37071a3dd5ec0c7f27a1f440d6f50ed8171665e7e3b6ae9a6228e6c48d21570c"},
+ {file = "zopfli-0.2.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:c9b9f73d99080ca2d79600683b8be3395d46a386b4e1d351c6ed6dc261d267e9"},
+ {file = "zopfli-0.2.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:195209711463a399f0c252b34042d8027f3b41dcb646fc112551cfaef507eab4"},
+ {file = "zopfli-0.2.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8bc92b4008854afa465d647be5ae51b01788ec47cfd10a362dcc865ff898c473"},
+ {file = "zopfli-0.2.1-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:aa18771d1d76c09d2ec2c859b6c55fdad29d2511549b4225437194aca102ad3c"},
+ {file = "zopfli-0.2.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:aa11904718fd27b2ccadf0ac88c1ca0b96ba67ad0c3c2bd584f85de060d04534"},
+ {file = "zopfli-0.2.1.zip", hash = "sha256:e5263d2806e2c1ccb23f52b2972a235d31d42f22f3fa3032cc9aded51e9bf2c6"},
+]
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..1445ebc
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,25 @@
+[tool.poetry]
+name = "wiki-postbot"
+version = "0.1.0"
+description = "Add posts to the wiki!"
+authors = ["sneakers-the-rat "]
+license = "GPL-3.0"
+packages = [
+ { include="wiki_postbot" }
+]
+
+[tool.poetry.scripts]
+wikipostbot = "wiki_postbot.main:main"
+
+[tool.poetry.dependencies]
+python = "^3.9"
+tweepy = "^4.10.0"
+rich = "^12.4.4"
+parse = "^1.19.0"
+
+[tool.poetry.dev-dependencies]
+pytest = "^7.1.2"
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_commands.py b/tests/test_commands.py
new file mode 100644
index 0000000..24beb5b
--- /dev/null
+++ b/tests/test_commands.py
@@ -0,0 +1,29 @@
+from wiki_postbot.bot import WikiPostBot
+from wiki_postbot.actions import commands
+import pytest
+from tweepy import Tweet, StreamResponse
+
+
+@pytest.mark.parametrize(
+ ['test_str', 'test_res'],
+ [
+ (
+ '@threadodo_bot identify\nname: Myname T. Identifier\norcid: 101001010101010',
+ {'username': 'threadodo_bot', 'command': 'identify',
+ 'args': {
+ 'name': 'Myname T. Identifier',
+ 'orcid': '101001010101010'
+ }}
+ )
+ ] )
+
+def test_parse_identity(test_str, test_res):
+ bot = WikiPostBot()
+ response = StreamResponse(Tweet({'text':test_str, 'id':'1095'}), {}, {}, {})
+ id = commands.Identify(bot)
+
+ assert id.check(response)
+
+ params = id.parse(response)
+
+ assert params.__dict__ == test_res
\ No newline at end of file
diff --git a/wiki_postbot/__init__.py b/wiki_postbot/__init__.py
new file mode 100644
index 0000000..f48c305
--- /dev/null
+++ b/wiki_postbot/__init__.py
@@ -0,0 +1 @@
+from wiki_postbot.bot import WikiPostBot
\ No newline at end of file
diff --git a/wiki_postbot/actions/__init__.py b/wiki_postbot/actions/__init__.py
new file mode 100644
index 0000000..268f30a
--- /dev/null
+++ b/wiki_postbot/actions/__init__.py
@@ -0,0 +1,2 @@
+from wiki_postbot.actions.action import Action
+from wiki_postbot.actions.checks import Mentioned
\ No newline at end of file
diff --git a/wiki_postbot/actions/action.py b/wiki_postbot/actions/action.py
new file mode 100644
index 0000000..1ad3d6e
--- /dev/null
+++ b/wiki_postbot/actions/action.py
@@ -0,0 +1,61 @@
+import sys
+from abc import ABC, abstractmethod
+import typing
+from typing import Optional, Union, Tuple, List, Dict
+from dataclasses import dataclass
+
+from tweepy import Response
+
+if typing.TYPE_CHECKING:
+ from wiki_postbot.bot import WikiPostBot
+
+
+@dataclass
+class Result:
+ ok:bool
+ """
+ Whether the action completed successfully
+ """
+ log: Optional[str] = None
+ """
+ Message to log
+ """
+ reply: Optional[str] = None
+ """
+ Message to reply with
+ """
+
+
+
+class Action(ABC):
+
+ def __init__(self, bot: 'WikiPostBot'):
+ super(Action, self).__init__()
+ self.bot = bot
+
+ #@abstractmethod
+ def do(self, response:Response) -> Result:
+ """
+ Encapsulate the other actions and uh do the action!
+ Returns:
+ :
+ """
+
+ #@abstractmethod
+ def check(self, response:Response) -> bool:
+ """
+ Check if the condition of this action is met
+ """
+
+
+ #@abstractmethod
+ def get(self, response:Response) -> typing.Any:
+ """
+ If this action sets something, get its value.
+
+ Eg. if this action sets a
+
+ Returns:
+
+ """
+
diff --git a/wiki_postbot/actions/checks.py b/wiki_postbot/actions/checks.py
new file mode 100644
index 0000000..595e32d
--- /dev/null
+++ b/wiki_postbot/actions/checks.py
@@ -0,0 +1,43 @@
+from wiki_postbot.actions.action import Action, Result
+from wiki_postbot.patterns import WIKILINK
+from tweepy import Response
+import re
+
+class Check(Action):
+ """
+ Base class for actions that check something about the tagged message
+ to see if we should handle it.
+ """
+
+
+class Mentioned(Check):
+ """
+ Check that we have been directly mentioned in the message
+ """
+
+ def do(self, response:Response) -> Result:
+ mentioned_users = [u['username'] == self.bot.username for u in response.data.entities.get('mentions', [])]
+ result = Result(ok=any(mentioned_users))
+ if not result.ok:
+ result.log = "Mentioned, but not directly mentioned"
+
+ return result
+
+class Wikilink(Check):
+ """
+ Check if a post contains a wikilink
+ """
+ pattern = WIKILINK
+ """
+ stolen from the agora bot
+ https://github.com/flancian/agora-bridge/blob/9cfe0a41e55bba4f628875ecf0c8fefd3ad509fd/bots/twitter/agora-bot.py#L48
+ """
+
+ def do(self, response:Response) -> Result:
+ wikilinks = self.pattern.findall(response.data.text)
+ if len(wikilinks)>0:
+ return Result(ok=True, log=f"Found wikilinks: {wikilinks}")
+ else:
+ return Result(ok=False, log="No wikilinks found")
+
+
diff --git a/wiki_postbot/actions/commands.py b/wiki_postbot/actions/commands.py
new file mode 100644
index 0000000..3a8ce2f
--- /dev/null
+++ b/wiki_postbot/actions/commands.py
@@ -0,0 +1,172 @@
+import pdb
+import sys
+import typing
+from typing import Optional, Union, Tuple, List, Dict
+from wiki_postbot.actions.action import Action, Result
+from wiki_postbot.logger import init_logger
+from dataclasses import dataclass
+import re
+
+from tweepy import Response, StreamResponse
+import parse
+
+@dataclass
+class Command_Params:
+ username: str
+ command: str
+ args: Optional[dict]
+
+class Command(Action):
+ """
+ An action invoked by tweeting at the bot and telling it to do something
+ """
+ name:str
+ command_str = r"@(?P\S{1,})\s*(?P\w*)"
+ arg_str = r"^(\w*?):\s*(.*)"
+
+ @dataclass
+ class Arguments:
+ pass
+
+
+ def __init__(self, *args, **kwargs):
+ super(Command, self).__init__(*args, **kwargs)
+ self.logger = init_logger('wiki_postbot.action.command.'+self.name, self.bot.basedir)
+ self.command_pattern = re.compile(self.command_str)
+ self.arg_pattern = re.compile(self.arg_str, re.MULTILINE)
+
+
+ @classmethod
+ def parse(cls, tweet:Union[str, Response]) -> Union[None, Command_Params]:
+ if isinstance(tweet, (Response, StreamResponse)):
+ tweet = tweet.data.text
+ command_pattern = re.compile(cls.command_str)
+ arg_pattern = re.compile(cls.arg_str, re.MULTILINE)
+
+ command_tup = command_pattern.findall(tweet)
+ command_tup = [c for c in command_tup if len(c) == 2 and len(c[0])>0 and len(c[1])>0]
+
+ if len(command_tup) == 0:
+ return None
+
+ else:
+ param_dict = {'username': command_tup[0][0], 'command': command_tup[0][1]}
+
+ args = arg_pattern.findall(tweet)
+ if args is not None:
+ param_dict['args'] = dict(args)
+
+ return Command_Params(**param_dict)
+
+
+
+ def check(self, response:Response) -> bool:
+ """
+ Check if the response has a first line like::
+
+ @{bot.username}: {Command.name}
+
+
+ Args:
+ response:
+
+ Returns:
+ bool
+ """
+ # like
+ # "{@
+
+ # split into lines
+ lines = str(response.data.text).split('\n')
+ test_str = lines[0].strip()
+ self.logger.debug(f"Testing string: {test_str}")
+
+ command_tup = self.command_pattern.findall(test_str)
+ command_tup = [c for c in command_tup if len(c) == 2 and len(c[0]) > 0 and len(c[1]) > 0]
+
+ if len(command_tup) == 0:
+ parse_res = None
+
+ else:
+ parse_res = {'username': command_tup[0][0], 'command': command_tup[0][1]}
+
+ self.logger.debug(f"parse result: {parse_res}")
+ if parse_res is None:
+ self.logger.debug('parse match was None')
+ return False
+
+ if parse_res['username'] == self.bot.username and \
+ parse_res['command'] == self.name:
+ return True
+ else:
+ return False
+
+ def get(self, response:Response) -> Union[Command_Params, None]:
+ """Get the value of a command
+
+ Response must have the 'user_id' expansion
+ """
+ matched = self.bot.client.search_recent_tweets(
+ f"from:{response.includes['users'][0]['username']} @{self.bot.username} \"{self.name}\"",
+ user_auth=True)
+ self.logger.debug("matched get request")
+ self.logger.debug(matched)
+ if matched.data is None:
+ return None
+
+ all_params = [self.parse(d.text) for d in matched.data]
+ found_params = [p for p in all_params if len(p.args)>0]
+ if len(found_params)>0:
+ return found_params[0]
+ else:
+ return all_params[0]
+
+
+
+
+
+ def arg_dict(self, tweet:Union[str, Response]) -> dict:
+ """
+ Get arguments given to this command
+ """
+ if isinstance(tweet, Response):
+ tweet = tweet.data.text
+
+ return dict(self.arg_pattern.findall(tweet))
+
+ @classmethod
+ def example(cls) -> str:
+ ex_str = f"@ {cls.name}"
+ keys = [f"{key}:" for key in cls.Arguments.__dataclass_fields__.keys()]
+ ex_str = "\n".join([ex_str, *keys])
+ return ex_str
+
+
+class Identify(Command):
+ name="identify"
+
+ @dataclass
+ class Arguments:
+ name: str
+ affiliation: Optional[str]
+ orcid: Optional[str]
+
+ def do(self, response:Response) -> Result:
+ params = self.parse(response)
+ if len(params.args) == 0:
+ id = self.get(response)
+
+ if id is None or len(id.args)==0:
+ reply = "No Identity Found! Set one by tweeting:\n"+self.example()
+ log = "Help message given"
+ else:
+ reply = "Previous Identity Found: \n" + "\n".join([f"{key}: {val}" for key, val in id.args.items()])
+ log = "previous identity given"
+ else:
+ reply = "Identity registered! This will be used to identify all threads from your username. If you need to update or remove your identity, delete this tweet, your information is not stored anywhere else."
+ log = f"Identity registered"
+ return Result(ok=True, reply=reply, log=log)
+
+
+
+
diff --git a/wiki_postbot/actions/inline.py b/wiki_postbot/actions/inline.py
new file mode 100644
index 0000000..8299c4f
--- /dev/null
+++ b/wiki_postbot/actions/inline.py
@@ -0,0 +1,41 @@
+"""
+Actions triggered by inline text content
+"""
+from typing import List, Optional
+from wiki_postbot.actions import Action
+
+class Inline(Action):
+ """
+ An action triggered by the content of a tweet
+ """
+
+class WikiLink(Inline):
+ """
+ Detect a wikilink and add it to the wiki!
+ """
+
+ def __init__(self, **kwargs):
+ super(WikiLink, self).__init__(**kwargs)
+
+ self.wikilinks = None # type: Optional[List[str]]
+
+ def check(self, response: Response) -> bool:
+ """
+ Check if the condition of this action is met
+ """
+ wikilinks = self.pattern.findall(response.data.text)
+ if len(wikilinks)>0:
+ return Result(ok=True, log=f"Found wikilinks: {wikilinks}")
+ else:
+ return Result(ok=False, log="No wikilinks found")
+
+
+
+ def do(self, response: Response) -> Result:
+ """
+ """
+
+ # @abstractmethod
+
+
+
diff --git a/wiki_postbot/bot.py b/wiki_postbot/bot.py
new file mode 100644
index 0000000..d25f9f3
--- /dev/null
+++ b/wiki_postbot/bot.py
@@ -0,0 +1,161 @@
+from pathlib import Path
+import logging
+import tweepy
+import typing
+import pdb
+from typing import List, Optional, Type
+
+from wiki_postbot.creds import Creds, Zenodo_Creds
+from wiki_postbot.thread import Thread
+from wiki_postbot.logger import init_logger
+from wiki_postbot.actions import checks, commands
+if typing.TYPE_CHECKING:
+ from wiki_postbot.actions import Action
+
+
+class WikiPostBot(tweepy.StreamingClient):
+ check_classes = [checks.Wikilink] # type: List[Type[checks.Check]]
+ #command_classes = [commands.Identify] # type: List[Type[commands.Command]]
+ command_classes = []
+
+ def __init__(
+ self,
+ creds:Path = Path('twitter_creds.json'),
+ wiki_creds:Path = Path('wiki_creds.json'),
+ username:str='wikibot3k',
+ following:Optional[List[str]]=None,
+ basedir:Path=Path().home()/"wiki_postbot",
+ loglevel="DEBUG",
+ debug:bool = False
+ ):
+
+ self._creds = None
+ self._wiki_creds = None
+ self._client = None
+ self.basedir=Path(basedir)
+
+ self.creds_path = Path(creds)
+ self.wiki_creds_path = Path(wiki_creds)
+ self.username = username
+ self.debug = debug
+ self.following = following
+ self.logger = init_logger('wiki_postbot.bot', basedir, loglevel=loglevel)
+
+ super(WikiPostBot, self).__init__(self.creds.bearer_token)
+
+ self.add_rules(self.rule)
+
+ self.checks = [cls(self) for cls in self.check_classes]
+ self.commands = [cls(self) for cls in self.command_classes]
+
+ @property
+ def rule(self) -> tweepy.StreamRule:
+ """
+ StreamRule for accounts we're following or if we're mentioned
+ """
+ rule = f"@{self.username}"
+ if self.following is not None:
+ following = [f'from:{user}' for user in self.following]
+ rule = ' OR '.join([*following, rule])
+ return tweepy.StreamRule(rule)
+
+
+ def on_response(self, response:tweepy.Response):
+ """
+ Check if the tweet has a wikilink in it
+ """
+
+ if self.debug:
+ pdb.set_trace()
+
+ self.logger.info(f'Mentioned: {response.data.text}')
+
+
+ # Do checks to see if we should do anything!
+ for check in self.checks:
+ res = check.do(response)
+ if not res.ok:
+ if res.log:
+ self.logger.info(res.log)
+ return
+
+ # Determine what action we should do!
+ for command in self.commands:
+ if command.check(response):
+ self.logger.info(f"Given Command {command.name}")
+ res = command.do(response)
+ if res.log:
+ self.logger.debug(res.log)
+ if res.reply:
+ self.reply(response, res.reply)
+ return
+
+ # TODO: move this to a command class
+ # thread = Thread.from_tweet(self.creds, response)
+ # self.logger.info('thread received')
+ # try:
+ # pdf = thread.to_pdf()
+ # self.logger.info('pdf created')
+ # depo = post_pdf(pdf, thread, self.zenodo_creds)
+ # self.logger.info('posted pdf')
+ # self.logger.debug(depo)
+ #
+ # finally:
+ # pdf.unlink()
+ #
+ # self.reply_completed(response, depo)
+
+
+ def reply_completed(self, response: tweepy.Response, deposit:Deposition):
+
+ self.client.create_tweet(text=f"The preprint of your thread is ready: {deposit.doi_url} - {deposit.title}",
+ in_reply_to_tweet_id=response.data.id)
+ self.logger.info('replied')
+
+ def reply(self, response: tweepy.Response, text:str):
+ self.client.create_tweet(text=text,
+ in_reply_to_tweet_id=response.data.id)
+
+
+
+ def run(self, threaded:bool=False):
+ self.logger.debug('starting')
+ self.filter(threaded=threaded,
+ tweet_fields=[
+ "in_reply_to_user_id",
+ "author_id",
+ "created_at",
+ "conversation_id",
+ "entities",
+ "referenced_tweets"
+ ],
+ expansions=[
+ 'author_id'
+ ]
+ )
+ self.logger.debug('stopped')
+
+ @property
+ def client(self) -> tweepy.Client:
+ if self._client is None:
+ self._client = tweepy.Client(
+ consumer_key=self.creds.api_key,
+ consumer_secret=self.creds.api_secret,
+ access_token=self.creds.access_token,
+ access_token_secret=self.creds.access_secret)
+ return self._client
+
+ @property
+ def creds(self) -> Creds:
+ if self._creds is None:
+ self._creds = Creds.from_json(self.creds_path)
+ return self._creds
+
+ @property
+ def wiki_creds(self) -> Zenodo_Creds:
+ if self._wiki_creds is None:
+ self._wiki_creds = Zenodo_Creds.from_json(self.wiki_creds_path)
+ return self._wiki_creds
+
+
+
diff --git a/wiki_postbot/checks/__init__.py b/wiki_postbot/checks/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/wiki_postbot/creds.py b/wiki_postbot/creds.py
new file mode 100644
index 0000000..e4afcd3
--- /dev/null
+++ b/wiki_postbot/creds.py
@@ -0,0 +1,34 @@
+"""
+No, not the actual creds. and dont go lookin for em in the git history cuz they aint in there neither
+"""
+
+import json
+from dataclasses import dataclass
+from pathlib import Path
+import typing
+
+@dataclass
+class Creds:
+ api_key: str
+ api_secret: str
+ bearer_token: str
+ access_token: typing.Optional[str]
+ access_secret: typing.Optional[str]
+
+ @classmethod
+ def from_json(cls, path:Path) -> 'Creds':
+ with open(path, 'r') as jfile:
+ creds = json.load(jfile)
+ return Creds(**creds)
+
+
+@dataclass
+class Zenodo_Creds:
+ access_token:str
+
+ @classmethod
+ def from_json(cls, path:Path) -> 'Zenodo_Creds':
+ with open(path, 'r') as jfile:
+ creds = json.load(jfile)
+ return Zenodo_Creds(**creds)
+
diff --git a/wiki_postbot/logger.py b/wiki_postbot/logger.py
new file mode 100644
index 0000000..c2f5bd8
--- /dev/null
+++ b/wiki_postbot/logger.py
@@ -0,0 +1,56 @@
+import logging
+from rich.logging import RichHandler
+from pathlib import Path
+import sys
+import typing
+from typing import Optional, Union, Tuple, List, Dict, Literal
+from logging.handlers import RotatingFileHandler
+
+def init_logger(
+ name:Optional[str]=None,
+ basedir:Optional[Path]=None,
+ loglevel:str='DEBUG',
+ loglevel_disk:Optional[str]=None
+ ):
+ if name is None:
+ name = 'wiki_postbot'
+ else:
+ if not name.startswith('wiki_postbot'):
+ name = '.'.join(['wiki_postbot', name])
+
+ if loglevel_disk is None:
+ loglevel_disk = loglevel
+
+ logger = logging.getLogger(name)
+ logger.setLevel(loglevel)
+
+
+ if basedir is not None:
+ logger.addHandler(_file_handler(basedir, name, loglevel_disk))
+
+ logger.addHandler(_rich_handler())
+ return logger
+
+
+def _file_handler(basedir:Path, name:str, loglevel:str="DEBUG") -> RotatingFileHandler:
+ filename = Path(basedir) / '.'.join([name, 'log'])
+ basedir.mkdir(parents=True, exist_ok=True)
+ file_handler = RotatingFileHandler(
+ str(filename),
+ mode='a',
+ maxBytes=2 ** 24,
+ backupCount=5
+ )
+ file_formatter = logging.Formatter("[%(asctime)s] %(levelname)s [%(name)s]: %(message)s")
+ file_handler.setLevel(loglevel)
+ file_handler.setFormatter(file_formatter)
+ return file_handler
+
+def _rich_handler() -> RichHandler:
+ rich_handler = RichHandler(rich_tracebacks=True, markup=True)
+ rich_formatter = logging.Formatter(
+ "[bold green]\[%(name)s][/bold green] %(message)s",
+ datefmt='[%y-%m-%dT%H:%M:%S]'
+ )
+ rich_handler.setFormatter(rich_formatter)
+ return rich_handler
\ No newline at end of file
diff --git a/wiki_postbot/main.py b/wiki_postbot/main.py
new file mode 100644
index 0000000..35a8b8c
--- /dev/null
+++ b/wiki_postbot/main.py
@@ -0,0 +1,26 @@
+import argparse
+from wiki_postbot.bot import WikiPostBot
+from pathlib import Path
+
+def parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(description="Run a wiki_postbot bot!")
+ parser.add_argument('--creds', type=Path, help="Location of twitter credentials .json file",
+ default=Path('twitter_creds.json'))
+ parser.add_argument('--wiki_creds', type=Path, help="Path to wiki credentials .json file",
+ default=Path('wiki_creds.json'))
+ parser.add_argument('-u', '--username', type=str, help="Username of our bot",
+ default="wikibot3k")
+ parser.add_argument('-b', '--basedir', help="Base directory to store files",
+ type=Path, default=Path().home() / "wiki_postbot")
+ parser.add_argument('-l', '--loglevel', type=str, help="Loglevel for wiki_postbot bot, DEBUG, INFO, WARNING, or ERROR",
+ default='DEBUG')
+ parser.add_argument('-d', '--debug', action='store_true', help="Debug responses from bot")
+ return parser.parse_args()
+
+def main():
+ args = parse_args()
+ tt = WikiPostBot(**args.__dict__)
+ tt.run()
+
+if __name__ == "__main__":
+ main()
diff --git a/wiki_postbot/patterns.py b/wiki_postbot/patterns.py
new file mode 100644
index 0000000..31256b5
--- /dev/null
+++ b/wiki_postbot/patterns.py
@@ -0,0 +1,6 @@
+"""
+Regex patterns
+"""
+import re
+
+WIKILINK = re.compile(r'\[\[(.*?)\]\]', re.IGNORECASE)
\ No newline at end of file
diff --git a/wiki_postbot/templates/report.css b/wiki_postbot/templates/report.css
new file mode 100644
index 0000000..3917871
--- /dev/null
+++ b/wiki_postbot/templates/report.css
@@ -0,0 +1,372 @@
+
+/*stolen from https://github.com/CourtBouillon/weasyprint-samples/blob/master/report/report.css*/
+
+@page {
+ @top-left {
+ background: #fbc847;
+ content: counter(page);
+ height: 1cm;
+ text-align: center;
+ width: 1cm;
+ }
+ @top-center {
+ background: #fbc847;
+ content: '';
+ display: block;
+ height: .05cm;
+ opacity: .5;
+ width: 100%;
+ }
+ @top-right {
+ content: string(heading);
+ font-size: 9pt;
+ height: 1cm;
+ vertical-align: middle;
+ width: 100%;
+ }
+}
+@page :blank {
+ @top-left { background: none; content: '' }
+ @top-center { content: none }
+ @top-right { content: none }
+}
+@page no-chapter {
+ @top-left { background: none; content: none }
+ @top-center { content: none }
+ @top-right { content: none }
+}
+@page :first {
+ background: url(report-cover.jpg) no-repeat center;
+ background-size: cover;
+ margin: 0;
+}
+@page chapter {
+ background: #fbc847;
+ margin: 0;
+ @top-left { content: none }
+ @top-center { content: none }
+ @top-right { content: none }
+}
+
+html {
+ color: #393939;
+ font-family: Fira Sans;
+ font-size: 11pt;
+ font-weight: 300;
+ line-height: 1.5;
+}
+
+h1 {
+ color: #fbc847;
+ font-size: 38pt;
+ margin: 5cm 2cm 0 2cm;
+ page: no-chapter;
+ width: 100%;
+}
+h2, h3, h4 {
+ color: black;
+ font-weight: 400;
+}
+h2 {
+ font-size: 28pt;
+ string-set: heading content();
+}
+h3 {
+ font-weight: 300;
+ font-size: 15pt;
+}
+h4 {
+ font-size: 13pt;
+}
+
+#cover {
+ align-content: space-between;
+ display: flex;
+ flex-wrap: wrap;
+ height: 297mm;
+}
+#cover address {
+ background: #fbc847;
+ flex: 1 50%;
+ margin: 0 -2cm;
+ padding: 1cm 0;
+ white-space: pre-wrap;
+}
+#cover address:first-of-type {
+ padding-left: 3cm;
+}
+#contents {
+ break-before: right;
+ break-after: left;
+ page: no-chapter;
+}
+#contents h2 {
+ font-size: 20pt;
+ font-weight: 400;
+ margin-bottom: 3cm;
+}
+#contents h3 {
+ font-weight: 500;
+ margin: 3em 0 1em;
+}
+#contents h3::before {
+ background: #fbc847;
+ content: '';
+ display: block;
+ height: .08cm;
+ margin-bottom: .25cm;
+ width: 2cm;
+}
+#contents ul {
+ list-style: none;
+ padding-left: 0;
+}
+#contents ul li {
+ border-top: .25pt solid #c1c1c1;
+ margin: .25cm 0;
+ padding-top: .25cm;
+}
+#contents ul li::before {
+ color: #fbc847;
+ content: '• ';
+ font-size: 40pt;
+ line-height: 16pt;
+ vertical-align: bottom;
+}
+#contents ul li a {
+ color: inherit;
+ text-decoration-line: inherit;
+}
+#contents ul li a::before {
+ content: target-text(attr(href));
+}
+#contents ul li a::after {
+ color: #fbc847;
+ content: target-counter(attr(href), page);
+ float: right;
+}
+
+#columns section {
+ columns: 2;
+ column-gap: 1cm;
+ padding-top: 1cm;
+}
+#columns section p {
+ text-align: justify;
+}
+#columns section p:first-of-type {
+ font-weight: 700;
+}
+
+#skills h3 {
+ background: #fbc847;
+ margin: 0 -3cm 1cm;
+ padding: 1cm 1cm 1cm 3cm;
+ width: 21cm;
+}
+#skills section {
+ padding: .5cm 0;
+}
+#skills section#table-content::before {
+ background: url(table-content.svg) no-repeat center #fbc847;
+ background-size: 50%;
+ content: '';
+ display: inline-block;
+ float: left;
+ height: 2cm;
+ margin-right: .5cm;
+ vertical-align: middle;
+ width: 2cm;
+}
+#skills section#heading::before {
+ background: url(heading.svg) no-repeat center #fbc847;
+ background-size: 50%;
+ content: '';
+ display: inline-block;
+ float: left;
+ height: 2cm;
+ margin-right: .5cm;
+ vertical-align: middle;
+ width: 2cm;
+}
+#skills section#multi-columns::before {
+ background: url(multi-columns.svg) no-repeat center #fbc847;
+ background-size: 50%;
+ content: '';
+ display: inline-block;
+ float: left;
+ height: 2cm;
+ margin-right: .5cm;
+ vertical-align: middle;
+ width: 2cm;
+}
+#skills section#internal-links::before {
+ background: url(internal-links.svg) no-repeat center #fbc847;
+ background-size: 50%;
+ content: '';
+ display: inline-block;
+ float: left;
+ height: 2cm;
+ margin-right: .5cm;
+ vertical-align: middle;
+ width: 2cm;
+}
+#skills section#style::before {
+ background: url(style.svg) no-repeat center #fbc847;
+ background-size: 50%;
+ content: '';
+ display: inline-block;
+ float: left;
+ height: 2cm;
+ margin-right: .5cm;
+ vertical-align: middle;
+ width: 2cm;
+}
+#skills section h4 {
+ margin: 0;
+}
+#skills section p {
+ margin-top: 0;
+}
+
+#offers {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+}
+#offers h2, #offers h3 {
+ width: 100%;
+}
+#offers section {
+ width: 30%;
+}
+#offers section h4 {
+ margin-bottom: 0;
+}
+#offers section ul {
+ list-style: none;
+ margin: 0;
+ padding-left: 0;
+}
+#offers section ul li:not(:last-of-type) {
+ margin: .5cm 0;
+}
+#offers section p {
+ background: #fbc847;
+ display: block;
+ font-size: 15pt;
+ font-weight: 700;
+ margin-bottom: 0;
+ padding: .25cm 0;
+ text-align: center;
+}
+
+#chapter {
+ align-items: center;
+ display: flex;
+ height: 297mm;
+ justify-content: center;
+ page: chapter;
+}
+
+#typography section {
+ display: flex;
+ flex-wrap: wrap;
+ margin: 1cm 0;
+}
+#typography section h4 {
+ border-top: 1pt solid;
+ flex: 1 25%;
+ margin: 0;
+}
+#typography section h4 + * {
+ flex: 1 75%;
+ margin: 0;
+ padding-left: .5cm;
+}
+#typography section p {
+ text-align: justify;
+}
+#typography section ul {
+ line-height: 2;
+ list-style: none;
+}
+#typography section#small-caps p {
+ font-variant: small-caps;
+}
+#typography section#ligatures dl {
+ display: flex;
+ flex-wrap: wrap;
+}
+#typography section#ligatures dl dt {
+ font-weight: 400;
+ width: 30%;
+}
+#typography section#ligatures dl dd {
+ flex: 1 70%;
+ margin: 0;
+ padding: 0;
+}
+#typography section#ligatures .none {
+ font-variant-ligatures: none;
+}
+#typography section#ligatures .common {
+ font-variant-ligatures: common-ligatures;
+}
+#typography section#ligatures .discretionary {
+ font-variant-ligatures: discretionary-ligatures;
+}
+#typography section#ligatures .contextual {
+ font-variant-ligatures: contextual;
+}
+#typography section#numbers dl {
+ display: flex;
+ flex-wrap: wrap;
+}
+#typography section#numbers dl dt {
+ font-weight: 400;
+ width: 30%;
+}
+#typography section#numbers dl dd {
+ flex: 1 70%;
+ margin: 0;
+ padding: 0;
+}
+#typography section#numbers #fractions {
+ font-variant-numeric: diagonal-fractions;
+}
+#typography section#numbers #ordinals {
+ font-variant-numeric: ordinal;
+}
+#typography section#numbers #slashed {
+ font-variant-numeric: slashed-zero;
+}
+#typography section#numbers #super {
+ font-variant-position: super;
+}
+#typography section#numbers #sub {
+ font-variant-position: sub;
+}
+#typography section#figures dl {
+ columns: 4;
+}
+#typography section#figures dl dt {
+ font-weight: 400;
+}
+#typography section#figures dl dd {
+ display: flex;
+ margin: 0;
+ padding: 0;
+}
+#typography section#figures dl dd ul {
+ padding: 0 1em 0 0;
+}
+#typography section#figures #oldstyle {
+ font-variant-numeric: oldstyle-nums;
+}
+#typography section#figures #tabular {
+ font-variant-numeric: tabular-nums;
+}
+#typography section#figures #old-tabular {
+ font-variant-numeric: oldstyle-nums tabular-nums;
+}
diff --git a/wiki_postbot/templates/template.html b/wiki_postbot/templates/template.html
new file mode 100644
index 0000000..b1f1dfd
--- /dev/null
+++ b/wiki_postbot/templates/template.html
@@ -0,0 +1,390 @@
+
+
+
+
+
+
+
+
+
+
+
+
+$body$
+
+
+
+
+
+
\ No newline at end of file
diff --git a/wiki_postbot/thread.py b/wiki_postbot/thread.py
new file mode 100644
index 0000000..4025d08
--- /dev/null
+++ b/wiki_postbot/thread.py
@@ -0,0 +1,103 @@
+from wiki_postbot.creds import Creds
+import tweepy
+import typing
+import re
+from pathlib import Path
+import pypandoc
+from dataclasses import dataclass
+
+@dataclass
+class Author:
+ username: str
+ name: str
+
+
+class Thread:
+ def __init__(self,
+ tweets: typing.List[tweepy.tweet.Tweet],
+ responses: typing.List[tweepy.Response]
+ ):
+ self.tweets = tweets
+ self.responses = responses
+ self.pdf = None
+ self.sort()
+
+ @property
+ def text(self) -> str:
+ """
+ Text from all the tweets in a thread, double new lines between each
+
+ also replacing any @'s with markdown links to profiles i guess
+ """
+ text = "\n\n".join([t.text for t in self.tweets])
+ # make @'s links
+ text = re.sub(r"@([\w\d_]{1,})", r"[@\1](https://twitter.com/\1)", text)
+ return text
+
+ @property
+ def author(self) -> Author:
+ return Author(
+ "@" + self.responses[0].includes['users'][0].username,
+ self.responses[0].includes['users'][0].name,
+
+ )
+
+ @property
+ def title(self) -> str:
+ return f"{self.author.name}'s thread - {self.tweets[0].created_at.strftime('%y-%m-%d %H:%M')}"
+
+ def sort(self):
+ """
+ sort order of tweets
+ """
+ self.tweets = sorted(self.tweets, key=lambda x: x.created_at)
+ self.responses = sorted(self.responses, key=lambda x: x.data.created_at)
+
+
+ def to_pdf(self, output_file:typing.Optional[Path]=None,
+ ) -> Path:
+ if output_file is None:
+ output_file = Path('.') / f"{self.tweets[0].author_id}_{self.tweets[0].created_at.isoformat()}.pdf"
+
+ pypandoc.convert_text(
+ self.text, to="html", format="md",
+ outputfile=str(output_file),
+ extra_args = [
+ '--pdf-engine=weasyprint',
+ f'--data-dir={Path(__file__).parent}',
+ '--template=./template.html',
+ ])
+ self.pdf = output_file
+ return output_file
+
+
+ @classmethod
+ def from_tweet(cls,
+ creds:Creds,
+ response:tweepy.Response,
+ limit:int=100) -> 'Thread':
+ """
+ Starting with the mentioned tweet, get all of the tweets in a thread in order
+ """
+ tweet = response.data
+ client = tweepy.Client(creds.bearer_token)
+ tweets = [tweet]
+ responses = [response]
+ while tweet.referenced_tweets is not None and len(tweet.referenced_tweets)>0 and len(tweets)<=limit:
+ replied = [t.id for t in tweet.referenced_tweets if t.type=="replied_to"]
+ if len(replied)==0:
+ # top tweet was qt
+ break
+ replied = replied[0]
+ response = client.get_tweet(id=replied, tweet_fields=[
+ "referenced_tweets",
+ "author_id",
+ "created_at"
+ ], expansions=[
+ 'author_id'
+ ])
+ tweet = response.data
+ tweets.append(tweet)
+ responses.append(response)
+
+ return Thread(tweets, responses)