httpx-limiter 0.2.0__tar.gz → 0.3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: httpx-limiter
3
- Version: 0.2.0
3
+ Version: 0.3.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
@@ -49,12 +49,16 @@ Install it, for example, with
49
49
  pip install httpx-limiter
50
50
  ```
51
51
 
52
- ## Usage
52
+ ## Tutorial
53
53
 
54
- For situations when you need to make a large number of asynchronous request with
55
- a controlled number of requests per unit time, you can apply rate limiting to an
56
- HTTPX client using the provided transport. If you want to be able to make ten
57
- requests per second, for example, use the following:
54
+ You can limit the number of requests made by an HTTPX client using the transports
55
+ provided in this package. That is useful in situations when you need to make a large
56
+ number of asynchronous requests against endpoints that implement a rate limit,
57
+
58
+ ### Single Rate Limit
59
+
60
+ The simplest use case is to apply a single rate limit to all requests. If you want to be
61
+ able to make twenty requests per second, for example, use the following code:
58
62
 
59
63
  ```python
60
64
  import httpx
@@ -62,11 +66,77 @@ from httpx_limiter import AsyncRateLimitedTransport, Rate
62
66
 
63
67
  async def main():
64
68
  async with httpx.AsyncClient(
65
- transport=AsyncRateLimitedTransport.create(rate=Rate.create(magnitude=10)),
69
+ transport=AsyncRateLimitedTransport.create(rate=Rate.create(magnitude=20)),
66
70
  ) as client:
67
71
  response = await client.get("https://httpbin.org")
68
72
  ```
69
73
 
74
+ > [!IMPORTANT]
75
+ > Due to limitations in the design of the underlying leaky bucket implementation,
76
+ > which is used to implement the rate limiting, the magnitude of the rate is also the
77
+ > maximum capacity of the bucket. That means, if you set a rate that is larger than one,
78
+ > a burst of requests equal to that capacity will be allowed. If you do not want to
79
+ > allow any bursts, set the magnitude to one, but the duration to the inverse of your
80
+ > desired rate. If you want to allow twenty requests per second, for example, set the
81
+ > magnitude to 1 and the duration to 0.05 seconds.
82
+
83
+ ### Multiple Rate Limits
84
+
85
+ For more advanced use cases, you can apply different rate limits based on a concrete
86
+ implementation of the `AbstractRateLimiterRepository`. There are two relevant methods
87
+ that both get passed the current request. One method needs to identify which rate limit
88
+ to apply, and the other method sets the rate limit itself. See the following example:
89
+
90
+ ```python
91
+ import httpx
92
+ from httpx_limiter import AbstractRateLimiterRepository, AsyncMultiRateLimitedTransport, Rate
93
+
94
+ class DomainBasedRateLimiterRepository(AbstractRateLimiterRepository):
95
+ """Apply different rate limits based on the domain being requested."""
96
+
97
+ def get_identifier(self, request: httpx.Request) -> str:
98
+ """Return the domain as the identifier for rate limiting."""
99
+ return request.url.host
100
+
101
+ def get_rate(self, request: httpx.Request) -> Rate:
102
+ """Apply the same, but independent rate limit to each domain."""
103
+ return Rate.create(magnitude=25)
104
+
105
+ client = httpx.AsyncClient(
106
+ transport=AsyncMultiRateLimitedTransport.create(
107
+ repository=DomainBasedRateLimiterRepository(),
108
+ ),
109
+ )
110
+ ```
111
+
112
+ > [!NOTE]
113
+ > You are free to ignore the request argument and use global information like the time
114
+ > of day to determine the rate limit.
115
+
116
+ ```python
117
+ from datetime import datetime, timezone
118
+
119
+ import httpx
120
+ from httpx_limiter import AbstractRateLimiterRepository, AsyncMultiRateLimitedTransport, Rate
121
+
122
+ class DayNightRateLimiterRepository(AbstractRateLimiterRepository):
123
+ """Apply different rate limits based on the time of day."""
124
+
125
+ def get_identifier(self, _: httpx.Request) -> str:
126
+ """Identify whether it is currently day or night."""
127
+ if 6 <= datetime.now(tz=timezone.utc).hour < 18:
128
+ return "day"
129
+
130
+ return "night"
131
+
132
+ def get_rate(self, _: httpx.Request) -> Rate:
133
+ """Apply different rate limits during the day or night."""
134
+ if self.get_identifier(_) == "day":
135
+ return Rate.create(magnitude=10)
136
+
137
+ return Rate.create(magnitude=100)
138
+ ```
139
+
70
140
  ## Copyright
71
141
 
72
142
  - Copyright © 2024,2025 Moritz E. Beber.
@@ -17,12 +17,16 @@ Install it, for example, with
17
17
  pip install httpx-limiter
18
18
  ```
19
19
 
20
- ## Usage
20
+ ## Tutorial
21
21
 
22
- For situations when you need to make a large number of asynchronous request with
23
- a controlled number of requests per unit time, you can apply rate limiting to an
24
- HTTPX client using the provided transport. If you want to be able to make ten
25
- requests per second, for example, use the following:
22
+ You can limit the number of requests made by an HTTPX client using the transports
23
+ provided in this package. That is useful in situations when you need to make a large
24
+ number of asynchronous requests against endpoints that implement a rate limit,
25
+
26
+ ### Single Rate Limit
27
+
28
+ The simplest use case is to apply a single rate limit to all requests. If you want to be
29
+ able to make twenty requests per second, for example, use the following code:
26
30
 
27
31
  ```python
28
32
  import httpx
@@ -30,11 +34,77 @@ from httpx_limiter import AsyncRateLimitedTransport, Rate
30
34
 
31
35
  async def main():
32
36
  async with httpx.AsyncClient(
33
- transport=AsyncRateLimitedTransport.create(rate=Rate.create(magnitude=10)),
37
+ transport=AsyncRateLimitedTransport.create(rate=Rate.create(magnitude=20)),
34
38
  ) as client:
35
39
  response = await client.get("https://httpbin.org")
36
40
  ```
37
41
 
42
+ > [!IMPORTANT]
43
+ > Due to limitations in the design of the underlying leaky bucket implementation,
44
+ > which is used to implement the rate limiting, the magnitude of the rate is also the
45
+ > maximum capacity of the bucket. That means, if you set a rate that is larger than one,
46
+ > a burst of requests equal to that capacity will be allowed. If you do not want to
47
+ > allow any bursts, set the magnitude to one, but the duration to the inverse of your
48
+ > desired rate. If you want to allow twenty requests per second, for example, set the
49
+ > magnitude to 1 and the duration to 0.05 seconds.
50
+
51
+ ### Multiple Rate Limits
52
+
53
+ For more advanced use cases, you can apply different rate limits based on a concrete
54
+ implementation of the `AbstractRateLimiterRepository`. There are two relevant methods
55
+ that both get passed the current request. One method needs to identify which rate limit
56
+ to apply, and the other method sets the rate limit itself. See the following example:
57
+
58
+ ```python
59
+ import httpx
60
+ from httpx_limiter import AbstractRateLimiterRepository, AsyncMultiRateLimitedTransport, Rate
61
+
62
+ class DomainBasedRateLimiterRepository(AbstractRateLimiterRepository):
63
+ """Apply different rate limits based on the domain being requested."""
64
+
65
+ def get_identifier(self, request: httpx.Request) -> str:
66
+ """Return the domain as the identifier for rate limiting."""
67
+ return request.url.host
68
+
69
+ def get_rate(self, request: httpx.Request) -> Rate:
70
+ """Apply the same, but independent rate limit to each domain."""
71
+ return Rate.create(magnitude=25)
72
+
73
+ client = httpx.AsyncClient(
74
+ transport=AsyncMultiRateLimitedTransport.create(
75
+ repository=DomainBasedRateLimiterRepository(),
76
+ ),
77
+ )
78
+ ```
79
+
80
+ > [!NOTE]
81
+ > You are free to ignore the request argument and use global information like the time
82
+ > of day to determine the rate limit.
83
+
84
+ ```python
85
+ from datetime import datetime, timezone
86
+
87
+ import httpx
88
+ from httpx_limiter import AbstractRateLimiterRepository, AsyncMultiRateLimitedTransport, Rate
89
+
90
+ class DayNightRateLimiterRepository(AbstractRateLimiterRepository):
91
+ """Apply different rate limits based on the time of day."""
92
+
93
+ def get_identifier(self, _: httpx.Request) -> str:
94
+ """Identify whether it is currently day or night."""
95
+ if 6 <= datetime.now(tz=timezone.utc).hour < 18:
96
+ return "day"
97
+
98
+ return "night"
99
+
100
+ def get_rate(self, _: httpx.Request) -> Rate:
101
+ """Apply different rate limits during the day or night."""
102
+ if self.get_identifier(_) == "day":
103
+ return Rate.create(magnitude=10)
104
+
105
+ return Rate.create(magnitude=100)
106
+ ```
107
+
38
108
  ## Copyright
39
109
 
40
110
  - Copyright © 2024,2025 Moritz E. Beber.
@@ -161,12 +161,7 @@ ignore_missing_imports = true
161
161
  installer = "uv"
162
162
 
163
163
  [tool.hatch.envs.dev]
164
- description = """
165
- Development environment.
166
-
167
- Defining a path is a workaround for PyCharm,
168
- see https://youtrack.jetbrains.com/issue/PY-60410.
169
- """
164
+ description = """Development environment."""
170
165
  extra-dependencies = [
171
166
  "anyio",
172
167
  "pytest",
@@ -174,7 +169,6 @@ extra-dependencies = [
174
169
  "pytest-watcher",
175
170
  "ruff",
176
171
  ]
177
- path = ".venv"
178
172
 
179
173
  [tool.hatch.envs.style]
180
174
  description = """Check the style of the codebase."""
@@ -256,7 +250,6 @@ run = "pytest {args:--cov=httpx_limiter --cov-report=term-missing} {root}/tests/
256
250
  description = """Run the acceptance tests."""
257
251
  extra-dependencies = [
258
252
  "anyio",
259
- # TODO: Use behave in future.
260
253
  "pytest",
261
254
  "pytest-cov",
262
255
  "pytest-httpx",
@@ -17,3 +17,5 @@
17
17
 
18
18
  from .rate import Number, Rate
19
19
  from .async_rate_limited_transport import AsyncRateLimitedTransport
20
+ from .abstract_rate_limiter_repository import AbstractRateLimiterRepository
21
+ from .async_multi_rate_limited_transport import AsyncMultiRateLimitedTransport
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.2.0'
21
- __version_tuple__ = version_tuple = (0, 2, 0)
20
+ __version__ = version = '0.3.0'
21
+ __version_tuple__ = version_tuple = (0, 3, 0)
@@ -0,0 +1,53 @@
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
+
@@ -0,0 +1,84 @@
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 multiple rate-limited transport."""
17
+
18
+ from __future__ import annotations
19
+
20
+ from typing import TYPE_CHECKING
21
+
22
+ import httpx
23
+
24
+
25
+ if TYPE_CHECKING: # pragma: no cover
26
+ from .abstract_rate_limiter_repository import AbstractRateLimiterRepository
27
+
28
+
29
+ class AsyncMultiRateLimitedTransport(httpx.AsyncBaseTransport):
30
+ """
31
+ Define the asynchronous multiple rate-limited transport.
32
+
33
+ This transport consists of a composed transport for handling requests and a
34
+ repository for rate limiters that are selected based on the request.
35
+
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ *,
41
+ repository: AbstractRateLimiterRepository,
42
+ transport: httpx.AsyncBaseTransport,
43
+ **kwargs,
44
+ ) -> None:
45
+ super().__init__(**kwargs)
46
+ self._repo = repository
47
+ self._transport = transport
48
+
49
+ @classmethod
50
+ def create(
51
+ cls,
52
+ *,
53
+ repository: AbstractRateLimiterRepository,
54
+ **kwargs: dict,
55
+ ) -> AsyncMultiRateLimitedTransport:
56
+ """
57
+ Create an instance of an asynchronous multiple rate-limited transport.
58
+
59
+ This factory method constructs the instance with an underlying
60
+ `httpx.AsyncHTTPTransport`.
61
+ That transport is passed any additional keyword arguments.
62
+
63
+ Args:
64
+ repository: The repository to use for rate limiters.
65
+ **kwargs: Additional keyword arguments are used in the construction of an
66
+ `httpx.AsyncHTTPTransport`.
67
+
68
+ Returns:
69
+ A default instance of the class created from the given arguments.
70
+
71
+ """
72
+ return cls(
73
+ repository=repository,
74
+ transport=httpx.AsyncHTTPTransport(**kwargs), # type: ignore[arg-type]
75
+ )
76
+
77
+ async def handle_async_request(
78
+ self,
79
+ request: httpx.Request,
80
+ ) -> httpx.Response:
81
+ """Handle an asynchronous request with rate limiting."""
82
+ limiter = self._repo.get(request)
83
+ async with limiter:
84
+ return await self._transport.handle_async_request(request)
@@ -23,7 +23,7 @@ import httpx
23
23
  from aiolimiter import AsyncLimiter
24
24
 
25
25
 
26
- if TYPE_CHECKING:
26
+ if TYPE_CHECKING: # pragma: no cover
27
27
  from .rate import Rate
28
28
 
29
29
 
File without changes
File without changes