limits 4.2__tar.gz → 4.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {limits-4.2 → limits-4.3}/CLASSIFIERS +0 -1
- {limits-4.2 → limits-4.3}/HISTORY.rst +16 -0
- {limits-4.2 → limits-4.3}/PKG-INFO +9 -4
- {limits-4.2 → limits-4.3}/README.rst +1 -1
- {limits-4.2 → limits-4.3}/doc/source/conf.py +1 -0
- {limits-4.2 → limits-4.3}/doc/source/index.rst +2 -2
- {limits-4.2 → limits-4.3}/doc/source/installation.rst +22 -0
- {limits-4.2 → limits-4.3}/doc/source/storage.rst +29 -10
- {limits-4.2 → limits-4.3}/doc/source/strategies.rst +1 -1
- {limits-4.2 → limits-4.3}/limits/_version.py +3 -3
- {limits-4.2 → limits-4.3}/limits/aio/storage/base.py +5 -8
- {limits-4.2 → limits-4.3}/limits/aio/storage/etcd.py +4 -4
- {limits-4.2 → limits-4.3}/limits/aio/storage/memcached.py +4 -4
- {limits-4.2 → limits-4.3}/limits/aio/storage/memory.py +40 -26
- {limits-4.2 → limits-4.3}/limits/aio/storage/mongodb.py +4 -7
- {limits-4.2 → limits-4.3}/limits/aio/storage/redis/__init__.py +87 -26
- {limits-4.2 → limits-4.3}/limits/aio/storage/redis/bridge.py +8 -9
- {limits-4.2 → limits-4.3}/limits/aio/storage/redis/coredis.py +18 -18
- {limits-4.2 → limits-4.3}/limits/aio/storage/redis/redispy.py +17 -17
- limits-4.3/limits/aio/storage/redis/valkey.py +9 -0
- {limits-4.2 → limits-4.3}/limits/aio/strategies.py +2 -2
- {limits-4.2 → limits-4.3}/limits/storage/__init__.py +12 -11
- {limits-4.2 → limits-4.3}/limits/storage/base.py +5 -10
- {limits-4.2 → limits-4.3}/limits/storage/etcd.py +4 -4
- {limits-4.2 → limits-4.3}/limits/storage/memcached.py +4 -7
- {limits-4.2 → limits-4.3}/limits/storage/memory.py +40 -31
- {limits-4.2 → limits-4.3}/limits/storage/mongodb.py +7 -10
- {limits-4.2 → limits-4.3}/limits/storage/redis.py +48 -18
- {limits-4.2 → limits-4.3}/limits/storage/redis_cluster.py +29 -11
- {limits-4.2 → limits-4.3}/limits/storage/redis_sentinel.py +33 -11
- {limits-4.2 → limits-4.3}/limits/storage/registry.py +1 -3
- {limits-4.2 → limits-4.3}/limits/strategies.py +9 -9
- {limits-4.2 → limits-4.3}/limits/typing.py +35 -40
- {limits-4.2 → limits-4.3}/limits/util.py +10 -12
- {limits-4.2 → limits-4.3}/limits.egg-info/PKG-INFO +9 -4
- {limits-4.2 → limits-4.3}/limits.egg-info/SOURCES.txt +3 -0
- {limits-4.2 → limits-4.3}/limits.egg-info/requires.txt +7 -0
- limits-4.3/requirements/storage/async-valkey.txt +1 -0
- limits-4.3/requirements/storage/valkey.txt +1 -0
- {limits-4.2 → limits-4.3}/requirements/test.txt +2 -0
- {limits-4.2 → limits-4.3}/setup.py +3 -1
- {limits-4.2 → limits-4.3}/tests/test_storage.py +3 -3
- {limits-4.2 → limits-4.3}/CONTRIBUTIONS.rst +0 -0
- {limits-4.2 → limits-4.3}/LICENSE.txt +0 -0
- {limits-4.2 → limits-4.3}/MANIFEST.in +0 -0
- {limits-4.2 → limits-4.3}/doc/Makefile +0 -0
- {limits-4.2 → limits-4.3}/doc/source/_static/custom.css +0 -0
- {limits-4.2 → limits-4.3}/doc/source/api.rst +0 -0
- {limits-4.2 → limits-4.3}/doc/source/async.rst +0 -0
- {limits-4.2 → limits-4.3}/doc/source/changelog.rst +0 -0
- {limits-4.2 → limits-4.3}/doc/source/custom-storage.rst +0 -0
- {limits-4.2 → limits-4.3}/doc/source/quickstart.rst +0 -0
- {limits-4.2 → limits-4.3}/doc/source/theme_config.py +0 -0
- {limits-4.2 → limits-4.3}/limits/__init__.py +5 -5
- {limits-4.2 → limits-4.3}/limits/aio/__init__.py +0 -0
- {limits-4.2 → limits-4.3}/limits/aio/storage/__init__.py +4 -4
- {limits-4.2 → limits-4.3}/limits/errors.py +0 -0
- {limits-4.2 → limits-4.3}/limits/limits.py +0 -0
- {limits-4.2 → limits-4.3}/limits/py.typed +0 -0
- {limits-4.2 → limits-4.3}/limits/resources/redis/lua_scripts/acquire_moving_window.lua +0 -0
- {limits-4.2 → limits-4.3}/limits/resources/redis/lua_scripts/acquire_sliding_window.lua +0 -0
- {limits-4.2 → limits-4.3}/limits/resources/redis/lua_scripts/clear_keys.lua +0 -0
- {limits-4.2 → limits-4.3}/limits/resources/redis/lua_scripts/incr_expire.lua +0 -0
- {limits-4.2 → limits-4.3}/limits/resources/redis/lua_scripts/moving_window.lua +0 -0
- {limits-4.2 → limits-4.3}/limits/resources/redis/lua_scripts/sliding_window.lua +0 -0
- {limits-4.2 → limits-4.3}/limits/version.py +0 -0
- {limits-4.2 → limits-4.3}/limits.egg-info/dependency_links.txt +0 -0
- {limits-4.2 → limits-4.3}/limits.egg-info/not-zip-safe +0 -0
- {limits-4.2 → limits-4.3}/limits.egg-info/top_level.txt +0 -0
- {limits-4.2 → limits-4.3}/pyproject.toml +0 -0
- {limits-4.2 → limits-4.3}/requirements/ci.txt +0 -0
- {limits-4.2 → limits-4.3}/requirements/dev.txt +0 -0
- {limits-4.2 → limits-4.3}/requirements/docs.txt +0 -0
- {limits-4.2 → limits-4.3}/requirements/main.txt +0 -0
- {limits-4.2 → limits-4.3}/requirements/storage/async-etcd.txt +0 -0
- {limits-4.2 → limits-4.3}/requirements/storage/async-memcached.txt +0 -0
- {limits-4.2 → limits-4.3}/requirements/storage/async-mongodb.txt +0 -0
- {limits-4.2 → limits-4.3}/requirements/storage/async-redis.txt +0 -0
- {limits-4.2 → limits-4.3}/requirements/storage/etcd.txt +0 -0
- {limits-4.2 → limits-4.3}/requirements/storage/memcached.txt +0 -0
- {limits-4.2 → limits-4.3}/requirements/storage/mongodb.txt +0 -0
- {limits-4.2 → limits-4.3}/requirements/storage/redis.txt +0 -0
- {limits-4.2 → limits-4.3}/requirements/storage/rediscluster.txt +0 -0
- {limits-4.2 → limits-4.3}/setup.cfg +0 -0
- {limits-4.2 → limits-4.3}/tests/test_limit_granularities.py +0 -0
- {limits-4.2 → limits-4.3}/tests/test_limits.py +0 -0
- {limits-4.2 → limits-4.3}/tests/test_ratelimit_parser.py +0 -0
- {limits-4.2 → limits-4.3}/tests/test_strategy.py +0 -0
- {limits-4.2 → limits-4.3}/tests/test_utils.py +0 -0
- {limits-4.2 → limits-4.3}/versioneer.py +0 -0
|
@@ -5,7 +5,6 @@ Operating System :: MacOS
|
|
|
5
5
|
Operating System :: POSIX :: Linux
|
|
6
6
|
Operating System :: OS Independent
|
|
7
7
|
Topic :: Software Development :: Libraries :: Python Modules
|
|
8
|
-
Programming Language :: Python :: 3.9
|
|
9
8
|
Programming Language :: Python :: 3.10
|
|
10
9
|
Programming Language :: Python :: 3.11
|
|
11
10
|
Programming Language :: Python :: 3.12
|
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
Changelog
|
|
4
4
|
=========
|
|
5
5
|
|
|
6
|
+
v4.3
|
|
7
|
+
----
|
|
8
|
+
Release Date: 2025-03-14
|
|
9
|
+
|
|
10
|
+
* Feature
|
|
11
|
+
|
|
12
|
+
* Add support for ``valkey://`` schemas and using ``valkey-py``
|
|
13
|
+
dependency
|
|
14
|
+
|
|
15
|
+
* Compatibility
|
|
16
|
+
|
|
17
|
+
* Drop support for python 3.9
|
|
18
|
+
* Improve typing to use python 3.10+ features
|
|
19
|
+
|
|
20
|
+
|
|
6
21
|
v4.2
|
|
7
22
|
----
|
|
8
23
|
Release Date: 2025-03-11
|
|
@@ -792,5 +807,6 @@ Release Date: 2015-01-08
|
|
|
792
807
|
|
|
793
808
|
|
|
794
809
|
|
|
810
|
+
|
|
795
811
|
|
|
796
812
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: limits
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.3
|
|
4
4
|
Summary: Rate limiting utilities
|
|
5
5
|
Home-page: https://limits.readthedocs.org
|
|
6
6
|
Author: Ali-Akber Saifee
|
|
@@ -14,13 +14,12 @@ Classifier: Operating System :: MacOS
|
|
|
14
14
|
Classifier: Operating System :: POSIX :: Linux
|
|
15
15
|
Classifier: Operating System :: OS Independent
|
|
16
16
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.10
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.13
|
|
22
21
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
23
|
-
Requires-Python: >=3.
|
|
22
|
+
Requires-Python: >=3.10
|
|
24
23
|
License-File: LICENSE.txt
|
|
25
24
|
Requires-Dist: deprecated>=1.2
|
|
26
25
|
Requires-Dist: packaging<25,>=21
|
|
@@ -35,6 +34,8 @@ Provides-Extra: mongodb
|
|
|
35
34
|
Requires-Dist: pymongo<5,>4.1; extra == "mongodb"
|
|
36
35
|
Provides-Extra: etcd
|
|
37
36
|
Requires-Dist: etcd3; extra == "etcd"
|
|
37
|
+
Provides-Extra: valkey
|
|
38
|
+
Requires-Dist: valkey>=6; extra == "valkey"
|
|
38
39
|
Provides-Extra: async-redis
|
|
39
40
|
Requires-Dist: coredis<5,>=3.4.0; extra == "async-redis"
|
|
40
41
|
Provides-Extra: async-memcached
|
|
@@ -44,17 +45,21 @@ Provides-Extra: async-mongodb
|
|
|
44
45
|
Requires-Dist: motor<4,>=3; extra == "async-mongodb"
|
|
45
46
|
Provides-Extra: async-etcd
|
|
46
47
|
Requires-Dist: aetcd; extra == "async-etcd"
|
|
48
|
+
Provides-Extra: async-valkey
|
|
49
|
+
Requires-Dist: valkey>=6; extra == "async-valkey"
|
|
47
50
|
Provides-Extra: all
|
|
48
51
|
Requires-Dist: redis!=4.5.2,!=4.5.3,<6.0.0,>3; extra == "all"
|
|
49
52
|
Requires-Dist: redis!=4.5.2,!=4.5.3,>=4.2.0; extra == "all"
|
|
50
53
|
Requires-Dist: pymemcache<5.0.0,>3; extra == "all"
|
|
51
54
|
Requires-Dist: pymongo<5,>4.1; extra == "all"
|
|
52
55
|
Requires-Dist: etcd3; extra == "all"
|
|
56
|
+
Requires-Dist: valkey>=6; extra == "all"
|
|
53
57
|
Requires-Dist: coredis<5,>=3.4.0; extra == "all"
|
|
54
58
|
Requires-Dist: emcache>=0.6.1; python_version < "3.11" and extra == "all"
|
|
55
59
|
Requires-Dist: emcache>=1; (python_version >= "3.11" and python_version < "3.13.0") and extra == "all"
|
|
56
60
|
Requires-Dist: motor<4,>=3; extra == "all"
|
|
57
61
|
Requires-Dist: aetcd; extra == "all"
|
|
62
|
+
Requires-Dist: valkey>=6; extra == "all"
|
|
58
63
|
Dynamic: author
|
|
59
64
|
Dynamic: author-email
|
|
60
65
|
Dynamic: classifier
|
|
@@ -126,7 +131,7 @@ Moving Window
|
|
|
126
131
|
|
|
127
132
|
This strategy adds each request’s timestamp to a log if the ``nth`` oldest entry (where ``n``
|
|
128
133
|
is the limit) is either not present or is older than the duration of the window (for example with a rate limit of
|
|
129
|
-
``10 requests per minute`` if there are either less than 10 entries or the 10th oldest entry is
|
|
134
|
+
``10 requests per minute`` if there are either less than 10 entries or the 10th oldest entry is at least
|
|
130
135
|
60 seconds old). Upon adding a new entry to the log "expired" entries are truncated.
|
|
131
136
|
|
|
132
137
|
For example, with a rate limit of 10 requests per minute:
|
|
@@ -57,7 +57,7 @@ Moving Window
|
|
|
57
57
|
|
|
58
58
|
This strategy adds each request’s timestamp to a log if the ``nth`` oldest entry (where ``n``
|
|
59
59
|
is the limit) is either not present or is older than the duration of the window (for example with a rate limit of
|
|
60
|
-
``10 requests per minute`` if there are either less than 10 entries or the 10th oldest entry is
|
|
60
|
+
``10 requests per minute`` if there are either less than 10 entries or the 10th oldest entry is at least
|
|
61
61
|
60 seconds old). Upon adding a new entry to the log "expired" entries are truncated.
|
|
62
62
|
|
|
63
63
|
For example, with a rate limit of 10 requests per minute:
|
|
@@ -81,4 +81,5 @@ intersphinx_mapping = {
|
|
|
81
81
|
"pymongo": ("https://pymongo.readthedocs.io/en/stable/", None),
|
|
82
82
|
"python-etcd3": ("https://python-etcd3.readthedocs.io/en/latest/", None),
|
|
83
83
|
"aetcd": ("https://aetcd.readthedocs.io/en/latest/", None),
|
|
84
|
+
"valkey-py": ("https://valkey-py.readthedocs.io/en/latest/", None),
|
|
84
85
|
}
|
|
@@ -55,12 +55,12 @@ To get started
|
|
|
55
55
|
|
|
56
56
|
.. code:: console
|
|
57
57
|
|
|
58
|
-
$ git clone
|
|
58
|
+
$ git clone https://github.com/alisaifee/limits.git
|
|
59
59
|
$ cd limits
|
|
60
60
|
$ pip install -r requirements/dev.txt
|
|
61
61
|
|
|
62
62
|
Since `limits` integrates with various backend storages, local development and running tests
|
|
63
|
-
requires a
|
|
63
|
+
requires a working `docker & docker-compose installation <https://docs.docker.com/compose/gettingstarted/>`_.
|
|
64
64
|
|
|
65
65
|
Running the tests will start the relevant containers automatically - but will leave them running
|
|
66
66
|
so as to not incur the overhead of starting up on each test run. To run the tests:
|
|
@@ -58,6 +58,16 @@ Install the package with pip:
|
|
|
58
58
|
|
|
59
59
|
.. literalinclude:: ../../requirements/storage/etcd.txt
|
|
60
60
|
|
|
61
|
+
.. tab:: Valkey
|
|
62
|
+
|
|
63
|
+
.. code:: console
|
|
64
|
+
|
|
65
|
+
$ pip install limits[valkey]
|
|
66
|
+
|
|
67
|
+
Includes:
|
|
68
|
+
|
|
69
|
+
.. literalinclude:: ../../requirements/storage/valkey.txt
|
|
70
|
+
|
|
61
71
|
More details around the specifics of each storage backend can be
|
|
62
72
|
found in :ref:`storage`
|
|
63
73
|
|
|
@@ -114,3 +124,15 @@ along with the package using the following extras:
|
|
|
114
124
|
Includes:
|
|
115
125
|
|
|
116
126
|
.. literalinclude:: ../../requirements/storage/async-etcd.txt
|
|
127
|
+
|
|
128
|
+
.. tab:: Valkey
|
|
129
|
+
|
|
130
|
+
.. code:: console
|
|
131
|
+
|
|
132
|
+
$ pip install limits[async-valkey]
|
|
133
|
+
|
|
134
|
+
Includes:
|
|
135
|
+
|
|
136
|
+
.. literalinclude:: ../../requirements/storage/async-valkey.txt
|
|
137
|
+
|
|
138
|
+
|
|
@@ -38,15 +38,15 @@ the results in `github <https://github.com/alisaifee/limits/actions/workflows/co
|
|
|
38
38
|
|
|
39
39
|
`Redis <https://redis.io>`_
|
|
40
40
|
|
|
41
|
-
.. program-output:: bash -c "cat ../../.github/workflows/compatibility.yml | grep -o -P 'LIMITS_REDIS_SERVER_VERSION=[\d\.]+' | cut -d = -f 2"
|
|
41
|
+
.. program-output:: bash -c "cat ../../.github/workflows/compatibility.yml | grep -o -P 'LIMITS_REDIS_SERVER_VERSION=[\d\.]+' | cut -d = -f 2 | sort --version-sort | uniq"
|
|
42
42
|
|
|
43
43
|
Redis with SSL
|
|
44
44
|
|
|
45
|
-
.. program-output:: bash -c "cat ../../.github/workflows/compatibility.yml | grep -o -P 'LIMITS_REDIS_SERVER_SSL_VERSION=[\d\.]+' | cut -d = -f 2"
|
|
45
|
+
.. program-output:: bash -c "cat ../../.github/workflows/compatibility.yml | grep -o -P 'LIMITS_REDIS_SERVER_SSL_VERSION=[\d\.]+' | cut -d = -f 2 | sort --version-sort | uniq"
|
|
46
46
|
|
|
47
47
|
`Redis Sentinel <https://redis.io/topics/sentinel>`_
|
|
48
48
|
|
|
49
|
-
.. program-output:: bash -c "cat ../../.github/workflows/compatibility.yml | grep -o -P 'LIMITS_REDIS_SENTINEL_SERVER_VERSION=[\d\.]+' | cut -d = -f 2"
|
|
49
|
+
.. program-output:: bash -c "cat ../../.github/workflows/compatibility.yml | grep -o -P 'LIMITS_REDIS_SENTINEL_SERVER_VERSION=[\d\.]+' | cut -d = -f 2 | sort --version-sort | uniq"
|
|
50
50
|
|
|
51
51
|
.. tab:: Redis Cluster
|
|
52
52
|
|
|
@@ -65,7 +65,7 @@ the results in `github <https://github.com/alisaifee/limits/actions/workflows/co
|
|
|
65
65
|
|
|
66
66
|
`Redis cluster <https://redis.io/topics/cluster-tutorial>`_
|
|
67
67
|
|
|
68
|
-
.. program-output:: bash -c "cat ../../.github/workflows/compatibility.yml | grep -o -P 'LIMITS_REDIS_SERVER_VERSION=[\d\.]+' | cut -d = -f 2"
|
|
68
|
+
.. program-output:: bash -c "cat ../../.github/workflows/compatibility.yml | grep -o -P 'LIMITS_REDIS_SERVER_VERSION=[\d\.]+' | cut -d = -f 2 | sort --version-sort | uniq"
|
|
69
69
|
|
|
70
70
|
.. tab:: Memcached
|
|
71
71
|
|
|
@@ -79,7 +79,7 @@ the results in `github <https://github.com/alisaifee/limits/actions/workflows/co
|
|
|
79
79
|
|
|
80
80
|
`Memcached <https://memcached.org/>`_
|
|
81
81
|
|
|
82
|
-
.. program-output:: bash -c "cat ../../.github/workflows/compatibility.yml | grep -o -P 'LIMITS_MEMCACHED_SERVER_VERSION=[\d\.]+' | cut -d = -f 2"
|
|
82
|
+
.. program-output:: bash -c "cat ../../.github/workflows/compatibility.yml | grep -o -P 'LIMITS_MEMCACHED_SERVER_VERSION=[\d\.]+' | cut -d = -f 2 | sort --version-sort | uniq"
|
|
83
83
|
|
|
84
84
|
.. tab:: MongoDB
|
|
85
85
|
|
|
@@ -93,7 +93,7 @@ the results in `github <https://github.com/alisaifee/limits/actions/workflows/co
|
|
|
93
93
|
|
|
94
94
|
`MongoDB <https://www.mongodb.com/>`_
|
|
95
95
|
|
|
96
|
-
.. program-output:: bash -c "cat ../../.github/workflows/compatibility.yml | grep -o -P 'LIMITS_MONGODB_SERVER_VERSION=[\d\.]+' | cut -d = -f 2"
|
|
96
|
+
.. program-output:: bash -c "cat ../../.github/workflows/compatibility.yml | grep -o -P 'LIMITS_MONGODB_SERVER_VERSION=[\d\.]+' | cut -d = -f 2 | sort --version-sort | uniq"
|
|
97
97
|
|
|
98
98
|
.. tab:: Etcd
|
|
99
99
|
|
|
@@ -107,8 +107,21 @@ the results in `github <https://github.com/alisaifee/limits/actions/workflows/co
|
|
|
107
107
|
|
|
108
108
|
`Etcd <https://www.etcd.io/>`_
|
|
109
109
|
|
|
110
|
-
.. program-output:: bash -c "cat ../../.github/workflows/compatibility.yml | grep -o -P 'LIMITS_ETCD_SERVER_VERSION=[\d\.]+' | cut -d = -f 2"
|
|
110
|
+
.. program-output:: bash -c "cat ../../.github/workflows/compatibility.yml | grep -o -P 'LIMITS_ETCD_SERVER_VERSION=[\d\.]+' | cut -d = -f 2 | sort --version-sort | uniq"
|
|
111
111
|
|
|
112
|
+
.. tab:: Valkey
|
|
113
|
+
|
|
114
|
+
Dependency versions:
|
|
115
|
+
|
|
116
|
+
.. literalinclude:: ../../requirements/storage/valkey.txt
|
|
117
|
+
|
|
118
|
+
Dependency versions (async):
|
|
119
|
+
|
|
120
|
+
.. literalinclude:: ../../requirements/storage/async-valkey.txt
|
|
121
|
+
|
|
122
|
+
`Valkey <https://www.valkey.io/>`_
|
|
123
|
+
|
|
124
|
+
.. program-output:: bash -c "cat ../../.github/workflows/compatibility.yml | grep -o -P 'LIMITS_VALKEY_SERVER_VERSION=[\d\.]+' | cut -d = -f 2 | sort --version-sort | uniq"
|
|
112
125
|
|
|
113
126
|
Storage scheme
|
|
114
127
|
==============
|
|
@@ -162,7 +175,7 @@ If the redis server is listening over a unix domain socket you can use :code:`re
|
|
|
162
175
|
or :code:`redis+unix:///path/to/socket?db=n` (for database `n`).
|
|
163
176
|
|
|
164
177
|
If the database is password protected the password can be provided in the url, for example
|
|
165
|
-
:code:`redis://:foobared@localhost:6379` or :code:`redis+unix://:foobered/path/to/socket` if using a UDS
|
|
178
|
+
:code:`redis://:foobared@localhost:6379` or :code:`redis+unix://:foobered/path/to/socket` if using a UDS.
|
|
166
179
|
|
|
167
180
|
For scenarios where a redis connection pool is already available and can be reused, it can be provided
|
|
168
181
|
in :paramref:`~limits.storage.storage_from_string.options`, for example::
|
|
@@ -172,6 +185,12 @@ in :paramref:`~limits.storage.storage_from_string.options`, for example::
|
|
|
172
185
|
|
|
173
186
|
Depends on: :pypi:`redis`
|
|
174
187
|
|
|
188
|
+
|
|
189
|
+
.. versionadded:: 4.3
|
|
190
|
+
|
|
191
|
+
If the database ``uri`` scheme uses ``valkey`` instead of ``redis`` the implementation
|
|
192
|
+
used will be from :pypi:`valkey` instead of :pypi:`redis`.
|
|
193
|
+
|
|
175
194
|
Redis+SSL Storage
|
|
176
195
|
-----------------
|
|
177
196
|
|
|
@@ -185,7 +204,7 @@ Depends on: :pypi:`redis`
|
|
|
185
204
|
Redis+Sentinel Storage
|
|
186
205
|
----------------------
|
|
187
206
|
|
|
188
|
-
Requires the location(s) of the redis
|
|
207
|
+
Requires the location(s) of the redis sentinel instances and the `service-name`
|
|
189
208
|
that is monitored by the sentinels.
|
|
190
209
|
:code:`redis+sentinel://localhost:26379/my-redis-service`
|
|
191
210
|
or :code:`redis+sentinel://localhost:26379,localhost:26380/my-redis-service`.
|
|
@@ -197,7 +216,7 @@ When authentication details are provided in the url they will be used for both t
|
|
|
197
216
|
and as connection arguments for the underlying redis nodes managed by the sentinel.
|
|
198
217
|
|
|
199
218
|
If you need fine grained control it is recommended to use the additional :paramref:`~limits.storage.storage_from_string.options`
|
|
200
|
-
arguments. More details can be found in the API documentation for :class:`~limits.storage.RedisSentinelStorage` (or the
|
|
219
|
+
arguments. More details can be found in the API documentation for :class:`~limits.storage.RedisSentinelStorage` (or the async version: :class:`~limits.aio.storage.RedisSentinelStorage`).
|
|
201
220
|
|
|
202
221
|
Depends on: :pypi:`redis`
|
|
203
222
|
|
|
@@ -57,7 +57,7 @@ Moving Window
|
|
|
57
57
|
|
|
58
58
|
This strategy adds each request’s timestamp to a log if the ``nth`` oldest entry (where ``n``
|
|
59
59
|
is the limit) is either not present or is older than the duration of the window (for example with a rate limit of
|
|
60
|
-
``10 requests per minute`` if there are either less than 10 entries or the 10th oldest entry is
|
|
60
|
+
``10 requests per minute`` if there are either less than 10 entries or the 10th oldest entry is at least
|
|
61
61
|
60 seconds old). Upon adding a new entry to the log "expired" entries are truncated.
|
|
62
62
|
|
|
63
63
|
For example, with a rate limit of 10 requests per minute:
|
|
@@ -8,11 +8,11 @@ import json
|
|
|
8
8
|
|
|
9
9
|
version_json = '''
|
|
10
10
|
{
|
|
11
|
-
"date": "2025-03-
|
|
11
|
+
"date": "2025-03-14T16:19:10-0700",
|
|
12
12
|
"dirty": false,
|
|
13
13
|
"error": null,
|
|
14
|
-
"full-revisionid": "
|
|
15
|
-
"version": "4.
|
|
14
|
+
"full-revisionid": "0bcebd7b69d035e3df82779a50fb2d1e901b9ef9",
|
|
15
|
+
"version": "4.3"
|
|
16
16
|
}
|
|
17
17
|
''' # END VERSION_JSON
|
|
18
18
|
|
|
@@ -11,11 +11,8 @@ from limits.typing import (
|
|
|
11
11
|
Any,
|
|
12
12
|
Awaitable,
|
|
13
13
|
Callable,
|
|
14
|
-
Optional,
|
|
15
14
|
P,
|
|
16
15
|
R,
|
|
17
|
-
Type,
|
|
18
|
-
Union,
|
|
19
16
|
cast,
|
|
20
17
|
)
|
|
21
18
|
from limits.util import LazyDependency
|
|
@@ -43,7 +40,7 @@ class Storage(LazyDependency, metaclass=StorageRegistry):
|
|
|
43
40
|
Base class to extend when implementing an async storage backend.
|
|
44
41
|
"""
|
|
45
42
|
|
|
46
|
-
STORAGE_SCHEME:
|
|
43
|
+
STORAGE_SCHEME: list[str] | None
|
|
47
44
|
"""The storage schemes to register against this implementation"""
|
|
48
45
|
|
|
49
46
|
def __init_subclass__(cls, **kwargs: Any) -> None: # type:ignore[explicit-any]
|
|
@@ -61,9 +58,9 @@ class Storage(LazyDependency, metaclass=StorageRegistry):
|
|
|
61
58
|
|
|
62
59
|
def __init__(
|
|
63
60
|
self,
|
|
64
|
-
uri:
|
|
61
|
+
uri: str | None = None,
|
|
65
62
|
wrap_exceptions: bool = False,
|
|
66
|
-
**options:
|
|
63
|
+
**options: float | str | bool,
|
|
67
64
|
) -> None:
|
|
68
65
|
"""
|
|
69
66
|
:param wrap_exceptions: Whether to wrap storage exceptions in
|
|
@@ -74,7 +71,7 @@ class Storage(LazyDependency, metaclass=StorageRegistry):
|
|
|
74
71
|
|
|
75
72
|
@property
|
|
76
73
|
@abstractmethod
|
|
77
|
-
def base_exceptions(self) ->
|
|
74
|
+
def base_exceptions(self) -> type[Exception] | tuple[type[Exception], ...]:
|
|
78
75
|
raise NotImplementedError
|
|
79
76
|
|
|
80
77
|
@abstractmethod
|
|
@@ -114,7 +111,7 @@ class Storage(LazyDependency, metaclass=StorageRegistry):
|
|
|
114
111
|
raise NotImplementedError
|
|
115
112
|
|
|
116
113
|
@abstractmethod
|
|
117
|
-
async def reset(self) ->
|
|
114
|
+
async def reset(self) -> int | None:
|
|
118
115
|
"""
|
|
119
116
|
reset storage to clear limits
|
|
120
117
|
"""
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import time
|
|
5
5
|
import urllib.parse
|
|
6
|
-
from typing import TYPE_CHECKING
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
7
|
|
|
8
8
|
from limits.aio.storage.base import Storage
|
|
9
9
|
from limits.errors import ConcurrentUpdateError
|
|
@@ -46,7 +46,7 @@ class EtcdStorage(Storage):
|
|
|
46
46
|
"""
|
|
47
47
|
parsed = urllib.parse.urlparse(uri)
|
|
48
48
|
self.lib = self.dependencies["aetcd"].module
|
|
49
|
-
self.storage:
|
|
49
|
+
self.storage: aetcd.Client = self.lib.Client(
|
|
50
50
|
host=parsed.hostname, port=parsed.port, **options
|
|
51
51
|
)
|
|
52
52
|
self.max_retries = max_retries
|
|
@@ -55,7 +55,7 @@ class EtcdStorage(Storage):
|
|
|
55
55
|
@property
|
|
56
56
|
def base_exceptions(
|
|
57
57
|
self,
|
|
58
|
-
) ->
|
|
58
|
+
) -> type[Exception] | tuple[type[Exception], ...]: # pragma: no cover
|
|
59
59
|
return self.lib.ClientError # type: ignore[no-any-return]
|
|
60
60
|
|
|
61
61
|
def prefixed_key(self, key: str) -> bytes:
|
|
@@ -136,7 +136,7 @@ class EtcdStorage(Storage):
|
|
|
136
136
|
except: # noqa
|
|
137
137
|
return False
|
|
138
138
|
|
|
139
|
-
async def reset(self) ->
|
|
139
|
+
async def reset(self) -> int | None:
|
|
140
140
|
return (await self.storage.delete_prefix(f"{self.PREFIX}/".encode())).deleted
|
|
141
141
|
|
|
142
142
|
async def clear(self, key: str) -> None:
|
|
@@ -9,7 +9,7 @@ from deprecated.sphinx import versionadded
|
|
|
9
9
|
|
|
10
10
|
from limits.aio.storage.base import SlidingWindowCounterSupport, Storage
|
|
11
11
|
from limits.storage.base import TimestampedSlidingWindow
|
|
12
|
-
from limits.typing import EmcacheClientP, ItemP
|
|
12
|
+
from limits.typing import EmcacheClientP, ItemP
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
@versionadded(version="2.1")
|
|
@@ -29,7 +29,7 @@ class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingW
|
|
|
29
29
|
self,
|
|
30
30
|
uri: str,
|
|
31
31
|
wrap_exceptions: bool = False,
|
|
32
|
-
**options:
|
|
32
|
+
**options: float | str | bool,
|
|
33
33
|
) -> None:
|
|
34
34
|
"""
|
|
35
35
|
:param uri: memcached location of the form
|
|
@@ -56,7 +56,7 @@ class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingW
|
|
|
56
56
|
@property
|
|
57
57
|
def base_exceptions(
|
|
58
58
|
self,
|
|
59
|
-
) ->
|
|
59
|
+
) -> type[Exception] | tuple[type[Exception], ...]: # pragma: no cover
|
|
60
60
|
return (
|
|
61
61
|
self.dependency.ClusterNoAvailableNodes,
|
|
62
62
|
self.dependency.CommandError,
|
|
@@ -206,7 +206,7 @@ class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingW
|
|
|
206
206
|
except: # noqa
|
|
207
207
|
return False
|
|
208
208
|
|
|
209
|
-
async def reset(self) ->
|
|
209
|
+
async def reset(self) -> int | None:
|
|
210
210
|
raise NotImplementedError
|
|
211
211
|
|
|
212
212
|
async def acquire_sliding_window_entry(
|
|
@@ -14,14 +14,12 @@ from limits.aio.storage.base import (
|
|
|
14
14
|
Storage,
|
|
15
15
|
)
|
|
16
16
|
from limits.storage.base import TimestampedSlidingWindow
|
|
17
|
-
from limits.typing import Optional, Type, Union
|
|
18
17
|
|
|
19
18
|
|
|
20
|
-
class
|
|
19
|
+
class Entry:
|
|
21
20
|
def __init__(self, expiry: int) -> None:
|
|
22
21
|
self.atime = time.time()
|
|
23
22
|
self.expiry = self.atime + expiry
|
|
24
|
-
super().__init__()
|
|
25
23
|
|
|
26
24
|
|
|
27
25
|
@versionadded(version="2.1")
|
|
@@ -41,27 +39,36 @@ class MemoryStorage(
|
|
|
41
39
|
"""
|
|
42
40
|
|
|
43
41
|
def __init__(
|
|
44
|
-
self, uri:
|
|
42
|
+
self, uri: str | None = None, wrap_exceptions: bool = False, **_: str
|
|
45
43
|
) -> None:
|
|
46
44
|
self.storage: limits.typing.Counter[str] = Counter()
|
|
47
45
|
self.locks: defaultdict[str, asyncio.Lock] = defaultdict(asyncio.Lock)
|
|
48
46
|
self.expirations: dict[str, float] = {}
|
|
49
|
-
self.events: dict[str, list[
|
|
50
|
-
self.timer:
|
|
47
|
+
self.events: dict[str, list[Entry]] = {}
|
|
48
|
+
self.timer: asyncio.Task[None] | None = None
|
|
51
49
|
super().__init__(uri, wrap_exceptions=wrap_exceptions, **_)
|
|
52
50
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return
|
|
51
|
+
def __getstate__(self) -> dict[str, limits.typing.Any]: # type: ignore[explicit-any]
|
|
52
|
+
state = self.__dict__.copy()
|
|
53
|
+
del state["timer"]
|
|
54
|
+
del state["locks"]
|
|
55
|
+
return state
|
|
56
|
+
|
|
57
|
+
def __setstate__(self, state: dict[str, limits.typing.Any]) -> None: # type: ignore[explicit-any]
|
|
58
|
+
self.__dict__.update(state)
|
|
59
|
+
self.timer = None
|
|
60
|
+
self.locks = defaultdict(asyncio.Lock)
|
|
61
|
+
asyncio.ensure_future(self.__schedule_expiry())
|
|
58
62
|
|
|
59
63
|
async def __expire_events(self) -> None:
|
|
60
64
|
for key in self.events.keys():
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
async with self.locks[key]:
|
|
66
|
+
for event in list(self.events[key]):
|
|
63
67
|
if event.expiry <= time.time() and event in self.events[key]:
|
|
64
68
|
self.events[key].remove(event)
|
|
69
|
+
if not self.events.get(key, None):
|
|
70
|
+
self.events.pop(key, None)
|
|
71
|
+
self.locks.pop(key, None)
|
|
65
72
|
|
|
66
73
|
for key in list(self.expirations.keys()):
|
|
67
74
|
if self.expirations[key] <= time.time():
|
|
@@ -73,6 +80,12 @@ class MemoryStorage(
|
|
|
73
80
|
if not self.timer or self.timer.done():
|
|
74
81
|
self.timer = asyncio.create_task(self.__expire_events())
|
|
75
82
|
|
|
83
|
+
@property
|
|
84
|
+
def base_exceptions(
|
|
85
|
+
self,
|
|
86
|
+
) -> type[Exception] | tuple[type[Exception], ...]: # pragma: no cover
|
|
87
|
+
return ValueError
|
|
88
|
+
|
|
76
89
|
async def incr(
|
|
77
90
|
self, key: str, expiry: float, elastic_expiry: bool = False, amount: int = 1
|
|
78
91
|
) -> int:
|
|
@@ -140,18 +153,19 @@ class MemoryStorage(
|
|
|
140
153
|
if amount > limit:
|
|
141
154
|
return False
|
|
142
155
|
|
|
143
|
-
self.events.setdefault(key, [])
|
|
144
156
|
await self.__schedule_expiry()
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
157
|
+
async with self.locks[key]:
|
|
158
|
+
self.events.setdefault(key, [])
|
|
159
|
+
timestamp = time.time()
|
|
160
|
+
try:
|
|
161
|
+
entry: Entry | None = self.events[key][limit - amount]
|
|
162
|
+
except IndexError:
|
|
163
|
+
entry = None
|
|
164
|
+
|
|
165
|
+
if entry and entry.atime >= timestamp - expiry:
|
|
166
|
+
return False
|
|
167
|
+
else:
|
|
168
|
+
self.events[key][:0] = [Entry(expiry) for _ in range(amount)]
|
|
155
169
|
|
|
156
170
|
return True
|
|
157
171
|
|
|
@@ -172,7 +186,7 @@ class MemoryStorage(
|
|
|
172
186
|
timestamp = time.time()
|
|
173
187
|
|
|
174
188
|
return (
|
|
175
|
-
len([k for k in self.events[
|
|
189
|
+
len([k for k in self.events.get(key, []) if k.atime >= timestamp - expiry])
|
|
176
190
|
if self.events.get(key)
|
|
177
191
|
else 0
|
|
178
192
|
)
|
|
@@ -264,7 +278,7 @@ class MemoryStorage(
|
|
|
264
278
|
|
|
265
279
|
return True
|
|
266
280
|
|
|
267
|
-
async def reset(self) ->
|
|
281
|
+
async def reset(self) -> int | None:
|
|
268
282
|
num_items = max(len(self.storage), len(self.events))
|
|
269
283
|
self.storage.clear()
|
|
270
284
|
self.expirations.clear()
|
|
@@ -12,11 +12,8 @@ from limits.aio.storage.base import (
|
|
|
12
12
|
Storage,
|
|
13
13
|
)
|
|
14
14
|
from limits.typing import (
|
|
15
|
-
Optional,
|
|
16
15
|
ParamSpec,
|
|
17
|
-
Type,
|
|
18
16
|
TypeVar,
|
|
19
|
-
Union,
|
|
20
17
|
cast,
|
|
21
18
|
)
|
|
22
19
|
from limits.util import get_dependency
|
|
@@ -51,7 +48,7 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
51
48
|
counter_collection_name: str = "counters",
|
|
52
49
|
window_collection_name: str = "windows",
|
|
53
50
|
wrap_exceptions: bool = False,
|
|
54
|
-
**options:
|
|
51
|
+
**options: float | str | bool,
|
|
55
52
|
) -> None:
|
|
56
53
|
"""
|
|
57
54
|
:param uri: uri of the form ``async+mongodb://[user:password]@host:port?...``,
|
|
@@ -94,7 +91,7 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
94
91
|
@property
|
|
95
92
|
def base_exceptions(
|
|
96
93
|
self,
|
|
97
|
-
) ->
|
|
94
|
+
) -> type[Exception] | tuple[type[Exception], ...]: # pragma: no cover
|
|
98
95
|
return self.lib_errors.PyMongoError # type: ignore
|
|
99
96
|
|
|
100
97
|
@property
|
|
@@ -113,7 +110,7 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
113
110
|
)
|
|
114
111
|
self.__indices_created = True
|
|
115
112
|
|
|
116
|
-
async def reset(self) ->
|
|
113
|
+
async def reset(self) -> int | None:
|
|
117
114
|
"""
|
|
118
115
|
Delete all rate limit keys in the rate limit collections (counters, windows)
|
|
119
116
|
"""
|
|
@@ -294,7 +291,7 @@ class MongoDBStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
|
|
|
294
291
|
try:
|
|
295
292
|
updates: dict[
|
|
296
293
|
str,
|
|
297
|
-
dict[str,
|
|
294
|
+
dict[str, datetime.datetime | dict[str, list[float] | int]],
|
|
298
295
|
] = {
|
|
299
296
|
"$push": {
|
|
300
297
|
"entries": {
|