limits 4.6__tar.gz → 4.7.1__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 (97) hide show
  1. {limits-4.6 → limits-4.7.1}/HISTORY.rst +18 -0
  2. {limits-4.6 → limits-4.7.1}/PKG-INFO +1 -1
  3. {limits-4.6 → limits-4.7.1}/doc/source/conf.py +26 -1
  4. limits-4.7.1/doc/source/ext/_static/benchmark-chart.css +10 -0
  5. limits-4.7.1/doc/source/ext/_static/js/benchmark-chart.js +194 -0
  6. limits-4.7.1/doc/source/ext/_static/js/benchmark-details.js +87 -0
  7. limits-4.7.1/doc/source/ext/_static/js/benchmark-loader.js +32 -0
  8. limits-4.7.1/doc/source/ext/_templates/git_info.js +2 -0
  9. limits-4.7.1/doc/source/ext/bench_chart.py +128 -0
  10. {limits-4.6 → limits-4.7.1}/doc/source/index.rst +1 -0
  11. limits-4.7.1/doc/source/performance.rst +221 -0
  12. {limits-4.6 → limits-4.7.1}/doc/source/theme_config.py +18 -0
  13. {limits-4.6 → limits-4.7.1}/limits/_version.py +3 -3
  14. {limits-4.6 → limits-4.7.1}/limits.egg-info/PKG-INFO +1 -1
  15. {limits-4.6 → limits-4.7.1}/limits.egg-info/SOURCES.txt +7 -0
  16. {limits-4.6 → limits-4.7.1}/CLASSIFIERS +0 -0
  17. {limits-4.6 → limits-4.7.1}/CONTRIBUTIONS.rst +0 -0
  18. {limits-4.6 → limits-4.7.1}/LICENSE.txt +0 -0
  19. {limits-4.6 → limits-4.7.1}/MANIFEST.in +0 -0
  20. {limits-4.6 → limits-4.7.1}/README.rst +0 -0
  21. {limits-4.6 → limits-4.7.1}/doc/Makefile +0 -0
  22. {limits-4.6 → limits-4.7.1}/doc/source/_static/custom.css +0 -0
  23. {limits-4.6 → limits-4.7.1}/doc/source/api.rst +0 -0
  24. {limits-4.6 → limits-4.7.1}/doc/source/async.rst +0 -0
  25. {limits-4.6 → limits-4.7.1}/doc/source/changelog.rst +0 -0
  26. {limits-4.6 → limits-4.7.1}/doc/source/custom-storage.rst +0 -0
  27. {limits-4.6 → limits-4.7.1}/doc/source/installation.rst +0 -0
  28. {limits-4.6 → limits-4.7.1}/doc/source/quickstart.rst +0 -0
  29. {limits-4.6 → limits-4.7.1}/doc/source/storage.rst +0 -0
  30. {limits-4.6 → limits-4.7.1}/doc/source/strategies.rst +0 -0
  31. {limits-4.6 → limits-4.7.1}/limits/__init__.py +0 -0
  32. {limits-4.6 → limits-4.7.1}/limits/aio/__init__.py +0 -0
  33. {limits-4.6 → limits-4.7.1}/limits/aio/storage/__init__.py +0 -0
  34. {limits-4.6 → limits-4.7.1}/limits/aio/storage/base.py +0 -0
  35. {limits-4.6 → limits-4.7.1}/limits/aio/storage/etcd.py +0 -0
  36. {limits-4.6 → limits-4.7.1}/limits/aio/storage/memcached.py +0 -0
  37. {limits-4.6 → limits-4.7.1}/limits/aio/storage/memory.py +0 -0
  38. {limits-4.6 → limits-4.7.1}/limits/aio/storage/mongodb.py +0 -0
  39. {limits-4.6 → limits-4.7.1}/limits/aio/storage/redis/__init__.py +0 -0
  40. {limits-4.6 → limits-4.7.1}/limits/aio/storage/redis/bridge.py +0 -0
  41. {limits-4.6 → limits-4.7.1}/limits/aio/storage/redis/coredis.py +0 -0
  42. {limits-4.6 → limits-4.7.1}/limits/aio/storage/redis/redispy.py +0 -0
  43. {limits-4.6 → limits-4.7.1}/limits/aio/storage/redis/valkey.py +0 -0
  44. {limits-4.6 → limits-4.7.1}/limits/aio/strategies.py +0 -0
  45. {limits-4.6 → limits-4.7.1}/limits/errors.py +0 -0
  46. {limits-4.6 → limits-4.7.1}/limits/limits.py +0 -0
  47. {limits-4.6 → limits-4.7.1}/limits/py.typed +0 -0
  48. {limits-4.6 → limits-4.7.1}/limits/resources/redis/lua_scripts/acquire_moving_window.lua +0 -0
  49. {limits-4.6 → limits-4.7.1}/limits/resources/redis/lua_scripts/acquire_sliding_window.lua +0 -0
  50. {limits-4.6 → limits-4.7.1}/limits/resources/redis/lua_scripts/clear_keys.lua +0 -0
  51. {limits-4.6 → limits-4.7.1}/limits/resources/redis/lua_scripts/incr_expire.lua +0 -0
  52. {limits-4.6 → limits-4.7.1}/limits/resources/redis/lua_scripts/moving_window.lua +0 -0
  53. {limits-4.6 → limits-4.7.1}/limits/resources/redis/lua_scripts/sliding_window.lua +0 -0
  54. {limits-4.6 → limits-4.7.1}/limits/storage/__init__.py +0 -0
  55. {limits-4.6 → limits-4.7.1}/limits/storage/base.py +0 -0
  56. {limits-4.6 → limits-4.7.1}/limits/storage/etcd.py +0 -0
  57. {limits-4.6 → limits-4.7.1}/limits/storage/memcached.py +0 -0
  58. {limits-4.6 → limits-4.7.1}/limits/storage/memory.py +0 -0
  59. {limits-4.6 → limits-4.7.1}/limits/storage/mongodb.py +0 -0
  60. {limits-4.6 → limits-4.7.1}/limits/storage/redis.py +0 -0
  61. {limits-4.6 → limits-4.7.1}/limits/storage/redis_cluster.py +0 -0
  62. {limits-4.6 → limits-4.7.1}/limits/storage/redis_sentinel.py +0 -0
  63. {limits-4.6 → limits-4.7.1}/limits/storage/registry.py +0 -0
  64. {limits-4.6 → limits-4.7.1}/limits/strategies.py +0 -0
  65. {limits-4.6 → limits-4.7.1}/limits/typing.py +0 -0
  66. {limits-4.6 → limits-4.7.1}/limits/util.py +0 -0
  67. {limits-4.6 → limits-4.7.1}/limits/version.py +0 -0
  68. {limits-4.6 → limits-4.7.1}/limits.egg-info/dependency_links.txt +0 -0
  69. {limits-4.6 → limits-4.7.1}/limits.egg-info/not-zip-safe +0 -0
  70. {limits-4.6 → limits-4.7.1}/limits.egg-info/requires.txt +0 -0
  71. {limits-4.6 → limits-4.7.1}/limits.egg-info/top_level.txt +0 -0
  72. {limits-4.6 → limits-4.7.1}/pyproject.toml +0 -0
  73. {limits-4.6 → limits-4.7.1}/requirements/ci.txt +0 -0
  74. {limits-4.6 → limits-4.7.1}/requirements/dev.txt +0 -0
  75. {limits-4.6 → limits-4.7.1}/requirements/docs.txt +0 -0
  76. {limits-4.6 → limits-4.7.1}/requirements/main.txt +0 -0
  77. {limits-4.6 → limits-4.7.1}/requirements/storage/async-etcd.txt +0 -0
  78. {limits-4.6 → limits-4.7.1}/requirements/storage/async-memcached.txt +0 -0
  79. {limits-4.6 → limits-4.7.1}/requirements/storage/async-mongodb.txt +0 -0
  80. {limits-4.6 → limits-4.7.1}/requirements/storage/async-redis.txt +0 -0
  81. {limits-4.6 → limits-4.7.1}/requirements/storage/async-valkey.txt +0 -0
  82. {limits-4.6 → limits-4.7.1}/requirements/storage/etcd.txt +0 -0
  83. {limits-4.6 → limits-4.7.1}/requirements/storage/memcached.txt +0 -0
  84. {limits-4.6 → limits-4.7.1}/requirements/storage/mongodb.txt +0 -0
  85. {limits-4.6 → limits-4.7.1}/requirements/storage/redis.txt +0 -0
  86. {limits-4.6 → limits-4.7.1}/requirements/storage/rediscluster.txt +0 -0
  87. {limits-4.6 → limits-4.7.1}/requirements/storage/valkey.txt +0 -0
  88. {limits-4.6 → limits-4.7.1}/requirements/test.txt +0 -0
  89. {limits-4.6 → limits-4.7.1}/setup.cfg +0 -0
  90. {limits-4.6 → limits-4.7.1}/setup.py +0 -0
  91. {limits-4.6 → limits-4.7.1}/tests/test_limit_granularities.py +0 -0
  92. {limits-4.6 → limits-4.7.1}/tests/test_limits.py +0 -0
  93. {limits-4.6 → limits-4.7.1}/tests/test_ratelimit_parser.py +0 -0
  94. {limits-4.6 → limits-4.7.1}/tests/test_storage.py +0 -0
  95. {limits-4.6 → limits-4.7.1}/tests/test_strategy.py +0 -0
  96. {limits-4.6 → limits-4.7.1}/tests/test_utils.py +0 -0
  97. {limits-4.6 → limits-4.7.1}/versioneer.py +0 -0
@@ -3,6 +3,22 @@
3
3
  Changelog
4
4
  =========
5
5
 
6
+ v4.7.1
7
+ ------
8
+ Release Date: 2025-04-08
9
+
10
+ * Testing
11
+
12
+ * Fix incorrect benchmark for async test method
13
+
14
+ v4.7
15
+ ----
16
+ Release Date: 2025-04-08
17
+
18
+ * Documentation
19
+
20
+ * Add benchmarking results in documentation
21
+
6
22
  v4.6
7
23
  ----
8
24
  Release Date: 2025-04-03
@@ -845,6 +861,8 @@ Release Date: 2015-01-08
845
861
 
846
862
 
847
863
 
864
+
865
+
848
866
 
849
867
 
850
868
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: limits
3
- Version: 4.6
3
+ Version: 4.7.1
4
4
  Summary: Rate limiting utilities
5
5
  Home-page: https://limits.readthedocs.org
6
6
  Author: Ali-Akber Saifee
@@ -2,6 +2,11 @@
2
2
 
3
3
  import os
4
4
  import sys
5
+ from pathlib import Path
6
+
7
+ from docutils import nodes
8
+ from sphinx.application import Sphinx
9
+ from sphinx.util.docutils import SphinxDirective
5
10
 
6
11
  sys.path.insert(0, os.path.abspath("../../"))
7
12
  sys.path.insert(0, os.path.abspath("./"))
@@ -13,6 +18,7 @@ import limits
13
18
  project = "limits"
14
19
  description = "limits is a python library to perform rate limiting with commonly used storage backends"
15
20
  copyright = "2023, Ali-Akber Saifee"
21
+
16
22
  if ".post0.dev" in limits.__version__:
17
23
  version, ahead = limits.__version__.split(".post0.dev")
18
24
  else:
@@ -20,7 +26,22 @@ else:
20
26
 
21
27
  release = version
22
28
 
23
- html_static_path = ["./_static"]
29
+
30
+ if branch_from_env := os.environ.get("READTHEDOCS_VERSION", None):
31
+ benchmark_git_context = {
32
+ "branch": branch_from_env,
33
+ "sha": os.environ.get("READTHEDOCS_GIT_COMMIT_HASH", "")
34
+ }
35
+ else:
36
+ import limits._version
37
+ git_info = limits._version.git_pieces_from_vcs("", os.path.abspath("../../"), False)
38
+ benchmark_git_context = {
39
+ "branch": git_info.get("branch", ""),
40
+ "sha": git_info.get("long", None)
41
+ }
42
+
43
+ html_static_path = ["_static"]
44
+
24
45
  html_css_files = [
25
46
  "custom.css",
26
47
  "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",
@@ -38,6 +59,7 @@ try:
38
59
  html_title = f"{project} <small><b style='color: var(--color-brand-primary)'>{{dev}}</b></small>"
39
60
  except:
40
61
  pass
62
+ sys.path.append(str(Path('ext').resolve()))
41
63
 
42
64
  extensions = [
43
65
  "sphinx.ext.autodoc",
@@ -53,6 +75,7 @@ extensions = [
53
75
  "sphinx_copybutton",
54
76
  "sphinx_inline_tabs",
55
77
  "sphinx_paramlinks",
78
+ "bench_chart",
56
79
  ]
57
80
 
58
81
  autodoc_default_options = {
@@ -70,6 +93,8 @@ autosectionlabel_prefix_document = True
70
93
 
71
94
  extlinks = {"pypi": ("https://pypi.org/project/%s", "%s")}
72
95
 
96
+ copybutton_exclude = '.gp, .go'
97
+
73
98
  intersphinx_mapping = {
74
99
  "python": ("http://docs.python.org/", None),
75
100
  "coredis": ("https://coredis.readthedocs.io/en/latest/", None),
@@ -0,0 +1,10 @@
1
+ .benchmark-chart {
2
+ width: 100%;
3
+ }
4
+ .benchmark-details{
5
+ .benchmark-details-section {
6
+ thead > tr {
7
+ color: var(--color-purple);
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,194 @@
1
+ const KNOWN_PARAMS = ["storage_type", "limit", "strategy", "async"];
2
+
3
+ function getBenchmarkData(result, query) {
4
+ let benchmarks = result.benchmarks;
5
+ return benchmarks.filter(function (benchmark) {
6
+ let okay = true;
7
+ if (query) {
8
+ Object.entries(query).forEach((entry) => {
9
+ if (entry[0] != "group" && benchmark.params[entry[0]] != entry[1]) {
10
+ okay = false;
11
+ }
12
+ if (entry[0] == "group" && entry[1] != benchmark.group) {
13
+ okay = false;
14
+ }
15
+ });
16
+ }
17
+ return okay;
18
+ });
19
+ }
20
+
21
+ function formatRateLimit(str) {
22
+ var m = str.match(/(\d+(?:\.\d+)?)\s+per\s+1\s+(\w+)/i);
23
+ if (!m) return str;
24
+ var n = parseFloat(m[1]),
25
+ u = m[2].toLowerCase(),
26
+ num =
27
+ n >= 1000
28
+ ? (n / 1000) % 1 === 0
29
+ ? n / 1000 + "K"
30
+ : (n / 1000).toFixed(1) + "K"
31
+ : n.toString(),
32
+ umap = {
33
+ second: "s",
34
+ seconds: "s",
35
+ minute: "min",
36
+ minutes: "mins",
37
+ hour: "hr",
38
+ hours: "hr",
39
+ day: "day",
40
+ days: "day",
41
+ };
42
+ return num + "/" + (umap[u] || u);
43
+ }
44
+
45
+ function nameTransform(benchmark, stripParams, query) {
46
+ let name = benchmark.name;
47
+ params = benchmark.params;
48
+ name = name
49
+ .replace(/\[.*?\]/, "")
50
+ .replace("_async", "")
51
+ .replaceAll("_", "-");
52
+ name = name.replace(benchmark.group, "");
53
+ let queryParam = Object.entries(query).map((entry) => entry[0]);
54
+ let additional = getRemainingGroups(benchmark, query);
55
+ Object.entries(additional).forEach((param) => {
56
+ let value = param[1];
57
+ if (param[0] === "limit") {
58
+ value = formatRateLimit(value);
59
+ }
60
+ if (name) {
61
+ name += ` - ${value}`;
62
+ } else {
63
+ name = `${value}`;
64
+ }
65
+ });
66
+ return name;
67
+ }
68
+
69
+ function getRemainingGroups(benchmark, query) {
70
+ let queryParam = Object.entries(query).map((entry) => entry[0]);
71
+ let additional = {};
72
+ Object.entries(benchmark.params).forEach((param) => {
73
+ if (!queryParam.includes(param[0]) && KNOWN_PARAMS.includes(param[0])) {
74
+ additional[param[0]] = param[1];
75
+ }
76
+ });
77
+ return additional;
78
+ }
79
+
80
+ function getColorForStorage(storageType) {
81
+ const storageColorMap = {
82
+ memory: window
83
+ .getComputedStyle(document.body)
84
+ .getPropertyValue("--color-purple"),
85
+ mongodb: window
86
+ .getComputedStyle(document.body)
87
+ .getPropertyValue("--color-yellow"),
88
+ memcached: window
89
+ .getComputedStyle(document.body)
90
+ .getPropertyValue("--color-aqua"),
91
+ redis: window
92
+ .getComputedStyle(document.body)
93
+ .getPropertyValue("--color-red"),
94
+ };
95
+
96
+ // Fallback color if an unknown storageType appears
97
+ return storageColorMap[storageType] || "#7f7f7f"; // gray
98
+ }
99
+
100
+ function sortBenchmarksByParams(benchmarks, sortKeys) {
101
+ return benchmarks.sort((a, b) => {
102
+ for (const key of sortKeys) {
103
+ let valA = (a.params?.[key] || "").toLowerCase();
104
+ let valB = (b.params?.[key] || "").toLowerCase();
105
+ if (key == "limit") {
106
+ valA = parseInt(valA.split(" ")[0]);
107
+ valB = parseInt(valB.split(" ")[0]);
108
+ }
109
+ return valA < valB ? -1 : valA > valB ? 1 : 0;
110
+ }
111
+
112
+ return a.name.localeCompare(b.name);
113
+ });
114
+ }
115
+ function sortBenchmarksByParams(benchmarks, sortKeys) {
116
+ return benchmarks.sort(function (a, b) {
117
+ for (const key of sortKeys) {
118
+ let valA = (a.params?.[key] || "").toLowerCase();
119
+ let valB = (b.params?.[key] || "").toLowerCase();
120
+ if (key === "limit") {
121
+ valA = parseInt(valA.split(" ")[0], 10);
122
+ valB = parseInt(valB.split(" ")[0], 10);
123
+ }
124
+ if (valA < valB) return -1;
125
+ if (valA > valB) return 1;
126
+ }
127
+ return a.name.localeCompare(b.name);
128
+ });
129
+ }
130
+ let dispatched = new Set();
131
+
132
+ document.addEventListener("DOMContentLoaded", function () {
133
+ const charts = document.querySelectorAll(".benchmark-chart");
134
+ charts.forEach((chart) => {
135
+ let source = chart.dataset.source;
136
+ let filters = JSON.parse(chart.dataset.filters);
137
+ let query = JSON.parse(chart.dataset.query);
138
+ let sortBy = JSON.parse(
139
+ chart.dataset.sortBy || '["storage_type", "limit"]',
140
+ );
141
+ if (!dispatched.has(source)) {
142
+ fetchBenchmarkData(`${source}.json`).then((result) => {
143
+ window.Benchmarks[source] = result;
144
+ event = new Event(`${source}-loaded`);
145
+ window.dispatchEvent(event);
146
+ });
147
+ }
148
+ dispatched.add(source);
149
+ window.addEventListener(`${chart.dataset.source}-loaded`, function () {
150
+ let results = Benchmarks[chart.dataset.source];
151
+ let unsorted = getBenchmarkData(results, query);
152
+ let data = sortBenchmarksByParams(
153
+ getBenchmarkData(results, query),
154
+ sortBy,
155
+ );
156
+
157
+ const layout = {
158
+ yaxis: {
159
+ title: { text: "Time (ms)" },
160
+ exponentformat: "none",
161
+ ticksuffix: " ms",
162
+ tickformat: ",.2f",
163
+ },
164
+ };
165
+ Plotly.newPlot(
166
+ chart,
167
+ data.map(function (benchmark) {
168
+ let item = {
169
+ type: "box",
170
+ name: nameTransform(benchmark, true, query),
171
+ y: benchmark.stats.data || [
172
+ benchmark.stats.min * 1e3,
173
+ benchmark.stats.q1 * 1e3,
174
+ benchmark.stats.median * 1e3,
175
+ benchmark.stats.q3 * 1e3,
176
+ benchmark.stats.max * 1e3,
177
+ ],
178
+ boxmean: true,
179
+ boxpoints: false,
180
+ line: { width: 1 },
181
+ marker: {
182
+ color: getColorForStorage(benchmark.params.storage_type),
183
+ },
184
+ showlegend: true,
185
+ legendgroup: benchmark.params.storage_type,
186
+ legendgrouptitle: { text: benchmark.params.storage_type },
187
+ };
188
+ return item;
189
+ }),
190
+ layout,
191
+ );
192
+ });
193
+ });
194
+ });
@@ -0,0 +1,87 @@
1
+ import { render, html } from "https://unpkg.com/uhtml@3.2.1?module";
2
+ document.addEventListener("DOMContentLoaded", function () {
3
+ const details = document.querySelectorAll(".benchmark-details");
4
+ details.forEach((detail) => {
5
+ let source = detail.dataset.source;
6
+ if (!dispatched.has(source)) {
7
+ fetchBenchmarkData(`${source}.json`).then((result) => {
8
+ window.Benchmarks[source] = result;
9
+ event = new Event(`${source}-loaded`);
10
+ window.dispatchEvent(event);
11
+ });
12
+ }
13
+ dispatched.add(source);
14
+ window.addEventListener(`${detail.dataset.source}-loaded`, function () {
15
+ const machine_info = window.Benchmarks[source].machine_info;
16
+ const commit_info = window.Benchmarks[source].commit_info;
17
+ const cpu = window.Benchmarks[source].machine_info.cpu;
18
+ console.log(machine_info);
19
+ render(
20
+ detail,
21
+ html`
22
+ <table class="benchmark-details-section">
23
+ <thead>
24
+ <tr>
25
+ <th>Machine Information</th>
26
+ </tr>
27
+ </thead>
28
+ <tbody>
29
+ <tr>
30
+ <td>Operating System</td>
31
+ <td>${machine_info.system} (${machine_info.release})</td>
32
+ </tr>
33
+ <tr>
34
+ <td>CPU</td>
35
+ <td>
36
+ ${cpu.brand_raw} ${cpu.processor} @ ${cpu.hz_actual_friendly}
37
+ </td>
38
+ </tr>
39
+ <tr>
40
+ <td>Python</td>
41
+ <td>${machine_info.python_version}</td>
42
+ </tr>
43
+ </tbody>
44
+ </table>
45
+ <table class="benchmark-details-section">
46
+ <thead>
47
+ <tr>
48
+ <th>Source Information</th>
49
+ </tr>
50
+ </thead>
51
+ <tbody>
52
+ <tr>
53
+ <td>Branch</td>
54
+ <td>${commit_info.branch}</td>
55
+ </tr>
56
+ <tr>
57
+ <td>Commit Hash</td>
58
+ <td>${commit_info.id}</td>
59
+ </tr>
60
+ </tbody>
61
+ </table>
62
+ <table class="benchmark-details-section">
63
+ <thead>
64
+ <tr>
65
+ <th>Storage Information</th>
66
+ </tr>
67
+ </thead>
68
+ <tbody>
69
+ <tr>
70
+ <td>Redis</td>
71
+ <td>${machine_info.redis.redis_version}</td>
72
+ </tr>
73
+ <tr>
74
+ <td>Memcached</td>
75
+ <td>${machine_info.memcached.version}</td>
76
+ </tr>
77
+ <tr>
78
+ <td>MongoDB</td>
79
+ <td>${machine_info.mongodb.version}</td>
80
+ </tr>
81
+ </tbody>
82
+ </table>
83
+ `,
84
+ );
85
+ });
86
+ });
87
+ });
@@ -0,0 +1,32 @@
1
+ const BENCHMARK_PATHS = [
2
+ `https://${GITSHA}--py-limits.netlify.app/`,
3
+ `https://${GITBRANCH}--py-limits.netlify.app/`,
4
+ ];
5
+
6
+ window.Benchmarks = new Map();
7
+
8
+ function fetchBenchmarkData(filename) {
9
+ let attempts = 0;
10
+
11
+ function tryFetch() {
12
+ if (attempts >= BENCHMARK_PATHS.length) {
13
+ return Promise.reject("All fetch attempts failed.");
14
+ }
15
+
16
+ const base = BENCHMARK_PATHS[attempts++];
17
+ const url = base + filename;
18
+
19
+ // First send a HEAD request to quietly check existence
20
+ return fetch(url, { method: "HEAD" })
21
+ .then((headRes) => {
22
+ if (!headRes.ok) throw new Error("HEAD check failed");
23
+ return fetch(url).then((res) => {
24
+ if (!res.ok) throw new Error(`GET failed from ${url}`);
25
+ return res.json();
26
+ });
27
+ })
28
+ .catch(() => tryFetch());
29
+ }
30
+
31
+ return tryFetch();
32
+ }
@@ -0,0 +1,2 @@
1
+ window.GITBRANCH = "{{branch}}";
2
+ window.GITSHA = "{{sha}}";
@@ -0,0 +1,128 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from typing import TYPE_CHECKING
6
+
7
+ from docutils import nodes
8
+ from sphinx.util.docutils import SphinxDirective
9
+ from sphinx.util.osutil import ensuredir
10
+
11
+ if TYPE_CHECKING:
12
+ from sphinx.application import Sphinx
13
+
14
+ here = os.path.dirname(os.path.abspath(__file__))
15
+
16
+ def check_bool(value):
17
+ if value.lower() in ["true", "false"]:
18
+ return value.lower() == "true"
19
+ return value
20
+
21
+ def query(argument):
22
+ if not argument.strip():
23
+ return {}
24
+ queries = {}
25
+ for query in argument.strip().split(","):
26
+ key, value = query.split("=")
27
+ queries[key] = check_bool(value)
28
+ return queries
29
+
30
+ def filters(argument):
31
+ filters: dict[str, list|bool] = {}
32
+ for filter in argument.strip().split(","):
33
+ if ":" in filter:
34
+ source, value = filter.split(":")
35
+ filters.setdefault(source, []).append(value)
36
+ else:
37
+ filters[filter] = True
38
+ return filters
39
+
40
+ def sortBy(argument):
41
+ return [k.strip() for k in argument.split(",")] if argument else []
42
+
43
+ class BenchmarkDetails(SphinxDirective):
44
+ required_arguments = 0
45
+ final_argument_whitespace = False
46
+ option_spec = {
47
+ "source": str,
48
+ }
49
+ has_content = False
50
+ def run(self):
51
+ source = self.options.get("source", "benchmark-summary")
52
+ html = f"""
53
+ <div
54
+ class='benchmark-details'
55
+ data-source='{source}'
56
+ </div>
57
+ """
58
+
59
+ return [nodes.raw("", html, format="html")]
60
+
61
+ class BenchmarkChart(SphinxDirective):
62
+ required_arguments = 0
63
+ final_argument_whitespace = False
64
+ option_spec = {
65
+ "source": str,
66
+ "query": query,
67
+ "filters": filters,
68
+ "sort": sortBy,
69
+ }
70
+ has_content = False
71
+ def run(self):
72
+ source = self.options.get("source", "benchmark-summary")
73
+ filters = self.options.get("filters", ["group"])
74
+ query = self.options.get("query", {})
75
+ sortBy = self.options.get("sort", [])
76
+
77
+ html = f"""
78
+ <div
79
+ class='benchmark-chart'
80
+ data-source='{source}'
81
+ data-filters='{json.dumps(filters)}'
82
+ data-query='{json.dumps(query)}'
83
+ data-sortBy='{json.dumps(sortBy)}'>
84
+ </div>
85
+ """
86
+
87
+ return [nodes.raw("", html, format="html")]
88
+
89
+ def render_js_template(app) -> None:
90
+ context = {
91
+ "branch": app.config.benchmark_git_context.get("branch", ""),
92
+ "sha": app.config.benchmark_git_context.get("sha", ""),
93
+ }
94
+
95
+ template = app.builder.templates.environment.get_template("git_info.js")
96
+ rendered_js = template.render(context)
97
+
98
+ out_dir = os.path.join(app.outdir, "_static", "js")
99
+ ensuredir(out_dir)
100
+ out_path = os.path.join(out_dir, "git_info.js")
101
+
102
+ with open(out_path, "w", encoding="utf-8") as f:
103
+ f.write(rendered_js)
104
+ app.add_js_file("js/git_info.js")
105
+
106
+ def setup(app: Sphinx):
107
+ app.add_directive("benchmark-chart", BenchmarkChart)
108
+ app.add_directive("benchmark-details", BenchmarkDetails)
109
+ app.add_config_value("benchmark_git_context", default={}, rebuild="env")
110
+ def add_assets(app, env) -> None:
111
+ static_path = os.path.join(here, "_static")
112
+ if static_path not in app.config.html_static_path:
113
+ app.config.html_static_path.append(static_path)
114
+ app.add_js_file("js/benchmark-chart.js")
115
+ app.add_js_file("js/benchmark-details.js", type="module")
116
+ app.add_js_file("js/benchmark-loader.js")
117
+ app.add_js_file("https://cdn.plot.ly/plotly-3.0.1.min.js")
118
+ app.add_css_file("benchmark-chart.css")
119
+ app.config.templates_path += [os.path.join(here, "_templates")]
120
+ app.connect("env-updated", add_assets)
121
+ app.connect("builder-inited", render_js_template)
122
+
123
+ return {
124
+ "version": "0.1",
125
+ "parallel_read_safe": True,
126
+ "parallel_write_safe": True,
127
+
128
+ }
@@ -37,6 +37,7 @@ For an overview of supported backends refer to :ref:`storage:storage backends`.
37
37
  quickstart
38
38
  strategies
39
39
  storage
40
+ performance
40
41
  async
41
42
  api
42
43
  custom-storage
@@ -0,0 +1,221 @@
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 when using
10
+ a rate limit of ``500/minute``. (For details about the benchmarking environment
11
+ please refer to :ref:`performance:benchmark run details`).
12
+
13
+
14
+ .. tab:: Hit
15
+
16
+ .. benchmark-chart::
17
+ :source: benchmark-summary
18
+ :query: limit=500 per 1 minute,group=hit,async=false
19
+ :sort: storage_type,strategy
20
+
21
+
22
+ .. tab:: Hit (Async)
23
+
24
+ .. benchmark-chart::
25
+ :source: benchmark-summary
26
+ :query: limit=500 per 1 minute,group=hit,async=true
27
+ :sort: storage_type,strategy
28
+
29
+
30
+
31
+ .. tab:: Test
32
+
33
+ .. benchmark-chart::
34
+ :source: benchmark-summary
35
+ :query: limit=500 per 1 minute,group=test,async=false
36
+ :sort: storage_type,strategy
37
+
38
+
39
+ .. tab:: Test (Async)
40
+
41
+ .. benchmark-chart::
42
+ :source: benchmark-summary
43
+ :query: limit=500 per 1 minute,group=test,async=true
44
+ :sort: storage_type,strategy
45
+
46
+
47
+
48
+ .. tab:: Get Window Stats
49
+
50
+ .. benchmark-chart::
51
+ :source: benchmark-summary
52
+ :query: limit=500 per 1 minute,group=get-window-stats,async=false
53
+ :sort: storage_type,strategy
54
+
55
+
56
+ .. tab:: Get Window Stats (Async)
57
+
58
+ .. benchmark-chart::
59
+ :source: benchmark-summary
60
+ :query: limit=500 per 1 minute,group=get-window-stats,async=true
61
+ :sort: storage_type,strategy
62
+
63
+
64
+ Performance implication of limit sizes
65
+ --------------------------------------
66
+
67
+ Though for :ref:`strategies:fixed window` and :ref:`strategies:sliding window counter` both the
68
+ storage cost and performance of operations remains constant when the limit window and size varies,
69
+ this is not true for :ref:`strategies:moving window` which maintains a complete log of successful
70
+ requests within the window.
71
+
72
+ The following benchmarks demonstrate the implications when using various limits.
73
+
74
+ Fixed Window
75
+ ~~~~~~~~~~~~
76
+
77
+ .. tab:: Hit
78
+
79
+ .. benchmark-chart::
80
+ :source: benchmark-summary
81
+ :query: async=false,group=hit,strategy=fixed-window
82
+ :sort: storage_type,limit
83
+
84
+
85
+ .. tab:: Hit (Async)
86
+
87
+ .. benchmark-chart::
88
+ :source: benchmark-summary
89
+ :query: async=true,group=hit,strategy=fixed-window
90
+ :sort: storage_type,limit
91
+
92
+
93
+ .. tab:: Test
94
+
95
+ .. benchmark-chart::
96
+ :source: benchmark-summary
97
+ :query: async=false,group=test,strategy=fixed-window
98
+ :sort: storage_type,limit
99
+
100
+
101
+ .. tab:: Test (Async)
102
+
103
+ .. benchmark-chart::
104
+ :source: benchmark-summary
105
+ :query: async=true,group=test,strategy=fixed-window
106
+ :sort: storage_type,limit
107
+
108
+
109
+ .. tab:: Get Window Stats
110
+
111
+ .. benchmark-chart::
112
+ :source: benchmark-summary
113
+ :query: async=false,group=get-window-stats,strategy=fixed-window
114
+ :sort: storage_type,limit
115
+
116
+ .. tab:: Get Window Stats (Async)
117
+
118
+ .. benchmark-chart::
119
+ :source: benchmark-summary
120
+ :query: async=true,group=get-window-stats,strategy=fixed-window
121
+ :sort: storage_type,limit
122
+
123
+ Moving Window
124
+ ~~~~~~~~~~~~~
125
+
126
+ .. tab:: Hit
127
+
128
+ .. benchmark-chart::
129
+ :source: benchmark-summary
130
+ :query: async=false,group=hit,strategy=moving-window
131
+ :sort: storage_type,limit
132
+
133
+ .. tab:: Hit (Async)
134
+
135
+ .. benchmark-chart::
136
+ :source: benchmark-summary
137
+ :query: async=true,group=hit,strategy=moving-window
138
+ :sort: storage_type,limit
139
+
140
+
141
+ .. tab:: Test
142
+
143
+ .. benchmark-chart::
144
+ :source: benchmark-summary
145
+ :query: async=false,group=test,strategy=moving-window
146
+ :sort: storage_type,limit
147
+
148
+ .. tab:: Test (Async)
149
+
150
+ .. benchmark-chart::
151
+ :source: benchmark-summary
152
+ :query: async=true,group=test,strategy=moving-window
153
+ :sort: storage_type,limit
154
+
155
+
156
+ .. tab:: Get Window Stats
157
+
158
+ .. benchmark-chart::
159
+ :source: benchmark-summary
160
+ :query: async=false,group=get-window-stats,strategy=moving-window
161
+ :sort: storage_type,limit
162
+
163
+ .. tab:: Get Window Stats (Async)
164
+
165
+ .. benchmark-chart::
166
+ :source: benchmark-summary
167
+ :query: async=true,group=get-window-stats,strategy=moving-window
168
+ :sort: storage_type,limit
169
+
170
+
171
+ Sliding Window
172
+ ~~~~~~~~~~~~~~
173
+
174
+ .. tab:: Hit
175
+
176
+ .. benchmark-chart::
177
+ :source: benchmark-summary
178
+ :query: async=false,group=hit,strategy=sliding-window
179
+ :sort: storage_type,limit
180
+
181
+ .. tab:: Hit (Async)
182
+
183
+ .. benchmark-chart::
184
+ :source: benchmark-summary
185
+ :query: async=true,group=hit,strategy=sliding-window
186
+ :sort: storage_type,limit
187
+
188
+ .. tab:: Test
189
+
190
+ .. benchmark-chart::
191
+ :source: benchmark-summary
192
+ :query: async=false,group=test,strategy=sliding-window
193
+ :sort: storage_type,limit
194
+
195
+ .. tab:: Test (Async)
196
+
197
+ .. benchmark-chart::
198
+ :source: benchmark-summary
199
+ :query: async=true,group=test,strategy=sliding-window
200
+ :sort: storage_type,limit
201
+
202
+
203
+ .. tab:: Get Window Stats
204
+
205
+ .. benchmark-chart::
206
+ :source: benchmark-summary
207
+ :query: async=false,group=get-window-stats,strategy=sliding-window
208
+ :sort: storage_type,limit
209
+
210
+ .. tab:: Get Window (Async)
211
+
212
+ .. benchmark-chart::
213
+ :source: benchmark-summary
214
+ :query: async=true,group=get-window-stats,strategy=sliding-window
215
+ :sort: storage_type,limit
216
+
217
+
218
+ Benchmark run details
219
+ ---------------------
220
+ .. benchmark-details::
221
+ :source: benchmark-summary
@@ -33,6 +33,15 @@ html_theme_options = {
33
33
  "font-stack--monospace": "Fira Code, monospace",
34
34
  "color-brand-primary": colors["purple2"],
35
35
  "color-brand-content": colors["blue2"],
36
+ "color-bg": colors["bg2"],
37
+ "color-fg": colors["fg2"],
38
+ "color-red": colors["red2"],
39
+ "color-orange": colors["orange2"],
40
+ "color-yellow": colors["yellow2"],
41
+ "color-green": colors["green2"],
42
+ "color-aqua": colors["aqua2"],
43
+ "color-blue": colors["blue2"],
44
+ "color-purple": colors["purple2"],
36
45
  },
37
46
  "dark_css_variables": {
38
47
  "color-brand-primary": colors["purple"],
@@ -43,6 +52,15 @@ html_theme_options = {
43
52
  "color-foreground-secondary": colors["bg1"],
44
53
  "color-highlighted-background": colors["yellow"],
45
54
  "color-highlight-on-target": colors["fg2"],
55
+ "color-bg": colors["fg1"],
56
+ "color-fg": colors["bg0"],
57
+ "color-red": colors["red"],
58
+ "color-orange": colors["orange"],
59
+ "color-yellow": colors["yellow"],
60
+ "color-green": colors["green"],
61
+ "color-aqua": colors["aqua"],
62
+ "color-blue": colors["blue"],
63
+ "color-purple": colors["purple"],
46
64
  },
47
65
  }
48
66
 
@@ -8,11 +8,11 @@ import json
8
8
 
9
9
  version_json = '''
10
10
  {
11
- "date": "2025-04-03T09:33:38-0700",
11
+ "date": "2025-04-08T15:55:28-0700",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "32d9fb7f2e290c890e52fd64b27550f59dd35583",
15
- "version": "4.6"
14
+ "full-revisionid": "47c207f7d014fe33f24688ccdff740c1e10654a7",
15
+ "version": "4.7.1"
16
16
  }
17
17
  ''' # END VERSION_JSON
18
18
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: limits
3
- Version: 4.6
3
+ Version: 4.7.1
4
4
  Summary: Rate limiting utilities
5
5
  Home-page: https://limits.readthedocs.org
6
6
  Author: Ali-Akber Saifee
@@ -16,11 +16,18 @@ doc/source/conf.py
16
16
  doc/source/custom-storage.rst
17
17
  doc/source/index.rst
18
18
  doc/source/installation.rst
19
+ doc/source/performance.rst
19
20
  doc/source/quickstart.rst
20
21
  doc/source/storage.rst
21
22
  doc/source/strategies.rst
22
23
  doc/source/theme_config.py
23
24
  doc/source/_static/custom.css
25
+ doc/source/ext/bench_chart.py
26
+ doc/source/ext/_static/benchmark-chart.css
27
+ doc/source/ext/_static/js/benchmark-chart.js
28
+ doc/source/ext/_static/js/benchmark-details.js
29
+ doc/source/ext/_static/js/benchmark-loader.js
30
+ doc/source/ext/_templates/git_info.js
24
31
  limits/__init__.py
25
32
  limits/_version.py
26
33
  limits/errors.py
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes