pfmsoft-api-request 0.1.3__py3-none-any.whl
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.
- pfmsoft/api_request/__init__.py +37 -0
- pfmsoft/api_request/cache/__init__.py +36 -0
- pfmsoft/api_request/cache/memory_cache.py +177 -0
- pfmsoft/api_request/cache/metadata_helpers.py +55 -0
- pfmsoft/api_request/cache/models.py +83 -0
- pfmsoft/api_request/cache/protocols.py +166 -0
- pfmsoft/api_request/cache/sqlite_cache/__init__.py +7 -0
- pfmsoft/api_request/cache/sqlite_cache/connection_helpers.py +126 -0
- pfmsoft/api_request/cache/sqlite_cache/query_helpers.py +107 -0
- pfmsoft/api_request/cache/sqlite_cache/sqlite_cache.py +233 -0
- pfmsoft/api_request/cache/sqlite_cache/table_definitions.sql +23 -0
- pfmsoft/api_request/cli/__init__.py +17 -0
- pfmsoft/api_request/cli/cache/__init__.py +12 -0
- pfmsoft/api_request/cli/cache/info.py +11 -0
- pfmsoft/api_request/cli/helpers.py +41 -0
- pfmsoft/api_request/cli/main_typer.py +52 -0
- pfmsoft/api_request/cli/request/__init__.py +11 -0
- pfmsoft/api_request/cli/request/request.py +222 -0
- pfmsoft/api_request/cli/request/validate.py +11 -0
- pfmsoft/api_request/helpers/http_session_factory.py +80 -0
- pfmsoft/api_request/helpers/json_io.py +256 -0
- pfmsoft/api_request/helpers/save_text_file.py +43 -0
- pfmsoft/api_request/helpers/whenever/__init__.py +4 -0
- pfmsoft/api_request/helpers/whenever/instant_helpers.py +41 -0
- pfmsoft/api_request/logging_config.py +202 -0
- pfmsoft/api_request/py.typed +0 -0
- pfmsoft/api_request/rate_limit/__init__.py +24 -0
- pfmsoft/api_request/rate_limit/aio_limiter.py +89 -0
- pfmsoft/api_request/rate_limit/protocols.py +101 -0
- pfmsoft/api_request/request/__init__.py +11 -0
- pfmsoft/api_request/request/api_requester.py +681 -0
- pfmsoft/api_request/request/intermediate_models.py +143 -0
- pfmsoft/api_request/request/models.py +355 -0
- pfmsoft/api_request/request/protocols.py +54 -0
- pfmsoft/api_request/settings.py +65 -0
- pfmsoft_api_request-0.1.3.dist-info/METADATA +171 -0
- pfmsoft_api_request-0.1.3.dist-info/RECORD +40 -0
- pfmsoft_api_request-0.1.3.dist-info/WHEEL +4 -0
- pfmsoft_api_request-0.1.3.dist-info/entry_points.txt +3 -0
- pfmsoft_api_request-0.1.3.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Logging configuration helpers for Eve Auth Manager.
|
|
2
|
+
|
|
3
|
+
Provides the default dictConfig-based logging setup used by the application,
|
|
4
|
+
including console output and rotating file handlers.
|
|
5
|
+
|
|
6
|
+
Notes:
|
|
7
|
+
The logging configuration dictionary is intended to be customized in the
|
|
8
|
+
handlers and loggers sections if the application needs different output
|
|
9
|
+
destinations or verbosity.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import logging.config
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
logger.addHandler(logging.NullHandler())
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def setup_logging(log_dir: Path) -> None:
|
|
22
|
+
"""Configure application logging handlers and formatters.
|
|
23
|
+
|
|
24
|
+
Creates the target log directory when needed, then installs the default
|
|
25
|
+
dictConfig-based logging setup for console and rotating file output.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
log_dir: Directory where log files should be created.
|
|
29
|
+
|
|
30
|
+
Notes:
|
|
31
|
+
The configured root logger writes DEBUG-and-above messages to the info
|
|
32
|
+
rotating file, WARNING-and-above messages to the warning rotating
|
|
33
|
+
file, and CRITICAL-and-above messages to the console handler.
|
|
34
|
+
"""
|
|
35
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
|
|
37
|
+
# dictConfig replaces root handlers; keep deferred buffers so they can be
|
|
38
|
+
# flushed after real handlers are installed.
|
|
39
|
+
root_logger = logging.getLogger()
|
|
40
|
+
deferred_handlers = [
|
|
41
|
+
h for h in root_logger.handlers if isinstance(h, DeferredHandler)
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
log_config: dict[str, Any] = {
|
|
45
|
+
"version": 1,
|
|
46
|
+
"disable_existing_loggers": False,
|
|
47
|
+
"formatters": {
|
|
48
|
+
"consoleFormatter": {
|
|
49
|
+
"format": "%(asctime)s | %(name)s | %(levelname)s : %(message)s",
|
|
50
|
+
},
|
|
51
|
+
"fileFormatter": {
|
|
52
|
+
"format": "%(asctime)s | %(name)s | %(levelname)-8s : %(message)s",
|
|
53
|
+
},
|
|
54
|
+
"brief": {
|
|
55
|
+
"datefmt": "%H:%M:%S",
|
|
56
|
+
"format": "%(levelname)-8s; %(name)s; %(message)s;",
|
|
57
|
+
},
|
|
58
|
+
"single-line": {
|
|
59
|
+
"datefmt": "%H:%M:%S",
|
|
60
|
+
"format": "%(levelname)-8s; %(asctime)s; %(name)s; %(module)s:%(funcName)s;%(lineno)d: %(message)s",
|
|
61
|
+
},
|
|
62
|
+
"multi-process": {
|
|
63
|
+
"datefmt": "%H:%M:%S",
|
|
64
|
+
"format": "%(levelname)-8s; [%(process)d]; %(name)s; %(module)s:%(funcName)s;%(lineno)d: %(message)s",
|
|
65
|
+
},
|
|
66
|
+
"multi-thread": {
|
|
67
|
+
"datefmt": "%H:%M:%S",
|
|
68
|
+
"format": "%(levelname)-8s; %(threadName)s; %(name)s; %(module)s:%(funcName)s;%(lineno)d: %(message)s",
|
|
69
|
+
},
|
|
70
|
+
"verbose": {
|
|
71
|
+
"format": "%(levelname)-8s; [%(process)d]; %(threadName)s; %(name)s; %(module)s:%(funcName)s;%(lineno)d"
|
|
72
|
+
": %(message)s"
|
|
73
|
+
},
|
|
74
|
+
"multiline": {
|
|
75
|
+
"format": "Level: %(levelname)s\nTime: %(asctime)s\nProcess: %(process)d\nThread: %(threadName)s\nLogger"
|
|
76
|
+
": %(name)s\nPath: %(module)s:%(lineno)d\nFunction :%(funcName)s\nMessage: %(message)s\n"
|
|
77
|
+
},
|
|
78
|
+
"mine": {
|
|
79
|
+
"format": "%(asctime)s | %(levelname)-8s | %(funcName)s | %(message)s | [in %(pathname)s | %(lineno)d]"
|
|
80
|
+
},
|
|
81
|
+
"mine-multi": {
|
|
82
|
+
"format": "%(asctime)s | %(levelname)-8s | %(funcName)s | [in %(pathname)s | %(lineno)d]\n\t %(message)s"
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
"handlers": {
|
|
86
|
+
"file": {
|
|
87
|
+
"filename": log_dir / "debug.log",
|
|
88
|
+
"level": "DEBUG",
|
|
89
|
+
"class": "logging.FileHandler",
|
|
90
|
+
"formatter": "mine",
|
|
91
|
+
},
|
|
92
|
+
"console": {
|
|
93
|
+
"level": "CRITICAL",
|
|
94
|
+
"class": "logging.StreamHandler",
|
|
95
|
+
"formatter": "consoleFormatter",
|
|
96
|
+
},
|
|
97
|
+
"rot_file_info": {
|
|
98
|
+
"class": "logging.handlers.RotatingFileHandler",
|
|
99
|
+
"formatter": "mine-multi",
|
|
100
|
+
"level": "INFO",
|
|
101
|
+
"filename": log_dir / "rotating_info.log",
|
|
102
|
+
"mode": "a",
|
|
103
|
+
"encoding": "utf-8",
|
|
104
|
+
"maxBytes": 10000000,
|
|
105
|
+
"backupCount": 10,
|
|
106
|
+
},
|
|
107
|
+
"rot_file_warn": {
|
|
108
|
+
"class": "logging.handlers.RotatingFileHandler",
|
|
109
|
+
"formatter": "mine-multi",
|
|
110
|
+
"level": "WARNING",
|
|
111
|
+
"filename": log_dir / "rotating_warn.log",
|
|
112
|
+
"mode": "a",
|
|
113
|
+
"encoding": "utf-8",
|
|
114
|
+
"maxBytes": 500000,
|
|
115
|
+
"backupCount": 4,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
"loggers": {
|
|
119
|
+
"": {
|
|
120
|
+
"handlers": ["rot_file_info", "rot_file_warn", "console"],
|
|
121
|
+
"level": "DEBUG",
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
logging.config.dictConfig(log_config)
|
|
126
|
+
|
|
127
|
+
for deferred in deferred_handlers:
|
|
128
|
+
if deferred not in root_logger.handlers:
|
|
129
|
+
root_logger.addHandler(deferred)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class DeferredHandler(logging.Handler):
|
|
133
|
+
"""Buffers log records until real handlers are ready, then replays them."""
|
|
134
|
+
|
|
135
|
+
def __init__(self, level: int = logging.NOTSET) -> None:
|
|
136
|
+
"""Initialize an in-memory record buffer."""
|
|
137
|
+
super().__init__(level)
|
|
138
|
+
self.buffer: list[logging.LogRecord] = []
|
|
139
|
+
|
|
140
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
141
|
+
"""Store records until configured handlers are available."""
|
|
142
|
+
self.buffer.append(record)
|
|
143
|
+
|
|
144
|
+
def flush_to(self, handlers: list[logging.Handler]) -> None:
|
|
145
|
+
"""Replay buffered records through the given handlers, then clear."""
|
|
146
|
+
for record in self.buffer:
|
|
147
|
+
for handler in handlers:
|
|
148
|
+
if record.levelno >= handler.level:
|
|
149
|
+
handler.handle(record)
|
|
150
|
+
self.buffer.clear()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def init_deferred_handler() -> None:
|
|
154
|
+
"""Initialize a deferred handler for logging.
|
|
155
|
+
|
|
156
|
+
This funtion sets up a deferred handler that can be used to buffer log messages in
|
|
157
|
+
memory before they are flushed to the appropriate handlers.
|
|
158
|
+
|
|
159
|
+
This is useful for capturing log messages emitted before the logging configuration
|
|
160
|
+
is fully set up. The deferred handler will buffer these messages in memory and can
|
|
161
|
+
be flushed to the appropriate handlers once they are configured.
|
|
162
|
+
|
|
163
|
+
DeferredHandler is added to the root logger, and it captures all log messages at
|
|
164
|
+
DEBUG level and above.
|
|
165
|
+
|
|
166
|
+
It must be flushed to the appropriate handlers once they are configured to log the
|
|
167
|
+
buffered messages.
|
|
168
|
+
"""
|
|
169
|
+
root_logger = logging.getLogger()
|
|
170
|
+
if any(isinstance(h, DeferredHandler) for h in root_logger.handlers):
|
|
171
|
+
return
|
|
172
|
+
root_logger.setLevel(logging.DEBUG) # capture everything; filter at handler level
|
|
173
|
+
deferred = DeferredHandler()
|
|
174
|
+
root_logger.addHandler(deferred)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def flush_deferred_handler() -> None:
|
|
178
|
+
"""Replay buffered records into whatever handlers are currently configured.
|
|
179
|
+
|
|
180
|
+
Call this after your real logging setup (dictConfig, basicConfig, manual
|
|
181
|
+
addHandler calls, etc.) has run. Finds any DeferredHandler on the root
|
|
182
|
+
logger, replays its buffer into all *other* handlers currently attached,
|
|
183
|
+
then removes itself.
|
|
184
|
+
|
|
185
|
+
Safe to call multiple times or when no DeferredHandler is present — it's
|
|
186
|
+
a no-op in that case rather than an error.
|
|
187
|
+
"""
|
|
188
|
+
root_logger = logging.getLogger()
|
|
189
|
+
|
|
190
|
+
deferred_handlers = [
|
|
191
|
+
h for h in root_logger.handlers if isinstance(h, DeferredHandler)
|
|
192
|
+
]
|
|
193
|
+
if not deferred_handlers:
|
|
194
|
+
return # nothing to flush, or already finalized
|
|
195
|
+
|
|
196
|
+
real_handlers = [
|
|
197
|
+
h for h in root_logger.handlers if not isinstance(h, DeferredHandler)
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
for deferred in deferred_handlers:
|
|
201
|
+
deferred.flush_to(real_handlers)
|
|
202
|
+
root_logger.removeHandler(deferred)
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Rate-limiter implementations used by request orchestration.
|
|
2
|
+
|
|
3
|
+
Exports:
|
|
4
|
+
- `AiolimiterRateLimiter`: concrete limiter wrapper around
|
|
5
|
+
`aiolimiter.AsyncLimiter`.
|
|
6
|
+
- `AiolimiterRateLimiterFactory`: dependency-injection factory used by
|
|
7
|
+
requester construction.
|
|
8
|
+
|
|
9
|
+
Typical usage:
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from api_request.rate_limit import AiolimiterRateLimiterFactory
|
|
13
|
+
|
|
14
|
+
factory = AiolimiterRateLimiterFactory(max_rate=100.0, time_period=60.0)
|
|
15
|
+
limiter = factory()
|
|
16
|
+
|
|
17
|
+
async with limiter.limit("market/orders"):
|
|
18
|
+
...
|
|
19
|
+
```
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from .aio_limiter import AiolimiterRateLimiter, AiolimiterRateLimiterFactory
|
|
23
|
+
|
|
24
|
+
__all__ = ["AiolimiterRateLimiter", "AiolimiterRateLimiterFactory"]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Rate limiter implementation using aiolimiter.
|
|
2
|
+
|
|
3
|
+
This module provides a protocol-compatible wrapper around one shared
|
|
4
|
+
`aiolimiter.AsyncLimiter` and a small factory for requester construction.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from contextlib import AbstractAsyncContextManager
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
from aiolimiter import AsyncLimiter
|
|
11
|
+
|
|
12
|
+
from pfmsoft.api_request.rate_limit.protocols import (
|
|
13
|
+
RateLimiterFactoryProtocol,
|
|
14
|
+
RateLimiterProtocol,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(slots=True)
|
|
19
|
+
class AiolimiterRateLimiter(RateLimiterProtocol):
|
|
20
|
+
"""Shared rate limiter backed by one AsyncLimiter instance.
|
|
21
|
+
|
|
22
|
+
This implementation enforces a single global bucket for all operations.
|
|
23
|
+
The provided `subject` value is currently ignored.
|
|
24
|
+
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
limiter: AsyncLimiter
|
|
28
|
+
"""The shared AsyncLimiter used by all gated operations."""
|
|
29
|
+
|
|
30
|
+
def limit(self, subject: str | None) -> AbstractAsyncContextManager[None]:
|
|
31
|
+
"""Return the shared AsyncLimiter as an async context manager.
|
|
32
|
+
|
|
33
|
+
NOTE: `subject` is accepted for protocol compatibility and future
|
|
34
|
+
extensibility. It is currently ignored.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
subject: Domain input for the gated operation.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
An async context manager that acquires capacity from the shared limiter.
|
|
41
|
+
"""
|
|
42
|
+
return self.limiter
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(slots=True, frozen=True)
|
|
46
|
+
class AiolimiterRateLimiterFactory(RateLimiterFactoryProtocol):
|
|
47
|
+
"""Factory that builds shared aiolimiter-backed rate limiters.
|
|
48
|
+
|
|
49
|
+
Use one factory per limiter configuration and one produced limiter per
|
|
50
|
+
requester/task-group that should share budget.
|
|
51
|
+
|
|
52
|
+
This factory is designed to be passed into requester constructors that build
|
|
53
|
+
their shared limiter in `__aenter__`.
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
```python
|
|
57
|
+
from api_request.rate_limit import AiolimiterRateLimiterFactory
|
|
58
|
+
|
|
59
|
+
factory = AiolimiterRateLimiterFactory(max_rate=100.0, time_period=60.0)
|
|
60
|
+
limiter = factory()
|
|
61
|
+
async with limiter.limit("esi-status"):
|
|
62
|
+
...
|
|
63
|
+
```
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
max_rate: float
|
|
67
|
+
"""The maximum acquisitions allowed within `time_period`.
|
|
68
|
+
|
|
69
|
+
This should be greater than zero.
|
|
70
|
+
"""
|
|
71
|
+
time_period: float = 60.0
|
|
72
|
+
"""The limiting window length in seconds.
|
|
73
|
+
|
|
74
|
+
This should be greater than zero.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __call__(self) -> AiolimiterRateLimiter:
|
|
78
|
+
"""Build a shared rate limiter instance.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
A shared rate limiter backed by one configured AsyncLimiter.
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
ValueError: Propagated from `AsyncLimiter` for invalid constructor
|
|
85
|
+
arguments.
|
|
86
|
+
"""
|
|
87
|
+
return AiolimiterRateLimiter(
|
|
88
|
+
limiter=AsyncLimiter(self.max_rate, self.time_period)
|
|
89
|
+
)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Rate-limiter protocol contracts.
|
|
2
|
+
|
|
3
|
+
This module defines the protocol shape used to gate outbound request work.
|
|
4
|
+
Concrete implementations live in sibling modules.
|
|
5
|
+
|
|
6
|
+
Protocol contract:
|
|
7
|
+
- Each `limit(subject)` call returns an async context manager used to gate
|
|
8
|
+
one operation.
|
|
9
|
+
- Implementations may choose how `subject` influences behavior.
|
|
10
|
+
|
|
11
|
+
The default concrete implementation in this package currently uses one shared
|
|
12
|
+
bucket and ignores `subject`.
|
|
13
|
+
|
|
14
|
+
Typical usage:
|
|
15
|
+
limiter = AiolimiterRateLimiterFactory(max_rate=100.0, time_period=60.0)()
|
|
16
|
+
async with limiter.limit("market/orders"):
|
|
17
|
+
# perform one rate-limited operation
|
|
18
|
+
...
|
|
19
|
+
|
|
20
|
+
Typed construction with ApiRequester:
|
|
21
|
+
from uuid import UUID, uuid4
|
|
22
|
+
|
|
23
|
+
from api_request import ApiRequester, Request
|
|
24
|
+
from api_request.rate_limit import AiolimiterRateLimiterFactory
|
|
25
|
+
|
|
26
|
+
# Replace with your chosen cache factory.
|
|
27
|
+
cache_factory = ...
|
|
28
|
+
rate_limiter_factory = AiolimiterRateLimiterFactory(
|
|
29
|
+
max_rate=100.0,
|
|
30
|
+
time_period=60.0,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
requests: dict[UUID, Request] = {
|
|
34
|
+
uuid4(): Request(
|
|
35
|
+
request_key=uuid4(),
|
|
36
|
+
url="https://esi.evetech.net/latest/status/",
|
|
37
|
+
method="GET",
|
|
38
|
+
rate_key="public-status",
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def run() -> None:
|
|
44
|
+
async with ApiRequester(
|
|
45
|
+
cache_factory=cache_factory,
|
|
46
|
+
rate_limiter_factory=rate_limiter_factory,
|
|
47
|
+
) as requester:
|
|
48
|
+
await requester.process_requests(requests)
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
from contextlib import AbstractAsyncContextManager
|
|
52
|
+
from typing import Protocol
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class RateLimiterProtocol(Protocol):
|
|
56
|
+
"""Protocol for a rate limiter that gates async work.
|
|
57
|
+
|
|
58
|
+
Implementations may produce one-shot limiters for each operation or may
|
|
59
|
+
return a shared limiter context that gates all operations.
|
|
60
|
+
|
|
61
|
+
The protocol does not require any grouping strategy. Implementations may
|
|
62
|
+
ignore `subject`, or they may use it to select or derive limiter behavior.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def limit(self, subject: str | None) -> AbstractAsyncContextManager[None]:
|
|
66
|
+
"""Create an async gate for one operation.
|
|
67
|
+
|
|
68
|
+
Depending on implementation, `subject` may influence behavior. For
|
|
69
|
+
example, a limiter might group requests by endpoint family, tenant, or
|
|
70
|
+
priority class.
|
|
71
|
+
|
|
72
|
+
Examples:
|
|
73
|
+
async with rate_limiter.limit(subject):
|
|
74
|
+
# performs the operation that is subject to rate limiting, after the
|
|
75
|
+
# gate is acquired.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
subject: Domain input that may influence limiter behavior. Prefer a
|
|
79
|
+
stable, hashable value that represents the grouping intent.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
An async context manager that acquires limiter capacity.
|
|
83
|
+
|
|
84
|
+
Notes:
|
|
85
|
+
The contract does not require distinct limiter instances per call.
|
|
86
|
+
Returning a shared context manager object is valid.
|
|
87
|
+
"""
|
|
88
|
+
...
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class RateLimiterFactoryProtocol(Protocol):
|
|
92
|
+
"""Protocol for callables that build shared rate-limiter instances.
|
|
93
|
+
|
|
94
|
+
This protocol is used to create configured rate limiters for use in
|
|
95
|
+
requesters. One produced limiter instance is typically reused by one
|
|
96
|
+
requester/task-group so concurrent work coordinates against shared budget.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def __call__(self) -> RateLimiterProtocol:
|
|
100
|
+
"""Build and return a configured rate-limiter instance."""
|
|
101
|
+
...
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Request orchestration package.
|
|
2
|
+
|
|
3
|
+
This package contains:
|
|
4
|
+
- Public request/response data models.
|
|
5
|
+
- Internal intermediate models used by the orchestration pipeline.
|
|
6
|
+
- The requester implementation and protocol contract.
|
|
7
|
+
|
|
8
|
+
Most callers should import public request types from `api_request` package
|
|
9
|
+
root exports (`Request`, `Response`, `ResponseMetadata`, `Source`) and use
|
|
10
|
+
`ApiRequester` as an async context manager.
|
|
11
|
+
"""
|