httpx-limiter 0.3.0__tar.gz → 0.4.0__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.
- {httpx_limiter-0.3.0 → httpx_limiter-0.4.0}/PKG-INFO +52 -38
- {httpx_limiter-0.3.0 → httpx_limiter-0.4.0}/README.md +49 -36
- {httpx_limiter-0.3.0 → httpx_limiter-0.4.0}/pyproject.toml +45 -59
- {httpx_limiter-0.3.0 → httpx_limiter-0.4.0}/src/httpx_limiter/__init__.py +2 -0
- {httpx_limiter-0.3.0 → httpx_limiter-0.4.0}/src/httpx_limiter/_version.py +16 -3
- httpx_limiter-0.4.0/src/httpx_limiter/abstract_rate_limiter_repository.py +74 -0
- httpx_limiter-0.4.0/src/httpx_limiter/async_limiter.py +121 -0
- {httpx_limiter-0.3.0 → httpx_limiter-0.4.0}/src/httpx_limiter/async_multi_rate_limited_transport.py +10 -2
- {httpx_limiter-0.3.0 → httpx_limiter-0.4.0}/src/httpx_limiter/async_rate_limited_transport.py +15 -10
- {httpx_limiter-0.3.0 → httpx_limiter-0.4.0}/src/httpx_limiter/rate.py +14 -6
- httpx_limiter-0.4.0/src/httpx_limiter/types.py +38 -0
- httpx_limiter-0.3.0/src/httpx_limiter/abstract_rate_limiter_repository.py +0 -53
- {httpx_limiter-0.3.0 → httpx_limiter-0.4.0}/.gitignore +0 -0
- {httpx_limiter-0.3.0 → httpx_limiter-0.4.0}/LICENSE +0 -0
- {httpx_limiter-0.3.0 → httpx_limiter-0.4.0}/src/httpx_limiter/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: httpx-limiter
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: A lightweight package that provides rate-limited httpx transports.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Midnighter/httpx-limiter
|
|
6
6
|
Project-URL: Documentation, https://httpx-limiter.readthedocs.io
|
|
@@ -26,17 +26,18 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
26
26
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
27
27
|
Classifier: Typing :: Typed
|
|
28
28
|
Requires-Python: >=3.10
|
|
29
|
-
Requires-Dist: aiolimiter~=1.2
|
|
30
29
|
Requires-Dist: httpx~=0.25
|
|
30
|
+
Requires-Dist: pyrate-limiter~=3.9
|
|
31
|
+
Requires-Dist: typing-extensions~=4.6; python_version < '3.11'
|
|
31
32
|
Description-Content-Type: text/markdown
|
|
32
33
|
|
|
33
34
|
# HTTPX Limiter
|
|
34
35
|
|
|
35
|
-
|
|
|
36
|
-
|
|
37
|
-
| Package
|
|
38
|
-
| Meta
|
|
39
|
-
| Automation |
|
|
36
|
+
| | |
|
|
37
|
+
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
38
|
+
| Package | [](https://pypi.org/project/httpx-limiter/) [](https://pypi.org/project/httpx-limiter/) |
|
|
39
|
+
| Meta | [](LICENSE) [](.github/CODE_OF_CONDUCT.md) [](https://mypy-lang.org/) [](https://github.com/ambv/black) [](https://github.com/astral-sh/ruff) |
|
|
40
|
+
| Automation | [](https://github.com/Midnighter/httpx-limiter/actions/workflows/main.yml) |
|
|
40
41
|
|
|
41
42
|
_A lightweight package that provides rate-limited httpx transports._
|
|
42
43
|
|
|
@@ -51,14 +52,16 @@ pip install httpx-limiter
|
|
|
51
52
|
|
|
52
53
|
## Tutorial
|
|
53
54
|
|
|
54
|
-
You can limit the number of requests made by an HTTPX client using the
|
|
55
|
-
provided in this package. That is useful in situations when you need
|
|
56
|
-
number of asynchronous requests against endpoints that implement
|
|
55
|
+
You can limit the number of requests made by an HTTPX client using the
|
|
56
|
+
transports provided in this package. That is useful in situations when you need
|
|
57
|
+
to make a large number of asynchronous requests against endpoints that implement
|
|
58
|
+
a rate limit.
|
|
57
59
|
|
|
58
60
|
### Single Rate Limit
|
|
59
61
|
|
|
60
|
-
The simplest use case is to apply a single rate limit to all requests. If you
|
|
61
|
-
able to make twenty requests per second, for example, use the
|
|
62
|
+
The simplest use case is to apply a single rate limit to all requests. If you
|
|
63
|
+
want to be able to make twenty requests per second, for example, use the
|
|
64
|
+
following code:
|
|
62
65
|
|
|
63
66
|
```python
|
|
64
67
|
import httpx
|
|
@@ -66,38 +69,48 @@ from httpx_limiter import AsyncRateLimitedTransport, Rate
|
|
|
66
69
|
|
|
67
70
|
async def main():
|
|
68
71
|
async with httpx.AsyncClient(
|
|
69
|
-
transport=AsyncRateLimitedTransport.create(
|
|
72
|
+
transport=AsyncRateLimitedTransport.create(Rate.create(magnitude=20)),
|
|
70
73
|
) as client:
|
|
71
74
|
response = await client.get("https://httpbin.org")
|
|
72
75
|
```
|
|
73
76
|
|
|
74
77
|
> [!IMPORTANT]
|
|
75
|
-
> Due to limitations in the design of the underlying leaky
|
|
76
|
-
>
|
|
77
|
-
>
|
|
78
|
-
>
|
|
79
|
-
>
|
|
80
|
-
>
|
|
81
|
-
>
|
|
78
|
+
> Due to limitations in the design of the underlying [leaky
|
|
79
|
+
> bucket](https://en.wikipedia.org/wiki/Leaky_bucket) implementation, which is
|
|
80
|
+
> used to implement the rate limiting, the magnitude of the rate is also the
|
|
81
|
+
> maximum capacity of the bucket. That means, if you set a rate that is larger
|
|
82
|
+
> than one, a burst of requests equal to that capacity will be allowed. If you
|
|
83
|
+
> do not want to allow any bursts, set the magnitude to one, but the duration to
|
|
84
|
+
> the inverse of your desired rate. If you want to allow twenty requests per
|
|
85
|
+
> second, for example, set the magnitude to 1 and the duration to 0.05 seconds.
|
|
86
|
+
>
|
|
87
|
+
> ```python
|
|
88
|
+
> Rate.create(magnitude=1, duration=1/20)
|
|
89
|
+
> ```
|
|
82
90
|
|
|
83
91
|
### Multiple Rate Limits
|
|
84
92
|
|
|
85
|
-
For more advanced use cases, you can apply different rate limits based on a
|
|
86
|
-
implementation of the `AbstractRateLimiterRepository`. There are two
|
|
87
|
-
that both get passed the current request. One method needs to
|
|
88
|
-
to apply, and the other method sets the rate limit
|
|
93
|
+
For more advanced use cases, you can apply different rate limits based on a
|
|
94
|
+
concrete implementation of the `AbstractRateLimiterRepository`. There are two
|
|
95
|
+
relevant methods that both get passed the current request. One method needs to
|
|
96
|
+
identify which rate limit to apply, and the other method sets the rate limit
|
|
97
|
+
itself. See the following example:
|
|
89
98
|
|
|
90
99
|
```python
|
|
91
100
|
import httpx
|
|
92
|
-
from httpx_limiter import
|
|
101
|
+
from httpx_limiter import (
|
|
102
|
+
AbstractRateLimiterRepository,
|
|
103
|
+
AsyncMultiRateLimitedTransport,
|
|
104
|
+
Rate
|
|
105
|
+
)
|
|
93
106
|
|
|
94
107
|
class DomainBasedRateLimiterRepository(AbstractRateLimiterRepository):
|
|
95
108
|
"""Apply different rate limits based on the domain being requested."""
|
|
96
|
-
|
|
109
|
+
|
|
97
110
|
def get_identifier(self, request: httpx.Request) -> str:
|
|
98
111
|
"""Return the domain as the identifier for rate limiting."""
|
|
99
112
|
return request.url.host
|
|
100
|
-
|
|
113
|
+
|
|
101
114
|
def get_rate(self, request: httpx.Request) -> Rate:
|
|
102
115
|
"""Apply the same, but independent rate limit to each domain."""
|
|
103
116
|
return Rate.create(magnitude=25)
|
|
@@ -109,15 +122,16 @@ client = httpx.AsyncClient(
|
|
|
109
122
|
)
|
|
110
123
|
```
|
|
111
124
|
|
|
112
|
-
> [!
|
|
113
|
-
> You are free to ignore the request
|
|
114
|
-
> of day to determine the rate limit.
|
|
125
|
+
> [!TIP]
|
|
126
|
+
> You are free to ignore the request parameter and use global information like
|
|
127
|
+
> the time of day to determine the rate limit.
|
|
115
128
|
|
|
116
129
|
```python
|
|
117
130
|
from datetime import datetime, timezone
|
|
131
|
+
from collections.abc import Sequence
|
|
118
132
|
|
|
119
133
|
import httpx
|
|
120
|
-
from httpx_limiter import AbstractRateLimiterRepository,
|
|
134
|
+
from httpx_limiter import AbstractRateLimiterRepository, Rate
|
|
121
135
|
|
|
122
136
|
class DayNightRateLimiterRepository(AbstractRateLimiterRepository):
|
|
123
137
|
"""Apply different rate limits based on the time of day."""
|
|
@@ -126,18 +140,18 @@ class DayNightRateLimiterRepository(AbstractRateLimiterRepository):
|
|
|
126
140
|
"""Identify whether it is currently day or night."""
|
|
127
141
|
if 6 <= datetime.now(tz=timezone.utc).hour < 18:
|
|
128
142
|
return "day"
|
|
129
|
-
|
|
143
|
+
|
|
130
144
|
return "night"
|
|
131
145
|
|
|
132
|
-
def
|
|
146
|
+
def get_rates(self, _: httpx.Request) -> Sequence[Rate]:
|
|
133
147
|
"""Apply different rate limits during the day or night."""
|
|
134
148
|
if self.get_identifier(_) == "day":
|
|
135
|
-
return Rate.create(magnitude=10)
|
|
136
|
-
|
|
137
|
-
return Rate.create(magnitude=100)
|
|
149
|
+
return [Rate.create(magnitude=10)]
|
|
150
|
+
|
|
151
|
+
return [Rate.create(magnitude=100)]
|
|
138
152
|
```
|
|
139
153
|
|
|
140
154
|
## Copyright
|
|
141
155
|
|
|
142
|
-
-
|
|
143
|
-
-
|
|
156
|
+
- Copyright © 2024, 2025 Moritz E. Beber.
|
|
157
|
+
- Free software distributed under the [Apache Software License 2.0](./LICENSE).
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# HTTPX Limiter
|
|
2
2
|
|
|
3
|
-
|
|
|
4
|
-
|
|
5
|
-
| Package
|
|
6
|
-
| Meta
|
|
7
|
-
| Automation |
|
|
3
|
+
| | |
|
|
4
|
+
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
5
|
+
| Package | [](https://pypi.org/project/httpx-limiter/) [](https://pypi.org/project/httpx-limiter/) |
|
|
6
|
+
| Meta | [](LICENSE) [](.github/CODE_OF_CONDUCT.md) [](https://mypy-lang.org/) [](https://github.com/ambv/black) [](https://github.com/astral-sh/ruff) |
|
|
7
|
+
| Automation | [](https://github.com/Midnighter/httpx-limiter/actions/workflows/main.yml) |
|
|
8
8
|
|
|
9
9
|
_A lightweight package that provides rate-limited httpx transports._
|
|
10
10
|
|
|
@@ -19,14 +19,16 @@ pip install httpx-limiter
|
|
|
19
19
|
|
|
20
20
|
## Tutorial
|
|
21
21
|
|
|
22
|
-
You can limit the number of requests made by an HTTPX client using the
|
|
23
|
-
provided in this package. That is useful in situations when you need
|
|
24
|
-
number of asynchronous requests against endpoints that implement
|
|
22
|
+
You can limit the number of requests made by an HTTPX client using the
|
|
23
|
+
transports provided in this package. That is useful in situations when you need
|
|
24
|
+
to make a large number of asynchronous requests against endpoints that implement
|
|
25
|
+
a rate limit.
|
|
25
26
|
|
|
26
27
|
### Single Rate Limit
|
|
27
28
|
|
|
28
|
-
The simplest use case is to apply a single rate limit to all requests. If you
|
|
29
|
-
able to make twenty requests per second, for example, use the
|
|
29
|
+
The simplest use case is to apply a single rate limit to all requests. If you
|
|
30
|
+
want to be able to make twenty requests per second, for example, use the
|
|
31
|
+
following code:
|
|
30
32
|
|
|
31
33
|
```python
|
|
32
34
|
import httpx
|
|
@@ -34,38 +36,48 @@ from httpx_limiter import AsyncRateLimitedTransport, Rate
|
|
|
34
36
|
|
|
35
37
|
async def main():
|
|
36
38
|
async with httpx.AsyncClient(
|
|
37
|
-
transport=AsyncRateLimitedTransport.create(
|
|
39
|
+
transport=AsyncRateLimitedTransport.create(Rate.create(magnitude=20)),
|
|
38
40
|
) as client:
|
|
39
41
|
response = await client.get("https://httpbin.org")
|
|
40
42
|
```
|
|
41
43
|
|
|
42
44
|
> [!IMPORTANT]
|
|
43
|
-
> Due to limitations in the design of the underlying leaky
|
|
44
|
-
>
|
|
45
|
-
>
|
|
46
|
-
>
|
|
47
|
-
>
|
|
48
|
-
>
|
|
49
|
-
>
|
|
45
|
+
> Due to limitations in the design of the underlying [leaky
|
|
46
|
+
> bucket](https://en.wikipedia.org/wiki/Leaky_bucket) implementation, which is
|
|
47
|
+
> used to implement the rate limiting, the magnitude of the rate is also the
|
|
48
|
+
> maximum capacity of the bucket. That means, if you set a rate that is larger
|
|
49
|
+
> than one, a burst of requests equal to that capacity will be allowed. If you
|
|
50
|
+
> do not want to allow any bursts, set the magnitude to one, but the duration to
|
|
51
|
+
> the inverse of your desired rate. If you want to allow twenty requests per
|
|
52
|
+
> second, for example, set the magnitude to 1 and the duration to 0.05 seconds.
|
|
53
|
+
>
|
|
54
|
+
> ```python
|
|
55
|
+
> Rate.create(magnitude=1, duration=1/20)
|
|
56
|
+
> ```
|
|
50
57
|
|
|
51
58
|
### Multiple Rate Limits
|
|
52
59
|
|
|
53
|
-
For more advanced use cases, you can apply different rate limits based on a
|
|
54
|
-
implementation of the `AbstractRateLimiterRepository`. There are two
|
|
55
|
-
that both get passed the current request. One method needs to
|
|
56
|
-
to apply, and the other method sets the rate limit
|
|
60
|
+
For more advanced use cases, you can apply different rate limits based on a
|
|
61
|
+
concrete implementation of the `AbstractRateLimiterRepository`. There are two
|
|
62
|
+
relevant methods that both get passed the current request. One method needs to
|
|
63
|
+
identify which rate limit to apply, and the other method sets the rate limit
|
|
64
|
+
itself. See the following example:
|
|
57
65
|
|
|
58
66
|
```python
|
|
59
67
|
import httpx
|
|
60
|
-
from httpx_limiter import
|
|
68
|
+
from httpx_limiter import (
|
|
69
|
+
AbstractRateLimiterRepository,
|
|
70
|
+
AsyncMultiRateLimitedTransport,
|
|
71
|
+
Rate
|
|
72
|
+
)
|
|
61
73
|
|
|
62
74
|
class DomainBasedRateLimiterRepository(AbstractRateLimiterRepository):
|
|
63
75
|
"""Apply different rate limits based on the domain being requested."""
|
|
64
|
-
|
|
76
|
+
|
|
65
77
|
def get_identifier(self, request: httpx.Request) -> str:
|
|
66
78
|
"""Return the domain as the identifier for rate limiting."""
|
|
67
79
|
return request.url.host
|
|
68
|
-
|
|
80
|
+
|
|
69
81
|
def get_rate(self, request: httpx.Request) -> Rate:
|
|
70
82
|
"""Apply the same, but independent rate limit to each domain."""
|
|
71
83
|
return Rate.create(magnitude=25)
|
|
@@ -77,15 +89,16 @@ client = httpx.AsyncClient(
|
|
|
77
89
|
)
|
|
78
90
|
```
|
|
79
91
|
|
|
80
|
-
> [!
|
|
81
|
-
> You are free to ignore the request
|
|
82
|
-
> of day to determine the rate limit.
|
|
92
|
+
> [!TIP]
|
|
93
|
+
> You are free to ignore the request parameter and use global information like
|
|
94
|
+
> the time of day to determine the rate limit.
|
|
83
95
|
|
|
84
96
|
```python
|
|
85
97
|
from datetime import datetime, timezone
|
|
98
|
+
from collections.abc import Sequence
|
|
86
99
|
|
|
87
100
|
import httpx
|
|
88
|
-
from httpx_limiter import AbstractRateLimiterRepository,
|
|
101
|
+
from httpx_limiter import AbstractRateLimiterRepository, Rate
|
|
89
102
|
|
|
90
103
|
class DayNightRateLimiterRepository(AbstractRateLimiterRepository):
|
|
91
104
|
"""Apply different rate limits based on the time of day."""
|
|
@@ -94,18 +107,18 @@ class DayNightRateLimiterRepository(AbstractRateLimiterRepository):
|
|
|
94
107
|
"""Identify whether it is currently day or night."""
|
|
95
108
|
if 6 <= datetime.now(tz=timezone.utc).hour < 18:
|
|
96
109
|
return "day"
|
|
97
|
-
|
|
110
|
+
|
|
98
111
|
return "night"
|
|
99
112
|
|
|
100
|
-
def
|
|
113
|
+
def get_rates(self, _: httpx.Request) -> Sequence[Rate]:
|
|
101
114
|
"""Apply different rate limits during the day or night."""
|
|
102
115
|
if self.get_identifier(_) == "day":
|
|
103
|
-
return Rate.create(magnitude=10)
|
|
104
|
-
|
|
105
|
-
return Rate.create(magnitude=100)
|
|
116
|
+
return [Rate.create(magnitude=10)]
|
|
117
|
+
|
|
118
|
+
return [Rate.create(magnitude=100)]
|
|
106
119
|
```
|
|
107
120
|
|
|
108
121
|
## Copyright
|
|
109
122
|
|
|
110
|
-
-
|
|
111
|
-
-
|
|
123
|
+
- Copyright © 2024, 2025 Moritz E. Beber.
|
|
124
|
+
- Free software distributed under the [Apache Software License 2.0](./LICENSE).
|
|
@@ -14,11 +14,9 @@ requires = ["hatchling", "hatch-vcs"]
|
|
|
14
14
|
name = "httpx-limiter"
|
|
15
15
|
dynamic = ["version"]
|
|
16
16
|
description = "A lightweight package that provides rate-limited httpx transports."
|
|
17
|
-
authors = [
|
|
18
|
-
{ name = "Moritz E. Beber", email = "midnighter@posteo.net" },
|
|
19
|
-
]
|
|
17
|
+
authors = [{ name = "Moritz E. Beber", email = "midnighter@posteo.net" }]
|
|
20
18
|
license = "Apache-2.0"
|
|
21
|
-
readme = {"file" = "README.md", "content-type" = "text/markdown"}
|
|
19
|
+
readme = { "file" = "README.md", "content-type" = "text/markdown" }
|
|
22
20
|
requires-python = ">=3.10"
|
|
23
21
|
# Please consult https://pypi.org/classifiers/ for a full list.
|
|
24
22
|
classifiers = [
|
|
@@ -37,15 +35,11 @@ classifiers = [
|
|
|
37
35
|
"Topic :: Internet :: WWW/HTTP",
|
|
38
36
|
"Typing :: Typed",
|
|
39
37
|
]
|
|
40
|
-
keywords = [
|
|
41
|
-
"httpx",
|
|
42
|
-
"limiter",
|
|
43
|
-
"rate-limit",
|
|
44
|
-
"leaky bucket",
|
|
45
|
-
]
|
|
38
|
+
keywords = ["httpx", "limiter", "rate-limit", "leaky bucket"]
|
|
46
39
|
dependencies = [
|
|
47
40
|
"httpx ~=0.25",
|
|
48
|
-
"
|
|
41
|
+
"pyrate-limiter ~=3.9",
|
|
42
|
+
"typing-extensions ~=4.6; python_version < '3.11'",
|
|
49
43
|
]
|
|
50
44
|
|
|
51
45
|
[project.urls]
|
|
@@ -76,17 +70,12 @@ testpaths = ["tests"]
|
|
|
76
70
|
markers = ["raises"]
|
|
77
71
|
|
|
78
72
|
[tool.coverage.paths]
|
|
79
|
-
source = [
|
|
80
|
-
"src/httpx_limiter",
|
|
81
|
-
"*/site-packages/httpx_limiter",
|
|
82
|
-
]
|
|
73
|
+
source = ["src/httpx_limiter", "*/site-packages/httpx_limiter"]
|
|
83
74
|
|
|
84
75
|
[tool.coverage.run]
|
|
85
76
|
branch = true
|
|
86
77
|
parallel = true
|
|
87
|
-
omit = [
|
|
88
|
-
"src/httpx_limiter/_version.py",
|
|
89
|
-
]
|
|
78
|
+
omit = ["src/httpx_limiter/_version.py"]
|
|
90
79
|
|
|
91
80
|
[tool.coverage.report]
|
|
92
81
|
exclude_lines = ["pragma: no cover"]
|
|
@@ -98,29 +87,27 @@ line-length = 88
|
|
|
98
87
|
[tool.ruff.lint]
|
|
99
88
|
select = ["ALL"]
|
|
100
89
|
ignore = [
|
|
101
|
-
"D107",
|
|
102
|
-
"D203",
|
|
103
|
-
"D212",
|
|
104
|
-
"D407",
|
|
105
|
-
"ANN002",
|
|
106
|
-
"ANN003",
|
|
107
|
-
]
|
|
108
|
-
exclude = [
|
|
109
|
-
"src/httpx_limiter/_version.py",
|
|
90
|
+
"D107", # 'Missing docstring in __init__' ignored because pydoclint wants us to document the class instead.
|
|
91
|
+
"D203", # '1 blank line required before class docstring' ignored because we want no blank line.
|
|
92
|
+
"D212", # 'Multi-line docstring summary should start at the first line' ignored because we want the summary to start on the second line.
|
|
93
|
+
"D407", # 'Missing dashed underline after section' ignored because Google style docstrings don't underline.
|
|
94
|
+
"ANN002", # 'Missing type annotation for {*args} in method'.
|
|
95
|
+
"ANN003", # 'Missing type annotation for {*kwargs} in method'.
|
|
110
96
|
]
|
|
97
|
+
exclude = ["src/httpx_limiter/_version.py"]
|
|
111
98
|
|
|
112
99
|
[tool.ruff.lint.extend-per-file-ignores]
|
|
113
100
|
"__init__.py" = [
|
|
114
|
-
"E401",
|
|
115
|
-
"E402",
|
|
116
|
-
"F401",
|
|
117
|
-
"I001",
|
|
101
|
+
"E401", # 'Multiple imports on one line'
|
|
102
|
+
"E402", # 'Module level import not at top of file'
|
|
103
|
+
"F401", # 'Imported but unused'
|
|
104
|
+
"I001", # 'Import block is un-sorted or un-formatted' ignored because we may have to import in a particular, not-alphabetical order.
|
|
118
105
|
]
|
|
119
106
|
"tests/**/*.py" = [
|
|
120
|
-
"S101",
|
|
107
|
+
"S101", # 'Use of assert detected' ignored because we are using pytest.
|
|
121
108
|
"INP001", # 'Insecure input' ignored because we are testing.
|
|
122
109
|
"ANN201", # 'Missing type annotation for {return}' ignored because all tests return `None`.
|
|
123
|
-
"PLR2004",
|
|
110
|
+
"PLR2004", # Magic numbers are okay in tests.
|
|
124
111
|
]
|
|
125
112
|
|
|
126
113
|
[tool.ruff.lint.isort]
|
|
@@ -143,9 +130,7 @@ exclude = "_version.py"
|
|
|
143
130
|
|
|
144
131
|
# Stop mypy from complaining about missing types from imports.
|
|
145
132
|
[[tool.mypy.overrides]]
|
|
146
|
-
module = [
|
|
147
|
-
"limiter",
|
|
148
|
-
]
|
|
133
|
+
module = ["limiter"]
|
|
149
134
|
ignore_missing_imports = true
|
|
150
135
|
|
|
151
136
|
#[tool.pydantic-mypy]
|
|
@@ -170,12 +155,13 @@ extra-dependencies = [
|
|
|
170
155
|
"ruff",
|
|
171
156
|
]
|
|
172
157
|
|
|
158
|
+
[tool.hatch.envs.dev.env-vars]
|
|
159
|
+
PYTHONDEVMODE = "1"
|
|
160
|
+
PYTHONASYNCIODEBUG = "1"
|
|
161
|
+
|
|
173
162
|
[tool.hatch.envs.style]
|
|
174
163
|
description = """Check the style of the codebase."""
|
|
175
|
-
dependencies = [
|
|
176
|
-
"pydoclint ~=0.6",
|
|
177
|
-
"ruff",
|
|
178
|
-
]
|
|
164
|
+
dependencies = ["pydoclint", "ruff"]
|
|
179
165
|
detached = true
|
|
180
166
|
installer = "uv"
|
|
181
167
|
|
|
@@ -187,18 +173,14 @@ check = ["docstrings", "code"]
|
|
|
187
173
|
|
|
188
174
|
[tool.hatch.envs.audit]
|
|
189
175
|
description = """Check dependencies for security vulnerabilities."""
|
|
190
|
-
extra-dependencies = [
|
|
191
|
-
"pip-audit",
|
|
192
|
-
]
|
|
176
|
+
extra-dependencies = ["pip-audit"]
|
|
193
177
|
|
|
194
178
|
[tool.hatch.envs.audit.scripts]
|
|
195
179
|
check = ["pip-audit"]
|
|
196
180
|
|
|
197
181
|
[tool.hatch.envs.types]
|
|
198
182
|
description = """Check the static types of the codebase."""
|
|
199
|
-
dependencies = [
|
|
200
|
-
"mypy",
|
|
201
|
-
]
|
|
183
|
+
dependencies = ["mypy"]
|
|
202
184
|
|
|
203
185
|
[tool.hatch.envs.types.scripts]
|
|
204
186
|
check = "mypy src/httpx_limiter"
|
|
@@ -209,26 +191,22 @@ extra-dependencies = [
|
|
|
209
191
|
"mkdocs-material ~=9.5",
|
|
210
192
|
"mkdocstrings[python] ~=0.24",
|
|
211
193
|
"mkdocs-awesome-pages-plugin ~=2.9",
|
|
194
|
+
"mike ~=2.1",
|
|
212
195
|
]
|
|
213
196
|
|
|
214
197
|
[tool.hatch.envs.docs.scripts]
|
|
215
198
|
build = "mkdocs build {args:--clean --strict}"
|
|
216
199
|
serve = "mkdocs serve {args}"
|
|
200
|
+
deploy = "mike deploy {args}"
|
|
217
201
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
#]
|
|
224
|
-
#detached = true
|
|
202
|
+
[tool.hatch.envs.distribution]
|
|
203
|
+
description = """Test the source and wheel distribution of the package."""
|
|
204
|
+
dependencies = ["pip", "twine"]
|
|
205
|
+
detached = true
|
|
206
|
+
installer = "uv"
|
|
225
207
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
# "pip check",
|
|
229
|
-
# "hatch build {args:--clean}",
|
|
230
|
-
# "twine check dist/*",
|
|
231
|
-
#]
|
|
208
|
+
[tool.hatch.envs.distribution.scripts]
|
|
209
|
+
check = ["pip check", "hatch build {args:--clean}", "twine check dist/*"]
|
|
232
210
|
|
|
233
211
|
[tool.hatch.envs.unit-tests]
|
|
234
212
|
description = """Run the unit tests."""
|
|
@@ -240,6 +218,10 @@ extra-dependencies = [
|
|
|
240
218
|
"pytest-raises",
|
|
241
219
|
]
|
|
242
220
|
|
|
221
|
+
[tool.hatch.envs.unit-tests.env-vars]
|
|
222
|
+
PYTHONDEVMODE = "1"
|
|
223
|
+
PYTHONASYNCIODEBUG = "1"
|
|
224
|
+
|
|
243
225
|
[[tool.hatch.envs.unit-tests.matrix]]
|
|
244
226
|
python = ["3.10", "3.11", "3.12", "3.13"]
|
|
245
227
|
|
|
@@ -256,6 +238,10 @@ extra-dependencies = [
|
|
|
256
238
|
"pytest-raises",
|
|
257
239
|
]
|
|
258
240
|
|
|
241
|
+
[tool.hatch.envs.acceptance-tests.env-vars]
|
|
242
|
+
PYTHONDEVMODE = "1"
|
|
243
|
+
PYTHONASYNCIODEBUG = "1"
|
|
244
|
+
|
|
259
245
|
[[tool.hatch.envs.acceptance-tests.matrix]]
|
|
260
246
|
python = ["3.10", "3.11", "3.12", "3.13"]
|
|
261
247
|
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
|
|
16
16
|
"""Provide top level symbols."""
|
|
17
17
|
|
|
18
|
+
from .types import HTTPXAsyncHTTPTransportKeywordArguments
|
|
18
19
|
from .rate import Number, Rate
|
|
20
|
+
from .async_limiter import AsyncLimiter, PyRateLimiterKeywordArguments
|
|
19
21
|
from .async_rate_limited_transport import AsyncRateLimitedTransport
|
|
20
22
|
from .abstract_rate_limiter_repository import AbstractRateLimiterRepository
|
|
21
23
|
from .async_multi_rate_limited_transport import AsyncMultiRateLimitedTransport
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
# file generated by setuptools-scm
|
|
2
2
|
# don't change, don't track in version control
|
|
3
3
|
|
|
4
|
-
__all__ = [
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
5
12
|
|
|
6
13
|
TYPE_CHECKING = False
|
|
7
14
|
if TYPE_CHECKING:
|
|
@@ -9,13 +16,19 @@ if TYPE_CHECKING:
|
|
|
9
16
|
from typing import Union
|
|
10
17
|
|
|
11
18
|
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
12
20
|
else:
|
|
13
21
|
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
14
23
|
|
|
15
24
|
version: str
|
|
16
25
|
__version__: str
|
|
17
26
|
__version_tuple__: VERSION_TUPLE
|
|
18
27
|
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
19
30
|
|
|
20
|
-
__version__ = version = '0.
|
|
21
|
-
__version_tuple__ = version_tuple = (0,
|
|
31
|
+
__version__ = version = '0.4.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 4, 0)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Copyright (c) 2025 Moritz E. Beber
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
4
|
+
# use this file except in compliance with the License. You may obtain a copy of
|
|
5
|
+
# the License at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
11
|
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
12
|
+
# License for the specific language governing permissions and limitations under
|
|
13
|
+
# the License.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
"""Provide an abstract repository for rate limiters."""
|
|
17
|
+
|
|
18
|
+
from abc import ABC, abstractmethod
|
|
19
|
+
from collections.abc import Sequence
|
|
20
|
+
|
|
21
|
+
import httpx
|
|
22
|
+
|
|
23
|
+
from .async_limiter import AsyncLimiter, PyRateLimiterKeywordArguments
|
|
24
|
+
from .rate import Rate
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AbstractRateLimiterRepository(ABC):
|
|
28
|
+
"""
|
|
29
|
+
Define the abstract repository for rate limiters.
|
|
30
|
+
|
|
31
|
+
This abstract base class provides a framework for managing rate limiters
|
|
32
|
+
based on HTTP requests. It maintains a cache of rate limiters and provides
|
|
33
|
+
methods to retrieve request-specific identifiers, rates, and limiters.
|
|
34
|
+
|
|
35
|
+
Subclasses must implement methods to determine how requests are identified
|
|
36
|
+
and what rate limits should be applied to them.
|
|
37
|
+
|
|
38
|
+
Methods:
|
|
39
|
+
get_identifier: Return a request-specific identifier.
|
|
40
|
+
get_rates: Return one or more request-specific rates.
|
|
41
|
+
get: Return a request-specific rate limiter.
|
|
42
|
+
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, **kwargs: dict[str, object]) -> None:
|
|
46
|
+
super().__init__(**kwargs)
|
|
47
|
+
self._limiters: dict[str, AsyncLimiter] = {}
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def get_identifier(self, request: httpx.Request) -> str:
|
|
51
|
+
"""Return a request-specific identifier."""
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def get_rates(self, request: httpx.Request) -> Sequence[Rate]:
|
|
55
|
+
"""Return one or more request-specific rates."""
|
|
56
|
+
|
|
57
|
+
def _get_limiter_kwargs(
|
|
58
|
+
self,
|
|
59
|
+
request: httpx.Request, # noqa: ARG002
|
|
60
|
+
) -> PyRateLimiterKeywordArguments:
|
|
61
|
+
"""Return the keyword arguments for creating a rate limiter."""
|
|
62
|
+
return {}
|
|
63
|
+
|
|
64
|
+
def get(self, request: httpx.Request) -> AsyncLimiter:
|
|
65
|
+
"""Return a request-specific rate limiter."""
|
|
66
|
+
identifier = self.get_identifier(request)
|
|
67
|
+
|
|
68
|
+
if identifier not in self._limiters:
|
|
69
|
+
self._limiters[identifier] = AsyncLimiter.create(
|
|
70
|
+
*self.get_rates(request),
|
|
71
|
+
**self._get_limiter_kwargs(request),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return self._limiters[identifier]
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Copyright (c) 2025 Moritz E. Beber
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
4
|
+
# use this file except in compliance with the License. You may obtain a copy of
|
|
5
|
+
# the License at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
11
|
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
12
|
+
# License for the specific language governing permissions and limitations under
|
|
13
|
+
# the License.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
"""Provide an asynchronous rate-limited transport."""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import sys
|
|
21
|
+
from typing import TYPE_CHECKING, TypedDict
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if sys.version_info < (3, 11):
|
|
25
|
+
from typing_extensions import Unpack
|
|
26
|
+
else:
|
|
27
|
+
from typing import Unpack
|
|
28
|
+
|
|
29
|
+
from pyrate_limiter import (
|
|
30
|
+
AbstractClock,
|
|
31
|
+
BucketAsyncWrapper,
|
|
32
|
+
Duration,
|
|
33
|
+
InMemoryBucket,
|
|
34
|
+
Limiter,
|
|
35
|
+
TimeAsyncClock,
|
|
36
|
+
validate_rate_list,
|
|
37
|
+
)
|
|
38
|
+
from pyrate_limiter import Rate as PyRate
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
42
|
+
from types import TracebackType
|
|
43
|
+
|
|
44
|
+
from .rate import Rate
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class PyRateLimiterKeywordArguments(TypedDict, total=False):
|
|
48
|
+
"""Keyword arguments for the pyrate limiter."""
|
|
49
|
+
|
|
50
|
+
clock: AbstractClock
|
|
51
|
+
raise_when_fail: bool
|
|
52
|
+
max_delay: int | Duration | None
|
|
53
|
+
retry_until_max_delay: bool
|
|
54
|
+
buffer_ms: int
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class AsyncLimiter:
|
|
58
|
+
"""
|
|
59
|
+
Define an asynchronous limiter that composes the pyrate limiter.
|
|
60
|
+
|
|
61
|
+
This class encapsulates the creation and configuration of a pyrate-limiter
|
|
62
|
+
Limiter with appropriate async bucket wrapper and settings.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
limiter: An instance of an asynchronous pyrate limiter.
|
|
66
|
+
**kwargs: Additional keyword arguments for the parent classes.
|
|
67
|
+
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(self, *, limiter: Limiter, **kwargs: dict[str, object]) -> None:
|
|
71
|
+
super().__init__(**kwargs)
|
|
72
|
+
self._limiter = limiter
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def create(
|
|
76
|
+
cls,
|
|
77
|
+
*rates: Rate,
|
|
78
|
+
**kwargs: Unpack[PyRateLimiterKeywordArguments],
|
|
79
|
+
) -> AsyncLimiter:
|
|
80
|
+
"""Create an instance of AsyncLimiter."""
|
|
81
|
+
if not rates:
|
|
82
|
+
msg = "At least one rate must be provided."
|
|
83
|
+
raise ValueError(msg)
|
|
84
|
+
|
|
85
|
+
rate_limits = [
|
|
86
|
+
PyRate(limit=rate.magnitude, interval=rate.in_milliseconds())
|
|
87
|
+
for rate in rates
|
|
88
|
+
]
|
|
89
|
+
if not validate_rate_list(rates=rate_limits):
|
|
90
|
+
url = "https://pyratelimiter.readthedocs.io/en/latest/#defining-rate-limits-and-buckets"
|
|
91
|
+
msg = (
|
|
92
|
+
f"Invalid ordering of rates provided {rate_limits}. Please read "
|
|
93
|
+
f"{url} for more information."
|
|
94
|
+
)
|
|
95
|
+
raise ValueError(msg)
|
|
96
|
+
|
|
97
|
+
limiter = Limiter(
|
|
98
|
+
argument=BucketAsyncWrapper(InMemoryBucket(rate_limits)),
|
|
99
|
+
clock=kwargs.get("clock", TimeAsyncClock()),
|
|
100
|
+
raise_when_fail=kwargs.get("raise_when_fail", False),
|
|
101
|
+
max_delay=kwargs.get("max_delay", Duration.HOUR),
|
|
102
|
+
retry_until_max_delay=kwargs.get("retry_until_max_delay", False),
|
|
103
|
+
buffer_ms=kwargs.get("buffer_ms", 50),
|
|
104
|
+
)
|
|
105
|
+
return cls(limiter=limiter)
|
|
106
|
+
|
|
107
|
+
async def __aenter__(self) -> AsyncLimiter: # noqa: PYI034
|
|
108
|
+
"""Acquire a token upon entering the asynchronous context."""
|
|
109
|
+
# Keep trying to acquire, let timeouts be handled externally.
|
|
110
|
+
while not (await self._limiter.try_acquire_async("httpx-limiter")):
|
|
111
|
+
pass # pragma: no cover
|
|
112
|
+
|
|
113
|
+
return self
|
|
114
|
+
|
|
115
|
+
async def __aexit__(
|
|
116
|
+
self,
|
|
117
|
+
_exc_type: type[BaseException] | None,
|
|
118
|
+
_exc_val: BaseException | None,
|
|
119
|
+
_exc_tb: TracebackType | None,
|
|
120
|
+
) -> None:
|
|
121
|
+
"""Exit the asynchronous context."""
|
{httpx_limiter-0.3.0 → httpx_limiter-0.4.0}/src/httpx_limiter/async_multi_rate_limited_transport.py
RENAMED
|
@@ -17,13 +17,21 @@
|
|
|
17
17
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
+
import sys
|
|
20
21
|
from typing import TYPE_CHECKING
|
|
21
22
|
|
|
23
|
+
|
|
24
|
+
if sys.version_info < (3, 11):
|
|
25
|
+
from typing_extensions import Unpack
|
|
26
|
+
else:
|
|
27
|
+
from typing import Unpack
|
|
28
|
+
|
|
22
29
|
import httpx
|
|
23
30
|
|
|
24
31
|
|
|
25
32
|
if TYPE_CHECKING: # pragma: no cover
|
|
26
33
|
from .abstract_rate_limiter_repository import AbstractRateLimiterRepository
|
|
34
|
+
from .types import HTTPXAsyncHTTPTransportKeywordArguments
|
|
27
35
|
|
|
28
36
|
|
|
29
37
|
class AsyncMultiRateLimitedTransport(httpx.AsyncBaseTransport):
|
|
@@ -40,7 +48,7 @@ class AsyncMultiRateLimitedTransport(httpx.AsyncBaseTransport):
|
|
|
40
48
|
*,
|
|
41
49
|
repository: AbstractRateLimiterRepository,
|
|
42
50
|
transport: httpx.AsyncBaseTransport,
|
|
43
|
-
**kwargs,
|
|
51
|
+
**kwargs: dict[str, object],
|
|
44
52
|
) -> None:
|
|
45
53
|
super().__init__(**kwargs)
|
|
46
54
|
self._repo = repository
|
|
@@ -51,7 +59,7 @@ class AsyncMultiRateLimitedTransport(httpx.AsyncBaseTransport):
|
|
|
51
59
|
cls,
|
|
52
60
|
*,
|
|
53
61
|
repository: AbstractRateLimiterRepository,
|
|
54
|
-
**kwargs:
|
|
62
|
+
**kwargs: Unpack[HTTPXAsyncHTTPTransportKeywordArguments],
|
|
55
63
|
) -> AsyncMultiRateLimitedTransport:
|
|
56
64
|
"""
|
|
57
65
|
Create an instance of an asynchronous multiple rate-limited transport.
|
{httpx_limiter-0.3.0 → httpx_limiter-0.4.0}/src/httpx_limiter/async_rate_limited_transport.py
RENAMED
|
@@ -17,14 +17,23 @@
|
|
|
17
17
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
+
import sys
|
|
20
21
|
from typing import TYPE_CHECKING
|
|
21
22
|
|
|
23
|
+
|
|
24
|
+
if sys.version_info < (3, 11):
|
|
25
|
+
from typing_extensions import Unpack
|
|
26
|
+
else:
|
|
27
|
+
from typing import Unpack
|
|
28
|
+
|
|
22
29
|
import httpx
|
|
23
|
-
|
|
30
|
+
|
|
31
|
+
from .async_limiter import AsyncLimiter
|
|
24
32
|
|
|
25
33
|
|
|
26
34
|
if TYPE_CHECKING: # pragma: no cover
|
|
27
35
|
from .rate import Rate
|
|
36
|
+
from .types import HTTPXAsyncHTTPTransportKeywordArguments
|
|
28
37
|
|
|
29
38
|
|
|
30
39
|
class AsyncRateLimitedTransport(httpx.AsyncBaseTransport):
|
|
@@ -42,7 +51,7 @@ class AsyncRateLimitedTransport(httpx.AsyncBaseTransport):
|
|
|
42
51
|
*,
|
|
43
52
|
limiter: AsyncLimiter,
|
|
44
53
|
transport: httpx.AsyncBaseTransport,
|
|
45
|
-
**kwargs,
|
|
54
|
+
**kwargs: dict[str, object],
|
|
46
55
|
) -> None:
|
|
47
56
|
super().__init__(**kwargs)
|
|
48
57
|
self._limiter = limiter
|
|
@@ -51,9 +60,8 @@ class AsyncRateLimitedTransport(httpx.AsyncBaseTransport):
|
|
|
51
60
|
@classmethod
|
|
52
61
|
def create(
|
|
53
62
|
cls,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
**kwargs: dict,
|
|
63
|
+
*rates: Rate,
|
|
64
|
+
**kwargs: Unpack[HTTPXAsyncHTTPTransportKeywordArguments],
|
|
57
65
|
) -> AsyncRateLimitedTransport:
|
|
58
66
|
"""
|
|
59
67
|
Create an instance of asynchronous rate-limited transport.
|
|
@@ -63,7 +71,7 @@ class AsyncRateLimitedTransport(httpx.AsyncBaseTransport):
|
|
|
63
71
|
That transport is passed any additional keyword arguments.
|
|
64
72
|
|
|
65
73
|
Args:
|
|
66
|
-
|
|
74
|
+
*rates: One or more rate limits to apply.
|
|
67
75
|
**kwargs: Additional keyword arguments are used in the construction of an
|
|
68
76
|
`httpx.AsyncHTTPTransport`.
|
|
69
77
|
|
|
@@ -72,10 +80,7 @@ class AsyncRateLimitedTransport(httpx.AsyncBaseTransport):
|
|
|
72
80
|
|
|
73
81
|
"""
|
|
74
82
|
return cls(
|
|
75
|
-
limiter=AsyncLimiter(
|
|
76
|
-
max_rate=rate.magnitude,
|
|
77
|
-
time_period=rate.in_seconds(),
|
|
78
|
-
),
|
|
83
|
+
limiter=AsyncLimiter.create(*rates),
|
|
79
84
|
transport=httpx.AsyncHTTPTransport(**kwargs), # type: ignore[arg-type]
|
|
80
85
|
)
|
|
81
86
|
|
|
@@ -27,19 +27,27 @@ Number = float | int
|
|
|
27
27
|
class Rate(NamedTuple):
|
|
28
28
|
"""Define the rate."""
|
|
29
29
|
|
|
30
|
-
magnitude:
|
|
30
|
+
magnitude: int
|
|
31
31
|
duration: timedelta
|
|
32
32
|
|
|
33
33
|
@classmethod
|
|
34
|
-
def create(cls, magnitude:
|
|
34
|
+
def create(cls, magnitude: int = 1, duration: timedelta | Number = 1) -> Rate:
|
|
35
35
|
"""Create a rate."""
|
|
36
|
-
magnitude =
|
|
36
|
+
magnitude = int(magnitude)
|
|
37
|
+
|
|
38
|
+
if not (magnitude > 0):
|
|
39
|
+
msg = f"Magnitude must be strictly positive (provided {magnitude=})."
|
|
40
|
+
raise ValueError(msg)
|
|
37
41
|
|
|
38
42
|
if not isinstance(duration, timedelta):
|
|
39
43
|
duration = timedelta(seconds=float(duration))
|
|
40
44
|
|
|
45
|
+
if not (duration > timedelta(seconds=0)):
|
|
46
|
+
msg = f"Duration must be strictly positive (provided {duration=})."
|
|
47
|
+
raise ValueError(msg)
|
|
48
|
+
|
|
41
49
|
return cls(magnitude=magnitude, duration=duration)
|
|
42
50
|
|
|
43
|
-
def
|
|
44
|
-
"""Return the duration in unit
|
|
45
|
-
return self.duration.total_seconds()
|
|
51
|
+
def in_milliseconds(self) -> int:
|
|
52
|
+
"""Return the duration in unit milliseconds."""
|
|
53
|
+
return int(self.duration.total_seconds() * 1_000)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Copyright (c) 2025 Moritz E. Beber
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
4
|
+
# use this file except in compliance with the License. You may obtain a copy of
|
|
5
|
+
# the License at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
11
|
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
12
|
+
# License for the specific language governing permissions and limitations under
|
|
13
|
+
# the License.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
"""Provide additional type definitions."""
|
|
17
|
+
|
|
18
|
+
import ssl
|
|
19
|
+
import typing
|
|
20
|
+
from typing import TypedDict
|
|
21
|
+
|
|
22
|
+
import httpx
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class HTTPXAsyncHTTPTransportKeywordArguments(TypedDict, total=False):
|
|
26
|
+
"""Keyword arguments for the httpx.AsyncHTTPTransport constructor."""
|
|
27
|
+
|
|
28
|
+
verify: ssl.SSLContext | str | bool
|
|
29
|
+
cert: httpx._types.CertTypes | None
|
|
30
|
+
trust_env: bool
|
|
31
|
+
http1: bool
|
|
32
|
+
http2: bool
|
|
33
|
+
limits: httpx._config.Limits
|
|
34
|
+
proxy: httpx._types.ProxyTypes | None
|
|
35
|
+
uds: str | None
|
|
36
|
+
local_address: str | None
|
|
37
|
+
retries: int
|
|
38
|
+
socket_options: typing.Iterable[httpx._transports.default.SOCKET_OPTION] | None
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
# Copyright (c) 2025 Moritz E. Beber
|
|
2
|
-
#
|
|
3
|
-
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
4
|
-
# use this file except in compliance with the License. You may obtain a copy of
|
|
5
|
-
# the License at
|
|
6
|
-
#
|
|
7
|
-
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
#
|
|
9
|
-
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
11
|
-
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
12
|
-
# License for the specific language governing permissions and limitations under
|
|
13
|
-
# the License.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"""Provide an abstract repository for rate limiters."""
|
|
17
|
-
|
|
18
|
-
from abc import ABC, abstractmethod
|
|
19
|
-
|
|
20
|
-
import httpx
|
|
21
|
-
from aiolimiter import AsyncLimiter
|
|
22
|
-
|
|
23
|
-
from .rate import Rate
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class AbstractRateLimiterRepository(ABC):
|
|
27
|
-
"""Define the abstract repository for rate limiters."""
|
|
28
|
-
|
|
29
|
-
def __init__(self, **kwargs) -> None:
|
|
30
|
-
super().__init__(**kwargs)
|
|
31
|
-
self._limiters: dict[str, AsyncLimiter] = {}
|
|
32
|
-
|
|
33
|
-
@abstractmethod
|
|
34
|
-
def get_identifier(self, request: httpx.Request) -> str:
|
|
35
|
-
"""Return a request-specific identifier."""
|
|
36
|
-
|
|
37
|
-
@abstractmethod
|
|
38
|
-
def get_rate(self, request: httpx.Request) -> Rate:
|
|
39
|
-
"""Return a request-specific rate."""
|
|
40
|
-
|
|
41
|
-
def get(self, request: httpx.Request) -> AsyncLimiter:
|
|
42
|
-
"""Return a request-specific rate limiter."""
|
|
43
|
-
identifier = self.get_identifier(request)
|
|
44
|
-
|
|
45
|
-
if identifier not in self._limiters:
|
|
46
|
-
rate = self.get_rate(request)
|
|
47
|
-
self._limiters[identifier] = AsyncLimiter(
|
|
48
|
-
max_rate=rate.magnitude,
|
|
49
|
-
time_period=rate.in_seconds(),
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
return self._limiters[identifier]
|
|
53
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|