limits 4.7.2__tar.gz → 4.7.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. {limits-4.7.2 → limits-4.7.3}/HISTORY.rst +13 -0
  2. {limits-4.7.2 → limits-4.7.3}/PKG-INFO +1 -1
  3. {limits-4.7.2 → limits-4.7.3}/doc/source/conf.py +10 -1
  4. {limits-4.7.2 → limits-4.7.3}/doc/source/ext/_static/benchmark-chart.css +48 -12
  5. limits-4.7.3/doc/source/ext/_static/js/benchmark-chart.js +343 -0
  6. {limits-4.7.2 → limits-4.7.3}/doc/source/ext/bench_chart.py +12 -9
  7. limits-4.7.3/doc/source/performance.rst +126 -0
  8. {limits-4.7.2 → limits-4.7.3}/limits/_version.py +3 -3
  9. {limits-4.7.2 → limits-4.7.3}/limits/aio/storage/memcached.py +4 -1
  10. {limits-4.7.2 → limits-4.7.3}/limits.egg-info/PKG-INFO +1 -1
  11. {limits-4.7.2 → limits-4.7.3}/requirements/docs.txt +3 -3
  12. limits-4.7.2/doc/source/ext/_static/js/benchmark-chart.js +0 -219
  13. limits-4.7.2/doc/source/performance.rst +0 -221
  14. {limits-4.7.2 → limits-4.7.3}/CLASSIFIERS +0 -0
  15. {limits-4.7.2 → limits-4.7.3}/CONTRIBUTIONS.rst +0 -0
  16. {limits-4.7.2 → limits-4.7.3}/LICENSE.txt +0 -0
  17. {limits-4.7.2 → limits-4.7.3}/MANIFEST.in +0 -0
  18. {limits-4.7.2 → limits-4.7.3}/README.rst +0 -0
  19. {limits-4.7.2 → limits-4.7.3}/doc/Makefile +0 -0
  20. {limits-4.7.2 → limits-4.7.3}/doc/source/_static/custom.css +0 -0
  21. {limits-4.7.2 → limits-4.7.3}/doc/source/api.rst +0 -0
  22. {limits-4.7.2 → limits-4.7.3}/doc/source/async.rst +0 -0
  23. {limits-4.7.2 → limits-4.7.3}/doc/source/changelog.rst +0 -0
  24. {limits-4.7.2 → limits-4.7.3}/doc/source/custom-storage.rst +0 -0
  25. {limits-4.7.2 → limits-4.7.3}/doc/source/ext/_static/js/benchmark-details.js +0 -0
  26. {limits-4.7.2 → limits-4.7.3}/doc/source/ext/_static/js/benchmark-loader.js +0 -0
  27. {limits-4.7.2 → limits-4.7.3}/doc/source/ext/_templates/git_info.js +0 -0
  28. {limits-4.7.2 → limits-4.7.3}/doc/source/index.rst +0 -0
  29. {limits-4.7.2 → limits-4.7.3}/doc/source/installation.rst +0 -0
  30. {limits-4.7.2 → limits-4.7.3}/doc/source/quickstart.rst +0 -0
  31. {limits-4.7.2 → limits-4.7.3}/doc/source/storage.rst +0 -0
  32. {limits-4.7.2 → limits-4.7.3}/doc/source/strategies.rst +0 -0
  33. {limits-4.7.2 → limits-4.7.3}/doc/source/theme_config.py +0 -0
  34. {limits-4.7.2 → limits-4.7.3}/limits/__init__.py +0 -0
  35. {limits-4.7.2 → limits-4.7.3}/limits/aio/__init__.py +0 -0
  36. {limits-4.7.2 → limits-4.7.3}/limits/aio/storage/__init__.py +0 -0
  37. {limits-4.7.2 → limits-4.7.3}/limits/aio/storage/base.py +0 -0
  38. {limits-4.7.2 → limits-4.7.3}/limits/aio/storage/etcd.py +0 -0
  39. {limits-4.7.2 → limits-4.7.3}/limits/aio/storage/memory.py +0 -0
  40. {limits-4.7.2 → limits-4.7.3}/limits/aio/storage/mongodb.py +0 -0
  41. {limits-4.7.2 → limits-4.7.3}/limits/aio/storage/redis/__init__.py +0 -0
  42. {limits-4.7.2 → limits-4.7.3}/limits/aio/storage/redis/bridge.py +0 -0
  43. {limits-4.7.2 → limits-4.7.3}/limits/aio/storage/redis/coredis.py +0 -0
  44. {limits-4.7.2 → limits-4.7.3}/limits/aio/storage/redis/redispy.py +0 -0
  45. {limits-4.7.2 → limits-4.7.3}/limits/aio/storage/redis/valkey.py +0 -0
  46. {limits-4.7.2 → limits-4.7.3}/limits/aio/strategies.py +0 -0
  47. {limits-4.7.2 → limits-4.7.3}/limits/errors.py +0 -0
  48. {limits-4.7.2 → limits-4.7.3}/limits/limits.py +0 -0
  49. {limits-4.7.2 → limits-4.7.3}/limits/py.typed +0 -0
  50. {limits-4.7.2 → limits-4.7.3}/limits/resources/redis/lua_scripts/acquire_moving_window.lua +0 -0
  51. {limits-4.7.2 → limits-4.7.3}/limits/resources/redis/lua_scripts/acquire_sliding_window.lua +0 -0
  52. {limits-4.7.2 → limits-4.7.3}/limits/resources/redis/lua_scripts/clear_keys.lua +0 -0
  53. {limits-4.7.2 → limits-4.7.3}/limits/resources/redis/lua_scripts/incr_expire.lua +0 -0
  54. {limits-4.7.2 → limits-4.7.3}/limits/resources/redis/lua_scripts/moving_window.lua +0 -0
  55. {limits-4.7.2 → limits-4.7.3}/limits/resources/redis/lua_scripts/sliding_window.lua +0 -0
  56. {limits-4.7.2 → limits-4.7.3}/limits/storage/__init__.py +0 -0
  57. {limits-4.7.2 → limits-4.7.3}/limits/storage/base.py +0 -0
  58. {limits-4.7.2 → limits-4.7.3}/limits/storage/etcd.py +0 -0
  59. {limits-4.7.2 → limits-4.7.3}/limits/storage/memcached.py +0 -0
  60. {limits-4.7.2 → limits-4.7.3}/limits/storage/memory.py +0 -0
  61. {limits-4.7.2 → limits-4.7.3}/limits/storage/mongodb.py +0 -0
  62. {limits-4.7.2 → limits-4.7.3}/limits/storage/redis.py +0 -0
  63. {limits-4.7.2 → limits-4.7.3}/limits/storage/redis_cluster.py +0 -0
  64. {limits-4.7.2 → limits-4.7.3}/limits/storage/redis_sentinel.py +0 -0
  65. {limits-4.7.2 → limits-4.7.3}/limits/storage/registry.py +0 -0
  66. {limits-4.7.2 → limits-4.7.3}/limits/strategies.py +0 -0
  67. {limits-4.7.2 → limits-4.7.3}/limits/typing.py +0 -0
  68. {limits-4.7.2 → limits-4.7.3}/limits/util.py +0 -0
  69. {limits-4.7.2 → limits-4.7.3}/limits/version.py +0 -0
  70. {limits-4.7.2 → limits-4.7.3}/limits.egg-info/SOURCES.txt +0 -0
  71. {limits-4.7.2 → limits-4.7.3}/limits.egg-info/dependency_links.txt +0 -0
  72. {limits-4.7.2 → limits-4.7.3}/limits.egg-info/not-zip-safe +0 -0
  73. {limits-4.7.2 → limits-4.7.3}/limits.egg-info/requires.txt +0 -0
  74. {limits-4.7.2 → limits-4.7.3}/limits.egg-info/top_level.txt +0 -0
  75. {limits-4.7.2 → limits-4.7.3}/pyproject.toml +0 -0
  76. {limits-4.7.2 → limits-4.7.3}/requirements/ci.txt +0 -0
  77. {limits-4.7.2 → limits-4.7.3}/requirements/dev.txt +0 -0
  78. {limits-4.7.2 → limits-4.7.3}/requirements/main.txt +0 -0
  79. {limits-4.7.2 → limits-4.7.3}/requirements/storage/async-etcd.txt +0 -0
  80. {limits-4.7.2 → limits-4.7.3}/requirements/storage/async-memcached.txt +0 -0
  81. {limits-4.7.2 → limits-4.7.3}/requirements/storage/async-mongodb.txt +0 -0
  82. {limits-4.7.2 → limits-4.7.3}/requirements/storage/async-redis.txt +0 -0
  83. {limits-4.7.2 → limits-4.7.3}/requirements/storage/async-valkey.txt +0 -0
  84. {limits-4.7.2 → limits-4.7.3}/requirements/storage/etcd.txt +0 -0
  85. {limits-4.7.2 → limits-4.7.3}/requirements/storage/memcached.txt +0 -0
  86. {limits-4.7.2 → limits-4.7.3}/requirements/storage/mongodb.txt +0 -0
  87. {limits-4.7.2 → limits-4.7.3}/requirements/storage/redis.txt +0 -0
  88. {limits-4.7.2 → limits-4.7.3}/requirements/storage/rediscluster.txt +0 -0
  89. {limits-4.7.2 → limits-4.7.3}/requirements/storage/valkey.txt +0 -0
  90. {limits-4.7.2 → limits-4.7.3}/requirements/test.txt +0 -0
  91. {limits-4.7.2 → limits-4.7.3}/setup.cfg +0 -0
  92. {limits-4.7.2 → limits-4.7.3}/setup.py +0 -0
  93. {limits-4.7.2 → limits-4.7.3}/tests/test_limit_granularities.py +0 -0
  94. {limits-4.7.2 → limits-4.7.3}/tests/test_limits.py +0 -0
  95. {limits-4.7.2 → limits-4.7.3}/tests/test_ratelimit_parser.py +0 -0
  96. {limits-4.7.2 → limits-4.7.3}/tests/test_storage.py +0 -0
  97. {limits-4.7.2 → limits-4.7.3}/tests/test_strategy.py +0 -0
  98. {limits-4.7.2 → limits-4.7.3}/tests/test_utils.py +0 -0
  99. {limits-4.7.2 → limits-4.7.3}/versioneer.py +0 -0
@@ -3,6 +3,18 @@
3
3
  Changelog
4
4
  =========
5
5
 
6
+ v4.7.3
7
+ ------
8
+ Release Date: 2025-04-12
9
+
10
+ * Documentation
11
+
12
+ * Expand benchmark results to included preseeded limits
13
+
14
+ * Bug Fix
15
+
16
+ * Handle clearing missing key with memcache + async
17
+
6
18
  v4.7.2
7
19
  ------
8
20
  Release Date: 2025-04-09
@@ -873,5 +885,6 @@ Release Date: 2015-01-08
873
885
 
874
886
 
875
887
 
888
+
876
889
 
877
890
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: limits
3
- Version: 4.7.2
3
+ Version: 4.7.3
4
4
  Summary: Rate limiting utilities
5
5
  Home-page: https://limits.readthedocs.org
6
6
  Author: Ali-Akber Saifee
@@ -39,7 +39,15 @@ else:
39
39
  "branch": git_info.get("branch", ""),
40
40
  "sha": git_info.get("long", None)
41
41
  }
42
-
42
+ benchmark_param_mapping = {
43
+ "percentage_full": {
44
+ "display": "Percentage Seeded",
45
+ "info": "Percentage of rate limit already filled before the benchmark is run",
46
+ },
47
+ "async": {"display": "Asyncio", "info": "Using asyncio storage implementation"},
48
+ "storage_type": {"display": "Storage", "info": "Storage Backend"},
49
+ "strategy": {"display": "Strategy", "info": "Rate Limiting Strategy"},
50
+ }
43
51
  html_static_path = ["_static"]
44
52
 
45
53
  html_css_files = [
@@ -73,6 +81,7 @@ extensions = [
73
81
  "sphinxext.opengraph",
74
82
  "sphinxcontrib.programoutput",
75
83
  "sphinx_copybutton",
84
+ "sphinx_design",
76
85
  "sphinx_inline_tabs",
77
86
  "sphinx_paramlinks",
78
87
  "bench_chart",
@@ -1,3 +1,11 @@
1
+ .plot-container {
2
+ filter: invert(100%);
3
+ }
4
+
5
+ body[data-theme="light"] .plot-container {
6
+ filter: invert(0);
7
+ }
8
+
1
9
  .benchmark-chart {
2
10
  width: 100%;
3
11
  }
@@ -34,28 +42,56 @@
34
42
  0% {
35
43
  opacity: 0;
36
44
  }
37
- ,
38
45
  25% {
39
- opacity: 25%;
46
+ opacity: 0.25;
40
47
  }
41
- ,
42
48
  50% {
43
- opacity: 50%;
49
+ opacity: 0.5;
44
50
  }
45
- ,
46
51
  90% {
47
- opacity: 90%;
52
+ opacity: 0.9;
48
53
  }
49
- ,
50
54
  100% {
51
- opacity: 100%;
55
+ opacity: 1;
56
+ }
57
+ }
58
+ .benchmark-filters {
59
+ padding-top: 1em;
60
+ padding-bottom: 1em;
61
+ display: flex;
62
+ justify-content: flex-end;
63
+ }
64
+ .benchmark-filter-dropdowns {
65
+ display: flex;
66
+ align-items: flex-end;
67
+ justify-content: flex-end;
68
+ margin-top: 2em;
69
+ flex-direction: row;
70
+ @media only screen and (max-width: 800px) {
71
+ flex-basis: min-content;
72
+ }
73
+ @media only screen and (max-width: 400px) {
74
+ flex-basis: max-content;
75
+ flex-direction: column;
52
76
  }
53
77
  }
54
78
 
55
- .plot-container {
56
- filter: invert(100%);
79
+ .benchmark-filter {
80
+ display: flex;
81
+ flex-direction: row;
82
+ gap: 0.25em;
83
+ padding: 0.25em;
57
84
  }
58
85
 
59
- body[data-theme="light"] .plot-container {
60
- filter: invert(0);
86
+ .benchmark-filter label {
87
+ font-size: small;
88
+ font-weight: bold;
89
+ }
90
+
91
+ .benchmark-filter select {
92
+ font-size: small;
93
+ border: 1px solid;
94
+ border-radius: 0.4em;
95
+ }
96
+ .benchmark-filter input[type="checkbox"] {
61
97
  }
@@ -0,0 +1,343 @@
1
+ import { render, html } from "https://unpkg.com/uhtml@3.2.1?module";
2
+ import { fetchBenchmarkData } from "./benchmark-loader.js";
3
+ const KNOWN_PARAMS = [
4
+ "storage_type",
5
+ "limit",
6
+ "strategy",
7
+ "async",
8
+ "percentage_full",
9
+ ];
10
+
11
+ function getBenchmarkData(result, query) {
12
+ let benchmarks = result.benchmarks;
13
+ return benchmarks.filter(function (benchmark) {
14
+ let okay = true;
15
+ if (query) {
16
+ Object.entries(query).forEach((entry) => {
17
+ let key = entry[0];
18
+ let value = entry[1];
19
+ if (
20
+ key != "group" &&
21
+ !(value === "") && // i.e. any.
22
+ benchmark.params[key] != null &&
23
+ benchmark.params[key] != value
24
+ ) {
25
+ okay = false;
26
+ } else if (key == "group" && value != benchmark.group) {
27
+ okay = false;
28
+ }
29
+ });
30
+ }
31
+ return okay;
32
+ });
33
+ }
34
+
35
+ function formatParam(key, str) {
36
+ if (key === "limit") {
37
+ var m = str.match(/(\d+(?:\.\d+)?)\s+per\s+1\s+(\w+)/i);
38
+ if (!m) return str;
39
+ var n = parseFloat(m[1]),
40
+ u = m[2].toLowerCase(),
41
+ num =
42
+ n >= 1000
43
+ ? (n / 1000) % 1 === 0
44
+ ? n / 1000 + "K"
45
+ : (n / 1000).toFixed(1) + "K"
46
+ : n.toString(),
47
+ umap = {
48
+ second: "s",
49
+ seconds: "s",
50
+ minute: "min",
51
+ minutes: "mins",
52
+ hour: "hr",
53
+ hours: "hr",
54
+ day: "day",
55
+ days: "day",
56
+ };
57
+ return num + "/" + (umap[u] || u);
58
+ } else if (key === "percentage_full") {
59
+ return `${str}% Seeded`;
60
+ }
61
+ return str;
62
+ }
63
+
64
+ function nameTransform(benchmark, stripParams, query) {
65
+ let name = benchmark.name;
66
+ let params = benchmark.params;
67
+ name = name
68
+ .replace(/\[.*?\]/, "")
69
+ .replace("_async", "")
70
+ .replaceAll("_", "-");
71
+ name = name.replace(benchmark.group, "");
72
+ let queryParam = Object.entries(query).map((entry) => entry[0]);
73
+ let additional = getRemainingGroups(benchmark, query);
74
+ Object.entries(additional).forEach((param) => {
75
+ let value = formatParam(param[0], param[1]);
76
+ if (name) {
77
+ name += ` - ${value}`;
78
+ } else {
79
+ name = `${value}`;
80
+ }
81
+ });
82
+ return name;
83
+ }
84
+
85
+ function getRemainingGroups(benchmark, query) {
86
+ let queryParam = Object.entries(query).map((entry) => entry[0]);
87
+ let additional = {};
88
+ Object.entries(benchmark.params).forEach((param) => {
89
+ const key = param[0];
90
+ const value = param[1];
91
+ if (
92
+ (!queryParam.includes(key) || query?.[key] === "") &&
93
+ KNOWN_PARAMS.includes(key)
94
+ ) {
95
+ additional[key] = value;
96
+ }
97
+ });
98
+ return additional;
99
+ }
100
+
101
+ function getColorForStorage(storageType) {
102
+ const storageColorMap = {
103
+ memory: window
104
+ .getComputedStyle(document.body)
105
+ .getPropertyValue("--color-purple"),
106
+ mongodb: window
107
+ .getComputedStyle(document.body)
108
+ .getPropertyValue("--color-yellow"),
109
+ memcached: window
110
+ .getComputedStyle(document.body)
111
+ .getPropertyValue("--color-aqua"),
112
+ redis: window
113
+ .getComputedStyle(document.body)
114
+ .getPropertyValue("--color-red"),
115
+ };
116
+
117
+ // Fallback color if an unknown storageType appears
118
+ return storageColorMap[storageType] || "#7f7f7f"; // gray
119
+ }
120
+
121
+ function sortBenchmarksByParams(benchmarks, sortKeys) {
122
+ return benchmarks.sort(function (a, b) {
123
+ for (const key of sortKeys) {
124
+ let valA = (a.params?.[key] || "").toLowerCase();
125
+ let valB = (b.params?.[key] || "").toLowerCase();
126
+ if (key === "limit") {
127
+ valA = parseInt(valA.split(" ")[0], 10);
128
+ valB = parseInt(valB.split(" ")[0], 10);
129
+ }
130
+ if (valA < valB) return -1;
131
+ if (valA > valB) return 1;
132
+ }
133
+ return a.name.localeCompare(b.name);
134
+ });
135
+ }
136
+
137
+ let dispatched = new Set();
138
+
139
+ document.addEventListener("DOMContentLoaded", function () {
140
+ const charts = document.querySelectorAll(".benchmark-chart");
141
+ charts.forEach((chart) => {
142
+ const source = chart.dataset.source;
143
+ const filters = JSON.parse(chart.dataset.filters);
144
+ const query = JSON.parse(chart.dataset.query);
145
+ const paramMapping = JSON.parse(chart.dataset.paramMapping);
146
+ const chartId = chart.dataset.chartId;
147
+ let sortBy = JSON.parse(
148
+ chart.dataset.sortBy || '["storage_type", "limit"]',
149
+ );
150
+ render(
151
+ chart,
152
+ html`
153
+ <div class="benchmark-chart-loading">
154
+ <span>Loading</span>
155
+ </div>
156
+ `,
157
+ );
158
+ if (!dispatched.has(source)) {
159
+ fetchBenchmarkData(`${source}.json`)
160
+ .then((result) => {
161
+ window.Benchmarks[source] = result;
162
+ let event = new Event(`${source}-loaded`);
163
+ window.dispatchEvent(event);
164
+ })
165
+ .catch((error) => {
166
+ let event = new Event(`${source}-failed`);
167
+ window.dispatchEvent(event);
168
+ });
169
+ }
170
+ dispatched.add(source);
171
+ window.addEventListener(`${chart.dataset.source}-failed`, function () {
172
+ chart.querySelector(".benchmark-chart-loading")?.remove();
173
+ render(
174
+ chart,
175
+ html`
176
+ <div class="benchmark-chart-error">Benchmark data not available.</div>
177
+ `,
178
+ );
179
+ });
180
+ window.addEventListener(`${chart.dataset.source}-loaded`, function () {
181
+ chart.innerHTML = "";
182
+ chart.querySelector(".benchmark-chart-loading")?.remove();
183
+ const results = Benchmarks[chart.dataset.source];
184
+ const allBenchmarks = getBenchmarkData(results, query);
185
+ const currentFilters = Object.fromEntries(
186
+ Object.entries(filters).map(([key, value]) => {
187
+ return typeof value.default === "boolean"
188
+ ? [key, value.default]
189
+ : [key, value.default != null ? value.default.toString() : ""];
190
+ }),
191
+ );
192
+ const queryFilter = { ...query, ...currentFilters };
193
+ const dropdownTarget = document.createElement("div");
194
+ dropdownTarget.classList.add("benchmark-filters");
195
+ const chartTarget = document.createElement("div");
196
+ chart.append(chartTarget);
197
+ chart.append(dropdownTarget);
198
+ function renderDropdowns() {
199
+ const dropdowns = Object.entries(filters).map(([key]) => {
200
+ const fullName = `${chartId}-${key}`;
201
+ const uniqueValues = [
202
+ ...new Set(allBenchmarks.map((b) => b.params?.[key])),
203
+ ].sort();
204
+ const isBoolean =
205
+ uniqueValues.length === 2 &&
206
+ uniqueValues.includes(true) &&
207
+ uniqueValues.includes(false);
208
+ if (isBoolean) {
209
+ return html`
210
+ <div class="benchmark-filter" title=${paramMapping[key]?.info}>
211
+ <input
212
+ type="checkbox"
213
+ id=${fullName}
214
+ ?checked=${currentFilters[key] === true}
215
+ onchange=${(e) => {
216
+ currentFilters[key] = e.target.checked;
217
+ renderChartWithFilters(currentFilters);
218
+ }}
219
+ />
220
+ <label for=${fullName}>
221
+ ${paramMapping[key]?.display || key}
222
+ </label>
223
+ </div>
224
+ `;
225
+ } else {
226
+ return html`
227
+ <div class="benchmark-filter" title=${paramMapping[key]?.info}>
228
+ <label for=${fullName}>
229
+ ${paramMapping[key]?.display || key}
230
+ <select
231
+ id=${fullName}
232
+ onchange=${(e) => {
233
+ const value = e.target.value;
234
+ if (value) {
235
+ currentFilters[key] =
236
+ value === "false"
237
+ ? false
238
+ : value === "true"
239
+ ? true
240
+ : value;
241
+ } else {
242
+ currentFilters[key] = "";
243
+ }
244
+ renderChartWithFilters(currentFilters);
245
+ }}
246
+ >
247
+ <option value="" ?selected=${currentFilters[key] == ""}>
248
+ All
249
+ </option>
250
+ ${uniqueValues.map(
251
+ (val) => html`
252
+ <option
253
+ value=${val}
254
+ ?selected=${currentFilters[key] == val.toString()}
255
+ >
256
+ ${val}
257
+ </option>
258
+ `,
259
+ )}
260
+ </select>
261
+ </label>
262
+ </div>
263
+ `;
264
+ }
265
+ });
266
+ render(
267
+ dropdownTarget,
268
+ html`<div class="benchmark-filter-dropdowns">${dropdowns}</div>`,
269
+ );
270
+ }
271
+ function legendKeyFunc(benchmark, key) {
272
+ return key === "group" ? benchmark.group : benchmark.params[key];
273
+ }
274
+ function renderChartWithFilters(currentFilters) {
275
+ const queryFilter = { ...query, ...currentFilters };
276
+ const data = sortBenchmarksByParams(
277
+ getBenchmarkData(results, queryFilter),
278
+ sortBy,
279
+ );
280
+ let legendGroupKey =
281
+ queryFilter?.storage_type == ""
282
+ ? "storage_type"
283
+ : Object.entries(queryFilter).find((entry) => entry[1] === "")?.[0];
284
+ Plotly.newPlot(
285
+ chartTarget,
286
+ data.map((benchmark) => ({
287
+ type: "box",
288
+ name: nameTransform(benchmark, true, queryFilter),
289
+ y: benchmark.stats.data || [
290
+ benchmark.stats.min * 1e3,
291
+ benchmark.stats.q1 * 1e3,
292
+ benchmark.stats.median * 1e3,
293
+ benchmark.stats.q3 * 1e3,
294
+ benchmark.stats.max * 1e3,
295
+ ],
296
+ boxmean: true,
297
+ boxpoints: false,
298
+ line: { width: 1 },
299
+ marker: {
300
+ color: getColorForStorage(benchmark.params.storage_type),
301
+ },
302
+ showlegend: true,
303
+ legendgroup: legendKeyFunc(benchmark, legendGroupKey),
304
+ legendgrouptitle: {
305
+ text: formatParam(
306
+ legendGroupKey,
307
+ legendKeyFunc(benchmark, legendGroupKey),
308
+ ),
309
+ },
310
+ })),
311
+ {
312
+ yaxis: {
313
+ title: { text: "Time (ms)" },
314
+ exponentformat: "none",
315
+ ticksuffix: " ms",
316
+ tickformat: ",.2f",
317
+ },
318
+ },
319
+ {
320
+ responsive: true,
321
+ displaylogo: false,
322
+ },
323
+ );
324
+ }
325
+
326
+ renderDropdowns();
327
+ renderChartWithFilters(currentFilters);
328
+ let initial = true;
329
+ chartTarget.on("plotly_afterplot", function () {
330
+ const { hash } = window.location;
331
+ if (hash && initial) {
332
+ initial = false;
333
+ const target = document.querySelector(hash);
334
+ if (target) {
335
+ setTimeout(function () {
336
+ target.scrollIntoView({ behavior: "instant" });
337
+ }, 10);
338
+ }
339
+ }
340
+ });
341
+ });
342
+ });
343
+ });
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  import os
5
+ import uuid
5
6
  from typing import TYPE_CHECKING
6
7
 
7
8
  from docutils import nodes
@@ -31,13 +32,14 @@ def query(argument):
31
32
 
32
33
 
33
34
  def filters(argument):
34
- filters: dict[str, list | bool] = {}
35
+ filters: dict[str, dict[str, str | bool]] = {}
35
36
  for filter in argument.strip().split(","):
36
- if ":" in filter:
37
- source, value = filter.split(":")
38
- filters.setdefault(source, []).append(value)
37
+ source, default = filter.split("=")
38
+ if ":" in source:
39
+ source, label = source.split(":")
39
40
  else:
40
- filters[filter] = True
41
+ source, label = source, source
42
+ filters[source] = {"label": label, "default": check_bool(default)}
41
43
  return filters
42
44
 
43
45
 
@@ -66,7 +68,6 @@ class BenchmarkDetails(SphinxDirective):
66
68
 
67
69
 
68
70
  class BenchmarkChart(SphinxDirective):
69
- required_arguments = 0
70
71
  final_argument_whitespace = False
71
72
  option_spec = {
72
73
  "source": str,
@@ -78,17 +79,18 @@ class BenchmarkChart(SphinxDirective):
78
79
 
79
80
  def run(self):
80
81
  source = self.options.get("source", "benchmark-summary")
81
- filters = self.options.get("filters", ["group"])
82
+ filters = self.options.get("filters", {})
82
83
  query = self.options.get("query", {})
83
84
  sortBy = self.options.get("sort", [])
84
-
85
85
  html = f"""
86
86
  <div
87
87
  class='benchmark-chart'
88
88
  data-source='{source}'
89
89
  data-filters='{json.dumps(filters)}'
90
90
  data-query='{json.dumps(query)}'
91
- data-sortBy='{json.dumps(sortBy)}'>
91
+ data-sort-by='{json.dumps(sortBy)}'
92
+ data-chart-id='{uuid.uuid4().hex}'
93
+ data-param-mapping='{json.dumps(self.env.config.benchmark_param_mapping)}'>
92
94
  </div>
93
95
  """
94
96
 
@@ -117,6 +119,7 @@ def setup(app: Sphinx):
117
119
  app.add_directive("benchmark-chart", BenchmarkChart)
118
120
  app.add_directive("benchmark-details", BenchmarkDetails)
119
121
  app.add_config_value("benchmark_git_context", default={}, rebuild="env")
122
+ app.add_config_value("benchmark_param_mapping", default={}, rebuild="env")
120
123
 
121
124
  def add_assets(app, env) -> None:
122
125
  static_path = os.path.join(here, "_static")
@@ -0,0 +1,126 @@
1
+ Performance
2
+ ===========
3
+
4
+ The performance of each rate-limiting strategy and storage backend
5
+ differs in both throughput and storage cost characteristics.
6
+
7
+ Performance by storage and strategy
8
+ -----------------------------------
9
+ Below you will find benchmarks for each strategy and storage giving
10
+ a high level overview of the performance.
11
+
12
+
13
+ .. dropdown:: Benchmark parameters
14
+
15
+ - 100 unique virtual users (i.e. unique rate limit keys)
16
+ - A rate limit of ``500/minute``
17
+ - Each virtual user's limit was pre-seeded to be 50% full.
18
+
19
+ See :ref:`performance:benchmark run details` for information on the benchmarking
20
+ environment.
21
+
22
+ .. tab-set::
23
+
24
+ .. tab-item:: Hit
25
+
26
+ Performance of :meth:`~limits.strategies.RateLimiter.hit`
27
+ by storage & strategy.
28
+
29
+ .. benchmark-chart::
30
+ :source: benchmark-summary
31
+ :query: limit=500 per 1 minute,group=hit,percentage_full=50
32
+ :sort: storage_type,strategy
33
+ :filters: storage_type=,strategy=,async=false
34
+
35
+
36
+ .. tab-item:: Test
37
+
38
+ Performance of :meth:`~limits.strategies.RateLimiter.test`
39
+ by storage & strategy.
40
+
41
+ .. benchmark-chart::
42
+ :source: benchmark-summary
43
+ :query: limit=500 per 1 minute,group=test,percentage_full=50
44
+ :sort: storage_type,strategy
45
+ :filters: storage_type=,strategy=,async=false
46
+
47
+ .. tab-item:: Get Window Stats
48
+
49
+ Performance of :meth:`~limits.strategies.RateLimiter.get_window_stats`
50
+ by storage & strategy.
51
+
52
+ .. benchmark-chart::
53
+ :source: benchmark-summary
54
+ :query: limit=500 per 1 minute,group=get-window-stats,percentage_full=50
55
+ :sort: storage_type,strategy
56
+ :filters: storage_type=,strategy=,async=false
57
+
58
+
59
+ Performance implication of limit sizes
60
+ --------------------------------------
61
+
62
+ Though for :ref:`strategies:fixed window` and :ref:`strategies:sliding window counter` both the
63
+ storage cost and performance of operations remains constant when the limit window and size varies,
64
+ this is not true for :ref:`strategies:moving window` which maintains a complete log of successful
65
+ requests within the window.
66
+
67
+ The following benchmarks demonstrate the implications when using various limits.
68
+
69
+ .. dropdown:: Benchmark parameters
70
+
71
+ - 100 unique virtual users
72
+ - Rate limits of
73
+
74
+ - ``500/minute``
75
+ - ``10000/day``
76
+ - ``100000/day``
77
+ - Each virtual user's limit was pre-seeded to be:
78
+
79
+ - 5% full.
80
+ - 50% full.
81
+ - 90% full.
82
+
83
+ See :ref:`performance:benchmark run details` for information on the benchmarking
84
+ environment.
85
+
86
+ .. tab-set::
87
+
88
+ .. tab-item:: Hit
89
+
90
+ Performance of :meth:`~limits.strategies.RateLimiter.hit`
91
+ with various rate limits
92
+
93
+ .. benchmark-chart::
94
+ :source: benchmark-summary
95
+ :query: group=hit
96
+ :sort: storage_type,limit
97
+ :filters: strategy=,percentage_full=50,storage_type=,async=false
98
+
99
+ .. tab-item:: Test
100
+
101
+ Performance of :meth:`~limits.strategies.RateLimiter.test`
102
+ with various rate limits
103
+
104
+ .. benchmark-chart::
105
+ :source: benchmark-summary
106
+ :query: group=test
107
+ :sort: storage_type,limit
108
+ :filters: strategy=,percentage_full=50,storage_type=,async=false
109
+
110
+
111
+ .. tab-item:: Get Window Stats
112
+
113
+ Performance of :meth:`~limits.strategies.RateLimiter.get_window_stats`
114
+ with various rate limits
115
+
116
+ .. benchmark-chart::
117
+ :source: benchmark-summary
118
+ :query: group=get-window-stats
119
+ :sort: storage_type,limit
120
+ :filters: strategy=,percentage_full=50,storage_type=,async=false
121
+
122
+
123
+ Benchmark run details
124
+ ---------------------
125
+ .. benchmark-details::
126
+ :source: benchmark-summary
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2025-04-09T14:27:52-0700",
11
+ "date": "2025-04-12T18:43:47-0700",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "451bd270b52aff6324de3ed6576a8fc87fbdaf9a",
15
- "version": "4.7.2"
14
+ "full-revisionid": "58af4445a2d0b1de9251cd60e847d25267b4830d",
15
+ "version": "4.7.3"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -93,7 +93,10 @@ class MemcachedStorage(Storage, SlidingWindowCounterSupport, TimestampedSlidingW
93
93
  """
94
94
  :param key: the key to clear rate limits for
95
95
  """
96
- await (await self.get_storage()).delete(key.encode("utf-8"))
96
+ try:
97
+ await (await self.get_storage()).delete(key.encode("utf-8"))
98
+ except self.dependency.NotFoundCommandError:
99
+ pass
97
100
 
98
101
  async def decr(self, key: str, amount: int = 1, noreply: bool = False) -> int:
99
102
  """