Codebase list flask-limiter / aec40e7
Update upstream source from tag 'upstream/3.2.0' Update to upstream version '3.2.0' with Debian dir 41813551c659e4b67e78a807b1ad63233767feb0 Carsten Schoenert 1 year, 2 months ago
24 changed file(s) with 163 addition(s) and 135 deletion(s). Raw diff Collapse all Expand all
+0
-4
.codeclimate.yml less more
0 exclude_paths:
1 - versioneer.py
2 - flask_limiter/_version.py
3 - tests/*
2424 run: |
2525 python -m pip install --upgrade pip setuptools wheel
2626 pip install -r requirements/ci.txt
27 - name: Lint with flake8
27 - name: Lint with ruff
2828 run: |
29 flake8 . --count --show-source --statistics
30 flake8 . --count --exit-zero --max-complexity=10
29 ruff flask_limiter tests
3130 - name: Lint with black
3231 run: |
3332 black tests flask_limiter
+0
-6
.overcommit.yml less more
0 PreCommit:
1 PythonFlake8:
2 enabled: true
3 on_warn: fail
4 TrailingWhitespace:
5 enabled: true
11
22 Changelog
33 =========
4
5 v3.2.0
6 ------
7 Release Date: 2023-02-15
8
9 * Feature
10
11 * Allow configuring request identity
12
13 * Chores
14
15 * Improve linting with ruff
16 * Update development dependencies
17
418
519 v3.1.0
620 ------
3044
3145 * Allow scoping regular limit decorators / context managers
3246
47 v3.2.0
48 ------
49 Release Date: 2023-02-15
50
3351 v3.1.0
3452 ------
3553 Release Date: 2022-12-29
4866
4967 * Simplify registration of decorated function & blueprint limits
5068
69
70 v3.2.0
71 ------
72 Release Date: 2023-02-15
5173
5274 v3.1.0
5375 ------
902924
903925
904926
927
0 Copyright (c) 2022 Ali-Akber Saifee
0 Copyright (c) 2023 Ali-Akber Saifee
11
22 Permission is hereby granted, free of charge, to any person obtaining a copy
33 of this software and associated documentation files (the "Software"), to deal
00 lint:
11 black --check tests flask_limiter
22 mypy flask_limiter
3 flake8 flask_limiter tests
3 ruff flask_limiter tests
44
55 lint-fix:
66 black tests flask_limiter
77 mypy flask_limiter
88 isort -r --profile=black tests flask_limiter
9 autoflake8 -i -r tests flask_limiter
9 ruff --fix flask_limiter tests
100100 ---------------------------------------------------
101101 .. code-block:: bash
102102
103 $ FLASK_APP=app:app flask limiter list
103 $ FLASK_APP=app:app flask limiter limits
104104
105105 app
106106 ├── fast: /fast
1111 from theme_config import *
1212
1313 description = "Flask-Limiter adds rate limiting to flask applications."
14 copyright = "2022, Ali-Akber Saifee"
14 copyright = "2023, Ali-Akber Saifee"
1515 project = "Flask-Limiter"
1616
1717 ahead = 0
102102
103103 - A dictionary to set extra options to be passed to the storage implementation
104104 upon initialization.
105 * - .. data:: RATELIMIT_REQUEST_IDENTIFIER
106
107 Constructor argument: :paramref:`~flask_limiter.Limiter.request_identifier`
108
109 - A callable that returns the unique identity of the current request. Defaults to :attr:`flask.Request.endpoint`
105110 * - .. data:: RATELIMIT_STRATEGY
106111
107112 Constructor argument: :paramref:`~flask_limiter.Limiter.strategy`
295295
296296
297297 Decorators to declare rate limits
298 ---------------------------------
298 =================================
299299 Decorators made available as instance methods of the :class:`~flask_limiter.Limiter`
300300 instance to be used with the :class:`flask.Flask` application.
301301
302302 .. _ratelimit-decorator-limit:
303303
304304 Route specific limits
305 ^^^^^^^^^^^^^^^^^^^^^
305 ---------------------
306306
307307 .. automethod:: Limiter.limit
308308 :noindex:
407407 .. _ratelimit-decorator-shared-limit:
408408
409409 Reusable limits
410 ^^^^^^^^^^^^^^^
410 ---------------
411411
412412 For scenarios where a rate limit should be shared by multiple routes
413413 (For example when you want to protect routes using the same resource
3232 return gi.record_by_name(request.remote_addr)['region_name']
3333
3434 app = Flask(__name__)
35 limiter = Limiter(app, default_limits=["10/hour"], key_func = get_request_country)
35 limiter = Limiter(get_request_country, app=app, default_limits=["10/hour"])
3636
3737
3838
2323 # setup.py/versioneer.py will grep for the variable names, so they must
2424 # each be defined on a line of their own. _version.py will just call
2525 # get_keywords().
26 git_refnames = " (tag: 3.1.0, stable)"
27 git_full = "745964acbc31048af1fedca09a1637df8278c760"
28 git_date = "2022-12-29 11:54:32 -0800"
26 git_refnames = " (HEAD -> master, tag: 3.2.0, stable)"
27 git_full = "b764e15950729baf12820086f9713c47414990d1"
28 git_date = "2023-02-15 16:29:02 -0800"
2929 keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
3030 return keywords
3131
00 import time
11 from functools import partial
2 from typing import Any, Callable, Dict, Generator, List, Optional, Set, Union
2 from typing import Any, Callable, Dict, Generator, List, Optional, Set, Tuple, Union
33 from urllib.parse import urlparse
44
55 import click
9898 def render_limits(
9999 app: Flask,
100100 limiter: Limiter,
101 limits: List[Limit],
101 limits: Tuple[List[Limit], ...],
102102 endpoint: Optional[str] = None,
103103 blueprint: Optional[str] = None,
104104 rule: Optional[Rule] = None,
115115 renderable = Tree(label)
116116 entries = []
117117
118 for limit in limits:
118 for limit in limits[0] + limits[1]:
119119 if endpoint:
120120 view_func = app.view_functions.get(endpoint, None)
121121 source = (
409409 yield render_limits(
410410 current_app,
411411 limiter,
412 limiter.limit_manager.application_limits,
412 (limiter.limit_manager.application_limits, []),
413413 test=key,
414414 method=method,
415415 label="[gold3]Application Limits[/gold3]",
471471 "Details",
472472 {
473473 "rule": Rule,
474 "limits": List[Limit],
474 "limits": Tuple[List[Limit], ...],
475475 },
476476 )
477477 rule_limits: Dict[str, Details] = {}
504504 render_limits(
505505 current_app,
506506 limiter,
507 application_limits,
507 (application_limits, []),
508508 label="Application Limits",
509509 test=key,
510510 )
537537 for endpoint, details in rule_limits.items():
538538 if details["limits"]:
539539 node = Tree(endpoint)
540 for limit in details["limits"]:
540 default, decorated = details["limits"]
541 for limit in default + decorated:
541542 if (
542543 limit.per_method
543544 and details["rule"]
1616 DEFAULT_LIMITS_EXEMPT_WHEN = "RATELIMIT_DEFAULTS_EXEMPT_WHEN"
1717 DEFAULT_LIMITS_DEDUCT_WHEN = "RATELIMIT_DEFAULTS_DEDUCT_WHEN"
1818 DEFAULT_LIMITS_COST = "RATELIMIT_DEFAULTS_COST"
19 REQUEST_IDENTIFIER = "RATELIMIT_REQUEST_IDENTIFIER"
1920 STRATEGY = "RATELIMIT_STRATEGY"
2021 STORAGE_URI = "RATELIMIT_STORAGE_URI"
2122 STORAGE_OPTIONS = "RATELIMIT_STORAGE_OPTIONS"
0 from __future__ import annotations
1
2 import dataclasses
3 import traceback
4 import warnings
5 from types import TracebackType
6
7 from ordered_set import OrderedSet
8
9 from .util import get_qualified_name
10
110 """
121 Flask-Limiter Extension
132 """
3 from __future__ import annotations
4
5 import dataclasses
146 import datetime
157 import itertools
168 import logging
179 import time
10 import traceback
11 import warnings
1812 import weakref
1913 from collections import defaultdict
2014 from functools import partial, wraps
15 from types import TracebackType
2116 from typing import Type, overload
2217
2318 import flask
2520 from limits.errors import ConfigurationError
2621 from limits.storage import MemoryStorage, Storage, storage_from_string
2722 from limits.strategies import STRATEGIES, RateLimiter
23 from ordered_set import OrderedSet
2824 from werkzeug.http import http_date, parse_date
2925
3026 from ._compat import request_context
4541 Union,
4642 cast,
4743 )
44 from .util import get_qualified_name
4845 from .wrappers import Limit, LimitGroup, RequestLimit
4946
5047
5148 @dataclasses.dataclass
5249 class LimiterContext:
53 rate_limiting_complete: dict[str, bool] = dataclasses.field(default_factory=dict)
5450 view_rate_limit: Optional[RequestLimit] = None
5551 view_rate_limits: List[RequestLimit] = dataclasses.field(default_factory=list)
5652 conditional_deductions: Dict[Limit, List[str]] = dataclasses.field(
5955 seen_limits: OrderedSet[Limit] = dataclasses.field(default_factory=OrderedSet)
6056
6157 def reset(self) -> None:
62 self.rate_limiting_complete.clear()
6358 self.view_rate_limit = None
6459 self.view_rate_limits.clear()
6560 self.conditional_deductions.clear()
61 self.seen_limits.clear()
6662
6763
6864 class Limiter:
120116 :param retry_after: Allows configuration of how the value of the
121117 `Retry-After` header is rendered. One of `http-date` or `delta-seconds`.
122118 :param key_prefix: prefix prepended to rate limiter keys and app context global names.
119 :param request_identifier: a callable that returns the unique identity the current request.
120 Defaults to :attr:`flask.Request.endpoint`
123121 :param enabled: Whether the extension is enabled or not
124122 """
125123
152150 in_memory_fallback_enabled: Optional[bool] = None,
153151 retry_after: Optional[str] = None,
154152 key_prefix: str = "",
153 request_identifier: Optional[Callable[..., str]] = None,
155154 enabled: bool = True,
156155 ) -> None:
157156 self.app = app
189188
190189 self._key_func = key_func
191190 self._key_prefix = key_prefix
191 self._request_identifier = request_identifier
192192
193193 _default_limits = (
194194 [
337337 )
338338
339339 self._key_prefix = self._key_prefix or config.get(ConfigVars.KEY_PREFIX, "")
340
340 self._request_identifier = self._request_identifier or config.get(
341 ConfigVars.REQUEST_IDENTIFIER, lambda: flask.request.endpoint or ""
342 )
341343 app_limits = config.get(ConfigVars.APPLICATION_LIMITS, None)
342344 self._application_limits_cost = self._application_limits_cost or config.get(
343345 ConfigVars.APPLICATION_LIMITS_COST, 1
397399
398400 @property
399401 def context(self) -> LimiterContext:
402 """
403 The context is meant to exist for the lifetime
404 of a request/response cycle per instance of the extension
405 so as to keep track of any state used at different steps
406 in the lifecycle (for example to pass information
407 from the before request hook to the after_request hook)
408
409 :meta private:
410 """
400411 ctx = request_context()
401412 if not hasattr(ctx, "_limiter_request_context"):
402413 ctx._limiter_request_context = defaultdict(LimiterContext) # type: ignore
403414 return cast(
404 Dict[str, LimiterContext],
415 Dict[Limiter, LimiterContext],
405416 ctx._limiter_request_context, # type: ignore
406 )[self._key_prefix]
417 )[self]
407418
408419 def limit(
409420 self,
776787
777788 return self.context.view_rate_limits
778789
790 def identify_request(self) -> str:
791 """
792 Returns the identity of the request (by default this is the
793 :attr:`flask.Request.endpoint` associated by the view function
794 that is handling the request). The behavior can be customized
795 by initializing the extension with a callable argument for
796 :paramref:`~flask_limiter.Limiter.request_identifier`.
797 """
798 if self.initialized and self.enabled:
799 assert self._request_identifier
800 return self._request_identifier()
801 return ""
802
779803 def __check_conditional_deductions(self, response: flask.wrappers.Response) -> None:
780804 for lim, args in self.context.conditional_deductions.items():
781805 if lim.deduct_when and lim.deduct_when(response):
856880 return response
857881
858882 def __check_all_limits_exempt(
859 self, endpoint: Optional[str], callable_name: Optional[str] = None
883 self,
884 endpoint: Optional[str],
860885 ) -> bool:
861886 return bool(
862887 not endpoint
863888 or not (self.enabled and self.initialized)
864889 or endpoint == "static"
865890 or any(fn() for fn in self._request_filters)
866 or (
867 self.context.rate_limiting_complete.get(callable_name, False)
868 if callable_name
869 else any(self.context.rate_limiting_complete.values())
870 )
871891 )
872892
873893 def __filter_limits(
877897 callable_name: Optional[str],
878898 in_middleware: bool = False,
879899 ) -> List[Limit]:
880
881900 if callable_name:
882901 name = callable_name
883902 else:
884903 view_func = flask.current_app.view_functions.get(endpoint or "", None)
885904 name = get_qualified_name(view_func) if view_func else ""
886905
887 if self.__check_all_limits_exempt(endpoint, callable_name):
906 if self.__check_all_limits_exempt(endpoint):
888907 return []
889908
890909 marked_for_limiting = (
907926 self.__check_backend_count = 0
908927 else:
909928 fallback_limits = list(itertools.chain(*self._in_memory_fallback))
910 resolved_limits = self.limit_manager.resolve_limits(
929 if fallback_limits:
930 return fallback_limits
931
932 defaults, decorated = self.limit_manager.resolve_limits(
911933 flask.current_app,
912934 endpoint,
913935 blueprint,
914936 name,
915937 in_middleware,
916938 marked_for_limiting,
917 fallback_limits,
918 )
919 limits = OrderedSet(resolved_limits) - self.context.seen_limits
920 self.context.seen_limits.update(limits)
921 return list(limits)
939 )
940 limits = OrderedSet(defaults) - self.context.seen_limits
941 self.context.seen_limits.update(defaults)
942 return list(limits) + list(decorated)
922943
923944 def __evaluate_limits(self, endpoint: str, limits: List[Limit]) -> None:
924945 failed_limits: List[Tuple[Limit, List[str]]] = []
9961017 def _check_request_limit(
9971018 self, callable_name: Optional[str] = None, in_middleware: bool = True
9981019 ) -> None:
999 endpoint = flask.request.endpoint or ""
1020 endpoint = self.identify_request()
10001021 try:
10011022 all_limits = self.__filter_limits(
1002 flask.request.endpoint,
1023 endpoint,
10031024 flask.request.blueprint,
10041025 callable_name,
10051026 in_middleware,
10261047 raise e
10271048
10281049 def __release_context(self, _: Optional[BaseException] = None) -> None:
1029 if self.context:
1030 self.context.reset()
1050 self.context.reset()
10311051
10321052
10331053 class LimitDecorator:
10991119 )
11001120
11011121 self.limiter.limit_manager.add_endpoint_hint(
1102 flask.request.endpoint, qualified_location
1122 self.limiter.identify_request(), qualified_location
11031123 )
11041124
11051125 self.limiter._check_request_limit(
11411161 def __inner(*a: P.args, **k: P.kwargs) -> R:
11421162 if (
11431163 self.limiter._auto_check
1144 and not self.limiter.context.rate_limiting_complete.get(name, False)
1164 and not getattr(obj, "__wrapper-limiter-instance", None)
1165 == self.limiter
11451166 ):
1146 if flask.request.endpoint:
1147 view_func = flask.current_app.view_functions.get(
1148 flask.request.endpoint, None
1149 )
1167 identity = self.limiter.identify_request()
1168 if identity:
1169 view_func = flask.current_app.view_functions.get(identity, None)
11501170 if view_func and not get_qualified_name(view_func) == name:
1151 self.limiter.limit_manager.add_endpoint_hint(
1152 flask.request.endpoint, name
1153 )
1171 self.limiter.limit_manager.add_endpoint_hint(identity, name)
11541172
11551173 self.limiter._check_request_limit(
11561174 in_middleware=False, callable_name=name
11571175 )
11581176
1159 self.limiter.context.rate_limiting_complete[name] = True
11601177 return cast(
11611178 R, flask.current_app.ensure_sync(cast(Callable[P, R], obj))(*a, **k)
11621179 )
11631180
1181 # mark this wrapper as wrapped by a decorator from the limiter
1182 # from which the decorator was created. This ensures that stacked
1183 # decorations only trigger rate limiting from the inner most
1184 # decorator from each limiter instance (the weird need for
1185 # keeping track of the instance is to handle cases where multiple
1186 # limiter extensions are registered on the same application).
1187 setattr(__inner, "__wrapper-limiter-instance", self.limiter)
11641188 return __inner
7777 callable_name: Optional[str] = None,
7878 in_middleware: bool = False,
7979 marked_for_limiting: bool = False,
80 fallback_limits: Optional[List[Limit]] = None,
81 ) -> List[Limit]:
80 ) -> Tuple[List[Limit], ...]:
8281 before_request_context = in_middleware and marked_for_limiting
8382 decorated_limits = []
8483 hinted_limits = []
101100 ):
102101 decorated_limits.extend(self.blueprint_limits(app, blueprint))
103102 exemption_scope = self.exemption_scope(app, endpoint, blueprint)
104 all_limits = []
105
106 if fallback_limits:
107 all_limits.extend(fallback_limits)
108
109 if not all_limits:
110 all_limits = (
111 self.application_limits
112 if in_middleware and not (exemption_scope & ExemptionScope.APPLICATION)
113 else []
103
104 all_limits = (
105 self.application_limits
106 if in_middleware and not (exemption_scope & ExemptionScope.APPLICATION)
107 else []
108 )
109 # all_limits += decorated_limits
110 explicit_limits_exempt = all(limit.method_exempt for limit in decorated_limits)
111
112 # all the decorated limits explicitly declared
113 # that they don't override the defaults - so, they should
114 # be included.
115 combined_defaults = all(
116 not limit.override_defaults for limit in decorated_limits
117 )
118 # previous requests to this endpoint have exercised decorated
119 # rate limits on callables that are not view functions. check
120 # if all of them declared that they don't override defaults
121 # and if so include the default limits.
122 hinted_limits_request_defaults = (
123 all(not limit.override_defaults for limit in hinted_limits)
124 if hinted_limits
125 else False
126 )
127 if (
128 (explicit_limits_exempt or combined_defaults)
129 and (
130 not (before_request_context or exemption_scope & ExemptionScope.DEFAULT)
114131 )
115 all_limits += decorated_limits
116 explicit_limits_exempt = all(
117 limit.method_exempt for limit in decorated_limits
118 )
119
120 # all the decorated limits explicitly declared
121 # that they don't override the defaults - so, they should
122 # be included.
123 combined_defaults = all(
124 not limit.override_defaults for limit in decorated_limits
125 )
126 # previous requests to this endpoint have exercised decorated
127 # rate limits on callables that are not view functions. check
128 # if all of them declared that they don't override defaults
129 # and if so include the default limits.
130 hinted_limits_request_defaults = (
131 all(not limit.override_defaults for limit in hinted_limits)
132 if hinted_limits
133 else False
134 )
135 if (
136 (explicit_limits_exempt or combined_defaults)
137 and (
138 not (
139 before_request_context
140 or exemption_scope & ExemptionScope.DEFAULT
141 )
142 )
143 ) or hinted_limits_request_defaults:
144 all_limits += self.default_limits
145 return all_limits
132 ) or hinted_limits_request_defaults:
133 all_limits += self.default_limits
134 return all_limits, decorated_limits
146135
147136 def exemption_scope(
148137 self, app: flask.Flask, endpoint: Optional[str], blueprint: Optional[str]
0 [tool.ruff]
1 line-length=100
2
00 -r test.txt
11 -r docs.txt
2 autoflake8
2 ruff
33 black
4 flake8
54 isort
65 keyring
76 mypy
00 -r main.txt
11 enum-tools[sphinx]==0.9.0.post1
22 furo==2022.12.7
3 Sphinx>4,<6
3 Sphinx>4,<7
44 sphinx-autobuild==2021.3.14
55 sphinx-copybutton==0.5.1
66 sphinx-inline-tabs==2022.1.2b11
77 sphinx-issues==3.0.1
8 sphinxext-opengraph==0.7.4
8 sphinxext-opengraph==0.8.1
99 sphinx-paramlinks==0.5.4
1010 sphinxcontrib-programoutput==0.17
1111
00 limits>=2.8
11 Flask>=2
22 ordered-set>4,<5
3 rich>=12,<13
3 rich>=12,<14
44 typing_extensions>=4
44 """
55 __author__ = "Ali-Akber Saifee"
66 __email__ = "ali@indydevs.org"
7 __copyright__ = "Copyright 2022, Ali-Akber Saifee"
7 __copyright__ = "Copyright 2023, Ali-Akber Saifee"
88
99 from setuptools import setup, find_packages
1010 import os
504504
505505
506506 def test_whitelisting():
507
508507 app = Flask(__name__)
509508 limiter = Limiter(
510509 get_ip_from_header,
594593
595594
596595 def test_decorated_shared_limit_immediate(extension_factory):
597
598596 app, limiter = extension_factory(default_limits=["1/minute"])
599597 shared = limiter.shared_limit(lambda: g.rate_limit, "shared")
600598
151151
152152 with app.test_client() as cli:
153153 with patch("limits.strategies.FixedWindowRateLimiter.hit") as hit:
154
155154 hit.side_effect = raiser
156155 assert 500 == cli.get("/").status_code
157156 assert "underlying" == cli.get("/").data.decode()
158157 with patch(
159158 "limits.strategies.FixedWindowRateLimiter.get_window_stats"
160159 ) as get_window_stats:
161
162160 get_window_stats.side_effect = raiser
163161 assert 500 == cli.get("/").status_code
164162 assert "underlying" == cli.get("/").data.decode()
516516
517517
518518 def test_callable_application_limit(extension_factory):
519
520519 app, limiter = extension_factory(
521520 application_limits=[
522521 lambda: request.headers.get("suspect", 0) and "1/minute" or "2/minute"
687686 assert 429 == resp.status_code
688687
689688
690 def test_second_instance_bypassed_by_shared_g():
689 def test_multiple_instances_no_key_prefix():
691690 app = Flask(__name__)
692691 limiter1 = Limiter(get_remote_address, app=app)
693692
707706 with hiro.Timeline().freeze() as timeline:
708707 with app.test_client() as cli:
709708 assert cli.get("/test1").status_code == 200
710 assert cli.get("/test2").status_code == 200
711709 assert cli.get("/test1").status_code == 429
712710 assert cli.get("/test2").status_code == 200
711 assert cli.get("/test2").status_code == 429
713712
714713 for i in range(8):
715 assert cli.get("/test1").status_code == 429
714 timeline.forward(1)
715 assert cli.get("/test1").status_code == 200
716716 assert cli.get("/test2").status_code == 200
717 assert cli.get("/test2").status_code == 429
718717 timeline.forward(1)
719718 assert cli.get("/test1").status_code == 200
720719 assert cli.get("/test2").status_code == 429
721720 timeline.forward(59)
722 assert cli.get("/test1").status_code == 200
723721 assert cli.get("/test2").status_code == 200
724722
725723