Update upstream source from tag 'upstream/3.2.0'
Update to upstream version '3.2.0'
with Debian dir 41813551c659e4b67e78a807b1ad63233767feb0
Carsten Schoenert
1 year, 2 months ago
24 | 24 | run: | |
25 | 25 | python -m pip install --upgrade pip setuptools wheel |
26 | 26 | pip install -r requirements/ci.txt |
27 | - name: Lint with flake8 | |
27 | - name: Lint with ruff | |
28 | 28 | run: | |
29 | flake8 . --count --show-source --statistics | |
30 | flake8 . --count --exit-zero --max-complexity=10 | |
29 | ruff flask_limiter tests | |
31 | 30 | - name: Lint with black |
32 | 31 | run: | |
33 | 32 | black tests flask_limiter |
1 | 1 | |
2 | 2 | Changelog |
3 | 3 | ========= |
4 | ||
5 | v3.2.0 | |
6 | ------ | |
7 | Release Date: 2023-02-15 | |
8 | ||
9 | * Feature | |
10 | ||
11 | * Allow configuring request identity | |
12 | ||
13 | * Chores | |
14 | ||
15 | * Improve linting with ruff | |
16 | * Update development dependencies | |
17 | ||
4 | 18 | |
5 | 19 | v3.1.0 |
6 | 20 | ------ |
30 | 44 | |
31 | 45 | * Allow scoping regular limit decorators / context managers |
32 | 46 | |
47 | v3.2.0 | |
48 | ------ | |
49 | Release Date: 2023-02-15 | |
50 | ||
33 | 51 | v3.1.0 |
34 | 52 | ------ |
35 | 53 | Release Date: 2022-12-29 |
48 | 66 | |
49 | 67 | * Simplify registration of decorated function & blueprint limits |
50 | 68 | |
69 | ||
70 | v3.2.0 | |
71 | ------ | |
72 | Release Date: 2023-02-15 | |
51 | 73 | |
52 | 74 | v3.1.0 |
53 | 75 | ------ |
902 | 924 | |
903 | 925 | |
904 | 926 | |
927 |
0 | Copyright (c) 2022 Ali-Akber Saifee | |
0 | Copyright (c) 2023 Ali-Akber Saifee | |
1 | 1 | |
2 | 2 | Permission is hereby granted, free of charge, to any person obtaining a copy |
3 | 3 | of this software and associated documentation files (the "Software"), to deal |
0 | 0 | lint: |
1 | 1 | black --check tests flask_limiter |
2 | 2 | mypy flask_limiter |
3 | flake8 flask_limiter tests | |
3 | ruff flask_limiter tests | |
4 | 4 | |
5 | 5 | lint-fix: |
6 | 6 | black tests flask_limiter |
7 | 7 | mypy flask_limiter |
8 | 8 | isort -r --profile=black tests flask_limiter |
9 | autoflake8 -i -r tests flask_limiter | |
9 | ruff --fix flask_limiter tests |
100 | 100 | --------------------------------------------------- |
101 | 101 | .. code-block:: bash |
102 | 102 | |
103 | $ FLASK_APP=app:app flask limiter list | |
103 | $ FLASK_APP=app:app flask limiter limits | |
104 | 104 | |
105 | 105 | app |
106 | 106 | ├── fast: /fast |
11 | 11 | from theme_config import * |
12 | 12 | |
13 | 13 | description = "Flask-Limiter adds rate limiting to flask applications." |
14 | copyright = "2022, Ali-Akber Saifee" | |
14 | copyright = "2023, Ali-Akber Saifee" | |
15 | 15 | project = "Flask-Limiter" |
16 | 16 | |
17 | 17 | ahead = 0 |
102 | 102 | |
103 | 103 | - A dictionary to set extra options to be passed to the storage implementation |
104 | 104 | upon initialization. |
105 | * - .. data:: RATELIMIT_REQUEST_IDENTIFIER | |
106 | ||
107 | Constructor argument: :paramref:`~flask_limiter.Limiter.request_identifier` | |
108 | ||
109 | - A callable that returns the unique identity of the current request. Defaults to :attr:`flask.Request.endpoint` | |
105 | 110 | * - .. data:: RATELIMIT_STRATEGY |
106 | 111 | |
107 | 112 | Constructor argument: :paramref:`~flask_limiter.Limiter.strategy` |
295 | 295 | |
296 | 296 | |
297 | 297 | Decorators to declare rate limits |
298 | --------------------------------- | |
298 | ================================= | |
299 | 299 | Decorators made available as instance methods of the :class:`~flask_limiter.Limiter` |
300 | 300 | instance to be used with the :class:`flask.Flask` application. |
301 | 301 | |
302 | 302 | .. _ratelimit-decorator-limit: |
303 | 303 | |
304 | 304 | Route specific limits |
305 | ^^^^^^^^^^^^^^^^^^^^^ | |
305 | --------------------- | |
306 | 306 | |
307 | 307 | .. automethod:: Limiter.limit |
308 | 308 | :noindex: |
407 | 407 | .. _ratelimit-decorator-shared-limit: |
408 | 408 | |
409 | 409 | Reusable limits |
410 | ^^^^^^^^^^^^^^^ | |
410 | --------------- | |
411 | 411 | |
412 | 412 | For scenarios where a rate limit should be shared by multiple routes |
413 | 413 | (For example when you want to protect routes using the same resource |
32 | 32 | return gi.record_by_name(request.remote_addr)['region_name'] |
33 | 33 | |
34 | 34 | app = Flask(__name__) |
35 | limiter = Limiter(app, default_limits=["10/hour"], key_func = get_request_country) | |
35 | limiter = Limiter(get_request_country, app=app, default_limits=["10/hour"]) | |
36 | 36 | |
37 | 37 | |
38 | 38 |
23 | 23 | # setup.py/versioneer.py will grep for the variable names, so they must |
24 | 24 | # each be defined on a line of their own. _version.py will just call |
25 | 25 | # get_keywords(). |
26 | git_refnames = " (tag: 3.1.0, stable)" | |
27 | git_full = "745964acbc31048af1fedca09a1637df8278c760" | |
28 | git_date = "2022-12-29 11:54:32 -0800" | |
26 | git_refnames = " (HEAD -> master, tag: 3.2.0, stable)" | |
27 | git_full = "b764e15950729baf12820086f9713c47414990d1" | |
28 | git_date = "2023-02-15 16:29:02 -0800" | |
29 | 29 | keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} |
30 | 30 | return keywords |
31 | 31 |
0 | 0 | import time |
1 | 1 | from functools import partial |
2 | from typing import Any, Callable, Dict, Generator, List, Optional, Set, Union | |
2 | from typing import Any, Callable, Dict, Generator, List, Optional, Set, Tuple, Union | |
3 | 3 | from urllib.parse import urlparse |
4 | 4 | |
5 | 5 | import click |
98 | 98 | def render_limits( |
99 | 99 | app: Flask, |
100 | 100 | limiter: Limiter, |
101 | limits: List[Limit], | |
101 | limits: Tuple[List[Limit], ...], | |
102 | 102 | endpoint: Optional[str] = None, |
103 | 103 | blueprint: Optional[str] = None, |
104 | 104 | rule: Optional[Rule] = None, |
115 | 115 | renderable = Tree(label) |
116 | 116 | entries = [] |
117 | 117 | |
118 | for limit in limits: | |
118 | for limit in limits[0] + limits[1]: | |
119 | 119 | if endpoint: |
120 | 120 | view_func = app.view_functions.get(endpoint, None) |
121 | 121 | source = ( |
409 | 409 | yield render_limits( |
410 | 410 | current_app, |
411 | 411 | limiter, |
412 | limiter.limit_manager.application_limits, | |
412 | (limiter.limit_manager.application_limits, []), | |
413 | 413 | test=key, |
414 | 414 | method=method, |
415 | 415 | label="[gold3]Application Limits[/gold3]", |
471 | 471 | "Details", |
472 | 472 | { |
473 | 473 | "rule": Rule, |
474 | "limits": List[Limit], | |
474 | "limits": Tuple[List[Limit], ...], | |
475 | 475 | }, |
476 | 476 | ) |
477 | 477 | rule_limits: Dict[str, Details] = {} |
504 | 504 | render_limits( |
505 | 505 | current_app, |
506 | 506 | limiter, |
507 | application_limits, | |
507 | (application_limits, []), | |
508 | 508 | label="Application Limits", |
509 | 509 | test=key, |
510 | 510 | ) |
537 | 537 | for endpoint, details in rule_limits.items(): |
538 | 538 | if details["limits"]: |
539 | 539 | node = Tree(endpoint) |
540 | for limit in details["limits"]: | |
540 | default, decorated = details["limits"] | |
541 | for limit in default + decorated: | |
541 | 542 | if ( |
542 | 543 | limit.per_method |
543 | 544 | and details["rule"] |
16 | 16 | DEFAULT_LIMITS_EXEMPT_WHEN = "RATELIMIT_DEFAULTS_EXEMPT_WHEN" |
17 | 17 | DEFAULT_LIMITS_DEDUCT_WHEN = "RATELIMIT_DEFAULTS_DEDUCT_WHEN" |
18 | 18 | DEFAULT_LIMITS_COST = "RATELIMIT_DEFAULTS_COST" |
19 | REQUEST_IDENTIFIER = "RATELIMIT_REQUEST_IDENTIFIER" | |
19 | 20 | STRATEGY = "RATELIMIT_STRATEGY" |
20 | 21 | STORAGE_URI = "RATELIMIT_STORAGE_URI" |
21 | 22 | STORAGE_OPTIONS = "RATELIMIT_STORAGE_OPTIONS" |
0 | from __future__ import annotations | |
1 | ||
2 | import dataclasses | |
3 | import traceback | |
4 | import warnings | |
5 | from types import TracebackType | |
6 | ||
7 | from ordered_set import OrderedSet | |
8 | ||
9 | from .util import get_qualified_name | |
10 | ||
11 | 0 | """ |
12 | 1 | Flask-Limiter Extension |
13 | 2 | """ |
3 | from __future__ import annotations | |
4 | ||
5 | import dataclasses | |
14 | 6 | import datetime |
15 | 7 | import itertools |
16 | 8 | import logging |
17 | 9 | import time |
10 | import traceback | |
11 | import warnings | |
18 | 12 | import weakref |
19 | 13 | from collections import defaultdict |
20 | 14 | from functools import partial, wraps |
15 | from types import TracebackType | |
21 | 16 | from typing import Type, overload |
22 | 17 | |
23 | 18 | import flask |
25 | 20 | from limits.errors import ConfigurationError |
26 | 21 | from limits.storage import MemoryStorage, Storage, storage_from_string |
27 | 22 | from limits.strategies import STRATEGIES, RateLimiter |
23 | from ordered_set import OrderedSet | |
28 | 24 | from werkzeug.http import http_date, parse_date |
29 | 25 | |
30 | 26 | from ._compat import request_context |
45 | 41 | Union, |
46 | 42 | cast, |
47 | 43 | ) |
44 | from .util import get_qualified_name | |
48 | 45 | from .wrappers import Limit, LimitGroup, RequestLimit |
49 | 46 | |
50 | 47 | |
51 | 48 | @dataclasses.dataclass |
52 | 49 | class LimiterContext: |
53 | rate_limiting_complete: dict[str, bool] = dataclasses.field(default_factory=dict) | |
54 | 50 | view_rate_limit: Optional[RequestLimit] = None |
55 | 51 | view_rate_limits: List[RequestLimit] = dataclasses.field(default_factory=list) |
56 | 52 | conditional_deductions: Dict[Limit, List[str]] = dataclasses.field( |
59 | 55 | seen_limits: OrderedSet[Limit] = dataclasses.field(default_factory=OrderedSet) |
60 | 56 | |
61 | 57 | def reset(self) -> None: |
62 | self.rate_limiting_complete.clear() | |
63 | 58 | self.view_rate_limit = None |
64 | 59 | self.view_rate_limits.clear() |
65 | 60 | self.conditional_deductions.clear() |
61 | self.seen_limits.clear() | |
66 | 62 | |
67 | 63 | |
68 | 64 | class Limiter: |
120 | 116 | :param retry_after: Allows configuration of how the value of the |
121 | 117 | `Retry-After` header is rendered. One of `http-date` or `delta-seconds`. |
122 | 118 | :param key_prefix: prefix prepended to rate limiter keys and app context global names. |
119 | :param request_identifier: a callable that returns the unique identity the current request. | |
120 | Defaults to :attr:`flask.Request.endpoint` | |
123 | 121 | :param enabled: Whether the extension is enabled or not |
124 | 122 | """ |
125 | 123 | |
152 | 150 | in_memory_fallback_enabled: Optional[bool] = None, |
153 | 151 | retry_after: Optional[str] = None, |
154 | 152 | key_prefix: str = "", |
153 | request_identifier: Optional[Callable[..., str]] = None, | |
155 | 154 | enabled: bool = True, |
156 | 155 | ) -> None: |
157 | 156 | self.app = app |
189 | 188 | |
190 | 189 | self._key_func = key_func |
191 | 190 | self._key_prefix = key_prefix |
191 | self._request_identifier = request_identifier | |
192 | 192 | |
193 | 193 | _default_limits = ( |
194 | 194 | [ |
337 | 337 | ) |
338 | 338 | |
339 | 339 | self._key_prefix = self._key_prefix or config.get(ConfigVars.KEY_PREFIX, "") |
340 | ||
340 | self._request_identifier = self._request_identifier or config.get( | |
341 | ConfigVars.REQUEST_IDENTIFIER, lambda: flask.request.endpoint or "" | |
342 | ) | |
341 | 343 | app_limits = config.get(ConfigVars.APPLICATION_LIMITS, None) |
342 | 344 | self._application_limits_cost = self._application_limits_cost or config.get( |
343 | 345 | ConfigVars.APPLICATION_LIMITS_COST, 1 |
397 | 399 | |
398 | 400 | @property |
399 | 401 | def context(self) -> LimiterContext: |
402 | """ | |
403 | The context is meant to exist for the lifetime | |
404 | of a request/response cycle per instance of the extension | |
405 | so as to keep track of any state used at different steps | |
406 | in the lifecycle (for example to pass information | |
407 | from the before request hook to the after_request hook) | |
408 | ||
409 | :meta private: | |
410 | """ | |
400 | 411 | ctx = request_context() |
401 | 412 | if not hasattr(ctx, "_limiter_request_context"): |
402 | 413 | ctx._limiter_request_context = defaultdict(LimiterContext) # type: ignore |
403 | 414 | return cast( |
404 | Dict[str, LimiterContext], | |
415 | Dict[Limiter, LimiterContext], | |
405 | 416 | ctx._limiter_request_context, # type: ignore |
406 | )[self._key_prefix] | |
417 | )[self] | |
407 | 418 | |
408 | 419 | def limit( |
409 | 420 | self, |
776 | 787 | |
777 | 788 | return self.context.view_rate_limits |
778 | 789 | |
790 | def identify_request(self) -> str: | |
791 | """ | |
792 | Returns the identity of the request (by default this is the | |
793 | :attr:`flask.Request.endpoint` associated by the view function | |
794 | that is handling the request). The behavior can be customized | |
795 | by initializing the extension with a callable argument for | |
796 | :paramref:`~flask_limiter.Limiter.request_identifier`. | |
797 | """ | |
798 | if self.initialized and self.enabled: | |
799 | assert self._request_identifier | |
800 | return self._request_identifier() | |
801 | return "" | |
802 | ||
779 | 803 | def __check_conditional_deductions(self, response: flask.wrappers.Response) -> None: |
780 | 804 | for lim, args in self.context.conditional_deductions.items(): |
781 | 805 | if lim.deduct_when and lim.deduct_when(response): |
856 | 880 | return response |
857 | 881 | |
858 | 882 | def __check_all_limits_exempt( |
859 | self, endpoint: Optional[str], callable_name: Optional[str] = None | |
883 | self, | |
884 | endpoint: Optional[str], | |
860 | 885 | ) -> bool: |
861 | 886 | return bool( |
862 | 887 | not endpoint |
863 | 888 | or not (self.enabled and self.initialized) |
864 | 889 | or endpoint == "static" |
865 | 890 | or any(fn() for fn in self._request_filters) |
866 | or ( | |
867 | self.context.rate_limiting_complete.get(callable_name, False) | |
868 | if callable_name | |
869 | else any(self.context.rate_limiting_complete.values()) | |
870 | ) | |
871 | 891 | ) |
872 | 892 | |
873 | 893 | def __filter_limits( |
877 | 897 | callable_name: Optional[str], |
878 | 898 | in_middleware: bool = False, |
879 | 899 | ) -> List[Limit]: |
880 | ||
881 | 900 | if callable_name: |
882 | 901 | name = callable_name |
883 | 902 | else: |
884 | 903 | view_func = flask.current_app.view_functions.get(endpoint or "", None) |
885 | 904 | name = get_qualified_name(view_func) if view_func else "" |
886 | 905 | |
887 | if self.__check_all_limits_exempt(endpoint, callable_name): | |
906 | if self.__check_all_limits_exempt(endpoint): | |
888 | 907 | return [] |
889 | 908 | |
890 | 909 | marked_for_limiting = ( |
907 | 926 | self.__check_backend_count = 0 |
908 | 927 | else: |
909 | 928 | fallback_limits = list(itertools.chain(*self._in_memory_fallback)) |
910 | resolved_limits = self.limit_manager.resolve_limits( | |
929 | if fallback_limits: | |
930 | return fallback_limits | |
931 | ||
932 | defaults, decorated = self.limit_manager.resolve_limits( | |
911 | 933 | flask.current_app, |
912 | 934 | endpoint, |
913 | 935 | blueprint, |
914 | 936 | name, |
915 | 937 | in_middleware, |
916 | 938 | marked_for_limiting, |
917 | fallback_limits, | |
918 | ) | |
919 | limits = OrderedSet(resolved_limits) - self.context.seen_limits | |
920 | self.context.seen_limits.update(limits) | |
921 | return list(limits) | |
939 | ) | |
940 | limits = OrderedSet(defaults) - self.context.seen_limits | |
941 | self.context.seen_limits.update(defaults) | |
942 | return list(limits) + list(decorated) | |
922 | 943 | |
923 | 944 | def __evaluate_limits(self, endpoint: str, limits: List[Limit]) -> None: |
924 | 945 | failed_limits: List[Tuple[Limit, List[str]]] = [] |
996 | 1017 | def _check_request_limit( |
997 | 1018 | self, callable_name: Optional[str] = None, in_middleware: bool = True |
998 | 1019 | ) -> None: |
999 | endpoint = flask.request.endpoint or "" | |
1020 | endpoint = self.identify_request() | |
1000 | 1021 | try: |
1001 | 1022 | all_limits = self.__filter_limits( |
1002 | flask.request.endpoint, | |
1023 | endpoint, | |
1003 | 1024 | flask.request.blueprint, |
1004 | 1025 | callable_name, |
1005 | 1026 | in_middleware, |
1026 | 1047 | raise e |
1027 | 1048 | |
1028 | 1049 | def __release_context(self, _: Optional[BaseException] = None) -> None: |
1029 | if self.context: | |
1030 | self.context.reset() | |
1050 | self.context.reset() | |
1031 | 1051 | |
1032 | 1052 | |
1033 | 1053 | class LimitDecorator: |
1099 | 1119 | ) |
1100 | 1120 | |
1101 | 1121 | self.limiter.limit_manager.add_endpoint_hint( |
1102 | flask.request.endpoint, qualified_location | |
1122 | self.limiter.identify_request(), qualified_location | |
1103 | 1123 | ) |
1104 | 1124 | |
1105 | 1125 | self.limiter._check_request_limit( |
1141 | 1161 | def __inner(*a: P.args, **k: P.kwargs) -> R: |
1142 | 1162 | if ( |
1143 | 1163 | self.limiter._auto_check |
1144 | and not self.limiter.context.rate_limiting_complete.get(name, False) | |
1164 | and not getattr(obj, "__wrapper-limiter-instance", None) | |
1165 | == self.limiter | |
1145 | 1166 | ): |
1146 | if flask.request.endpoint: | |
1147 | view_func = flask.current_app.view_functions.get( | |
1148 | flask.request.endpoint, None | |
1149 | ) | |
1167 | identity = self.limiter.identify_request() | |
1168 | if identity: | |
1169 | view_func = flask.current_app.view_functions.get(identity, None) | |
1150 | 1170 | if view_func and not get_qualified_name(view_func) == name: |
1151 | self.limiter.limit_manager.add_endpoint_hint( | |
1152 | flask.request.endpoint, name | |
1153 | ) | |
1171 | self.limiter.limit_manager.add_endpoint_hint(identity, name) | |
1154 | 1172 | |
1155 | 1173 | self.limiter._check_request_limit( |
1156 | 1174 | in_middleware=False, callable_name=name |
1157 | 1175 | ) |
1158 | 1176 | |
1159 | self.limiter.context.rate_limiting_complete[name] = True | |
1160 | 1177 | return cast( |
1161 | 1178 | R, flask.current_app.ensure_sync(cast(Callable[P, R], obj))(*a, **k) |
1162 | 1179 | ) |
1163 | 1180 | |
1181 | # mark this wrapper as wrapped by a decorator from the limiter | |
1182 | # from which the decorator was created. This ensures that stacked | |
1183 | # decorations only trigger rate limiting from the inner most | |
1184 | # decorator from each limiter instance (the weird need for | |
1185 | # keeping track of the instance is to handle cases where multiple | |
1186 | # limiter extensions are registered on the same application). | |
1187 | setattr(__inner, "__wrapper-limiter-instance", self.limiter) | |
1164 | 1188 | return __inner |
77 | 77 | callable_name: Optional[str] = None, |
78 | 78 | in_middleware: bool = False, |
79 | 79 | marked_for_limiting: bool = False, |
80 | fallback_limits: Optional[List[Limit]] = None, | |
81 | ) -> List[Limit]: | |
80 | ) -> Tuple[List[Limit], ...]: | |
82 | 81 | before_request_context = in_middleware and marked_for_limiting |
83 | 82 | decorated_limits = [] |
84 | 83 | hinted_limits = [] |
101 | 100 | ): |
102 | 101 | decorated_limits.extend(self.blueprint_limits(app, blueprint)) |
103 | 102 | exemption_scope = self.exemption_scope(app, endpoint, blueprint) |
104 | all_limits = [] | |
105 | ||
106 | if fallback_limits: | |
107 | all_limits.extend(fallback_limits) | |
108 | ||
109 | if not all_limits: | |
110 | all_limits = ( | |
111 | self.application_limits | |
112 | if in_middleware and not (exemption_scope & ExemptionScope.APPLICATION) | |
113 | else [] | |
103 | ||
104 | all_limits = ( | |
105 | self.application_limits | |
106 | if in_middleware and not (exemption_scope & ExemptionScope.APPLICATION) | |
107 | else [] | |
108 | ) | |
109 | # all_limits += decorated_limits | |
110 | explicit_limits_exempt = all(limit.method_exempt for limit in decorated_limits) | |
111 | ||
112 | # all the decorated limits explicitly declared | |
113 | # that they don't override the defaults - so, they should | |
114 | # be included. | |
115 | combined_defaults = all( | |
116 | not limit.override_defaults for limit in decorated_limits | |
117 | ) | |
118 | # previous requests to this endpoint have exercised decorated | |
119 | # rate limits on callables that are not view functions. check | |
120 | # if all of them declared that they don't override defaults | |
121 | # and if so include the default limits. | |
122 | hinted_limits_request_defaults = ( | |
123 | all(not limit.override_defaults for limit in hinted_limits) | |
124 | if hinted_limits | |
125 | else False | |
126 | ) | |
127 | if ( | |
128 | (explicit_limits_exempt or combined_defaults) | |
129 | and ( | |
130 | not (before_request_context or exemption_scope & ExemptionScope.DEFAULT) | |
114 | 131 | ) |
115 | all_limits += decorated_limits | |
116 | explicit_limits_exempt = all( | |
117 | limit.method_exempt for limit in decorated_limits | |
118 | ) | |
119 | ||
120 | # all the decorated limits explicitly declared | |
121 | # that they don't override the defaults - so, they should | |
122 | # be included. | |
123 | combined_defaults = all( | |
124 | not limit.override_defaults for limit in decorated_limits | |
125 | ) | |
126 | # previous requests to this endpoint have exercised decorated | |
127 | # rate limits on callables that are not view functions. check | |
128 | # if all of them declared that they don't override defaults | |
129 | # and if so include the default limits. | |
130 | hinted_limits_request_defaults = ( | |
131 | all(not limit.override_defaults for limit in hinted_limits) | |
132 | if hinted_limits | |
133 | else False | |
134 | ) | |
135 | if ( | |
136 | (explicit_limits_exempt or combined_defaults) | |
137 | and ( | |
138 | not ( | |
139 | before_request_context | |
140 | or exemption_scope & ExemptionScope.DEFAULT | |
141 | ) | |
142 | ) | |
143 | ) or hinted_limits_request_defaults: | |
144 | all_limits += self.default_limits | |
145 | return all_limits | |
132 | ) or hinted_limits_request_defaults: | |
133 | all_limits += self.default_limits | |
134 | return all_limits, decorated_limits | |
146 | 135 | |
147 | 136 | def exemption_scope( |
148 | 137 | self, app: flask.Flask, endpoint: Optional[str], blueprint: Optional[str] |
0 | 0 | -r main.txt |
1 | 1 | enum-tools[sphinx]==0.9.0.post1 |
2 | 2 | furo==2022.12.7 |
3 | Sphinx>4,<6 | |
3 | Sphinx>4,<7 | |
4 | 4 | sphinx-autobuild==2021.3.14 |
5 | 5 | sphinx-copybutton==0.5.1 |
6 | 6 | sphinx-inline-tabs==2022.1.2b11 |
7 | 7 | sphinx-issues==3.0.1 |
8 | sphinxext-opengraph==0.7.4 | |
8 | sphinxext-opengraph==0.8.1 | |
9 | 9 | sphinx-paramlinks==0.5.4 |
10 | 10 | sphinxcontrib-programoutput==0.17 |
11 | 11 |
0 | 0 | limits>=2.8 |
1 | 1 | Flask>=2 |
2 | 2 | ordered-set>4,<5 |
3 | rich>=12,<13 | |
3 | rich>=12,<14 | |
4 | 4 | typing_extensions>=4 |
4 | 4 | """ |
5 | 5 | __author__ = "Ali-Akber Saifee" |
6 | 6 | __email__ = "ali@indydevs.org" |
7 | __copyright__ = "Copyright 2022, Ali-Akber Saifee" | |
7 | __copyright__ = "Copyright 2023, Ali-Akber Saifee" | |
8 | 8 | |
9 | 9 | from setuptools import setup, find_packages |
10 | 10 | import os |
504 | 504 | |
505 | 505 | |
506 | 506 | def test_whitelisting(): |
507 | ||
508 | 507 | app = Flask(__name__) |
509 | 508 | limiter = Limiter( |
510 | 509 | get_ip_from_header, |
594 | 593 | |
595 | 594 | |
596 | 595 | def test_decorated_shared_limit_immediate(extension_factory): |
597 | ||
598 | 596 | app, limiter = extension_factory(default_limits=["1/minute"]) |
599 | 597 | shared = limiter.shared_limit(lambda: g.rate_limit, "shared") |
600 | 598 |
151 | 151 | |
152 | 152 | with app.test_client() as cli: |
153 | 153 | with patch("limits.strategies.FixedWindowRateLimiter.hit") as hit: |
154 | ||
155 | 154 | hit.side_effect = raiser |
156 | 155 | assert 500 == cli.get("/").status_code |
157 | 156 | assert "underlying" == cli.get("/").data.decode() |
158 | 157 | with patch( |
159 | 158 | "limits.strategies.FixedWindowRateLimiter.get_window_stats" |
160 | 159 | ) as get_window_stats: |
161 | ||
162 | 160 | get_window_stats.side_effect = raiser |
163 | 161 | assert 500 == cli.get("/").status_code |
164 | 162 | assert "underlying" == cli.get("/").data.decode() |
516 | 516 | |
517 | 517 | |
518 | 518 | def test_callable_application_limit(extension_factory): |
519 | ||
520 | 519 | app, limiter = extension_factory( |
521 | 520 | application_limits=[ |
522 | 521 | lambda: request.headers.get("suspect", 0) and "1/minute" or "2/minute" |
687 | 686 | assert 429 == resp.status_code |
688 | 687 | |
689 | 688 | |
690 | def test_second_instance_bypassed_by_shared_g(): | |
689 | def test_multiple_instances_no_key_prefix(): | |
691 | 690 | app = Flask(__name__) |
692 | 691 | limiter1 = Limiter(get_remote_address, app=app) |
693 | 692 | |
707 | 706 | with hiro.Timeline().freeze() as timeline: |
708 | 707 | with app.test_client() as cli: |
709 | 708 | assert cli.get("/test1").status_code == 200 |
710 | assert cli.get("/test2").status_code == 200 | |
711 | 709 | assert cli.get("/test1").status_code == 429 |
712 | 710 | assert cli.get("/test2").status_code == 200 |
711 | assert cli.get("/test2").status_code == 429 | |
713 | 712 | |
714 | 713 | for i in range(8): |
715 | assert cli.get("/test1").status_code == 429 | |
714 | timeline.forward(1) | |
715 | assert cli.get("/test1").status_code == 200 | |
716 | 716 | assert cli.get("/test2").status_code == 200 |
717 | assert cli.get("/test2").status_code == 429 | |
718 | 717 | timeline.forward(1) |
719 | 718 | assert cli.get("/test1").status_code == 200 |
720 | 719 | assert cli.get("/test2").status_code == 429 |
721 | 720 | timeline.forward(59) |
722 | assert cli.get("/test1").status_code == 200 | |
723 | 721 | assert cli.get("/test2").status_code == 200 |
724 | 722 | |
725 | 723 |