diff --git a/poetry.lock b/poetry.lock index 1634969..9bafbac 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,10 +1,9 @@ -# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "anyio" version = "3.6.2" description = "High level compatibility layer for multiple asynchronous event loop implementations" -category = "dev" optional = false python-versions = ">=3.6.2" files = [ @@ -15,7 +14,6 @@ files = [ [package.dependencies] idna = ">=2.8" sniffio = ">=1.1" -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] @@ -26,7 +24,6 @@ trio = ["trio (>=0.16,<0.22)"] name = "atomicwrites" version = "1.4.1" description = "Atomic file writes." -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -37,7 +34,6 @@ files = [ name = "attrs" version = "22.2.0" description = "Classes Without Boilerplate" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -56,7 +52,6 @@ tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy name = "black" version = "23.3.0" description = "The uncompromising code formatter." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -94,7 +89,6 @@ packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} [package.extras] @@ -107,7 +101,6 @@ uvloop = ["uvloop (>=0.15.2)"] name = "certifi" version = "2022.12.7" description = "Python package for providing Mozilla's CA Bundle." -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -119,7 +112,6 @@ files = [ name = "charset-normalizer" version = "3.1.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "dev" optional = false python-versions = ">=3.7.0" files = [ @@ -204,7 +196,6 @@ files = [ name = "click" version = "8.1.3" description = "Composable command line interface toolkit" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -214,13 +205,11 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -232,7 +221,6 @@ files = [ name = "coverage" version = "6.5.0" description = "Code coverage measurement for Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -295,7 +283,6 @@ toml = ["tomli"] name = "deprecated" version = "1.2.13" description = "Python @deprecated decorator to deprecate old python classes, functions or methods." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -313,7 +300,6 @@ dev = ["PyTest", "PyTest (<5)", "PyTest-Cov", "PyTest-Cov (<2.6)", "bump2version name = "fastapi" version = "0.89.1" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -335,7 +321,6 @@ test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.10.0)", "coverage[toml] (>= name = "flake8" version = "4.0.1" description = "the modular source code checker: pep8 pyflakes and co" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -344,7 +329,6 @@ files = [ ] [package.dependencies] -importlib-metadata = {version = "<4.3", markers = "python_version < \"3.8\""} mccabe = ">=0.6.0,<0.7.0" pycodestyle = ">=2.8.0,<2.9.0" pyflakes = ">=2.4.0,<2.5.0" @@ -353,7 +337,6 @@ pyflakes = ">=2.4.0,<2.5.0" name = "ghp-import" version = "2.1.0" description = "Copy your docs directly to the gh-pages branch." -category = "dev" optional = false python-versions = "*" files = [ @@ -371,7 +354,6 @@ dev = ["flake8", "markdown", "twine", "wheel"] name = "h11" version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -379,14 +361,10 @@ files = [ {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, ] -[package.dependencies] -typing-extensions = {version = "*", markers = "python_version < \"3.8\""} - [[package]] name = "hiro" version = "0.5.1" description = "time manipulation utilities for python" -category = "dev" optional = false python-versions = "*" files = [ @@ -401,7 +379,6 @@ six = ">=1.4.1" name = "httpcore" version = "0.16.3" description = "A minimal low-level HTTP client." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -413,17 +390,16 @@ files = [ anyio = ">=3.0,<5.0" certifi = "*" h11 = ">=0.13,<0.15" -sniffio = ">=1.0.0,<2.0.0" +sniffio = "==1.*" [package.extras] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] +socks = ["socksio (==1.*)"] [[package]] name = "httpx" version = "0.23.3" description = "The next generation HTTP client." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -439,15 +415,14 @@ sniffio = "*" [package.extras] brotli = ["brotli", "brotlicffi"] -cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<13)"] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] +socks = ["socksio (==1.*)"] [[package]] name = "idna" version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -459,7 +434,6 @@ files = [ name = "importlib-metadata" version = "4.2.0" description = "Read metadata from Python packages" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -468,7 +442,6 @@ files = [ ] [package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] @@ -479,7 +452,6 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pep517", name = "importlib-resources" version = "5.12.0" description = "Read resources from Python packages" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -498,7 +470,6 @@ testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-chec name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -510,7 +481,6 @@ files = [ name = "isort" version = "4.3.21" description = "A Python utility / library to sort Python imports." -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -528,7 +498,6 @@ xdg-home = ["appdirs (>=1.4.0)"] name = "jinja2" version = "3.1.2" description = "A very fast and expressive template engine." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -546,7 +515,6 @@ i18n = ["Babel (>=2.7)"] name = "limits" version = "3.3.1" description = "Rate limiting utilities" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -577,7 +545,6 @@ rediscluster = ["redis (>=4.2.0,!=4.5.2,!=4.5.3)"] name = "lxml" version = "4.9.2" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" files = [ @@ -670,7 +637,6 @@ source = ["Cython (>=0.29.7)"] name = "markdown" version = "3.3.4" description = "Python implementation of Markdown." -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -678,9 +644,6 @@ files = [ {file = "Markdown-3.3.4.tar.gz", hash = "sha256:31b5b491868dcc87d6c24b7e3d19a0d730d59d3e46f4eea6430a321bed387a49"}, ] -[package.dependencies] -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} - [package.extras] testing = ["coverage", "pyyaml"] @@ -688,7 +651,6 @@ testing = ["coverage", "pyyaml"] name = "markupsafe" version = "2.1.2" description = "Safely add untrusted strings to HTML/XML markup." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -748,7 +710,6 @@ files = [ name = "mccabe" version = "0.6.1" description = "McCabe checker, plugin for flake8" -category = "dev" optional = false python-versions = "*" files = [ @@ -760,7 +721,6 @@ files = [ name = "mergedeep" version = "1.3.4" description = "A deep merge function for 🐍." -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -772,7 +732,6 @@ files = [ name = "mkautodoc" version = "0.1.0" description = "AutoDoc for MarkDown" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -783,7 +742,6 @@ files = [ name = "mkdocs" version = "1.2.4" description = "Project documentation with Markdown." -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -810,7 +768,6 @@ i18n = ["babel (>=2.9.0)"] name = "mock" version = "4.0.3" description = "Rolling backport of unittest.mock for all Pythons" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -825,63 +782,66 @@ test = ["pytest (<5.4)", "pytest-cov"] [[package]] name = "mypy" -version = "0.910" +version = "1.10.0" description = "Optional static typing for Python" -category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.8" files = [ - {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"}, + {file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"}, + {file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"}, + {file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"}, + {file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"}, + {file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"}, + {file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"}, + {file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"}, + {file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"}, + {file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"}, + {file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"}, + {file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"}, + {file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"}, + {file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"}, + {file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"}, + {file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"}, + {file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"}, + {file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"}, + {file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"}, + {file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"}, + {file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"}, + {file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"}, + {file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"}, + {file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"}, + {file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"}, + {file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"}, + {file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"}, + {file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"}, ] [package.dependencies] -mypy-extensions = ">=0.4.3,<0.5.0" -toml = "*" -typed-ast = {version = ">=1.4.0,<1.5.0", markers = "python_version < \"3.8\""} -typing-extensions = ">=3.7.4" +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.1.0" [package.extras] dmypy = ["psutil (>=4.0)"] -python2 = ["typed-ast (>=1.4.0,<1.5.0)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] [[package]] name = "mypy-extensions" -version = "0.4.4" -description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "dev" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." optional = false -python-versions = ">=2.7" +python-versions = ">=3.5" files = [ - {file = "mypy_extensions-0.4.4.tar.gz", hash = "sha256:c8b707883a96efe9b4bb3aaf0dcc07e7e217d7d8368eec4db4049ee9e142f4fd"}, + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] [[package]] name = "packaging" version = "23.0" description = "Core utilities for Python packages" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -893,7 +853,6 @@ files = [ name = "pathspec" version = "0.11.1" description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -905,7 +864,6 @@ files = [ name = "platformdirs" version = "3.2.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -913,9 +871,6 @@ files = [ {file = "platformdirs-3.2.0.tar.gz", hash = "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08"}, ] -[package.dependencies] -typing-extensions = {version = ">=4.5", markers = "python_version < \"3.8\""} - [package.extras] docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] @@ -924,7 +879,6 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest- name = "pluggy" version = "1.0.0" description = "plugin and hook calling mechanisms for python" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -932,9 +886,6 @@ files = [ {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] -[package.dependencies] -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} - [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] @@ -943,7 +894,6 @@ testing = ["pytest", "pytest-benchmark"] 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.*" files = [ @@ -955,7 +905,6 @@ files = [ name = "pycodestyle" version = "2.8.0" description = "Python style guide checker" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -967,7 +916,6 @@ files = [ name = "pydantic" version = "1.10.7" description = "Data validation and settings management using python type hints" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1020,7 +968,6 @@ email = ["email-validator (>=1.0.3)"] name = "pyflakes" version = "2.4.0" description = "passive checker of Python programs" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1032,7 +979,6 @@ files = [ name = "pytest" version = "6.2.5" description = "pytest: simple powerful testing with Python" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1044,7 +990,6 @@ files = [ atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" @@ -1058,7 +1003,6 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xm name = "python-dateutil" version = "2.8.2" description = "Extensions to the standard Python datetime module" -category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ @@ -1073,7 +1017,6 @@ six = ">=1.5" name = "pyyaml" version = "6.0" description = "YAML parser and emitter for Python" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1123,7 +1066,6 @@ files = [ name = "pyyaml-env-tag" version = "0.1" description = "A custom YAML tag for referencing environment variables in YAML files. " -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1138,7 +1080,6 @@ pyyaml = "*" name = "redis" version = "3.5.3" description = "Python client for Redis key-value store" -category = "main" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -1153,7 +1094,6 @@ hiredis = ["hiredis (>=0.1.3)"] name = "requests" version = "2.28.2" description = "Python HTTP for Humans." -category = "dev" optional = false python-versions = ">=3.7, <4" files = [ @@ -1175,7 +1115,6 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] name = "rfc3986" version = "1.5.0" description = "Validating URI References per RFC 3986" -category = "dev" optional = false python-versions = "*" files = [ @@ -1193,7 +1132,6 @@ idna2008 = ["idna"] name = "setuptools" version = "65.7.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1210,7 +1148,6 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs ( name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1222,7 +1159,6 @@ files = [ name = "sniffio" version = "1.3.0" description = "Sniff out which async library your code is running under" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1234,7 +1170,6 @@ files = [ name = "starlette" version = "0.22.0" description = "The little ASGI library that shines." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1253,7 +1188,6 @@ full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyam name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -1265,7 +1199,6 @@ files = [ name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1273,51 +1206,10 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] -[[package]] -name = "typed-ast" -version = "1.4.3" -description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" -optional = false -python-versions = "*" -files = [ - {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, - {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, - {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"}, - {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"}, - {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"}, - {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"}, - {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"}, - {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"}, - {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"}, - {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"}, - {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"}, - {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"}, - {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"}, - {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"}, - {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"}, - {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"}, - {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"}, - {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"}, - {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"}, - {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"}, - {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"}, - {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"}, - {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"}, - {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"}, - {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"}, - {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"}, - {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"}, - {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"}, - {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, - {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, -] - [[package]] name = "types-redis" version = "3.5.18" description = "Typing stubs for redis" -category = "dev" optional = false python-versions = "*" files = [ @@ -1327,21 +1219,19 @@ files = [ [[package]] name = "typing-extensions" -version = "4.5.0" -description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, - {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] name = "urllib3" version = "1.26.15" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ @@ -1358,7 +1248,6 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] name = "watchdog" version = "3.0.0" description = "Filesystem events monitoring" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1398,7 +1287,6 @@ watchmedo = ["PyYAML (>=3.10)"] name = "wrapt" version = "1.15.0" description = "Module for decorators, wrappers and monkey patching." -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" files = [ @@ -1483,7 +1371,6 @@ files = [ name = "zipp" version = "3.15.0" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1500,5 +1387,5 @@ redis = ["redis"] [metadata] lock-version = "2.0" -python-versions = ">=3.7,<4.0" -content-hash = "b0fbb75051b47ba71537e15c61e7bbbccd8edf91d806dc2ce83be350ed711297" +python-versions = ">=3.8,<4.0" +content-hash = "57cf1f5e88f1b2914dcdcdc44ab2b6d89cd2c18fd64b7ed4eded57b3e244ef8f" diff --git a/pyproject.toml b/pyproject.toml index c242b7d..2774997 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,13 +14,14 @@ documentation = "https://slowapi.readthedocs.io/en/latest/" include = ["slowapi/py.typed"] [tool.poetry.dependencies] -python = ">=3.7,<4.0" +python = ">=3.8,<4.0" limits = ">=2.3" redis = {version = "^3.4.1", optional = true} +typing-extensions = ">4.10" [tool.poetry.dev-dependencies] isort = "^4.3.21" -mypy = "^0.910" +mypy = "^1.10.0" black = "^23.0.0" fastapi = "^0.89.0" lxml = "^4.9.1" diff --git a/slowapi/extension.py b/slowapi/extension.py index 6cbb681..14b2dc6 100644 --- a/slowapi/extension.py +++ b/slowapi/extension.py @@ -1,6 +1,7 @@ """ The starlette extension to rate-limit requests """ + import asyncio import functools import inspect @@ -10,36 +11,34 @@ import time from datetime import datetime from email.utils import formatdate, parsedate_to_datetime -from functools import wraps -from typing import ( - Any, - Callable, - Dict, - List, - Optional, - Set, - Tuple, - TypeVar, - Union, -) +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, TypeVar -from limits import RateLimitItem # type: ignore -from limits.errors import ConfigurationError # type: ignore -from limits.storage import MemoryStorage, storage_from_string # type: ignore -from limits.strategies import STRATEGIES, RateLimiter # type: ignore +from limits import RateLimitItem +from limits.errors import ConfigurationError +from limits.storage import MemoryStorage, storage_from_string +from limits.strategies import STRATEGIES, RateLimiter +from starlette.applications import Starlette from starlette.config import Config from starlette.datastructures import MutableHeaders from starlette.requests import Request from starlette.responses import JSONResponse, Response -from typing_extensions import Literal +from typing_extensions import Literal, ParamSpec from .errors import RateLimitExceeded +from .types import ( + Cost, + ErrorMessage, + ExemptWhen, + KeyFn, + LimitProvider, + Scope, + is_request_fn, +) from .wrappers import Limit, LimitGroup # used to annotate get_app_config method T = TypeVar("T") -# Define an alias for the most commonly used type -StrOrCallableStr = Union[str, Callable[..., str]] +P = ParamSpec("P") class C: @@ -129,16 +128,16 @@ class Limiter: def __init__( self, # app: Starlette = None, - key_func: Callable[..., str], - default_limits: List[StrOrCallableStr] = [], - application_limits: List[StrOrCallableStr] = [], + key_func: KeyFn, + default_limits: List[LimitProvider] = [], + application_limits: List[LimitProvider] = [], headers_enabled: bool = False, strategy: Optional[str] = None, storage_uri: Optional[str] = None, storage_options: Dict[str, str] = {}, auto_check: bool = True, swallow_errors: bool = False, - in_memory_fallback: List[StrOrCallableStr] = [], + in_memory_fallback: List[LimitProvider] = [], in_memory_fallback_enabled: bool = False, retry_after: Optional[str] = None, key_prefix: str = "", @@ -271,7 +270,7 @@ def emit(*_): C.HEADER_RETRY_AFTER_VALUE ) self._key_prefix = self._key_prefix or self.get_app_config(C.KEY_PREFIX) - app_limits: Optional[StrOrCallableStr] = self.get_app_config( + app_limits: Optional[LimitProvider] = self.get_app_config( C.APPLICATION_LIMITS, None ) if not self._application_limits and app_limits: @@ -289,7 +288,7 @@ def emit(*_): ) ] - conf_limits: Optional[StrOrCallableStr] = self.get_app_config( + conf_limits: Optional[LimitProvider] = self.get_app_config( C.DEFAULT_LIMITS, None ) if not self._default_limits and conf_limits: @@ -299,7 +298,7 @@ def emit(*_): ) ] fallback_enabled = self.get_app_config(C.IN_MEMORY_FALLBACK_ENABLED, False) - fallback_limits: Optional[StrOrCallableStr] = self.get_app_config( + fallback_limits: Optional[LimitProvider] = self.get_app_config( C.IN_MEMORY_FALLBACK, None ) if not self._in_memory_fallback and fallback_limits: @@ -325,14 +324,14 @@ def emit(*_): self._fallback_storage = MemoryStorage() self._fallback_limiter = STRATEGIES[strategy](self._fallback_storage) - def slowapi_startup(self) -> None: + def slowapi_startup(self, app: Starlette) -> None: """ Starlette startup event handler that links the app with the Limiter instance. """ app.state.limiter = self # type: ignore app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) # type: ignore - def get_app_config(self, key: str, default_value: T = None) -> T: + def get_app_config(self, key: str, default_value: Optional[T] = None) -> T: """ Place holder until we find a better way to load config from app """ @@ -485,7 +484,7 @@ def __evaluate_limits( failed_limit = None limit_for_header = None for lim in limits: - limit_scope = lim.scope or endpoint + limit_scope = lim.scope(request) or endpoint if lim.is_exempt: continue if lim.methods is not None and request.method.lower() not in lim.methods: @@ -493,7 +492,7 @@ def __evaluate_limits( if lim.per_method: limit_scope += ":%s" % request.method - if "request" in inspect.signature(lim.key_func).parameters.keys(): + if is_request_fn(lim.key_func): limit_key = lim.key_func(request) else: limit_key = lim.key_func() @@ -586,7 +585,7 @@ def _check_request_limit( if endpoint_func_name in self._dynamic_route_limits: for lim in self._dynamic_route_limits[endpoint_func_name]: try: - dynamic_limits.extend(list(lim.with_request(request))) + dynamic_limits.extend(lim.resolve(request)) except ValueError as e: self.logger.error( "failed to load ratelimit for view function %s (%s)", @@ -605,11 +604,19 @@ def _check_request_limit( self._storage_dead = False self.__check_backend_count = 0 else: - all_limits = list(itertools.chain(*self._in_memory_fallback)) + all_limits = list( + itertools.chain( + *(lim.resolve() for lim in self._in_memory_fallback) + ) + ) if not all_limits: route_limits: List[Limit] = limits + dynamic_limits all_limits = ( - list(itertools.chain(*self._application_limits)) + list( + itertools.chain( + *(lim.resolve() for lim in self._application_limits) + ) + ) if in_middleware else [] ) @@ -625,7 +632,11 @@ def _check_request_limit( ) or combined_defaults ): - all_limits += list(itertools.chain(*self._default_limits)) + all_limits += list( + itertools.chain( + *(lim.resolve() for lim in self._default_limits) + ) + ) # actually check the limits, so far we've only computed the list of limits to check self.__evaluate_limits(request, _endpoint_key, all_limits) except Exception as e: # no qa @@ -646,20 +657,20 @@ def _check_request_limit( def __limit_decorator( self, - limit_value: StrOrCallableStr, - key_func: Optional[Callable[..., str]] = None, + limit_value: LimitProvider, + key_func: Optional[KeyFn] = None, shared: bool = False, - scope: Optional[StrOrCallableStr] = None, + scope: Optional[Scope] = None, per_method: bool = False, methods: Optional[List[str]] = None, - error_message: Optional[str] = None, - exempt_when: Optional[Callable[..., bool]] = None, - cost: Union[int, Callable[..., int]] = 1, + error_message: Optional[ErrorMessage] = None, + exempt_when: Optional[ExemptWhen] = None, + cost: Cost = 1, override_defaults: bool = True, - ) -> Callable[..., Any]: + ) -> Callable[[Callable[P, T]], Callable[P, T]]: _scope = scope if shared else None - def decorator(func: Callable[..., Response]): + def decorator(func: Callable[P, T]) -> Callable[P, T]: keyfunc = key_func or self._key_func name = f"{func.__module__}.{func.__name__}" dynamic_limit = None @@ -689,7 +700,7 @@ def decorator(func: Callable[..., Response]): exempt_when, cost, override_defaults, - ) + ).resolve() ) except ValueError as e: self.logger.error( @@ -717,7 +728,7 @@ def decorator(func: Callable[..., Response]): if asyncio.iscoroutinefunction(func): # Handle async request/response functions. @functools.wraps(func) - async def async_wrapper(*args: Any, **kwargs: Any) -> Response: + async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> T: # get the request object from the decorated endpoint function if self.enabled: request = kwargs.get("request", args[idx] if args else None) @@ -731,12 +742,13 @@ async def async_wrapper(*args: Any, **kwargs: Any) -> Response: ): self._check_request_limit(request, func, False) request.state._rate_limiting_complete = True - response = await func(*args, **kwargs) # type: ignore + response: T = await func(*args, **kwargs) if self.enabled: if not isinstance(response, Response): # get the response object from the decorated endpoint function self._inject_headers( - kwargs.get("response"), request.state.view_rate_limit # type: ignore + kwargs.get("response"), # type: ignore + request.state.view_rate_limit, ) else: self._inject_headers( @@ -744,12 +756,12 @@ async def async_wrapper(*args: Any, **kwargs: Any) -> Response: ) return response - return async_wrapper + return async_wrapper # type: ignore else: # Handle sync request/response functions. @functools.wraps(func) - def sync_wrapper(*args: Any, **kwargs: Any) -> Response: + def sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> T: # get the request object from the decorated endpoint function if self.enabled: request = kwargs.get("request", args[idx] if args else None) @@ -768,7 +780,8 @@ def sync_wrapper(*args: Any, **kwargs: Any) -> Response: if not isinstance(response, Response): # get the response object from the decorated endpoint function self._inject_headers( - kwargs.get("response"), request.state.view_rate_limit # type: ignore + kwargs.get("response"), # type: ignore + request.state.view_rate_limit, ) else: self._inject_headers( @@ -782,15 +795,15 @@ def sync_wrapper(*args: Any, **kwargs: Any) -> Response: def limit( self, - limit_value: StrOrCallableStr, - key_func: Optional[Callable[..., str]] = None, + limit_value: LimitProvider, + key_func: Optional[KeyFn] = None, per_method: bool = False, methods: Optional[List[str]] = None, - error_message: Optional[str] = None, - exempt_when: Optional[Callable[..., bool]] = None, - cost: Union[int, Callable[..., int]] = 1, + error_message: Optional[ErrorMessage] = None, + exempt_when: Optional[ExemptWhen] = None, + cost: Cost = 1, override_defaults: bool = True, - ) -> Callable: + ) -> Callable[[Callable[P, T]], Callable[P, T]]: """ Decorator to be used for rate limiting individual routes. @@ -822,14 +835,14 @@ def limit( def shared_limit( self, - limit_value: StrOrCallableStr, - scope: StrOrCallableStr, - key_func: Optional[Callable[..., str]] = None, - error_message: Optional[str] = None, - exempt_when: Optional[Callable[..., bool]] = None, - cost: Union[int, Callable[..., int]] = 1, + limit_value: LimitProvider, + scope: Scope, + key_func: Optional[KeyFn] = None, + error_message: Optional[ErrorMessage] = None, + exempt_when: Optional[ExemptWhen] = None, + cost: Cost = 1, override_defaults: bool = True, - ) -> Callable: + ) -> Callable[[Callable[P, T]], Callable[P, T]]: """ Decorator to be applied to multiple routes sharing the same rate limit. @@ -861,25 +874,11 @@ def shared_limit( override_defaults=override_defaults, ) - def exempt(self, obj): + def exempt(self, obj: Callable[P, T]) -> Callable[P, T]: """ Decorator to mark a view as exempt from rate limits. """ name = "%s.%s" % (obj.__module__, obj.__name__) self._exempt_routes.add(name) - - if asyncio.iscoroutinefunction(obj): - - @wraps(obj) - async def __async_inner(*a, **k): - return await obj(*a, **k) - - return __async_inner - else: - - @wraps(obj) - def __inner(*a, **k): - return obj(*a, **k) - - return __inner + return obj diff --git a/slowapi/types.py b/slowapi/types.py new file mode 100644 index 0000000..65b1a2f --- /dev/null +++ b/slowapi/types.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +import inspect +from typing import Any, Callable, TypeVar, Union + +from starlette.requests import Request +from typing_extensions import Protocol, TypeIs + +T = TypeVar("T", covariant=True) + + +class RequestFn(Protocol[T]): + def __call__(self, request: Request) -> T: ... + + +PlainFn = Callable[[], T] +ValueOrFn = Union[T, PlainFn[T]] +MaybeRequestFn = Union[PlainFn[T], RequestFn[T]] +ValueOrRequestFn = Union[T, RequestFn[T]] + +KeyFn = MaybeRequestFn[str] +Scope = ValueOrRequestFn[str] +ErrorMessage = ValueOrFn[str] +ExemptWhen = PlainFn[bool] +Cost = ValueOrRequestFn[int] + + +class LimitProviderFn(Protocol): + def __call__(self, key: str) -> str: ... + + +LimitProvider = Union[str, PlainFn[str], LimitProviderFn] + + +def is_limit_provider_fn(func: Any) -> TypeIs[LimitProviderFn]: + return "key" in inspect.signature(func).parameters.keys() + + +def is_request_fn(func: Any) -> TypeIs[RequestFn]: + return "request" in inspect.signature(func).parameters.keys() diff --git a/slowapi/wrappers.py b/slowapi/wrappers.py index a1741c5..8c21553 100644 --- a/slowapi/wrappers.py +++ b/slowapi/wrappers.py @@ -1,7 +1,18 @@ -import inspect -from typing import Callable, Iterator, List, Optional, Union +from typing import Iterator, List, Optional -from limits import RateLimitItem, parse_many # type: ignore +from limits import RateLimitItem, parse_many +from starlette.requests import Request + +from .types import ( + Cost, + ErrorMessage, + ExemptWhen, + KeyFn, + LimitProvider, + Scope, + is_limit_provider_fn, + is_request_fn, +) class Limit(object): @@ -12,13 +23,13 @@ class Limit(object): def __init__( self, limit: RateLimitItem, - key_func: Callable[..., str], - scope: Optional[Union[str, Callable[..., str]]], + key_func: KeyFn, + scope: Optional[Scope], per_method: bool, methods: Optional[List[str]], - error_message: Optional[Union[str, Callable[..., str]]], - exempt_when: Optional[Callable[..., bool]], - cost: Union[int, Callable[..., int]], + error_message: Optional[ErrorMessage], + exempt_when: Optional[ExemptWhen], + cost: Cost, override_defaults: bool, ) -> None: self.limit = limit @@ -39,18 +50,11 @@ def is_exempt(self) -> bool: """ return self.exempt_when() if self.exempt_when is not None else False - @property - def scope(self) -> str: - # flack.request.endpoint is the name of the function for the endpoint - # FIXME: how to get the request here? + def scope(self, request: Request) -> str: if self.__scope is None: return "" else: - return ( - self.__scope(request.endpoint) # type: ignore - if callable(self.__scope) - else self.__scope - ) + return self.__scope(request) if callable(self.__scope) else self.__scope class LimitGroup(object): @@ -60,14 +64,14 @@ class LimitGroup(object): def __init__( self, - limit_provider: Union[str, Callable[..., str]], - key_function: Callable[..., str], - scope: Optional[Union[str, Callable[..., str]]], + limit_provider: LimitProvider, + key_function: KeyFn, + scope: Optional[Scope], per_method: bool, methods: Optional[List[str]], - error_message: Optional[Union[str, Callable[..., str]]], - exempt_when: Optional[Callable[..., bool]], - cost: Union[int, Callable[..., int]], + error_message: Optional[ErrorMessage], + exempt_when: Optional[ExemptWhen], + cost: Cost, override_defaults: bool, ): self.__limit_provider = limit_provider @@ -79,17 +83,16 @@ def __init__( self.exempt_when = exempt_when self.cost = cost self.override_defaults = override_defaults - self.request = None - def __iter__(self) -> Iterator[Limit]: + def resolve(self, request: Optional[Request] = None) -> Iterator[Limit]: if callable(self.__limit_provider): - if "key" in inspect.signature(self.__limit_provider).parameters.keys(): - assert ( - "request" in inspect.signature(self.key_function).parameters.keys() - ), f"Limit provider function {self.key_function.__name__} needs a `request` argument" - if self.request is None: + if is_limit_provider_fn(self.__limit_provider): + assert is_request_fn( + self.key_function + ), f"Limit provider function {getattr(self.key_function, '__name__', str(self.key_function))} needs a `request` argument" + if request is None: raise Exception("`request` object can't be None") - limit_raw = self.__limit_provider(self.key_function(self.request)) + limit_raw = self.__limit_provider(self.key_function(request)) else: limit_raw = self.__limit_provider() else: @@ -107,7 +110,3 @@ def __iter__(self) -> Iterator[Limit]: self.cost, self.override_defaults, ) - - def with_request(self, request): - self.request = request - return self