limits 4.2__tar.gz → 4.4__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.
Files changed (90) hide show
  1. {limits-4.2 → limits-4.4}/CLASSIFIERS +0 -1
  2. {limits-4.2 → limits-4.4}/HISTORY.rst +25 -0
  3. {limits-4.2 → limits-4.4}/PKG-INFO +9 -4
  4. {limits-4.2 → limits-4.4}/README.rst +1 -1
  5. {limits-4.2 → limits-4.4}/doc/source/conf.py +1 -0
  6. {limits-4.2 → limits-4.4}/doc/source/index.rst +2 -2
  7. {limits-4.2 → limits-4.4}/doc/source/installation.rst +22 -0
  8. {limits-4.2 → limits-4.4}/doc/source/storage.rst +29 -10
  9. {limits-4.2 → limits-4.4}/doc/source/strategies.rst +1 -1
  10. {limits-4.2 → limits-4.4}/limits/_version.py +3 -3
  11. {limits-4.2 → limits-4.4}/limits/aio/storage/base.py +5 -8
  12. {limits-4.2 → limits-4.4}/limits/aio/storage/etcd.py +7 -4
  13. {limits-4.2 → limits-4.4}/limits/aio/storage/memcached.py +4 -4
  14. {limits-4.2 → limits-4.4}/limits/aio/storage/memory.py +40 -26
  15. {limits-4.2 → limits-4.4}/limits/aio/storage/mongodb.py +4 -7
  16. {limits-4.2 → limits-4.4}/limits/aio/storage/redis/__init__.py +87 -26
  17. {limits-4.2 → limits-4.4}/limits/aio/storage/redis/bridge.py +8 -9
  18. {limits-4.2 → limits-4.4}/limits/aio/storage/redis/coredis.py +18 -18
  19. {limits-4.2 → limits-4.4}/limits/aio/storage/redis/redispy.py +17 -17
  20. limits-4.4/limits/aio/storage/redis/valkey.py +9 -0
  21. {limits-4.2 → limits-4.4}/limits/aio/strategies.py +2 -2
  22. {limits-4.2 → limits-4.4}/limits/storage/__init__.py +12 -11
  23. {limits-4.2 → limits-4.4}/limits/storage/base.py +5 -10
  24. {limits-4.2 → limits-4.4}/limits/storage/etcd.py +7 -4
  25. {limits-4.2 → limits-4.4}/limits/storage/memcached.py +4 -7
  26. {limits-4.2 → limits-4.4}/limits/storage/memory.py +40 -31
  27. {limits-4.2 → limits-4.4}/limits/storage/mongodb.py +7 -10
  28. {limits-4.2 → limits-4.4}/limits/storage/redis.py +48 -18
  29. {limits-4.2 → limits-4.4}/limits/storage/redis_cluster.py +29 -11
  30. {limits-4.2 → limits-4.4}/limits/storage/redis_sentinel.py +33 -11
  31. {limits-4.2 → limits-4.4}/limits/storage/registry.py +1 -3
  32. {limits-4.2 → limits-4.4}/limits/strategies.py +9 -9
  33. {limits-4.2 → limits-4.4}/limits/typing.py +35 -40
  34. {limits-4.2 → limits-4.4}/limits/util.py +10 -12
  35. {limits-4.2 → limits-4.4}/limits.egg-info/PKG-INFO +9 -4
  36. {limits-4.2 → limits-4.4}/limits.egg-info/SOURCES.txt +3 -0
  37. {limits-4.2 → limits-4.4}/limits.egg-info/requires.txt +7 -0
  38. limits-4.4/requirements/storage/async-valkey.txt +1 -0
  39. limits-4.4/requirements/storage/valkey.txt +1 -0
  40. {limits-4.2 → limits-4.4}/requirements/test.txt +2 -0
  41. {limits-4.2 → limits-4.4}/setup.py +3 -1
  42. {limits-4.2 → limits-4.4}/tests/test_storage.py +3 -3
  43. {limits-4.2 → limits-4.4}/CONTRIBUTIONS.rst +0 -0
  44. {limits-4.2 → limits-4.4}/LICENSE.txt +0 -0
  45. {limits-4.2 → limits-4.4}/MANIFEST.in +0 -0
  46. {limits-4.2 → limits-4.4}/doc/Makefile +0 -0
  47. {limits-4.2 → limits-4.4}/doc/source/_static/custom.css +0 -0
  48. {limits-4.2 → limits-4.4}/doc/source/api.rst +0 -0
  49. {limits-4.2 → limits-4.4}/doc/source/async.rst +0 -0
  50. {limits-4.2 → limits-4.4}/doc/source/changelog.rst +0 -0
  51. {limits-4.2 → limits-4.4}/doc/source/custom-storage.rst +0 -0
  52. {limits-4.2 → limits-4.4}/doc/source/quickstart.rst +0 -0
  53. {limits-4.2 → limits-4.4}/doc/source/theme_config.py +0 -0
  54. {limits-4.2 → limits-4.4}/limits/__init__.py +5 -5
  55. {limits-4.2 → limits-4.4}/limits/aio/__init__.py +0 -0
  56. {limits-4.2 → limits-4.4}/limits/aio/storage/__init__.py +4 -4
  57. {limits-4.2 → limits-4.4}/limits/errors.py +0 -0
  58. {limits-4.2 → limits-4.4}/limits/limits.py +0 -0
  59. {limits-4.2 → limits-4.4}/limits/py.typed +0 -0
  60. {limits-4.2 → limits-4.4}/limits/resources/redis/lua_scripts/acquire_moving_window.lua +0 -0
  61. {limits-4.2 → limits-4.4}/limits/resources/redis/lua_scripts/acquire_sliding_window.lua +0 -0
  62. {limits-4.2 → limits-4.4}/limits/resources/redis/lua_scripts/clear_keys.lua +0 -0
  63. {limits-4.2 → limits-4.4}/limits/resources/redis/lua_scripts/incr_expire.lua +0 -0
  64. {limits-4.2 → limits-4.4}/limits/resources/redis/lua_scripts/moving_window.lua +0 -0
  65. {limits-4.2 → limits-4.4}/limits/resources/redis/lua_scripts/sliding_window.lua +0 -0
  66. {limits-4.2 → limits-4.4}/limits/version.py +0 -0
  67. {limits-4.2 → limits-4.4}/limits.egg-info/dependency_links.txt +0 -0
  68. {limits-4.2 → limits-4.4}/limits.egg-info/not-zip-safe +0 -0
  69. {limits-4.2 → limits-4.4}/limits.egg-info/top_level.txt +0 -0
  70. {limits-4.2 → limits-4.4}/pyproject.toml +0 -0
  71. {limits-4.2 → limits-4.4}/requirements/ci.txt +0 -0
  72. {limits-4.2 → limits-4.4}/requirements/dev.txt +0 -0
  73. {limits-4.2 → limits-4.4}/requirements/docs.txt +0 -0
  74. {limits-4.2 → limits-4.4}/requirements/main.txt +0 -0
  75. {limits-4.2 → limits-4.4}/requirements/storage/async-etcd.txt +0 -0
  76. {limits-4.2 → limits-4.4}/requirements/storage/async-memcached.txt +0 -0
  77. {limits-4.2 → limits-4.4}/requirements/storage/async-mongodb.txt +0 -0
  78. {limits-4.2 → limits-4.4}/requirements/storage/async-redis.txt +0 -0
  79. {limits-4.2 → limits-4.4}/requirements/storage/etcd.txt +0 -0
  80. {limits-4.2 → limits-4.4}/requirements/storage/memcached.txt +0 -0
  81. {limits-4.2 → limits-4.4}/requirements/storage/mongodb.txt +0 -0
  82. {limits-4.2 → limits-4.4}/requirements/storage/redis.txt +0 -0
  83. {limits-4.2 → limits-4.4}/requirements/storage/rediscluster.txt +0 -0
  84. {limits-4.2 → limits-4.4}/setup.cfg +0 -0
  85. {limits-4.2 → limits-4.4}/tests/test_limit_granularities.py +0 -0
  86. {limits-4.2 → limits-4.4}/tests/test_limits.py +0 -0
  87. {limits-4.2 → limits-4.4}/tests/test_ratelimit_parser.py +0 -0
  88. {limits-4.2 → limits-4.4}/tests/test_strategy.py +0 -0
  89. {limits-4.2 → limits-4.4}/tests/test_utils.py +0 -0
  90. {limits-4.2 → limits-4.4}/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,29 @@
3
3
  Changelog
4
4
  =========
5
5
 
6
+ v4.4
7
+ ----
8
+ Release Date: 2025-03-14
9
+
10
+ * Compatibility
11
+
12
+ * Deprecate support for ``etcd``
13
+
14
+ v4.3
15
+ ----
16
+ Release Date: 2025-03-14
17
+
18
+ * Feature
19
+
20
+ * Add support for ``valkey://`` schemas and using ``valkey-py``
21
+ dependency
22
+
23
+ * Compatibility
24
+
25
+ * Drop support for python 3.9
26
+ * Improve typing to use python 3.10+ features
27
+
28
+
6
29
  v4.2
7
30
  ----
8
31
  Release Date: 2025-03-11
@@ -791,6 +814,8 @@ Release Date: 2015-01-08
791
814
 
792
815
 
793
816
 
817
+
818
+
794
819
 
795
820
 
796
821
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: limits
3
- Version: 4.2
3
+ Version: 4.4
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.9
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 atleast
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 atleast
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 git://github.com/alisaifee/limits.git
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 a working `docker & docker-compose installation <https://docs.docker.com/compose/gettingstarted/>`_.
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 sentinal instances and the `service-name`
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 aysnc version: :class:`~limits.aio.storage.RedisSentinelStorage`).
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 atleast
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-11T11:27:52-0700",
11
+ "date": "2025-03-14T18:16:10-0700",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "ef5c0911dd6e0c1a412b3f467d0d1503a2fa24ce",
15
- "version": "4.2"
14
+ "full-revisionid": "53df3264a638d5259ea9ce3b021affceb263f52d",
15
+ "version": "4.4"
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: Optional[list[str]]
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: Optional[str] = None,
61
+ uri: str | None = None,
65
62
  wrap_exceptions: bool = False,
66
- **options: Union[float, str, bool],
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) -> Union[Type[Exception], tuple[Type[Exception], ...]]:
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) -> Optional[int]:
114
+ async def reset(self) -> int | None:
118
115
  """
119
116
  reset storage to clear limits
120
117
  """
@@ -3,7 +3,9 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import time
5
5
  import urllib.parse
6
- from typing import TYPE_CHECKING, Optional, Union
6
+ from typing import TYPE_CHECKING
7
+
8
+ from deprecated import deprecated
7
9
 
8
10
  from limits.aio.storage.base import Storage
9
11
  from limits.errors import ConcurrentUpdateError
@@ -12,6 +14,7 @@ if TYPE_CHECKING:
12
14
  import aetcd
13
15
 
14
16
 
17
+ @deprecated(version="4.4")
15
18
  class EtcdStorage(Storage):
16
19
  """
17
20
  Rate limit storage with etcd as backend.
@@ -46,7 +49,7 @@ class EtcdStorage(Storage):
46
49
  """
47
50
  parsed = urllib.parse.urlparse(uri)
48
51
  self.lib = self.dependencies["aetcd"].module
49
- self.storage: "aetcd.Client" = self.lib.Client(
52
+ self.storage: aetcd.Client = self.lib.Client(
50
53
  host=parsed.hostname, port=parsed.port, **options
51
54
  )
52
55
  self.max_retries = max_retries
@@ -55,7 +58,7 @@ class EtcdStorage(Storage):
55
58
  @property
56
59
  def base_exceptions(
57
60
  self,
58
- ) -> Union[type[Exception], tuple[type[Exception], ...]]: # pragma: no cover
61
+ ) -> type[Exception] | tuple[type[Exception], ...]: # pragma: no cover
59
62
  return self.lib.ClientError # type: ignore[no-any-return]
60
63
 
61
64
  def prefixed_key(self, key: str) -> bytes:
@@ -136,7 +139,7 @@ class EtcdStorage(Storage):
136
139
  except: # noqa
137
140
  return False
138
141
 
139
- async def reset(self) -> Optional[int]:
142
+ async def reset(self) -> int | None:
140
143
  return (await self.storage.delete_prefix(f"{self.PREFIX}/".encode())).deleted
141
144
 
142
145
  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, Optional, Type, Union
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: Union[float, str, bool],
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
- ) -> Union[Type[Exception], tuple[Type[Exception], ...]]: # pragma: no cover
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) -> Optional[int]:
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 LockableEntry(asyncio.Lock):
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: Optional[str] = None, wrap_exceptions: bool = False, **_: str
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[LockableEntry]] = {}
50
- self.timer: Optional[asyncio.Task[None]] = None
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
- @property
54
- def base_exceptions(
55
- self,
56
- ) -> Union[Type[Exception], tuple[Type[Exception], ...]]: # pragma: no cover
57
- return ValueError
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
- for event in list(self.events[key]):
62
- async with event:
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
- timestamp = time.time()
146
- try:
147
- entry: Optional[LockableEntry] = self.events[key][limit - amount]
148
- except IndexError:
149
- entry = None
150
-
151
- if entry and entry.atime >= timestamp - expiry:
152
- return False
153
- else:
154
- self.events[key][:0] = [LockableEntry(expiry) for _ in range(amount)]
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[key] if k.atime >= timestamp - expiry])
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) -> Optional[int]:
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: Union[float, str, bool],
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
- ) -> Union[Type[Exception], tuple[Type[Exception], ...]]: # pragma: no cover
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) -> Optional[int]:
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, Union[datetime.datetime, dict[str, Union[list[float], int]]]],
294
+ dict[str, datetime.datetime | dict[str, list[float] | int]],
298
295
  ] = {
299
296
  "$push": {
300
297
  "entries": {