limits 4.0.1__py3-none-any.whl → 4.1__py3-none-any.whl

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/storage/mongodb.py CHANGED
@@ -3,27 +3,26 @@ from __future__ import annotations
3
3
  import datetime
4
4
  import time
5
5
  from abc import ABC, abstractmethod
6
- from typing import cast
7
6
 
8
7
  from deprecated.sphinx import versionadded, versionchanged
9
8
 
10
9
  from limits.typing import (
11
- Dict,
12
- List,
13
10
  MongoClient,
14
11
  MongoCollection,
15
12
  MongoDatabase,
16
13
  Optional,
17
- Tuple,
18
14
  Type,
19
15
  Union,
16
+ cast,
20
17
  )
21
18
 
22
19
  from ..util import get_dependency
23
- from .base import MovingWindowSupport, Storage
20
+ from .base import MovingWindowSupport, SlidingWindowCounterSupport, Storage
24
21
 
25
22
 
26
- class MongoDBStorageBase(Storage, MovingWindowSupport, ABC):
23
+ class MongoDBStorageBase(
24
+ Storage, MovingWindowSupport, SlidingWindowCounterSupport, ABC
25
+ ):
27
26
  """
28
27
  Rate limit storage with MongoDB as backend.
29
28
 
@@ -48,7 +47,8 @@ class MongoDBStorageBase(Storage, MovingWindowSupport, ABC):
48
47
  collections.
49
48
  :param counter_collection_name: The collection name to use for individual counters
50
49
  used in fixed window strategies
51
- :param window_collection_name: The collection name to use for moving window storage
50
+ :param window_collection_name: The collection name to use for sliding & moving window
51
+ storage
52
52
  :param wrap_exceptions: Whether to wrap storage exceptions in
53
53
  :exc:`limits.errors.StorageError` before raising it.
54
54
  :param options: all remaining keyword arguments are passed to the
@@ -98,7 +98,7 @@ class MongoDBStorageBase(Storage, MovingWindowSupport, ABC):
98
98
  @property
99
99
  def base_exceptions(
100
100
  self,
101
- ) -> Union[Type[Exception], Tuple[Type[Exception], ...]]: # pragma: no cover
101
+ ) -> Union[Type[Exception], tuple[Type[Exception], ...]]: # pragma: no cover
102
102
  return self.lib_errors.PyMongoError # type: ignore
103
103
 
104
104
  def __initialize_database(self) -> None:
@@ -203,7 +203,7 @@ class MongoDBStorageBase(Storage, MovingWindowSupport, ABC):
203
203
  except: # noqa: E722
204
204
  return False
205
205
 
206
- def get_moving_window(self, key: str, limit: int, expiry: int) -> Tuple[float, int]:
206
+ def get_moving_window(self, key: str, limit: int, expiry: int) -> tuple[float, int]:
207
207
  """
208
208
  returns the starting point and the number of entries in the moving
209
209
  window
@@ -257,9 +257,9 @@ class MongoDBStorageBase(Storage, MovingWindowSupport, ABC):
257
257
 
258
258
  timestamp = time.time()
259
259
  try:
260
- updates: Dict[
260
+ updates: dict[
261
261
  str,
262
- Dict[str, Union[datetime.datetime, Dict[str, Union[List[float], int]]]],
262
+ dict[str, Union[datetime.datetime, dict[str, Union[list[float], int]]]],
263
263
  ] = {
264
264
  "$push": {
265
265
  "entries": {
@@ -289,6 +289,199 @@ class MongoDBStorageBase(Storage, MovingWindowSupport, ABC):
289
289
  except self.lib.errors.DuplicateKeyError:
290
290
  return False
291
291
 
292
+ def get_sliding_window(
293
+ self, key: str, expiry: int
294
+ ) -> tuple[int, float, int, float]:
295
+ expiry_ms = expiry * 1000
296
+ if result := self.windows.find_one_and_update(
297
+ {"_id": key},
298
+ [
299
+ {
300
+ "$set": {
301
+ "previousCount": {
302
+ "$cond": {
303
+ "if": {
304
+ "$lte": [
305
+ {"$subtract": ["$expiresAt", "$$NOW"]},
306
+ expiry_ms,
307
+ ]
308
+ },
309
+ "then": {"$ifNull": ["$currentCount", 0]},
310
+ "else": {"$ifNull": ["$previousCount", 0]},
311
+ }
312
+ },
313
+ "currentCount": {
314
+ "$cond": {
315
+ "if": {
316
+ "$lte": [
317
+ {"$subtract": ["$expiresAt", "$$NOW"]},
318
+ expiry_ms,
319
+ ]
320
+ },
321
+ "then": 0,
322
+ "else": {"$ifNull": ["$currentCount", 0]},
323
+ }
324
+ },
325
+ "expiresAt": {
326
+ "$cond": {
327
+ "if": {
328
+ "$lte": [
329
+ {"$subtract": ["$expiresAt", "$$NOW"]},
330
+ expiry_ms,
331
+ ]
332
+ },
333
+ "then": {
334
+ "$add": ["$expiresAt", expiry_ms],
335
+ },
336
+ "else": "$expiresAt",
337
+ }
338
+ },
339
+ }
340
+ }
341
+ ],
342
+ return_document=self.lib.ReturnDocument.AFTER,
343
+ projection=["currentCount", "previousCount", "expiresAt"],
344
+ ):
345
+ expires_at = (
346
+ (result["expiresAt"].replace(tzinfo=datetime.timezone.utc).timestamp())
347
+ if result.get("expiresAt")
348
+ else time.time()
349
+ )
350
+ current_ttl = max(0, expires_at - time.time())
351
+ prev_ttl = max(0, current_ttl - expiry if result["previousCount"] else 0)
352
+
353
+ return (
354
+ result["previousCount"],
355
+ prev_ttl,
356
+ result["currentCount"],
357
+ current_ttl,
358
+ )
359
+ return 0, 0.0, 0, 0.0
360
+
361
+ def acquire_sliding_window_entry(
362
+ self, key: str, limit: int, expiry: int, amount: int = 1
363
+ ) -> bool:
364
+ expiry_ms = expiry * 1000
365
+ result = self.windows.find_one_and_update(
366
+ {"_id": key},
367
+ [
368
+ {
369
+ "$set": {
370
+ "previousCount": {
371
+ "$cond": {
372
+ "if": {
373
+ "$lte": [
374
+ {"$subtract": ["$expiresAt", "$$NOW"]},
375
+ expiry_ms,
376
+ ]
377
+ },
378
+ "then": {"$ifNull": ["$currentCount", 0]},
379
+ "else": {"$ifNull": ["$previousCount", 0]},
380
+ }
381
+ },
382
+ }
383
+ },
384
+ {
385
+ "$set": {
386
+ "currentCount": {
387
+ "$cond": {
388
+ "if": {
389
+ "$lte": [
390
+ {"$subtract": ["$expiresAt", "$$NOW"]},
391
+ expiry_ms,
392
+ ]
393
+ },
394
+ "then": 0,
395
+ "else": {"$ifNull": ["$currentCount", 0]},
396
+ }
397
+ },
398
+ "expiresAt": {
399
+ "$cond": {
400
+ "if": {
401
+ "$lte": [
402
+ {"$subtract": ["$expiresAt", "$$NOW"]},
403
+ expiry_ms,
404
+ ]
405
+ },
406
+ "then": {
407
+ "$cond": {
408
+ "if": {"$gt": ["$expiresAt", 0]},
409
+ "then": {"$add": ["$expiresAt", expiry_ms]},
410
+ "else": {"$add": ["$$NOW", 2 * expiry_ms]},
411
+ }
412
+ },
413
+ "else": "$expiresAt",
414
+ }
415
+ },
416
+ }
417
+ },
418
+ {
419
+ "$set": {
420
+ "curWeightedCount": {
421
+ "$floor": {
422
+ "$add": [
423
+ {
424
+ "$multiply": [
425
+ "$previousCount",
426
+ {
427
+ "$divide": [
428
+ {
429
+ "$max": [
430
+ 0,
431
+ {
432
+ "$subtract": [
433
+ "$expiresAt",
434
+ {
435
+ "$add": [
436
+ "$$NOW",
437
+ expiry_ms,
438
+ ]
439
+ },
440
+ ]
441
+ },
442
+ ]
443
+ },
444
+ expiry_ms,
445
+ ]
446
+ },
447
+ ]
448
+ },
449
+ "$currentCount",
450
+ ]
451
+ }
452
+ }
453
+ }
454
+ },
455
+ {
456
+ "$set": {
457
+ "currentCount": {
458
+ "$cond": {
459
+ "if": {
460
+ "$lte": [
461
+ {"$add": ["$curWeightedCount", amount]},
462
+ limit,
463
+ ]
464
+ },
465
+ "then": {"$add": ["$currentCount", amount]},
466
+ "else": "$currentCount",
467
+ }
468
+ }
469
+ }
470
+ },
471
+ {
472
+ "$set": {
473
+ "_acquired": {
474
+ "$lte": [{"$add": ["$curWeightedCount", amount]}, limit]
475
+ }
476
+ }
477
+ },
478
+ {"$unset": ["curWeightedCount"]},
479
+ ],
480
+ return_document=self.lib.ReturnDocument.AFTER,
481
+ upsert=True,
482
+ )
483
+ return cast(bool, result["_acquired"])
484
+
292
485
 
293
486
  @versionadded(version="2.1")
294
487
  @versionchanged(
limits/storage/redis.py CHANGED
@@ -1,20 +1,31 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import time
4
- from typing import TYPE_CHECKING
4
+ from typing import TYPE_CHECKING, cast
5
5
 
6
6
  from packaging.version import Version
7
7
 
8
- from limits.typing import Optional, RedisClient, ScriptP, Tuple, Type, Union
8
+ from limits.typing import Optional, RedisClient, Type, Union
9
9
 
10
10
  from ..util import get_package_data
11
- from .base import MovingWindowSupport, Storage
11
+ from .base import MovingWindowSupport, SlidingWindowCounterSupport, Storage
12
12
 
13
13
  if TYPE_CHECKING:
14
14
  import redis
15
15
 
16
16
 
17
- class RedisInteractor:
17
+ class RedisStorage(Storage, MovingWindowSupport, SlidingWindowCounterSupport):
18
+ """
19
+ Rate limit storage with redis as backend.
20
+
21
+ Depends on :pypi:`redis`.
22
+ """
23
+
24
+ STORAGE_SCHEME = ["redis", "rediss", "redis+unix"]
25
+ """The storage scheme for redis"""
26
+
27
+ DEPENDENCIES = {"redis": Version("3.0")}
28
+
18
29
  RES_DIR = "resources/redis/lua_scripts"
19
30
 
20
31
  SCRIPT_MOVING_WINDOW = get_package_data(f"{RES_DIR}/moving_window.lua")
@@ -24,123 +35,17 @@ class RedisInteractor:
24
35
  SCRIPT_CLEAR_KEYS = get_package_data(f"{RES_DIR}/clear_keys.lua")
25
36
  SCRIPT_INCR_EXPIRE = get_package_data(f"{RES_DIR}/incr_expire.lua")
26
37
 
27
- lua_moving_window: ScriptP[Tuple[int, int]]
28
- lua_acquire_window: ScriptP[bool]
29
-
30
- PREFIX = "LIMITS"
31
-
32
- def prefixed_key(self, key: str) -> str:
33
- return f"{self.PREFIX}:{key}"
34
-
35
- def get_moving_window(self, key: str, limit: int, expiry: int) -> Tuple[float, int]:
36
- """
37
- returns the starting point and the number of entries in the moving
38
- window
39
-
40
- :param key: rate limit key
41
- :param expiry: expiry of entry
42
- :return: (start of window, number of acquired entries)
43
- """
44
- key = self.prefixed_key(key)
45
- timestamp = time.time()
46
- if window := self.lua_moving_window([key], [timestamp - expiry, limit]):
47
- return float(window[0]), window[1]
48
-
49
- return timestamp, 0
50
-
51
- def _incr(
52
- self,
53
- key: str,
54
- expiry: int,
55
- connection: RedisClient,
56
- elastic_expiry: bool = False,
57
- amount: int = 1,
58
- ) -> int:
59
- """
60
- increments the counter for a given rate limit key
61
-
62
- :param connection: Redis connection
63
- :param key: the key to increment
64
- :param expiry: amount in seconds for the key to expire in
65
- :param amount: the number to increment by
66
- """
67
- key = self.prefixed_key(key)
68
- value = connection.incrby(key, amount)
69
-
70
- if elastic_expiry or value == amount:
71
- connection.expire(key, expiry)
72
-
73
- return value
74
-
75
- def _get(self, key: str, connection: RedisClient) -> int:
76
- """
77
- :param connection: Redis connection
78
- :param key: the key to get the counter value for
79
- """
80
-
81
- key = self.prefixed_key(key)
82
- return int(connection.get(key) or 0)
83
-
84
- def _clear(self, key: str, connection: RedisClient) -> None:
85
- """
86
- :param key: the key to clear rate limits for
87
- :param connection: Redis connection
88
- """
89
- key = self.prefixed_key(key)
90
- connection.delete(key)
91
-
92
- def _acquire_entry(
93
- self,
94
- key: str,
95
- limit: int,
96
- expiry: int,
97
- connection: RedisClient,
98
- amount: int = 1,
99
- ) -> bool:
100
- """
101
- :param key: rate limit key to acquire an entry in
102
- :param limit: amount of entries allowed
103
- :param expiry: expiry of the entry
104
- :param connection: Redis connection
105
- :param amount: the number of entries to acquire
106
- """
107
- key = self.prefixed_key(key)
108
- timestamp = time.time()
109
- acquired = self.lua_acquire_window([key], [timestamp, limit, expiry, amount])
110
-
111
- return bool(acquired)
112
-
113
- def _get_expiry(self, key: str, connection: RedisClient) -> float:
114
- """
115
- :param key: the key to get the expiry for
116
- :param connection: Redis connection
117
- """
118
-
119
- key = self.prefixed_key(key)
120
- return max(connection.ttl(key), 0) + time.time()
121
-
122
- def _check(self, connection: RedisClient) -> bool:
123
- """
124
- :param connection: Redis connection
125
- check if storage is healthy
126
- """
127
- try:
128
- return connection.ping()
129
- except: # noqa
130
- return False
131
-
132
-
133
- class RedisStorage(RedisInteractor, Storage, MovingWindowSupport):
134
- """
135
- Rate limit storage with redis as backend.
136
-
137
- Depends on :pypi:`redis`.
138
- """
38
+ SCRIPT_SLIDING_WINDOW = get_package_data(f"{RES_DIR}/sliding_window.lua")
39
+ SCRIPT_ACQUIRE_SLIDING_WINDOW = get_package_data(
40
+ f"{RES_DIR}/acquire_sliding_window.lua"
41
+ )
139
42
 
140
- STORAGE_SCHEME = ["redis", "rediss", "redis+unix"]
141
- """The storage scheme for redis"""
43
+ lua_moving_window: "redis.commands.core.Script"
44
+ lua_acquire_moving_window: "redis.commands.core.Script"
45
+ lua_sliding_window: "redis.commands.core.Script"
46
+ lua_acquire_sliding_window: "redis.commands.core.Script"
142
47
 
143
- DEPENDENCIES = {"redis": Version("3.0")}
48
+ PREFIX = "LIMITS"
144
49
 
145
50
  def __init__(
146
51
  self,
@@ -179,73 +84,189 @@ class RedisStorage(RedisInteractor, Storage, MovingWindowSupport):
179
84
  @property
180
85
  def base_exceptions(
181
86
  self,
182
- ) -> Union[Type[Exception], Tuple[Type[Exception], ...]]: # pragma: no cover
87
+ ) -> Union[Type[Exception], tuple[Type[Exception], ...]]: # pragma: no cover
183
88
  return self.dependency.RedisError # type: ignore[no-any-return]
184
89
 
185
90
  def initialize_storage(self, _uri: str) -> None:
186
- self.lua_moving_window = self.storage.register_script(self.SCRIPT_MOVING_WINDOW)
187
- self.lua_acquire_window = self.storage.register_script(
91
+ self.lua_moving_window = self.get_connection().register_script(
92
+ self.SCRIPT_MOVING_WINDOW
93
+ )
94
+ self.lua_acquire_moving_window = self.get_connection().register_script(
188
95
  self.SCRIPT_ACQUIRE_MOVING_WINDOW
189
96
  )
190
- self.lua_clear_keys = self.storage.register_script(self.SCRIPT_CLEAR_KEYS)
191
- self.lua_incr_expire = self.storage.register_script(
192
- RedisStorage.SCRIPT_INCR_EXPIRE
97
+ self.lua_clear_keys = self.get_connection().register_script(
98
+ self.SCRIPT_CLEAR_KEYS
99
+ )
100
+ self.lua_incr_expire = self.get_connection().register_script(
101
+ self.SCRIPT_INCR_EXPIRE
102
+ )
103
+ self.lua_sliding_window = self.get_connection().register_script(
104
+ self.SCRIPT_SLIDING_WINDOW
193
105
  )
106
+ self.lua_acquire_sliding_window = self.get_connection().register_script(
107
+ self.SCRIPT_ACQUIRE_SLIDING_WINDOW
108
+ )
109
+
110
+ def get_connection(self, readonly: bool = False) -> RedisClient:
111
+ return cast(RedisClient, self.storage)
112
+
113
+ def _current_window_key(self, key: str) -> str:
114
+ """
115
+ Return the current window's storage key (Sliding window strategy)
116
+
117
+ Contrary to other strategies that have one key per rate limit item,
118
+ this strategy has two keys per rate limit item than must be on the same machine.
119
+ To keep the current key and the previous key on the same Redis cluster node,
120
+ curly braces are added.
121
+
122
+ Eg: "{constructed_key}"
123
+ """
124
+ return f"{{{key}}}"
125
+
126
+ def _previous_window_key(self, key: str) -> str:
127
+ """
128
+ Return the previous window's storage key (Sliding window strategy).
129
+
130
+ Curvy braces are added on the common pattern with the current window's key,
131
+ so the current and the previous key are stored on the same Redis cluster node.
132
+
133
+ Eg: "{constructed_key}/-1"
134
+ """
135
+ return f"{self._current_window_key(key)}/-1"
136
+
137
+ def prefixed_key(self, key: str) -> str:
138
+ return f"{self.PREFIX}:{key}"
139
+
140
+ def get_moving_window(self, key: str, limit: int, expiry: int) -> tuple[float, int]:
141
+ """
142
+ returns the starting point and the number of entries in the moving
143
+ window
144
+
145
+ :param key: rate limit key
146
+ :param expiry: expiry of entry
147
+ :return: (start of window, number of acquired entries)
148
+ """
149
+ key = self.prefixed_key(key)
150
+ timestamp = time.time()
151
+ if window := self.lua_moving_window([key], [timestamp - expiry, limit]):
152
+ return float(window[0]), window[1]
153
+
154
+ return timestamp, 0
155
+
156
+ def get_sliding_window(
157
+ self, key: str, expiry: int
158
+ ) -> tuple[int, float, int, float]:
159
+ previous_key = self.prefixed_key(self._previous_window_key(key))
160
+ current_key = self.prefixed_key(self._current_window_key(key))
161
+ if window := self.lua_sliding_window([previous_key, current_key], [expiry]):
162
+ return (
163
+ int(window[0] or 0),
164
+ max(0, float(window[1] or 0)) / 1000,
165
+ int(window[2] or 0),
166
+ max(0, float(window[3] or 0)) / 1000,
167
+ )
168
+ return 0, 0.0, 0, 0.0
194
169
 
195
170
  def incr(
196
- self, key: str, expiry: int, elastic_expiry: bool = False, amount: int = 1
171
+ self,
172
+ key: str,
173
+ expiry: int,
174
+ elastic_expiry: bool = False,
175
+ amount: int = 1,
197
176
  ) -> int:
198
177
  """
199
178
  increments the counter for a given rate limit key
200
179
 
180
+
201
181
  :param key: the key to increment
202
182
  :param expiry: amount in seconds for the key to expire in
203
183
  :param amount: the number to increment by
204
184
  """
205
-
185
+ key = self.prefixed_key(key)
206
186
  if elastic_expiry:
207
- return super()._incr(key, expiry, self.storage, elastic_expiry, amount)
187
+ value = self.get_connection().incrby(key, amount)
188
+ self.get_connection().expire(key, expiry)
189
+ return value
208
190
  else:
209
- key = self.prefixed_key(key)
210
191
  return int(self.lua_incr_expire([key], [expiry, amount]))
211
192
 
212
193
  def get(self, key: str) -> int:
213
194
  """
195
+
214
196
  :param key: the key to get the counter value for
215
197
  """
216
198
 
217
- return super()._get(key, self.storage)
199
+ key = self.prefixed_key(key)
200
+ return int(self.get_connection(True).get(key) or 0)
218
201
 
219
202
  def clear(self, key: str) -> None:
220
203
  """
221
204
  :param key: the key to clear rate limits for
222
205
  """
206
+ key = self.prefixed_key(key)
207
+ self.get_connection().delete(key)
223
208
 
224
- return super()._clear(key, self.storage)
225
-
226
- def acquire_entry(self, key: str, limit: int, expiry: int, amount: int = 1) -> bool:
209
+ def acquire_entry(
210
+ self,
211
+ key: str,
212
+ limit: int,
213
+ expiry: int,
214
+ amount: int = 1,
215
+ ) -> bool:
227
216
  """
228
217
  :param key: rate limit key to acquire an entry in
229
218
  :param limit: amount of entries allowed
230
219
  :param expiry: expiry of the entry
231
- :param amount: the number to increment by
220
+
221
+ :param amount: the number of entries to acquire
232
222
  """
223
+ key = self.prefixed_key(key)
224
+ timestamp = time.time()
225
+ acquired = self.lua_acquire_moving_window(
226
+ [key], [timestamp, limit, expiry, amount]
227
+ )
233
228
 
234
- return super()._acquire_entry(key, limit, expiry, self.storage, amount)
229
+ return bool(acquired)
230
+
231
+ def acquire_sliding_window_entry(
232
+ self,
233
+ key: str,
234
+ limit: int,
235
+ expiry: int,
236
+ amount: int = 1,
237
+ ) -> bool:
238
+ """
239
+ Acquire an entry. Shift the current window to the previous window if it expired.
240
+
241
+ :param key: rate limit key to acquire an entry in
242
+ :param limit: amount of entries allowed
243
+ :param expiry: expiry of the entry
244
+ :param amount: the number of entries to acquire
245
+ """
246
+ previous_key = self.prefixed_key(self._previous_window_key(key))
247
+ current_key = self.prefixed_key(self._current_window_key(key))
248
+ acquired = self.lua_acquire_sliding_window(
249
+ [previous_key, current_key], [limit, expiry, amount]
250
+ )
251
+ return bool(acquired)
235
252
 
236
253
  def get_expiry(self, key: str) -> float:
237
254
  """
238
255
  :param key: the key to get the expiry for
256
+
239
257
  """
240
258
 
241
- return super()._get_expiry(key, self.storage)
259
+ key = self.prefixed_key(key)
260
+ return max(self.get_connection(True).ttl(key), 0) + time.time()
242
261
 
243
262
  def check(self) -> bool:
244
263
  """
245
264
  check if storage is healthy
246
265
  """
247
-
248
- return super()._check(self.storage)
266
+ try:
267
+ return self.get_connection().ping()
268
+ except: # noqa
269
+ return False
249
270
 
250
271
  def reset(self) -> Optional[int]:
251
272
  """
@@ -4,7 +4,7 @@ from deprecated.sphinx import versionchanged
4
4
  from packaging.version import Version
5
5
 
6
6
  from limits.storage.redis import RedisStorage
7
- from limits.typing import Dict, Optional, Union
7
+ from limits.typing import Optional, Union
8
8
 
9
9
 
10
10
  @versionchanged(
@@ -34,7 +34,7 @@ class RedisClusterStorage(RedisStorage):
34
34
  STORAGE_SCHEME = ["redis+cluster"]
35
35
  """The storage scheme for redis cluster"""
36
36
 
37
- DEFAULT_OPTIONS: Dict[str, Union[float, str, bool]] = {
37
+ DEFAULT_OPTIONS: dict[str, Union[float, str, bool]] = {
38
38
  "max_connections": 1000,
39
39
  }
40
40
  "Default options passed to the :class:`~redis.cluster.RedisCluster`"
@@ -60,7 +60,7 @@ class RedisClusterStorage(RedisStorage):
60
60
  available or if the redis cluster cannot be reached.
61
61
  """
62
62
  parsed = urllib.parse.urlparse(uri)
63
- parsed_auth: Dict[str, Union[float, str, bool]] = {}
63
+ parsed_auth: dict[str, Union[float, str, bool]] = {}
64
64
 
65
65
  if parsed.username:
66
66
  parsed_auth["username"] = parsed.username