Codebase list flask-limiter / 6c32f54
Update upstream source from tag 'upstream/3.1.0' Update to upstream version '3.1.0' with Debian dir c09863f36a09d42a467f8a1fb4912406344bc776 Carsten Schoenert 1 year, 4 months ago
33 changed file(s) with 1227 addition(s) and 740 deletion(s). Raw diff Collapse all Expand all
00 [run]
11 omit =
2 /*/flask_limiter/_compat.py
3 /*/flask_limiter/_version*
4 /*/tests/*.py
5 /*/flask_limiter/contrib/*.py
2 /**/flask_limiter/_compat.py
3 /**/flask_limiter/_version*
4 /**/tests/*.py
5 /**/flask_limiter/contrib/*.py
66 versioneer.py
77 setup.py
88 [report]
66 runs-on: ubuntu-latest
77 strategy:
88 matrix:
9 python-version: [3.8, 3.9, "3.10"]
9 python-version: [3.8, 3.9, "3.10", "3.11"]
1010 steps:
1111 - uses: actions/checkout@v3
1212 - name: Cache dependencies
4343 strategy:
4444 fail-fast: false
4545 matrix:
46 python-version: [3.8, 3.9, "3.10", "3.11.0-beta.5"]
46 python-version: [3.8, 3.9, "3.10", "3.11"]
4747 flask-version: [">2,<2.1", ">=2.1,<2.2", ">=2.2,<2.3"]
4848 steps:
4949 - uses: actions/checkout@v3
+0
-14
.landscape.yml less more
0 doc-warnings: no
1 test-warnings: no
2 strictness: veryhigh
3 max-line-length: 80
4 autodetect: yes
5 requirements:
6 - requirements/main.txt
7 ignore-paths:
8 - tests
9 - doc
10 ignore-patterns:
11 - versioneer.py
12 - flask_limiter/_versions.py
13
+0
-4
.pyup.yml less more
0 # autogenerated pyup.io config file
1 # see https://pyup.io/docs/configuration/ for all available options
2
3 schedule: every week
+0
-197
.style.yapf less more
0 [style]
1 # Align closing bracket with visual indentation.
2 align_closing_bracket_with_visual_indent=True
3
4 # Allow dictionary keys to exist on multiple lines. For example:
5 #
6 # x = {
7 # ('this is the first element of a tuple',
8 # 'this is the second element of a tuple'):
9 # value,
10 # }
11 allow_multiline_dictionary_keys=True
12
13 # Allow lambdas to be formatted on more than one line.
14 allow_multiline_lambdas=True
15
16 # Allow splits before the dictionary value.
17 allow_split_before_dict_value=True
18
19 # Insert a blank line before a class-level docstring.
20 blank_line_before_class_docstring=False
21
22 # Insert a blank line before a 'def' or 'class' immediately nested
23 # within another 'def' or 'class'. For example:
24 #
25 # class Foo:
26 # # <------ this blank line
27 # def method():
28 # ...
29 blank_line_before_nested_class_or_def=False
30
31 # Do not split consecutive brackets. Only relevant when
32 # dedent_closing_brackets is set. For example:
33 #
34 # call_func_that_takes_a_dict(
35 # {
36 # 'key1': 'value1',
37 # 'key2': 'value2',
38 # }
39 # )
40 #
41 # would reformat to:
42 #
43 # call_func_that_takes_a_dict({
44 # 'key1': 'value1',
45 # 'key2': 'value2',
46 # })
47 coalesce_brackets=True
48
49 # The column limit.
50 column_limit=79
51
52 # Indent width used for line continuations.
53 continuation_indent_width=4
54
55 # Put closing brackets on a separate line, dedented, if the bracketed
56 # expression can't fit in a single line. Applies to all kinds of brackets,
57 # including function definitions and calls. For example:
58 #
59 # config = {
60 # 'key1': 'value1',
61 # 'key2': 'value2',
62 # } # <--- this bracket is dedented and on a separate line
63 #
64 # time_series = self.remote_client.query_entity_counters(
65 # entity='dev3246.region1',
66 # key='dns.query_latency_tcp',
67 # transform=Transformation.AVERAGE(window=timedelta(seconds=60)),
68 # start_ts=now()-timedelta(days=3),
69 # end_ts=now(),
70 # ) # <--- this bracket is dedented and on a separate line
71 dedent_closing_brackets=True
72
73 # Place each dictionary entry onto its own line.
74 each_dict_entry_on_separate_line=True
75
76 # The regex for an i18n comment. The presence of this comment stops
77 # reformatting of that line, because the comments are required to be
78 # next to the string they translate.
79 i18n_comment=
80
81 # The i18n function call names. The presence of this function stops
82 # reformattting on that line, because the string it has cannot be moved
83 # away from the i18n comment.
84 i18n_function_call=
85
86 # Indent the dictionary value if it cannot fit on the same line as the
87 # dictionary key. For example:
88 #
89 # config = {
90 # 'key1':
91 # 'value1',
92 # 'key2': value1 +
93 # value2,
94 # }
95 indent_dictionary_value=False
96
97 # The number of columns to use for indentation.
98 indent_width=4
99
100 # Join short lines into one line. E.g., single line 'if' statements.
101 join_multiple_lines=True
102
103 # Do not include spaces around selected binary operators. For example:
104 #
105 # 1 + 2 * 3 - 4 / 5
106 #
107 # will be formatted as follows when configured with a value "*,/":
108 #
109 # 1 + 2*3 - 4/5
110 #
111 no_spaces_around_selected_binary_operators=set()
112
113 # Use spaces around default or named assigns.
114 spaces_around_default_or_named_assign=False
115
116 # Use spaces around the power operator.
117 spaces_around_power_operator=False
118
119 # The number of spaces required before a trailing comment.
120 spaces_before_comment=2
121
122 # Insert a space between the ending comma and closing bracket of a list,
123 # etc.
124 space_between_ending_comma_and_closing_bracket=True
125
126 # Split before arguments if the argument list is terminated by a
127 # comma.
128 split_arguments_when_comma_terminated=False
129
130 # Set to True to prefer splitting before '&', '|' or '^' rather than
131 # after.
132 split_before_bitwise_operator=True
133
134 # Split before a dictionary or set generator (comp_for). For example, note
135 # the split before the 'for':
136 #
137 # foo = {
138 # variable: 'Hello world, have a nice day!'
139 # for variable in bar if variable != 42
140 # }
141 split_before_dict_set_generator=True
142
143 # Split after the opening paren which surrounds an expression if it doesn't
144 # fit on a single line.
145 split_before_expression_after_opening_paren=False
146
147 # If an argument / parameter list is going to be split, then split before
148 # the first argument.
149 split_before_first_argument=False
150
151 # Set to True to prefer splitting before 'and' or 'or' rather than
152 # after.
153 split_before_logical_operator=True
154
155 # Split named assignments onto individual lines.
156 split_before_named_assigns=True
157
158 # The penalty for splitting right after the opening bracket.
159 split_penalty_after_opening_bracket=30
160
161 # The penalty for splitting the line after a unary operator.
162 split_penalty_after_unary_operator=10000
163
164 # The penalty for splitting right before an if expression.
165 split_penalty_before_if_expr=0
166
167 # The penalty of splitting the line around the '&', '|', and '^'
168 # operators.
169 split_penalty_bitwise_operator=300
170
171 # The penalty for characters over the column limit.
172 split_penalty_excess_character=4500
173
174 # The penalty incurred by adding a line split to the unwrapped line. The
175 # more line splits added the higher the penalty.
176 split_penalty_for_added_line_split=30
177
178 # The penalty of splitting a list of "import as" names. For example:
179 #
180 # from a_very_long_or_indented_module_name_yada_yad import (long_argument_1,
181 # long_argument_2,
182 # long_argument_3)
183 #
184 # would reformat to something like:
185 #
186 # from a_very_long_or_indented_module_name_yada_yad import (
187 # long_argument_1, long_argument_2, long_argument_3)
188 split_penalty_import_names=0
189
190 # The penalty of splitting the line around the 'and' and 'or'
191 # operators.
192 split_penalty_logical_operator=300
193
194 # Use the Tab character for indentation.
195 use_tabs=False
196
11
22 Changelog
33 =========
4
5 v3.1.0
6 ------
7 Release Date: 2022-12-29
8
9 * Feature
10
11 * Skip logging an error if a decorated limit uses a callable
12 to return the "current" rate limit and returns an empty string.
13 Treat this is a signal that the rate limit should be skipped for
14 this request.
15
16 v3.0.0
17 ------
18 Release Date: 2022-12-28
19
20 * Breaking changes
21
22 * Change order of extension constructor arguments to only require
23 ``key_func`` as the first positional argument and all other arguments
24 as keyword arguments.
25 * Separate positional/keyword arguments in limit/shared_limit decorators
26 * Remove deprecated config variable RATELIMIT_STORAGE_URL
27 * Remove legacy backward compatibility path for flask < 2
28
29 * Features
30
31 * Allow scoping regular limit decorators / context managers
32
33 v3.1.0
34 ------
35 Release Date: 2022-12-29
36
37 v3.0.0b2
38 --------
39 Release Date: 2022-12-28
40
41 * Breaking changes
42
43 * Remove deprecated config variable RATELIMIT_STORAGE_URL
44 * Remove legacy backward compatibility path for flask < 2
45 * Enforce key_func as a required argument
46
47 * Chores
48
49 * Simplify registration of decorated function & blueprint limits
50
51
52 v3.1.0
53 ------
54 Release Date: 2022-12-29
55
56 v3.0.0b1
57 --------
58 Release Date: 2022-12-26
59
60 * Breaking changes
61
62 * Change order of extension constructor arguments to only require
63 ``key_func`` as the first positional argument and all other arguments
64 as keyword arguments.
65 * Separate positional/keyword arguments in limit/shared_limit decorators
66
67 * Features
68
69 * Allow scoping regular limit decorators / context managers
70
71 v2.9.2
72 ------
73 Release Date: 2022-12-26
74
75 * Feature
76
77 * Extend customization by http method to shared_limit decorator
78
79 v2.9.1
80 ------
81 Release Date: 2022-12-26
82
83 * Chores
84
85 * Update documentation quick start
86 * Refresh documentation for class based views
87
88 v2.9.0
89 ------
90 Release Date: 2022-12-24
91
92 * Features
93
94 * Allow using `limit` & `shared_limit` decorators on pure
95 functions that are not decorated as routes. The functions
96 when called from within a request context will get rate limited.
97 * Allow using `limit` as a context manager to rate limit a code block
98 explicitly within a request
99
100 * Chores
101
102 * Updated development dependencies
103 * Fix error running tests depending on docker locally
104 * Update internals to use dataclasses
105
106 v2.8.1
107 ------
108 Release Date: 2022-11-15
109
110 * Chores
111
112 * Add sponsorship banner to rtd
113 * Update documentation dependencies
114
115 v2.8.0
116 ------
117 Release Date: 2022-11-13
118
119 * Breaking changes
120
121 * Any exception raised when calling an ``on_breach`` callback will
122 be re-raised instead of being absorbed unless ``swallow_errors`` is set.
123 In the case of ``swallow_errors`` the exception will now be logged
124 at ``ERROR`` level instead of ``WARN``
125 * Reduce log level of rate limit exceeded log messages to ``INFO``
126
127 v2.7.0
128 ------
129 Release Date: 2022-10-25
130
131 * Bug Fix
132
133 * Add default value for RateLimitExceeded optional parameter
134 * Fix suppression of errors when using conditional deduction (`Issue 363 <https://github.com/alisaifee/flask-limiter/issues/363>`_)
135
136 v2.6.3
137 ------
138 Release Date: 2022-09-22
139
140 * Compatibility
141
142 * Ensure typing_extensions dependency has a minimum version
143
144 * Chores
145
146 * Documentation tweaks
147 * Update CI to use 3.11 rc2
4148
5149 v2.6.2
6150 ------
747891
748892
749893
894
895
896
897
898
899
900
901
902
903
904
1515
1616 |docs| |ci| |codecov| |pypi| |license|
1717
18 Flask-Limiter provides rate limiting features to flask applications.
18 **Flask-Limiter** adds rate limiting to `Flask <https://flask.palletsprojects.com>`_ applications.
1919
20 It allows configuring various backends to persist the rate limits, which is
21 provided by the `limits <https://github.com/alisaifee/limits>`_ library.
20 ----
21
22 Sponsored by `Zuplo <https://zuplo.link/3NuX0co>`_ a fully-managed API Gateway for developers.
23 Add `dynamic rate-limiting <https://zuplo.link/flask-dynamic-rate-limit>`_ authentication and more to any API in minutes.
24 Learn more at `zuplo.com <https://zuplo.link/3NuX0co>`_
25
26 ----
27
28
29 You can configure rate limits at different levels such as:
30
31 - Application wide global limits per user
32 - Default limits per route
33 - By `Blueprints <https://flask-limiter.readthedocs.io/en/latest/recipes.html#rate-limiting-all-routes-in-a-blueprint>`_
34 - By `Class-based views <https://flask-limiter.readthedocs.io/en/latest/recipes.html#using-flask-pluggable-views>`_
35 - By `individual routes <https://flask-limiter.readthedocs.io/en/latest/index.html#decorators-to-declare-rate-limits>`_
36
37 **Flask-Limiter** can be `configured <https://flask-limiter.readthedocs.io/en/latest/configuration.html>`_ to fit your application in many ways, including:
38
39 - Persistance to various commonly used `storage backends <https://flask-limiter.readthedocs.io/en/latest/#configuring-a-storage-backend>`_
40 (such as Redis, Memcached & MongoDB)
41 via `limits <https://limits.readthedocs.io/en/stable/storage.html>`__
42 - Any rate limiting strategy supported by `limits <https://limits.readthedocs.io/en/stable/strategies.html>`__
43
44 Follow the quickstart below to get started or `read the documentation <http://flask-limiter.readthedocs.org/en/latest>`_ for more details.
45
2246
2347 Quickstart
2448 ===========
2549
26 Add the rate limiter to your flask app.
50 Install
51 -------
52 .. code-block:: bash
2753
54 pip install Flask-Limiter
55
56 Add the rate limiter to your flask app
57 ---------------------------------------
2858 .. code-block:: python
59
60 # app.py
2961
3062 from flask import Flask
3163 from flask_limiter import Limiter
3365
3466 app = Flask(__name__)
3567 limiter = Limiter(
36 app,
37 key_func=get_remote_address,
68 get_remote_address,
69 app=app,
3870 default_limits=["2 per minute", "1 per second"],
3971 storage_uri="memory://",
4072 # Redis
6496 def ping():
6597 return 'PONG'
6698
67 app.run()
99 Inspect the limits using the command line interface
100 ---------------------------------------------------
101 .. code-block:: bash
102
103 $ FLASK_APP=app:app flask limiter list
104
105 app
106 ├── fast: /fast
107 │ ├── 2 per 1 minute
108 │ └── 1 per 1 second
109 ├── ping: /ping
110 │ └── Exempt
111 └── slow: /slow
112 └── 1 per 1 day
113
114 Run the app
115 -----------
116 .. code-block:: bash
117
118 $ FLASK_APP=app:app flask run
68119
69120
70
71 Test it out. The ``fast`` endpoint respects the default rate limit while the
121 Test it out
122 -----------
123 The ``fast`` endpoint respects the default rate limit while the
72124 ``slow`` endpoint uses the decorated one. ``ping`` has no rate limit associated
73125 with it.
74126
75127 .. code-block:: bash
76128
77 $ curl localhost:5000/fast
78 42
79 $ curl localhost:5000/fast
80 42
81 $ curl localhost:5000/fast
82 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
83 <title>429 Too Many Requests</title>
84 <h1>Too Many Requests</h1>
85 <p>2 per 1 minute</p>
86 $ curl localhost:5000/slow
87 24
88 $ curl localhost:5000/slow
89 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
90 <title>429 Too Many Requests</title>
91 <h1>Too Many Requests</h1>
92 <p>1 per 1 day</p>
93 $ curl localhost:5000/ping
94 PONG
95 $ curl localhost:5000/ping
96 PONG
97 $ curl localhost:5000/ping
98 PONG
99 $ curl localhost:5000/ping
100 PONG
129 $ curl localhost:5000/fast
130 42
131 $ curl localhost:5000/fast
132 42
133 $ curl localhost:5000/fast
134 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
135 <title>429 Too Many Requests</title>
136 <h1>Too Many Requests</h1>
137 <p>2 per 1 minute</p>
138 $ curl localhost:5000/slow
139 24
140 $ curl localhost:5000/slow
141 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
142 <title>429 Too Many Requests</title>
143 <h1>Too Many Requests</h1>
144 <p>1 per 1 day</p>
145 $ curl localhost:5000/ping
146 PONG
147 $ curl localhost:5000/ping
148 PONG
149 $ curl localhost:5000/ping
150 PONG
151 $ curl localhost:5000/ping
152 PONG
101153
102154
103155
104156
105 For more details `read the docs <http://flask-limiter.readthedocs.org/en/latest>`_
1111 }
1212 .badges {
1313 display: flex;
14 padding: 5px;
15 flex-direction: rootow;
14 padding: 10px;
15 flex-direction: row;
1616 justify-content: center;
1717 }
1818 .header-badge {
1919 padding: 2px;
2020 }
21
22 .sponsorship {
23 display: flex;
24 padding: 5px;
25 flex-direction: row;
26 justify-content: center;
27 margin-top: 10px;
28 margin-bottom: 5px;
29 border-top: solid 1pt var(--bg3);
30 border-bottom: solid 1pt var(--bg3);
31 font-family: "Be Vietnam Pro", sans-serif;
32 font-weight: 500;
33 font-size: smaller;
34 }
35 .sponsorship a {
36 color: rgb(255, 0, 189);
37 text-decoration: none;
38 }
39
40 .sponsorship .left {
41 padding-left: 10px;
42 padding-right: 10px;
43 }
44 .sponsorship .right {
45 display: flex;
46 flex-direction: column;
47 justify-content: center;
48 padding: 10px;
49 }
50 .sponsorship-button {
51 display: flex;
52 justify-content: center;
53 align-items: center;
54 }
55 .sponsorship-button a {
56 background-color: rgb(255, 0, 189);
57 color: white;
58 text-decoration: none;
59 font-weight: bold;
60 font-family: "Be Vietnam Pro", sans-serif;
61 padding: 10px 10px 9px;
62 border-radius: 10px;
63 border: 1pt solid rgb(255, 0, 189);
64 text-align: center;
65 }
66 .sponsorship-button a:hover {
67 background-color: white;
68 text-align: center;
69 border: solid 1pt black;
70 color: black;
71 }
72
73 @media only screen and (max-width: 768px) {
74 .sponsorship {
75 flex-direction: column;
76 }
77 .sponsorship .left {
78 text-align: center;
79 }
80 .sponsorship .right {
81 padding: 0;
82 }
83 }
1010 ---------
1111 .. autoflag:: ExemptionScope
1212 .. autoclass:: RequestLimit
13 .. autoclass:: HEADERS
1413 .. automodule:: flask_limiter.util
1514
1615 Exceptions
1010
1111 from theme_config import *
1212
13 description = "Flask-Limiter provides rate limiting features to flask applications."
13 description = "Flask-Limiter adds rate limiting to flask applications."
1414 copyright = "2022, Ali-Akber Saifee"
1515 project = "Flask-Limiter"
1616
3131 html_theme_options[
3232 "announcement"
3333 ] = f"""
34 This is a development version. The documentation for the latest version: <b>{release}</b> can be found <a href="/en/stable">here</a>
34 This is a development version. The documentation for the latest stable version can be found <a href="/en/stable">here</a>
3535 """
3636 html_title = f"{project} <small><b style='color: var(--color-brand-primary)'>{{dev}}</b></small>"
3737 except:
4242 templates_path = ["./_templates"]
4343 html_css_files = [
4444 "custom.css",
45 "https://fonts.googleapis.com/css2?family=Fira+Code:wght@300;400;700&family=Fira+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap",
45 "colors.css",
46 "https://fonts.googleapis.com/css2?family=Fira+Code:wght@300;400;700&family=Fira+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&family=Be+Vietnam+Pro:wght@500&display=swap",
4647 ]
4748
4849 html_theme_options.update({"light_logo": "tap-icon.png", "dark_logo": "tap-icon.png"})
1919
2020 $ pytest
2121
22 |version|
2322
24 |release|
25
26 Running the tests will automatically invoke :program:`docker-compose` with the config at :githubsrc:`docker-compose.yml`
23 Running the tests will automatically invoke :program:`docker-compose` with the following config (:githubsrc:`docker-compose.yml`)
2724
2825 .. literalinclude:: ../../docker-compose.yml
77 :width: 600px
88 :align: center
99 :class: logo
10
11 .. container:: sponsorship
12
13 .. container:: left
14
15 | Sponsored by `Zuplo <https://zuplo.link/3hLbXv5>`_, a fully-managed API gateway for developers.
16 | Add `dynamic rate-limiting <https://zuplo.link/3hxWXAv>`_, authentication and more to any API in minutes.
17
18 .. container:: right
19
20 .. container:: sponsorship-button
21
22 `Try Zuplo <https://zuplo.link/3hLbXv5>`_
1023
1124 =============
1225 Flask-Limiter
3346 .. image:: https://img.shields.io/github/last-commit/alisaifee/flask-limiter?logo=github&style=for-the-badge&labelColor=#282828
3447 :target: https://github.com/alisaifee/flask-limiter
3548 :class: header-badge
36 .. image:: https://img.shields.io/github/workflow/status/alisaifee/flask-limiter/CI?logo=github&style=for-the-badge&labelColor=#282828
49 .. image:: https://img.shields.io/github/actions/workflow/status/alisaifee/flask-limiter/main.yml?logo=github&style=for-the-badge&labelColor=#282828
3750 :target: https://github.com/alisaifee/flask-limiter/actions/workflows/main.yml
3851 :class: header-badge
3952 .. image:: https://img.shields.io/codecov/c/github/alisaifee/flask-limiter?logo=codecov&style=for-the-badge&labelColor=#282828
4356 :target: https://pypi.org/project/flask-limiter
4457 :class: header-badge
4558
46 **Flask-Limiter** provides rate limiting features to :class:`~flask.Flask` applications.
59 **Flask-Limiter** adds rate limiting to :class:`~flask.Flask` applications.
4760
4861 By adding the extension to your flask application, you can configure various
4962 rate limits at different levels (e.g. application wide, per :class:`~flask.Blueprint`,
90103 ===========
91104 A very basic setup can be achieved as follows:
92105
93 .. code-block:: python
94
95 from flask import Flask
96 from flask_limiter import Limiter
97 from flask_limiter.util import get_remote_address
98
99 app = Flask(__name__)
100 limiter = Limiter(
101 app,
102 key_func=get_remote_address,
103 default_limits=["200 per day", "50 per hour"],
104 storage_uri="memory://",
105 )
106 @app.route("/slow")
107 @limiter.limit("1 per day")
108 def slow():
109 return ":("
110
111 @app.route("/medium")
112 @limiter.limit("1/second", override_defaults=False)
113 def medium():
114 return ":|"
115
116 @app.route("/fast")
117 def fast():
118 return ":)"
119
120 @app.route("/ping")
121 @limiter.exempt
122 def ping():
123 return "PONG"
124
106 .. literalinclude:: ../../examples/sample.py
107 :language: py
125108
126109 The above Flask app will have the following rate limiting characteristics:
127110
142125 Every time a request exceeds the rate limit, the view function will not get called and instead
143126 a `429 <http://tools.ietf.org/html/rfc6585#section-4>`_ http error will be raised.
144127
128 The extension adds a ``limiter`` subcommand to the :doc:`Flask CLI <flask:cli>` which can be used to inspect
129 the effective configuration and applied rate limits (See :ref:`cli:Command Line Interface` for more details).
130
131 Given the quick start example above:
132
133
134 .. code-block:: shell
135
136 $ flask limiter config
137
138 .. program-output:: FLASK_APP=../../examples/sample.py:app flask limiter config
139 :shell:
140
141 .. code-block:: shell
142
143 $ flask limiter limits
144
145 .. program-output:: FLASK_APP=../../examples/sample.py:app flask limiter limits
146 :shell:
145147
146148 The Flask-Limiter extension
147149 ---------------------------
156158 from flask_limiter.util import get_remote_address
157159 ....
158160
159 limiter = Limiter(app, key_func=get_remote_address)
161 limiter = Limiter(get_remote_address, app=app)
160162
161163 Deferred app initialization using :meth:`~flask_limiter.Limiter.init_app`
162164
163165 .. code-block:: python
164166
165 limiter = Limiter(key_func=get_remote_address)
167 limiter = Limiter(get_remote_address)
166168 limiter.init_app(app)
167169
168170 At this point it might be a good idea to look at the configuration options
190192 ....
191193
192194 limiter = Limiter(
193 app,
194 key_func=get_remote_address,
195 get_remote_address,
196 app=app,
195197 storage_uri="memcached://localhost:11211",
196198 storage_options={}
197199 )
209211 ....
210212
211213 limiter = Limiter(
212 app, key_func=get_remote_address,
214 get_remote_address,
215 app=app,
213216 storage_uri="redis://localhost:6379",
214 storage_options={"connect_timeout": 30},
217 storage_options={"socket_connect_timeout": 30},
215218 strategy="fixed-window", # or "moving-window"
216219 )
217220
229232
230233 pool = redis.connection.BlockingConnectionPool.from_url("redis://.....")
231234 limiter = Limiter(
232 app, key_func=get_remote_address,
235 get_remote_address,
236 app=app,
233237 storage_uri="redis://",
234238 storage_options={"connection_pool": pool},
235239 strategy="fixed-window", # or "moving-window"
248252 ....
249253
250254 limiter = Limiter(
251 app,
252 key_func=get_remote_address,
255 get_remote_address,
256 app=app,
253257 storage_uri="redis+cluster://localhost:7000,localhost:7001,localhost:7002",
254 storage_options={"connect_timeout": 30},
258 storage_options={"socket_connect_timeout": 30},
255259 strategy="fixed-window", # or "moving-window"
256260 )
257261
264268 ....
265269
266270 limiter = Limiter(
267 app, key_func=get_remote_address,
271 get_remote_address,
272 app=app,
268273 storage_uri="mongodb://localhost:27017",
269274 strategy="fixed-window", # or "moving-window"
270275 )
108108 429
109109 )
110110
111 .. note::
112 .. versionchanged:: 2.8.0
113 Any errors encountered when calling an :paramref:`~Limiter.on_breach` callback will
114 be re-raised unless :paramref:`~Limiter.swallow_errors` is set to ``True``
115
111116 For specific rate limit decorated routes
112117 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
113118 .. versionadded:: 2.6.0
176181 For scenarios where the decision to count the current request towards a rate limit
177182 can only be made after the request has completed, a callable that accepts the current
178183 :class:`flask.Response` object as its argument can be provided to the :meth:`~Limiter.limit` or
179 :meth:`~Limiter.shared_limit` decorators through the ``deduct_when`` keyword arugment.
184 :meth:`~Limiter.shared_limit` decorators through the ``deduct_when`` keyword argument.
180185 A truthy response from the callable will result in a deduction from the rate limit.
181186
182187 As an example, to only count non `200` responses towards the rate limit
194199
195200
196201 `deduct_when` can also be provided for default limits by providing the
197 :paramref:`~flask_limiter.Limiter.default_limits_deduct_when` paramter
202 :paramref:`~flask_limiter.Limiter.default_limits_deduct_when` parameter
198203 to the :class:`~flask_limiter.Limiter` constructor.
199204
200205
201206 .. note:: All requests will be tested for the rate limit and rejected accordingly
202 if the rate limit is already hit. The providion of the `deduct_when`
207 if the rate limit is already hit. The provision of the `deduct_when`
203208 argument only changes whether the request will count towards depleting the rate limit.
204209
205210
206 Using Flask Pluggable Views
207 ---------------------------
208
209 If you are using a class based approach to defining view function, the regular
210 method of decorating a view function to apply a per route rate limit will not
211 work. You can add rate limits to your view classes using the following approach.
211 .. _using-flask-pluggable-views:
212
213 Rate limiting Class-based Views
214 -------------------------------
215
216 If you are taking a class based approach for defining views,
217 the recommended method (:doc:`flask:views`) of adding decorators is
218 to add the :meth:`~Limiter.limit` decorator to :attr:`~flask.views.View.decorators` in your view subclass as shown in the
219 example below
212220
213221
214222 .. code-block:: python
215223
216224 app = Flask(__name__)
217 limiter = Limiter(app, key_func=get_remote_address)
225 limiter = Limiter(get_remote_address, app=app)
218226
219227 class MyView(flask.views.MethodView):
220228 decorators = [limiter.limit("10/second")]
229
221230 def get(self):
222231 return "get"
223232
233242 keyword argument.
234243
235244
236 The above approach has been tested with sub-classes of :class:`flask.views.View`,
237 :class:`flask.views.MethodView` and :class:`flask_restful.Resource`.
238
239245 Rate limiting all routes in a :class:`~flask.Blueprint`
240246 -------------------------------------------------------
241247
242248 .. warning:: :class:`~flask.Blueprint` instances that are registered on another blueprint
243249 instead of on the main :class:`~flask.Flask` instance had not been considered
244250 upto :ref:`changelog:v2.3.0`. Effectively **they neither inherited** the rate limits
245 explicitely registered on the parent :class:`~flask.Blueprint` **nor were they
251 explicitly registered on the parent :class:`~flask.Blueprint` **nor were they
246252 exempt** from rate limits if the parent had been marked exempt.
247253 (See :issue:`326`, and the :ref:`recipes:nested blueprints` section below).
248254
249255 :meth:`~Limiter.limit`, :meth:`~Limiter.shared_limit` &
250256 :meth:`~Limiter.exempt` can all be tpplied to :class:`flask.Blueprint` instances as well.
251 In the following example the **login** Blueprint has a special rate limit applied to all its routes, while
252 the **help** Blueprint is exempt from all rate limits. The **regular** Blueprint follows the default rate limits.
257 In the following example the ``login`` Blueprint has a special rate limit applied to all its routes, while
258 the ``doc`` Blueprint is exempt from all rate limits. The ``regular`` Blueprint follows the default rate limits.
253259
254260
255261 .. code-block:: python
273279 return "login"
274280
275281
276 limiter = Limiter(app, default_limits = ["1/second"], key_func=get_remote_address)
282 limiter = Limiter(get_remote_address, app=app, default_limits = ["1/second"])
277283 limiter.limit("60/hour")(login)
278284 limiter.exempt(doc)
279285
318324 ===========================================================
319325
320326 Using combinations of :paramref:`~Limiter.limit.override_defaults` parameter
321 when explicitely declaring limits on Blueprints and the :paramref:`~Limiter.exempt.flags`
327 when explicitly declaring limits on Blueprints and the :paramref:`~Limiter.exempt.flags`
322328 parameter when exempting Blueprints with :meth:`~Limiter.exempt`
323329 the resolution of inherited and descendent limits within the scope of a Blueprint
324330 can be controlled.
407413
408414
409415 app = Flask(__name__)
410 limiter = Limiter(app, key_func=get_remote_address)
416 limiter = Limiter(get_remote_address, app=app)
411417
412418 def error_handler():
413419 return app.config.get("DEFAULT_ERROR_MESSAGE")
436442
437443
438444 app = Flask(__name__)
439 limiter = Limiter(app, key_func=get_remote_address)
445 limiter = Limiter(get_remote_address, app=app)
440446
441447
442448 @app.route("/")
482488 # for example if the request goes through one proxy
483489 # before hitting your application server
484490 app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1)
485 limiter = Limiter(app, key_func=get_remote_address)
491 limiter = Limiter(get_remote_address, app=app)
0 import os
01 import jinja2
12 from flask import Blueprint, Flask, jsonify, request, render_template, make_response
23 from flask.views import View
2829 return 1
2930
3031 limiter = Limiter(
31 key_func=get_remote_address,
32 get_remote_address,
3233 default_limits=["20/hour", "1000/hour", default_limit_extra],
3334 default_limits_exempt_when=lambda: request.headers.get("X-Internal"),
3435 default_limits_deduct_when=lambda response: response.status_code == 200,
3536 default_limits_cost=default_cost,
3637 application_limits=["5000/hour"],
3738 headers_enabled=True,
39 storage_uri=os.environ.get("FLASK_RATELIMIT_STORAGE_URI", "memory://"),
3840 )
3941
4042 app = Flask(__name__)
0 from flask import Flask
1 from flask_limiter import Limiter
2 from flask_limiter.util import get_remote_address
3
4 app = Flask(__name__)
5 limiter = Limiter(
6 get_remote_address,
7 app=app,
8 default_limits=["200 per day", "50 per hour"],
9 storage_uri="memory://",
10 )
11
12
13 @app.route("/slow")
14 @limiter.limit("1 per day")
15 def slow():
16 return ":("
17
18
19 @app.route("/medium")
20 @limiter.limit("1/second", override_defaults=False)
21 def medium():
22 return ":|"
23
24
25 @app.route("/fast")
26 def fast():
27 return ":)"
28
29
30 @app.route("/ping")
31 @limiter.exempt
32 def ping():
33 return "PONG"
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: 2.6.2, stable)"
27 git_full = "f56d9c0af9930ee58d706bf7aabdf8751ded12e0"
28 git_date = "2022-08-24 09:35:12 -0700"
26 git_refnames = " (tag: 3.1.0, stable)"
27 git_full = "745964acbc31048af1fedca09a1637df8278c760"
28 git_date = "2022-12-29 11:54:32 -0800"
2929 keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
3030 return keywords
3131
1919
2020 from flask_limiter import Limiter
2121 from flask_limiter.constants import ConfigVars, ExemptionScope, HeaderNames
22 from flask_limiter.util import get_qualified_name
2223 from flask_limiter.wrappers import Limit
2324
2425 limiter_theme = Theme(
6768 def render_limit_state(
6869 limiter: Limiter, endpoint: str, limit: Limit, key: str, method: str
6970 ) -> str:
70 args = limit.args_for(endpoint, key, method)
71 args = [key, limit.scope_for(endpoint, method)]
7172 if not limiter.storage or (limiter.storage and not limiter.storage.check()):
7273 return ": [error]Storage not available[/error]"
7374 test = limiter.limiter.test(limit.limit, *args)
116117
117118 for limit in limits:
118119 if endpoint:
120 view_func = app.view_functions.get(endpoint, None)
119121 source = (
120122 "blueprint"
121123 if blueprint
122124 and limit in limiter.limit_manager.blueprint_limits(app, blueprint)
123125 else "route"
124 if limit in limiter.limit_manager.route_limits(app, endpoint)
126 if limit
127 in limiter.limit_manager.decorated_limits(
128 get_qualified_name(view_func) if view_func else ""
129 )
125130 else "default"
126131 )
127132 else:
524529 for limit in application_limits:
525530 limiter.limiter.clear(
526531 limit.limit,
527 *limit.args_for("", key, method),
532 key,
533 limit.scope_for("", method),
528534 )
529535 node.add(f"{render_limit(limit)}: [success]Cleared[/success]")
530536 console.print(node)
541547 for rule_method in details["rule"].methods:
542548 limiter.limiter.clear(
543549 limit.limit,
544 *limit.args_for(endpoint, key, rule_method),
550 key,
551 limit.scope_for(endpoint, rule_method),
545552 )
546553 else:
547554 limiter.limiter.clear(
548555 limit.limit,
549 *limit.args_for(endpoint, key, method),
556 key,
557 limit.scope_for(endpoint, method),
550558 )
551559 node.add(
552560 f"{render_limit(limit)}: [success]Cleared[/success]"
1818 DEFAULT_LIMITS_COST = "RATELIMIT_DEFAULTS_COST"
1919 STRATEGY = "RATELIMIT_STRATEGY"
2020 STORAGE_URI = "RATELIMIT_STORAGE_URI"
21 STORAGE_URL = "RATELIMIT_STORAGE_URL" # Deprecated due to inconsistency.
2221 STORAGE_OPTIONS = "RATELIMIT_STORAGE_OPTIONS"
2322 HEADERS_ENABLED = "RATELIMIT_HEADERS_ENABLED"
2423 HEADER_LIMIT = "RATELIMIT_HEADER_LIMIT"
99 class RateLimitExceeded(exceptions.TooManyRequests):
1010 """Exception raised when a rate limit is hit."""
1111
12 def __init__(self, limit: Limit, response: Optional[Response]) -> None:
12 def __init__(self, limit: Limit, response: Optional[Response] = None) -> None:
1313 """
1414 :param limit: The actual rate limit that was hit.
1515 Used to construct the default response message
00 from __future__ import annotations
11
22 import dataclasses
3 import traceback
34 import warnings
5 from types import TracebackType
6
7 from ordered_set import OrderedSet
8
9 from .util import get_qualified_name
410
511 """
612 Flask-Limiter Extension
1218 import weakref
1319 from collections import defaultdict
1420 from functools import partial, wraps
15 from typing import overload
21 from typing import Type, overload
1622
1723 import flask
1824 import flask.wrappers
4450
4551 @dataclasses.dataclass
4652 class LimiterContext:
47 rate_limiting_complete: bool = False
53 rate_limiting_complete: dict[str, bool] = dataclasses.field(default_factory=dict)
4854 view_rate_limit: Optional[RequestLimit] = None
4955 view_rate_limits: List[RequestLimit] = dataclasses.field(default_factory=list)
5056 conditional_deductions: Dict[Limit, List[str]] = dataclasses.field(
5157 default_factory=dict
5258 )
59 seen_limits: OrderedSet[Limit] = dataclasses.field(default_factory=OrderedSet)
5360
5461 def reset(self) -> None:
55 self.rate_limiting_complete = False
62 self.rate_limiting_complete.clear()
5663 self.view_rate_limit = None
5764 self.view_rate_limits.clear()
5865 self.conditional_deductions.clear()
6269 """
6370 The :class:`Limiter` class initializes the Flask-Limiter extension.
6471
65 :param app: :class:`flask.Flask` instance to initialize the extension with.
6672 :param key_func: a callable that returns the domain to rate limit
6773 by.
74 :param app: :class:`flask.Flask` instance to initialize the extension with.
6875 :param default_limits: a variable list of strings or callables
6976 returning strings denoting default limits to apply to all routes.
7077 :ref:`ratelimit-string` for more details.
96103 upon instantiation.
97104 :param auto_check: whether to automatically check the rate limit in
98105 the before_request chain of the application. default ``True``
99 :param swallow_errors: whether to swallow errors when hitting a rate
106 :param swallow_errors: whether to swallow any errors when hitting a rate
100107 limit. An exception will still be logged. default ``False``
101108 :param fail_on_first_breach: whether to stop processing remaining limits
102109 after the first breach. default ``True``
118125
119126 def __init__(
120127 self,
128 key_func: Callable[[], str],
129 *,
121130 app: Optional[flask.Flask] = None,
122 key_func: Optional[Callable[[], str]] = None,
123131 default_limits: Optional[List[Union[str, Callable[[], str]]]] = None,
124132 default_limits_per_method: Optional[bool] = None,
125133 default_limits_exempt_when: Optional[Callable[[], bool]] = None,
179187 self._fail_on_first_breach = fail_on_first_breach
180188 self._on_breach = on_breach
181189
182 # No longer optional
183 assert key_func
184
185190 self._key_func = key_func
186191 self._key_prefix = key_prefix
187192
188193 _default_limits = (
189194 [
190195 LimitGroup(
191 limit,
192 self._key_func,
193 None,
194 False,
195 None,
196 None,
197 None,
198 None,
199 None,
200 None,
201 1,
196 limit_provider=limit,
197 key_function=self._key_func,
202198 )
203199 for limit in default_limits
204200 ]
209205 _application_limits = (
210206 [
211207 LimitGroup(
212 limit,
213 self._key_func,
214 "global",
215 False,
216 None,
217 None,
218 None,
219 None,
220 None,
221 None,
222 1,
208 limit_provider=limit,
209 key_function=self._key_func,
210 scope="global",
211 shared=True,
223212 )
224213 for limit in application_limits
225214 ]
231220 for limit in in_memory_fallback:
232221 self._in_memory_fallback.append(
233222 LimitGroup(
234 limit,
235 self._key_func,
236 None,
237 False,
238 None,
239 None,
240 None,
241 None,
242 None,
243 None,
244 1,
223 limit_provider=limit,
224 key_function=self._key_func,
245225 )
246226 )
247227
259239 self.limit_manager = LimitManager(
260240 application_limits=_application_limits,
261241 default_limits=_default_limits,
262 static_route_limits={},
263 dynamic_route_limits={},
264 static_blueprint_limits={},
265 dynamic_blueprint_limits={},
242 decorated_limits={},
243 blueprint_limits={},
266244 route_exemptions=self._route_exemptions,
267245 blueprint_exemptions=self._blueprint_exemptions,
268246 )
308286 self._headers_enabled = bool(config.get(ConfigVars.HEADERS_ENABLED, False))
309287
310288 self._storage_options.update(config.get(ConfigVars.STORAGE_OPTIONS, {}))
311 storage_uri_from_config = config.get(
312 ConfigVars.STORAGE_URI, config.get(ConfigVars.STORAGE_URL, None)
313 )
289 storage_uri_from_config = config.get(ConfigVars.STORAGE_URI, None)
314290 if not storage_uri_from_config:
315291 if not self._storage_uri:
316292 warnings.warn(
371347 self.limit_manager.set_application_limits(
372348 [
373349 LimitGroup(
374 app_limits,
375 self._key_func,
376 "global",
377 False,
378 None,
379 None,
380 None,
381 None,
382 None,
383 None,
384 self._application_limits_cost,
350 limit_provider=app_limits,
351 key_function=self._key_func,
352 scope="global",
353 shared=True,
354 cost=self._application_limits_cost,
385355 )
386356 ]
387357 )
397367 self.limit_manager.set_default_limits(
398368 [
399369 LimitGroup(
400 conf_limits,
401 self._key_func,
402 None,
403 self._default_limits_per_method,
404 None,
405 None,
406 self._default_limits_exempt_when,
407 None,
408 self._default_limits_deduct_when,
409 None,
410 self._default_limits_cost,
370 limit_provider=conf_limits,
371 key_function=self._key_func,
372 per_method=self._default_limits_per_method,
373 exempt_when=self._default_limits_exempt_when,
374 deduct_when=self._default_limits_deduct_when,
375 cost=self._default_limits_cost,
411376 )
412377 ]
413378 )
421386 self.limit_manager.set_default_limits(default_limit_groups)
422387 self.__configure_fallbacks(app, self._strategy)
423388
424 # purely for backward compatibility as stated in flask documentation
425 if not hasattr(app, "extensions"):
426 app.extensions = {} # pragma: no cover
427
428389 if self not in app.extensions.setdefault("limiter", set()):
429390 if self._auto_check:
430391 app.before_request(self._check_request_limit)
447408 def limit(
448409 self,
449410 limit_value: Union[str, Callable[[], str]],
411 *,
450412 key_func: Optional[Callable[[], str]] = None,
451413 per_method: bool = False,
452414 methods: Optional[List[str]] = None,
458420 Callable[[RequestLimit], Optional[flask.wrappers.Response]]
459421 ] = None,
460422 cost: Union[int, Callable[[], int]] = 1,
423 scope: Optional[Union[str, Callable[[str], str]]] = None,
461424 ) -> LimitDecorator:
462425 """
463 decorator to be used for rate limiting individual routes or blueprints.
426 Decorator to be used for rate limiting individual routes or blueprints.
464427
465428 :param limit_value: rate limit string or a callable that returns a
466429 string. :ref:`ratelimit-string` for more details.
491454 raised.
492455 :param cost: The cost of a hit or a function that
493456 takes no parameters and returns the cost as an integer (Default: ``1``).
457 :param scope: a string or callable that returns a string
458 for further categorizing the rate limiting scope. This scope is combined
459 with the current endpoint of the request.
460
461
462 Changes
463 - .. versionadded:: 2.9.0 The returned object can also be used as a context manager
464 for rate limiting a code block inside a view. For example::
465
466 @app.route("/")
467 def route():
468 try:
469 with limiter.limit("10/second"):
470 # something expensive
471 except RateLimitExceeded: pass
494472 """
495473
496474 return LimitDecorator(
497475 self,
498476 limit_value,
499477 key_func,
478 False,
479 scope,
500480 per_method=per_method,
501481 methods=methods,
502482 error_message=error_message,
511491 self,
512492 limit_value: Union[str, Callable[[], str]],
513493 scope: Union[str, Callable[[str], str]],
494 *,
514495 key_func: Optional[Callable[[], str]] = None,
496 per_method: bool = False,
497 methods: Optional[List[str]] = None,
515498 error_message: Optional[str] = None,
516499 exempt_when: Optional[Callable[[], bool]] = None,
517500 override_defaults: bool = True,
531514 :param key_func: function/lambda to extract the unique
532515 identifier for the rate limit. defaults to remote address of the
533516 request.
517 :param per_method: whether the limit is sub categorized into the
518 http method of the request.
519 :param methods: if specified, only the methods in this list will
520 be rate limited (default: ``None``).
534521 :param error_message: string (or callable that returns one) to override
535522 the error message used in the response.
536523 :param function exempt_when: function/lambda used to decide if the rate
558545 key_func,
559546 True,
560547 scope,
548 per_method=per_method,
549 methods=methods,
561550 error_message=error_message,
562551 exempt_when=exempt_when,
563552 override_defaults=override_defaults,
647636 if isinstance(obj, flask.Blueprint):
648637 self.limit_manager.add_blueprint_exemption(obj.name, flags)
649638 elif obj:
650 self.limit_manager.add_route_exemption(
651 f"{obj.__module__}.{obj.__name__}", flags
652 )
639 self.limit_manager.add_route_exemption(get_qualified_name(obj), flags)
653640 else:
654641 _R = TypeVar("_R")
655642 _WO = TypeVar("_WO", Callable[..., _R], flask.Blueprint)
680667 if not self._in_memory_fallback and fallback_limits:
681668 self._in_memory_fallback = [
682669 LimitGroup(
683 fallback_limits,
684 self._key_func,
685 None,
686 False,
687 None,
688 None,
689 None,
690 None,
691 None,
692 None,
693 1,
670 limit_provider=fallback_limits,
671 key_function=self._key_func,
672 scope=None,
673 per_method=False,
674 cost=1,
694675 )
695676 ]
696677
724705
725706 :raises: RateLimitExceeded
726707 """
727 self._check_request_limit(False)
708 self._check_request_limit(in_middleware=False)
728709
729710 def reset(self) -> None:
730711 """
796777 return self.context.view_rate_limits
797778
798779 def __check_conditional_deductions(self, response: flask.wrappers.Response) -> None:
799
800780 for lim, args in self.context.conditional_deductions.items():
801781 if lim.deduct_when and lim.deduct_when(response):
802 self.limiter.hit(lim.limit, *args, cost=lim.cost)
782 try:
783 self.limiter.hit(lim.limit, *args, cost=lim.cost)
784 except Exception as err:
785 if self._swallow_errors:
786 self.logger.exception(
787 "Failed to deduct rate limit. " "Swallowing error"
788 )
789 else:
790 raise err
803791
804792 def __inject_headers(
805793 self, response: flask.wrappers.Response
867855
868856 return response
869857
870 def __check_all_limits_exempt(self, endpoint: Optional[str]) -> bool:
858 def __check_all_limits_exempt(
859 self, endpoint: Optional[str], callable_name: Optional[str] = None
860 ) -> bool:
871861 return bool(
872862 not endpoint
873863 or not (self.enabled and self.initialized)
874864 or endpoint == "static"
875865 or any(fn() for fn in self._request_filters)
876 or self.context.rate_limiting_complete
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 )
877871 )
878872
879873 def __filter_limits(
880874 self,
881875 endpoint: Optional[str],
882876 blueprint: Optional[str],
877 callable_name: Optional[str],
883878 in_middleware: bool = False,
884879 ) -> List[Limit]:
885 view_func = flask.current_app.view_functions.get(endpoint or "", None)
886 name = f"{view_func.__module__}.{view_func.__name__}" if view_func else ""
887
888 if self.__check_all_limits_exempt(endpoint):
880
881 if callable_name:
882 name = callable_name
883 else:
884 view_func = flask.current_app.view_functions.get(endpoint or "", None)
885 name = get_qualified_name(view_func) if view_func else ""
886
887 if self.__check_all_limits_exempt(endpoint, callable_name):
889888 return []
890889
891 marked_for_limiting = name in self._marked_for_limiting
890 marked_for_limiting = (
891 name in self._marked_for_limiting
892 or self.limit_manager.has_hints(endpoint or "")
893 )
892894 fallback_limits = []
893895
894896 if self._storage_dead and self._fallback_limiter:
905907 self.__check_backend_count = 0
906908 else:
907909 fallback_limits = list(itertools.chain(*self._in_memory_fallback))
908
909 return self.limit_manager.resolve_limits(
910 resolved_limits = self.limit_manager.resolve_limits(
910911 flask.current_app,
911912 endpoint,
912913 blueprint,
914 name,
913915 in_middleware,
914916 marked_for_limiting,
915917 fallback_limits,
916918 )
919 limits = OrderedSet(resolved_limits) - self.context.seen_limits
920 self.context.seen_limits.update(limits)
921 return list(limits)
917922
918923 def __evaluate_limits(self, endpoint: str, limits: List[Limit]) -> None:
919924 failed_limits: List[Tuple[Limit, List[str]]] = []
920925 limit_for_header: Optional[RequestLimit] = None
921926 view_limits: List[RequestLimit] = []
922927 for lim in sorted(limits, key=lambda x: x.limit):
923 limit_scope = lim.scope or endpoint
924
925928 if lim.is_exempt or lim.method_exempt:
926929 continue
927930
928 if lim.per_method:
929 limit_scope += f":{flask.request.method}"
931 limit_scope = lim.scope_for(endpoint, flask.request.method)
930932 limit_key = lim.key_func()
931933 args = [limit_key, limit_scope]
932934 kwargs = {}
954956 view_limits.append(RequestLimit(self, lim.limit, args, False))
955957
956958 if not method(lim.limit, *args, **kwargs):
957 self.logger.warning(
959 self.logger.info(
958960 "ratelimit %s (%s) exceeded at endpoint: %s",
959961 lim.limit,
960962 limit_key,
979981 if isinstance(cb_response, flask.wrappers.Response):
980982 on_breach_response = cb_response
981983 except Exception as err: # noqa
982 self.logger.warning(
983 "on_breach callback failed with error %s", err
984 )
985
984 if self._swallow_errors:
985 self.logger.exception(
986 "on_breach callback failed with error %s", err
987 )
988 else:
989 raise err
986990 if failed_limits:
987991 raise RateLimitExceeded(
988992 sorted(failed_limits, key=lambda x: x[0].limit)[0][0],
989993 response=on_breach_response,
990994 )
991995
992 def _check_request_limit(self, in_middleware: bool = True) -> None:
996 def _check_request_limit(
997 self, callable_name: Optional[str] = None, in_middleware: bool = True
998 ) -> None:
993999 endpoint = flask.request.endpoint or ""
9941000 try:
9951001 all_limits = self.__filter_limits(
996 flask.request.endpoint, flask.request.blueprint, in_middleware
1002 flask.request.endpoint,
1003 flask.request.blueprint,
1004 callable_name,
1005 in_middleware,
9971006 )
9981007 self.__evaluate_limits(endpoint, all_limits)
9991008 except Exception as e:
10061015 " in-memory storage"
10071016 )
10081017 self._storage_dead = True
1009 self._check_request_limit(in_middleware)
1018 self.context.seen_limits.clear()
1019 self._check_request_limit(
1020 callable_name=callable_name, in_middleware=in_middleware
1021 )
10101022 else:
10111023 if self._swallow_errors:
10121024 self.logger.exception("Failed to rate limit. Swallowing error")
10461058 self.limiter: weakref.ProxyType[Limiter] = weakref.proxy(limiter)
10471059 self.limit_value = limit_value
10481060 self.key_func = key_func or self.limiter._key_func
1049 self.scope = scope if shared else None
1061 self.scope = scope
10501062 self.per_method = per_method
1051 self.methods = methods
1063 self.methods = tuple(methods) if methods else None
10521064 self.error_message = error_message
10531065 self.exempt_when = exempt_when
10541066 self.override_defaults = override_defaults
10551067 self.deduct_when = deduct_when
10561068 self.on_breach = on_breach
10571069 self.cost = cost
1070 self.is_static = not callable(self.limit_value)
1071 self.shared = shared
1072
1073 @property
1074 def limit_group(self) -> LimitGroup:
1075 return LimitGroup(
1076 limit_provider=self.limit_value,
1077 key_function=self.key_func,
1078 scope=self.scope,
1079 per_method=self.per_method,
1080 methods=self.methods,
1081 error_message=self.error_message,
1082 exempt_when=self.exempt_when,
1083 override_defaults=self.override_defaults,
1084 deduct_when=self.deduct_when,
1085 on_breach=self.on_breach,
1086 cost=self.cost,
1087 shared=self.shared,
1088 )
1089
1090 def __enter__(self) -> None:
1091 tb = traceback.extract_stack(limit=2)
1092 qualified_location = f"{tb[0].filename}:{tb[0].name}:{tb[0].lineno}"
1093
1094 # TODO: if use as a context manager becomes interesting/valuable
1095 # a less hacky approach than using the traceback and piggy backing
1096 # on the limit manager's knowledge of decorated limits might be worth it.
1097 self.limiter.limit_manager.add_decorated_limit(
1098 qualified_location, self.limit_group, override=True
1099 )
1100
1101 self.limiter.limit_manager.add_endpoint_hint(
1102 flask.request.endpoint, qualified_location
1103 )
1104
1105 self.limiter._check_request_limit(
1106 in_middleware=False, callable_name=qualified_location
1107 )
1108
1109 def __exit__(
1110 self,
1111 exc_type: Optional[Type[BaseException]],
1112 exc_value: Optional[BaseException],
1113 traceback: Optional[TracebackType],
1114 ) -> None:
1115 ...
10581116
10591117 @overload
10601118 def __call__(self, obj: Callable[P, R]) -> Callable[P, R]:
10671125 def __call__(
10681126 self, obj: Union[Callable[P, R], flask.Blueprint]
10691127 ) -> Optional[Callable[P, R]]:
1070 is_route = not isinstance(obj, flask.Blueprint)
10711128 if isinstance(obj, flask.Blueprint):
10721129 name = obj.name
10731130 else:
1074 name = f"{obj.__module__}.{obj.__name__}"
1075
1076 dynamic_limit, static_limits = None, []
1077
1078 if callable(self.limit_value):
1079 dynamic_limit = LimitGroup(
1080 self.limit_value,
1081 self.key_func,
1082 self.scope,
1083 self.per_method,
1084 self.methods,
1085 self.error_message,
1086 self.exempt_when,
1087 self.override_defaults,
1088 self.deduct_when,
1089 self.on_breach,
1090 self.cost,
1091 )
1092 else:
1093 try:
1094 static_limits = list(
1095 LimitGroup(
1096 self.limit_value,
1097 self.key_func,
1098 self.scope,
1099 self.per_method,
1100 self.methods,
1101 self.error_message,
1102 self.exempt_when,
1103 self.override_defaults,
1104 self.deduct_when,
1105 self.on_breach,
1106 self.cost,
1107 )
1108 )
1109 except ValueError as e:
1110 self.limiter.logger.error(
1111 "failed to configure %s %s (%s)",
1112 "view function" if is_route else "blueprint",
1113 name,
1114 e,
1115 )
1131 name = get_qualified_name(obj)
11161132
11171133 if isinstance(obj, flask.Blueprint):
1118 if dynamic_limit:
1119 self.limiter.limit_manager.add_runtime_blueprint_limits(
1120 name, dynamic_limit
1121 )
1122 else:
1123 self.limiter.limit_manager.add_static_blueprint_limits(
1124 name, *static_limits
1125 )
1134 self.limiter.limit_manager.add_blueprint_limit(name, self.limit_group)
11261135 return None
11271136 else:
11281137 self.limiter._marked_for_limiting.add(name)
1129
1130 if dynamic_limit:
1131 self.limiter.limit_manager.add_runtime_route_limits(name, dynamic_limit)
1132 else:
1133 self.limiter.limit_manager.add_static_route_limits(name, *static_limits)
1138 self.limiter.limit_manager.add_decorated_limit(name, self.limit_group)
11341139
11351140 @wraps(obj)
11361141 def __inner(*a: P.args, **k: P.kwargs) -> R:
11371142 if (
11381143 self.limiter._auto_check
1139 and not self.limiter.context.rate_limiting_complete
1144 and not self.limiter.context.rate_limiting_complete.get(name, False)
11401145 ):
1141 self.limiter._check_request_limit(False)
1142 self.limiter.context.rate_limiting_complete = True
1146 if flask.request.endpoint:
1147 view_func = flask.current_app.view_functions.get(
1148 flask.request.endpoint, None
1149 )
1150 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 )
1154
1155 self.limiter._check_request_limit(
1156 in_middleware=False, callable_name=name
1157 )
1158
1159 self.limiter.context.rate_limiting_complete[name] = True
11431160 return cast(
11441161 R, flask.current_app.ensure_sync(cast(Callable[P, R], obj))(*a, **k)
11451162 )
44 from typing import Dict, Iterable, List, Optional, Tuple
55
66 import flask
7 from ordered_set import OrderedSet
78
89 from .constants import ExemptionScope
10 from .util import get_qualified_name
911 from .wrappers import Limit, LimitGroup
1012
1113
1416 self,
1517 application_limits: List[LimitGroup],
1618 default_limits: List[LimitGroup],
17 static_route_limits: Dict[str, List[Limit]],
18 dynamic_route_limits: Dict[str, List[LimitGroup]],
19 static_blueprint_limits: Dict[str, List[Limit]],
20 dynamic_blueprint_limits: Dict[str, List[LimitGroup]],
19 decorated_limits: Dict[str, OrderedSet[LimitGroup]],
20 blueprint_limits: Dict[str, OrderedSet[LimitGroup]],
2121 route_exemptions: Dict[str, ExemptionScope],
2222 blueprint_exemptions: Dict[str, ExemptionScope],
2323 ) -> None:
2424 self._application_limits = application_limits
2525 self._default_limits = default_limits
26 self._static_route_limits = static_route_limits
27 self._runtime_route_limits = dynamic_route_limits
28 self._static_blueprint_limits = static_blueprint_limits
29 self._runtime_blueprint_limits = dynamic_blueprint_limits
26 self._decorated_limits = decorated_limits
27 self._blueprint_limits = blueprint_limits
3028 self._route_exemptions = route_exemptions
3129 self._blueprint_exemptions = blueprint_exemptions
30 self._endpoint_hints: Dict[str, OrderedSet[str]] = {}
3231 self._logger = logging.getLogger("flask-limiter")
3332
3433 @property
4544 def set_default_limits(self, limits: List[LimitGroup]) -> None:
4645 self._default_limits = limits
4746
48 def add_runtime_route_limits(self, route: str, limit: LimitGroup) -> None:
49 self._runtime_route_limits.setdefault(route, []).append(limit)
50
51 def add_runtime_blueprint_limits(self, blueprint: str, limit: LimitGroup) -> None:
52 self._runtime_blueprint_limits.setdefault(blueprint, []).append(limit)
53
54 def add_static_route_limits(self, route: str, *limits: Limit) -> None:
55 self._static_route_limits.setdefault(route, []).extend(limits)
56
57 def add_static_blueprint_limits(self, blueprint: str, *limits: Limit) -> None:
58 self._static_blueprint_limits.setdefault(blueprint, []).extend(limits)
47 def add_decorated_limit(
48 self, route: str, limit: Optional[LimitGroup], override: bool = False
49 ) -> None:
50 if limit:
51 if not override:
52 self._decorated_limits.setdefault(route, OrderedSet()).add(limit)
53 else:
54 self._decorated_limits[route] = OrderedSet([limit])
55
56 def add_blueprint_limit(self, blueprint: str, limit: Optional[LimitGroup]) -> None:
57 if limit:
58 self._blueprint_limits.setdefault(blueprint, OrderedSet()).add(limit)
5959
6060 def add_route_exemption(self, route: str, scope: ExemptionScope) -> None:
6161 self._route_exemptions[route] = scope
6262
6363 def add_blueprint_exemption(self, blueprint: str, scope: ExemptionScope) -> None:
6464 self._blueprint_exemptions[blueprint] = scope
65
66 def add_endpoint_hint(self, endpoint: str, callable: str) -> None:
67 self._endpoint_hints.setdefault(endpoint, OrderedSet()).add(callable)
68
69 def has_hints(self, endpoint: str) -> bool:
70 return bool(self._endpoint_hints.get(endpoint))
6571
6672 def resolve_limits(
6773 self,
6874 app: flask.Flask,
6975 endpoint: Optional[str] = None,
7076 blueprint: Optional[str] = None,
77 callable_name: Optional[str] = None,
7178 in_middleware: bool = False,
7279 marked_for_limiting: bool = False,
7380 fallback_limits: Optional[List[Limit]] = None,
7481 ) -> List[Limit]:
7582 before_request_context = in_middleware and marked_for_limiting
76 route_limits = []
77 if not in_middleware and endpoint:
78 route_limits.extend(self.route_limits(app, endpoint))
83 decorated_limits = []
84 hinted_limits = []
85 if endpoint:
86 if not in_middleware:
87 if not callable_name:
88 view_func = app.view_functions.get(endpoint, None)
89 name = get_qualified_name(view_func) if view_func else ""
90 else:
91 name = callable_name
92 decorated_limits.extend(self.decorated_limits(name))
93
94 for hint in self._endpoint_hints.get(endpoint, OrderedSet()):
95 hinted_limits.extend(self.decorated_limits(hint))
96
7997 if blueprint:
8098 if not before_request_context and (
81 not route_limits
82 or all(not limit.override_defaults for limit in route_limits)
99 not decorated_limits
100 or all(not limit.override_defaults for limit in decorated_limits)
83101 ):
84 route_limits.extend(self.blueprint_limits(app, blueprint))
102 decorated_limits.extend(self.blueprint_limits(app, blueprint))
85103 exemption_scope = self.exemption_scope(app, endpoint, blueprint)
86104 all_limits = []
87105
94112 if in_middleware and not (exemption_scope & ExemptionScope.APPLICATION)
95113 else []
96114 )
97 all_limits += route_limits
98 explicit_limits_exempt = all(limit.method_exempt for limit in route_limits)
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.
99123 combined_defaults = all(
100 not limit.override_defaults for limit in route_limits
101 )
102
103 if (explicit_limits_exempt or combined_defaults) and not (
104 before_request_context or exemption_scope & ExemptionScope.DEFAULT
105 ):
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:
106144 all_limits += self.default_limits
107145 return all_limits
108146
110148 self, app: flask.Flask, endpoint: Optional[str], blueprint: Optional[str]
111149 ) -> ExemptionScope:
112150 view_func = app.view_functions.get(endpoint or "", None)
113 name = f"{view_func.__module__}.{view_func.__name__}" if view_func else ""
151 name = get_qualified_name(view_func) if view_func else ""
114152 route_exemption_scope = self._route_exemptions[name]
115153 blueprint_instance = app.blueprints.get(blueprint) if blueprint else None
116154
131169 blueprint_exemption_scope |= exemption
132170 return route_exemption_scope | blueprint_exemption_scope
133171
134 def route_limits(self, app: flask.Flask, endpoint: str) -> List[Limit]:
135 view_func = app.view_functions.get(endpoint, None)
136 name = f"{view_func.__module__}.{view_func.__name__}" if view_func else ""
137
172 def decorated_limits(self, callable_name: str) -> List[Limit]:
138173 limits = []
139 if not self._route_exemptions[name]:
140 for limit in self._static_route_limits.get(name, []):
141 limits.append(limit)
142
143 if name in self._runtime_route_limits:
144 for group in self._runtime_route_limits[name]:
174 if not self._route_exemptions[callable_name]:
175 if callable_name in self._decorated_limits:
176 for group in self._decorated_limits[callable_name]:
145177 try:
146178 for limit in group:
147179 limits.append(limit)
148180 except ValueError as e:
149181 self._logger.error(
150 f"failed to load ratelimit for view function {name}: {e}",
182 f"failed to load ratelimit for function {callable_name}: {e}",
151183 )
152184 return limits
153185
165197
166198 if not (
167199 self_exemption & ~(ExemptionScope.DEFAULT | ExemptionScope.APPLICATION)
168 or ancestor_exemptions
169200 ):
170 blueprint_self_dynamic_limits = self._runtime_blueprint_limits.get(
171 blueprint_name, []
201 blueprint_self_limits = self._blueprint_limits.get(
202 blueprint_name, OrderedSet()
172203 )
173 blueprint_dynamic_limits: Iterable[LimitGroup] = (
204 blueprint_limits: Iterable[LimitGroup] = (
174205 itertools.chain(
175206 *(
176 self._runtime_blueprint_limits.get(member, [])
207 self._blueprint_limits.get(member, [])
177208 for member in blueprint_ancestory.intersection(
178 self._runtime_blueprint_limits
179 )
209 self._blueprint_limits
210 ).difference(ancestor_exemptions)
180211 )
181212 )
182213 if not (
183 blueprint_self_dynamic_limits
184 or all(
185 limit.override_defaults
186 for limit in blueprint_self_dynamic_limits
214 blueprint_self_limits
215 and all(
216 limit.override_defaults for limit in blueprint_self_limits
187217 )
188218 )
189219 and not self._blueprint_exemptions[blueprint_name]
190220 & ExemptionScope.ANCESTORS
191 else blueprint_self_dynamic_limits
221 else blueprint_self_limits
192222 )
193 if blueprint_dynamic_limits:
194 for limit_group in blueprint_dynamic_limits:
223 if blueprint_limits:
224 for limit_group in blueprint_limits:
195225 try:
196226 limits.extend(
197227 [
207237 limit.deduct_when,
208238 limit.on_breach,
209239 limit.cost,
240 limit.shared,
210241 )
211242 for limit in limit_group
212243 ]
215246 self._logger.error(
216247 f"failed to load ratelimit for blueprint {blueprint_name}: {e}",
217248 )
218 blueprint_self_limits = self._static_blueprint_limits.get(
219 blueprint_name, []
220 )
221 if (
222 not (
223 blueprint_self_limits
224 and all(limit.override_defaults for limit in blueprint_self_limits)
225 )
226 and not self._blueprint_exemptions[blueprint_name]
227 & ExemptionScope.ANCESTORS
228 ):
229 for member in blueprint_ancestory.intersection(
230 self._static_blueprint_limits
231 ).difference(ancestor_exemptions):
232 limits.extend(self._static_blueprint_limits[member])
233 else:
234 limits.extend(blueprint_self_limits)
235249 return limits
236250
237251 def _blueprint_exemption_scope(
00 from __future__ import annotations
1
2 from typing import Any, Callable
13
24 from flask import request
35
911
1012 """
1113 return request.remote_addr or "127.0.0.1"
14
15
16 def get_qualified_name(callable: Callable[..., Any]) -> str:
17 """
18 Generate the fully qualified name of a callable for use in storing
19 mappings of decorated functions to rate limits
20
21 The __qualname__ of the callable is appended in case there is a name
22 clash in a module due to locally scoped functions that are decorated.
23
24 TODO: Ideally __qualname__ should be enough, however view functions
25 generated by class based views do not update that and therefore
26 would not be uniquely identifiable unless __module__ & __name__
27 are inspected.
28
29 :meta private:
30 """
31 return f"{callable.__module__}.{callable.__name__}.{callable.__qualname__}"
00 from __future__ import annotations
11
2 import dataclasses
23 import typing
34 import weakref
4 from typing import Callable, Iterator, List, Optional, Sequence, Tuple, Union
5 from typing import Callable, Iterator, List, Optional, Tuple, Union
56
67 from flask import request
78 from flask.wrappers import Response
6465 return self.window[1]
6566
6667
68 @dataclasses.dataclass(eq=True, unsafe_hash=True)
6769 class Limit:
6870 """
6971 simple wrapper to encapsulate limits and their context
7072 """
7173
72 def __init__(
73 self,
74 limit: RateLimitItem,
75 key_func: Callable[[], str],
76 scope: Optional[Union[str, Callable[[str], str]]],
77 per_method: bool,
78 methods: Optional[Sequence[str]],
79 error_message: Optional[str],
80 exempt_when: Optional[Callable[[], bool]],
81 override_defaults: Optional[bool],
82 deduct_when: Optional[Callable[[Response], bool]],
83 on_breach: Optional[Callable[[RequestLimit], Optional[Response]]],
84 cost: Union[Callable[[], int], int],
85 ) -> None:
86 self.limit = limit
87 self.key_func = key_func
88 self.__scope = scope
89 self.per_method = per_method
90 self.methods = methods
91 self.error_message = error_message
92 self.exempt_when = exempt_when
93 self.override_defaults = override_defaults
94 self.deduct_when = deduct_when
95 self.on_breach = on_breach
96 self._cost = cost
74 limit: RateLimitItem
75 key_func: Callable[[], str]
76 _scope: Optional[Union[str, Callable[[str], str]]]
77 per_method: bool = False
78 methods: Optional[Tuple[str, ...]] = None
79 error_message: Optional[str] = None
80 exempt_when: Optional[Callable[[], bool]] = None
81 override_defaults: Optional[bool] = False
82 deduct_when: Optional[Callable[[Response], bool]] = None
83 on_breach: Optional[Callable[[RequestLimit], Optional[Response]]] = None
84 _cost: Union[Callable[[], int], int] = 1
85 shared: bool = False
86
87 def __post_init__(self) -> None:
88 if self.methods:
89 self.methods = tuple([k.lower() for k in self.methods])
9790
9891 @property
9992 def is_exempt(self) -> bool:
10699 @property
107100 def scope(self) -> Optional[str]:
108101 return (
109 self.__scope(request.endpoint or "")
110 if callable(self.__scope)
111 else self.__scope
102 self._scope(request.endpoint or "")
103 if callable(self._scope)
104 else self._scope
112105 )
113106
114107 @property
124117
125118 return self.methods is not None and request.method.lower() not in self.methods
126119
127 def args_for(self, endpoint: str, key: str, method: Optional[str]) -> List[str]:
128 scope = self.scope or endpoint
120 def scope_for(self, endpoint: str, method: Optional[str]) -> str:
121 """
122 Derive final bucket (scope) for this limit given the endpoint
123 and request method. If the limit is shared between multiple
124 routes, the scope does not include the endpoint.
125 """
126 limit_scope = self.scope
127 if limit_scope:
128 if self.shared:
129 scope = limit_scope
130 else:
131 scope = f"{endpoint}:{limit_scope}"
132 else:
133 scope = endpoint
134
129135 if self.per_method:
130136 assert method
131137 scope += f":{method.upper()}"
132 args = [key, scope]
133 return args
138 return scope
134139
135140
141 @dataclasses.dataclass(eq=True, unsafe_hash=True)
136142 class LimitGroup:
137143 """
138144 represents a group of related limits either from a string or a callable
139145 that returns one
140146 """
141147
142 def __init__(
143 self,
144 limit_provider: Union[Callable[[], str], str],
145 key_function: Callable[[], str],
146 scope: Optional[Union[str, Callable[[str], str]]],
147 per_method: bool,
148 methods: Optional[Sequence[str]],
149 error_message: Optional[str],
150 exempt_when: Optional[Callable[[], bool]],
151 override_defaults: Optional[bool],
152 deduct_when: Optional[Callable[[Response], bool]],
153 on_breach: Optional[Callable[[RequestLimit], Optional[Response]]],
154 cost: Optional[Union[Callable[[], int], int]],
155 ) -> None:
156 self.__limit_provider = limit_provider
157 self.__scope = scope
158 self.key_function = key_function
159 self.per_method = per_method
160 self.methods = methods and [m.lower() for m in methods] or methods
161 self.error_message = error_message
162 self.exempt_when = exempt_when
163 self.override_defaults = override_defaults
164 self.deduct_when = deduct_when
165 self.on_breach = on_breach
166 self.cost = cost or 1
148 limit_provider: Union[Callable[[], str], str]
149 key_function: Callable[[], str]
150 scope: Optional[Union[str, Callable[[str], str]]] = None
151 methods: Optional[Tuple[str, ...]] = None
152 error_message: Optional[str] = None
153 exempt_when: Optional[Callable[[], bool]] = None
154 override_defaults: Optional[bool] = False
155 deduct_when: Optional[Callable[[Response], bool]] = None
156 on_breach: Optional[Callable[[RequestLimit], Optional[Response]]] = None
157 per_method: bool = False
158 cost: Optional[Union[Callable[[], int], int]] = None
159 shared: bool = False
167160
168161 def __iter__(self) -> Iterator[Limit]:
169 limit_items = parse_many(
170 self.__limit_provider()
171 if callable(self.__limit_provider)
172 else self.__limit_provider
162 limit_str = (
163 self.limit_provider()
164 if callable(self.limit_provider)
165 else self.limit_provider
173166 )
167 limit_items = parse_many(limit_str) if limit_str else []
174168
175169 for limit in limit_items:
176170 yield Limit(
177171 limit,
178172 self.key_function,
179 self.__scope,
173 self.scope,
180174 self.per_method,
181175 self.methods,
182176 self.error_message,
184178 self.override_defaults,
185179 self.deduct_when,
186180 self.on_breach,
187 self.cost,
181 self.cost or 1,
182 self.shared,
188183 )
00 -r main.txt
11 enum-tools[sphinx]==0.9.0.post1
2 furo==2022.6.21
2 furo==2022.12.7
33 Sphinx>4,<6
44 sphinx-autobuild==2021.3.14
5 sphinx-copybutton==0.5.0
5 sphinx-copybutton==0.5.1
66 sphinx-inline-tabs==2022.1.2b11
77 sphinx-issues==3.0.1
8 sphinxext-opengraph==0.6.3
8 sphinxext-opengraph==0.7.4
99 sphinx-paramlinks==0.5.4
1010 sphinxcontrib-programoutput==0.17
1111
0 limits>=2.3
0 limits>=2.8
11 Flask>=2
2 ordered-set>4,<5
23 rich>=12,<13
3 typing_extensions
4 typing_extensions>=4
1010 pymongo
1111
1212 # For the tests themselves
13 coverage
13 coverage<8
1414 hiro>0.1.6
1515 pytest
1616 pytest-cov
5151
5252 for k, v in config.items():
5353 app.config.setdefault(k, v)
54 limiter_args.setdefault("key_func", get_remote_address)
55 limiter = Limiter(app, **limiter_args)
54 key_func = limiter_args.pop("key_func", get_remote_address)
55 limiter = Limiter(key_func, app=app, **limiter_args)
5656
5757 return app, limiter
5858
116116
117117
118118 @pytest.fixture(scope="session")
119 def docker_services_project_name():
120 return "flask-limiter"
121
122
123 @pytest.fixture(scope="session")
119124 def docker_compose_files(pytestconfig):
120125 return ["docker-compose.yml"]
00 import datetime
1 import logging
12
23 import hiro
34 from flask import Blueprint, Flask, current_app
492493
493494
494495 def test_invalid_decorated_static_limit_blueprint(caplog):
496 caplog.set_level(logging.INFO)
495497 app = Flask(__name__)
496 limiter = Limiter(app, default_limits=["1/second"], key_func=get_remote_address)
498 limiter = Limiter(get_remote_address, app=app, default_limits=["1/second"])
497499 bp = Blueprint("bp1", __name__)
498500
499501 @bp.route("/t1")
507509 with hiro.Timeline().freeze():
508510 assert cli.get("/t1").status_code == 200
509511 assert cli.get("/t1").status_code == 429
510 assert "failed to configure" in caplog.records[0].msg
511 assert "exceeded at endpoint" in caplog.records[1].msg
512 assert "failed to load" in caplog.records[0].msg
513 assert "exceeded at endpoint" in caplog.records[-1].msg
512514
513515
514516 def test_invalid_decorated_dynamic_limits_blueprint(caplog):
517 caplog.set_level(logging.INFO)
515518 app = Flask(__name__)
516519 app.config.setdefault("X", "2 per sec")
517 limiter = Limiter(app, default_limits=["1/second"], key_func=get_remote_address)
520 limiter = Limiter(get_remote_address, app=app, default_limits=["1/second"])
518521 bp = Blueprint("bp1", __name__)
519522
520523 @bp.route("/t1")
1616 app = Flask(__name__)
1717 app.config.setdefault(ConfigVars.STRATEGY, "fubar")
1818 with pytest.raises(ConfigurationError):
19 Limiter(app, key_func=get_remote_address)
19 Limiter(get_remote_address, app=app)
2020
2121
2222 def test_invalid_storage_string():
2323 app = Flask(__name__)
2424 app.config.setdefault(ConfigVars.STORAGE_URI, "fubar://localhost:1234")
2525 with pytest.raises(ConfigurationError):
26 Limiter(app, key_func=get_remote_address)
26 Limiter(get_remote_address, app=app)
2727
2828
2929 def test_constructor_arguments_over_config(redis_connection):
3030 app = Flask(__name__)
3131 app.config.setdefault(ConfigVars.STRATEGY, "fixed-window-elastic-expiry")
32 limiter = Limiter(strategy="moving-window", key_func=get_remote_address)
32 limiter = Limiter(get_remote_address, strategy="moving-window")
3333 limiter.init_app(app)
3434 app.config.setdefault(ConfigVars.STORAGE_URI, "redis://localhost:46379")
3535 app.config.setdefault(ConfigVars.APPLICATION_LIMITS, "1/minute")
3636 assert type(limiter._limiter) == MovingWindowRateLimiter
37 limiter = Limiter(
38 storage_uri="memcached://localhost:31211", key_func=get_remote_address
39 )
37 limiter = Limiter(get_remote_address, storage_uri="memcached://localhost:31211")
4038 limiter.init_app(app)
4139 assert type(limiter._storage) == MemcachedStorage
4240
4745 app.config.setdefault(ConfigVars.HEADER_REMAINING, "XX-Remaining")
4846 app.config.setdefault(ConfigVars.HEADER_RESET, "XX-Reset")
4947 limiter = Limiter(
50 key_func=get_remote_address, headers_enabled=True, default_limits=["1/second"]
48 get_remote_address, headers_enabled=True, default_limits=["1/second"]
5149 )
5250 limiter.init_app(app)
5351
6563 def test_header_names_constructor():
6664 app = Flask(__name__)
6765 limiter = Limiter(
68 key_func=get_remote_address,
66 get_remote_address,
6967 headers_enabled=True,
7068 default_limits=["1/second"],
7169 header_name_mapping={
9290 app.config.setdefault(ConfigVars.ENABLED, False)
9391 app.config.setdefault(ConfigVars.STORAGE_URI, "fubar://")
9492
95 limiter = Limiter(app, key_func=get_remote_address, default_limits=["1/hour"])
93 limiter = Limiter(get_remote_address, app=app, default_limits=["1/hour"])
9694
9795 @app.route("/")
9896 def root():
113111
114112 def test_uninitialized_limiter():
115113 app = Flask(__name__)
116 limiter = Limiter(key_func=get_remote_address, default_limits=["1/hour"])
114 limiter = Limiter(get_remote_address, default_limits=["1/hour"])
117115
118116 @app.route("/")
119117 @limiter.limit("2/hour")
0 import hiro
1
2 from flask_limiter import RateLimitExceeded
3
4
5 def test_static_limit(extension_factory):
6 app, limiter = extension_factory()
7
8 @app.route("/t1")
9 def t1():
10 with limiter.limit("1/second"):
11 resp = "ok"
12 try:
13 with limiter.limit("1/day"):
14 resp += "maybe"
15 except RateLimitExceeded:
16 pass
17 finally:
18 return resp
19
20 with hiro.Timeline().freeze() as timeline:
21 with app.test_client() as cli:
22 response = cli.get("/t1")
23 assert 200 == response.status_code
24 assert "okmaybe" == response.text
25 assert 429 == cli.get("/t1").status_code
26 timeline.forward(1)
27 response = cli.get("/t1")
28 assert 200 == response.status_code
29 assert "ok" == response.text
30
31
32 def test_dynamic_limits(extension_factory):
33 app, limiter = extension_factory()
34
35 @app.route("/t1")
36 def t1():
37 with limiter.limit(lambda: "1/second"):
38 return "test"
39
40 with hiro.Timeline().freeze():
41 with app.test_client() as cli:
42 assert 200 == cli.get("/t1").status_code
43 assert 429 == cli.get("/t1").status_code
44
45
46 def test_scoped_context_manager(extension_factory):
47 app, limiter = extension_factory()
48
49 @app.route("/t1/<int:param>")
50 def t1(param: int):
51 with limiter.limit("1/second", scope=param):
52 return "p1"
53
54 with hiro.Timeline().freeze():
55 with app.test_client() as cli:
56 assert 200 == cli.get("/t1/1").status_code
57 assert 429 == cli.get("/t1/1").status_code
58 assert 200 == cli.get("/t1/2").status_code
59 assert 429 == cli.get("/t1/2").status_code
00 import asyncio
1 import logging
12 from functools import wraps
23 from unittest import mock
34
67 from werkzeug.exceptions import BadRequest
78
89 from flask_limiter import ExemptionScope, Limiter
10 from flask_limiter.util import get_remote_address
911
1012
1113 def get_ip_from_header():
1719
1820 @app.route("/t1")
1921 @limiter.limit(
20 "100 per minute", lambda: "test"
22 "100 per minute", key_func=lambda: "test"
2123 ) # effectively becomes a limit for all users
2224 @limiter.limit("50/minute") # per ip as per default key_func
2325 def t1():
7072 assert cli.get("/t3").status_code == 429
7173 # 2/minute for application is now taken up
7274 assert cli.get("/t4").status_code == 429
75
76
77 def test_decorated_limit_with_scope(extension_factory):
78 app, limiter = extension_factory()
79
80 @app.route("/t/<path:path>")
81 @limiter.limit("1/second", scope=lambda _: request.view_args["path"])
82 def t(path):
83 return "test"
84
85 with hiro.Timeline():
86 with app.test_client() as cli:
87 assert cli.get("/t/1").status_code == 200
88 assert cli.get("/t/1").status_code == 429
89 assert cli.get("/t/2").status_code == 200
90 assert cli.get("/t/2").status_code == 429
7391
7492
7593 def test_decorated_limit_with_conditional_deduction(extension_factory):
291309
292310
293311 def test_invalid_decorated_dynamic_limits(caplog):
312 caplog.set_level(logging.INFO)
294313 app = Flask(__name__)
295314 app.config.setdefault("X", "2 per sec")
296 limiter = Limiter(app, default_limits=["1/second"], key_func=get_ip_from_header)
315 limiter = Limiter(get_ip_from_header, app=app, default_limits=["1/second"])
297316
298317 @app.route("/t1")
299318 @limiter.limit(lambda: current_app.config.get("X"))
309328 assert "failed to load ratelimit" in caplog.records[0].msg
310329 assert "failed to load ratelimit" in caplog.records[1].msg
311330 assert "exceeded at endpoint" in caplog.records[2].msg
312 assert caplog.records[2].levelname == "WARNING"
331 assert caplog.records[2].levelname == "INFO"
332
333
334 def test_decorated_limit_empty_exempt(caplog):
335 app = Flask(__name__)
336 limiter = Limiter(get_remote_address, app=app)
337
338 @app.route("/t1")
339 @limiter.limit(lambda: "")
340 def t1():
341 return "42"
342
343 with app.test_client() as cli:
344 with hiro.Timeline().freeze():
345 assert cli.get("/t1").status_code == 200
346 assert cli.get("/t1").status_code == 200
347
348 assert not caplog.records
313349
314350
315351 def test_invalid_decorated_static_limits(caplog):
352 caplog.set_level(logging.INFO)
316353 app = Flask(__name__)
317 limiter = Limiter(app, default_limits=["1/second"], key_func=get_ip_from_header)
354 limiter = Limiter(get_ip_from_header, app=app, default_limits=["1/second"])
318355
319356 @app.route("/t1")
320357 @limiter.limit("2/sec")
325362 with hiro.Timeline().freeze():
326363 assert cli.get("/t1").status_code == 200
327364 assert cli.get("/t1").status_code == 429
328 assert "failed to configure" in caplog.records[0].msg
329 assert "exceeded at endpoint" in caplog.records[1].msg
365 assert "failed to load" in caplog.records[0].msg
366 assert "exceeded at endpoint" in caplog.records[-1].msg
330367
331368
332369 def test_named_shared_limit(extension_factory):
396433 def test_conditional_limits():
397434 """Test that the conditional activation of the limits work."""
398435 app = Flask(__name__)
399 limiter = Limiter(app, key_func=get_ip_from_header)
436 limiter = Limiter(get_ip_from_header, app=app)
400437
401438 @app.route("/limited")
402439 @limiter.limit("1 per day")
433470 def test_conditional_shared_limits():
434471 """Test that conditional shared limits work."""
435472 app = Flask(__name__)
436 limiter = Limiter(app, key_func=get_ip_from_header)
473 limiter = Limiter(get_ip_from_header, app=app)
437474
438475 @app.route("/limited")
439476 @limiter.shared_limit("1 per day", "test_scope")
470507
471508 app = Flask(__name__)
472509 limiter = Limiter(
473 app,
510 get_ip_from_header,
511 app=app,
474512 default_limits=["1/minute"],
475513 headers_enabled=True,
476 key_func=get_ip_from_header,
477514 )
478515
479516 @app.route("/")
614651 assert cli.get("/t2").status_code == 200
615652
616653
617 def test_on_breach_callback(extension_factory, caplog):
618 app, limiter = extension_factory()
654 def test_on_breach_callback_swallow_errors(extension_factory, caplog):
655 app, limiter = extension_factory(swallow_errors=True)
619656
620657 callbacks = []
621658
649686 assert cli.get("/fail").status_code == 429
650687
651688 assert len(callbacks) == 1
652 assert (
653 caplog.records[-1].message
654 == "on_breach callback failed with error division by zero"
655 )
689
690 log = caplog.records[-1]
691 assert log.message == "on_breach callback failed with error division by zero"
692 assert log.levelname == "ERROR"
656693
657694
658695 def test_on_breach_callback_custom_response(extension_factory):
669706 f"default custom response {request_limit.limit} @ {request.path}", 429
670707 )
671708
709 def on_breach_invalid():
710 ...
711
712 def on_breach_fail(request_limit):
713 1 / 0
714
672715 app, limiter = extension_factory(on_breach=default_on_breach_with_response)
673716
674717 @app.route("/")
676719 def root():
677720 return "root"
678721
679 @app.route("/other")
722 @app.route("/t1")
680723 @limiter.limit("1/second")
681 def other():
682 return "other"
683
684 @app.route("/fail")
724 def t1():
725 return "t1"
726
727 @app.route("/t2")
685728 @limiter.limit("1/second", on_breach=on_breach_with_response)
686 def fail():
687 return "fail"
729 def t2():
730 return "t2"
731
732 @app.route("/t3")
733 @limiter.limit("1/second", on_breach=on_breach_invalid)
734 def t3():
735 return "t3"
736
737 @app.route("/t4")
738 @limiter.limit("1/second", on_breach=on_breach_fail)
739 def t4():
740 return "t4"
688741
689742 with app.test_client() as cli:
690743 assert cli.get("/").status_code == 200
691744 resp = cli.get("/")
692745 assert resp.status_code == 429
693746 assert resp.text == "default custom response 1 per 1 second @ /"
694 assert cli.get("/other").status_code == 200
695 resp = cli.get("/other")
747 assert cli.get("/t1").status_code == 200
748 resp = cli.get("/t1")
696749 assert resp.status_code == 429
697 assert resp.text == "default custom response 1 per 1 second @ /other"
698 assert cli.get("/fail").status_code == 200
699 resp = cli.get("/fail")
750 assert resp.text == "default custom response 1 per 1 second @ /t1"
751 assert cli.get("/t2").status_code == 200
752 resp = cli.get("/t2")
700753 assert resp.status_code == 429
701 assert resp.text == "custom response 1 per 1 second @ /fail"
754 assert resp.text == "custom response 1 per 1 second @ /t2"
755 resp = cli.get("/t3")
756 assert resp.status_code == 200
757 resp = cli.get("/t3")
758 assert resp.status_code == 500
759 resp = cli.get("/t4")
760 assert resp.status_code == 200
761 resp = cli.get("/t4")
762 assert resp.status_code == 500
702763
703764
704765 def test_limit_multiple_cost(extension_factory):
771832 assert 200 == cli.get("/t1").status_code
772833 assert 200 == cli.get("/t2").status_code
773834 assert 429 == cli.get("/t2").status_code
835
836
837 def test_non_route_decoration_static_limits_override_defaults(extension_factory):
838 app, limiter = extension_factory(default_limits=["1/second"])
839
840 @limiter.limit("2/second")
841 def limited():
842 return "limited"
843
844 @app.route("/t1")
845 def route1():
846 return "t1"
847
848 @app.route("/t2")
849 @limiter.limit("2/second")
850 def route2():
851 return "t2"
852
853 @app.route("/t3")
854 def route3():
855 return limited()
856
857 @app.route("/t4")
858 def route4():
859 @limiter.limit("2/day", override_defaults=False)
860 def __inner():
861 return "inner"
862
863 return __inner()
864
865 @app.route("/t5/<int:param>")
866 def route5(param: int):
867 @limiter.limit("2/day", override_defaults=False)
868 def __inner1():
869 return "inner1"
870
871 @limiter.limit("3/day")
872 def __inner2():
873 return "inner2"
874
875 return __inner1() if param < 10 else __inner2()
876
877 with hiro.Timeline().freeze() as timeline:
878 with app.test_client() as cli:
879 assert 200 == cli.get("/t1").status_code
880 assert 429 == cli.get("/t1").status_code
881 for i in range(2):
882 assert 200 == cli.get("/t2").status_code
883 assert 429 == cli.get("/t2").status_code
884 for i in range(2):
885 assert 200 == cli.get("/t3").status_code, i
886 assert 429 == cli.get("/t3").status_code
887 assert 200 == cli.get("/t4").status_code
888 assert 429 == cli.get("/t4").status_code
889 timeline.forward(1)
890 assert 200 == cli.get("/t4").status_code
891 timeline.forward(1)
892 assert 429 == cli.get("/t4").status_code
893 assert 200 == cli.get("/t5/1").status_code
894 assert 429 == cli.get("/t5/1").status_code
895 timeline.forward(1)
896 assert 200 == cli.get("/t5/1").status_code
897 timeline.forward(1)
898 assert 429 == cli.get("/t5/1").status_code
899 timeline.forward(60 * 60 * 24)
900 assert 200 == cli.get("/t5/11").status_code
901 assert 200 == cli.get("/t5/11").status_code
902 assert 200 == cli.get("/t5/11").status_code
903 assert 429 == cli.get("/t5/11").status_code
904
905
906 def test_non_route_decoration_static_limits(extension_factory):
907 app, limiter = extension_factory()
908
909 @limiter.limit("1/second")
910 def limited():
911 return "limited"
912
913 @app.route("/t1")
914 def route1():
915 return limited()
916
917 with hiro.Timeline().freeze():
918 with app.test_client() as cli:
919 assert 200 == cli.get("/t1").status_code
920 assert 429 == cli.get("/t1").status_code
921
922
923 def test_non_route_decoration_dynamic_limits(extension_factory):
924 app, limiter = extension_factory()
925
926 def dynamic_limit_provider():
927 return "1/second"
928
929 @limiter.limit(dynamic_limit_provider)
930 def limited():
931 return "limited"
932
933 @app.route("/t1")
934 def route1():
935 return limited()
936
937 with hiro.Timeline().freeze():
938 with app.test_client() as cli:
939 assert 200 == cli.get("/t1").status_code
940 assert 429 == cli.get("/t1").status_code
941
942
943 def test_non_route_decoration_multiple_sequential_limits_per_request(extension_factory):
944 app, limiter = extension_factory()
945
946 @limiter.limit("10/second")
947 def l1():
948 return "l1"
949
950 @limiter.limit("1/second")
951 def l2():
952 return "l2"
953
954 @app.route("/t1")
955 def route1():
956 return l1() + l2()
957
958 with hiro.Timeline().freeze():
959 with app.test_client() as cli:
960 assert 200 == cli.get("/t1").status_code
961 assert 429 == cli.get("/t1").status_code
962
963
964 def test_inner_function_decoration(extension_factory):
965 app, limiter = extension_factory()
966
967 @app.route("/t1")
968 def route1():
969 @limiter.limit("5/second")
970 def l1():
971 return "l1"
972
973 return l1()
974
975 @app.route("/t2")
976 def route2():
977 @limiter.limit("1/second")
978 def l1():
979 return "l1"
980
981 return l1()
982
983 with hiro.Timeline().freeze():
984 with app.test_client() as cli:
985 assert 200 == cli.get("/t1").status_code
986 assert 200 == cli.get("/t2").status_code
987 for _ in range(4):
988 assert 200 == cli.get("/t1").status_code
989 assert 429 == cli.get("/t1").status_code
990 assert 429 == cli.get("/t2").status_code
107107 assert "ok" in cli.get("/").data.decode()
108108
109109
110 def test_swallow_error_conditional_deduction(extension_factory):
111 def conditional_deduct(_):
112 return True
113
114 app, limiter = extension_factory(
115 {
116 ConfigVars.DEFAULT_LIMITS: "1 per day",
117 ConfigVars.SWALLOW_ERRORS: True,
118 ConfigVars.DEFAULT_LIMITS_DEDUCT_WHEN: conditional_deduct,
119 }
120 )
121
122 @app.route("/")
123 def null():
124 return "ok"
125
126 with app.test_client() as cli:
127 with patch("limits.strategies.FixedWindowRateLimiter.hit") as hit:
128
129 def raiser(*a, **k):
130 raise Exception
131
132 hit.side_effect = raiser
133 assert "ok" in cli.get("/").data.decode()
134
135
110136 def test_no_swallow_error(extension_factory):
111137 app, limiter = extension_factory(
112138 {ConfigVars.DEFAULT_LIMITS: "1 per day", ConfigVars.HEADERS_ENABLED: True}
136162 get_window_stats.side_effect = raiser
137163 assert 500 == cli.get("/").status_code
138164 assert "underlying" == cli.get("/").data.decode()
165
166
167 def test_no_swallow_error_conditional_deduction(extension_factory):
168 def conditional_deduct(_):
169 return True
170
171 app, limiter = extension_factory(
172 {
173 ConfigVars.DEFAULT_LIMITS: "1 per day",
174 ConfigVars.SWALLOW_ERRORS: False,
175 ConfigVars.DEFAULT_LIMITS_DEDUCT_WHEN: conditional_deduct,
176 }
177 )
178
179 @app.route("/")
180 def null():
181 return "ok"
182
183 with app.test_client() as cli:
184 with patch("limits.strategies.FixedWindowRateLimiter.hit") as hit:
185
186 def raiser(*a, **k):
187 raise Exception
188
189 hit.side_effect = raiser
190 assert 500 == cli.get("/").status_code
139191
140192
141193 def test_fallback_to_memory_config(redis_connection, extension_factory):
189189 app, limiter = extension_factory()
190190
191191 @app.route("/t1")
192 @limiter.limit("100 per minute", lambda: "test")
192 @limiter.limit("100 per minute", key_func=lambda: "test")
193193 def t1():
194194 return "test"
195195
206206
207207
208208 def test_logging(caplog):
209 app = Flask(__name__)
210 limiter = Limiter(app, key_func=get_remote_address)
209 caplog.set_level(logging.INFO)
210 app = Flask(__name__)
211 limiter = Limiter(get_remote_address, app=app)
211212
212213 @app.route("/t1")
213214 @limiter.limit("1/minute")
218219 assert 200 == cli.get("/t1").status_code
219220 assert 429 == cli.get("/t1").status_code
220221 assert len(caplog.records) == 1
221 assert caplog.records[0].levelname == "WARNING"
222
223
224 def test_reuse_logging():
222 assert caplog.records[0].levelname == "INFO"
223
224
225 def test_reuse_logging(caplog):
226 caplog.set_level(logging.INFO)
225227 app = Flask(__name__)
226228 app_handler = mock.Mock()
227229 app_handler.level = logging.INFO
228230 app.logger.addHandler(app_handler)
229 limiter = Limiter(app, key_func=get_remote_address)
231 limiter = Limiter(get_remote_address, app=app)
230232
231233 for handler in app.logger.handlers:
232234 limiter.logger.addHandler(handler)
270272 app1 = Flask(__name__)
271273 app2 = Flask(__name__)
272274
273 limiter = Limiter(default_limits=["1/second"], key_func=get_remote_address)
275 limiter = Limiter(get_remote_address, default_limits=["1/second"])
274276 limiter.init_app(app1)
275277 limiter.init_app(app2)
276278
321323 def test_headers_no_breach():
322324 app = Flask(__name__)
323325 limiter = Limiter(
324 app,
326 get_remote_address,
327 app=app,
325328 default_limits=["10/minute"],
326329 headers_enabled=True,
327 key_func=get_remote_address,
328330 )
329331
330332 @app.route("/t1")
354356 def test_headers_breach():
355357 app = Flask(__name__)
356358 limiter = Limiter(
357 app,
359 get_remote_address,
360 app=app,
358361 default_limits=["10/minute"],
359362 headers_enabled=True,
360 key_func=get_remote_address,
361363 )
362364
363365 @app.route("/t1")
383385 def test_retry_after():
384386 app = Flask(__name__)
385387 _ = Limiter(
386 app,
388 get_remote_address,
389 app=app,
387390 default_limits=["1/minute"],
388391 headers_enabled=True,
389 key_func=get_remote_address,
390392 )
391393
392394 @app.route("/t1")
406408 def test_retry_after_exists_seconds():
407409 app = Flask(__name__)
408410 _ = Limiter(
409 app,
411 get_remote_address,
412 app=app,
410413 default_limits=["1/minute"],
411414 headers_enabled=True,
412 key_func=get_remote_address,
413415 )
414416
415417 @app.route("/t1")
426428 def test_retry_after_exists_rfc1123():
427429 app = Flask(__name__)
428430 _ = Limiter(
429 app,
431 get_remote_address,
432 app=app,
430433 default_limits=["1/minute"],
431434 headers_enabled=True,
432 key_func=get_remote_address,
433435 )
434436
435437 @app.route("/t1")
449451 app.config.setdefault(ConfigVars.HEADER_REMAINING, "X-Remaining")
450452 app.config.setdefault(ConfigVars.HEADER_RESET, "X-Reset")
451453 limiter = Limiter(
452 app,
454 get_remote_address,
455 app=app,
453456 default_limits=["10/minute"],
454457 headers_enabled=True,
455 key_func=get_remote_address,
456458 )
457459
458460 @app.route("/t1")
687689
688690 def test_second_instance_bypassed_by_shared_g():
689691 app = Flask(__name__)
690 limiter1 = Limiter(app, key_func=get_remote_address)
691
692 limiter2 = Limiter(app, key_func=get_remote_address)
692 limiter1 = Limiter(get_remote_address, app=app)
693
694 limiter2 = Limiter(get_remote_address, app=app)
693695
694696 @app.route("/test1")
695697 @limiter2.limit("1/second")
723725
724726 def test_independent_instances_by_key_prefix():
725727 app = Flask(__name__)
726 limiter1 = Limiter(app, key_prefix="lmt1", key_func=get_remote_address)
727
728 limiter2 = Limiter(app, key_prefix="lmt2", key_func=get_remote_address)
728 limiter1 = Limiter(get_remote_address, key_prefix="lmt1", app=app)
729
730 limiter2 = Limiter(get_remote_address, key_prefix="lmt2", app=app)
729731
730732 @app.route("/test1")
731733 @limiter2.limit("1/second")
765767
766768 def test_multiple_limiters_default_limits():
767769 app = Flask(__name__)
770 Limiter(get_remote_address, key_prefix="lmt1", app=app, default_limits=["1/second"])
768771 Limiter(
769 app, key_prefix="lmt1", default_limits=["1/second"], key_func=get_remote_address
770 )
771 Limiter(
772 app,
772 get_remote_address,
773773 key_prefix="lmt2",
774774 default_limits=["10/minute"],
775 key_func=get_remote_address,
775 app=app,
776776 )
777777
778778 @app.route("/test1")