ratelimitly 2.0.0__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.
- ratelimitly/__init__.py +63 -0
- ratelimitly/auth.py +165 -0
- ratelimitly/client.py +514 -0
- ratelimitly/discovery.py +224 -0
- ratelimitly/policy.py +149 -0
- ratelimitly/protocol.py +398 -0
- ratelimitly/steering.py +65 -0
- ratelimitly/types.py +72 -0
- ratelimitly-2.0.0.dist-info/METADATA +165 -0
- ratelimitly-2.0.0.dist-info/RECORD +12 -0
- ratelimitly-2.0.0.dist-info/WHEEL +5 -0
- ratelimitly-2.0.0.dist-info/top_level.txt +1 -0
ratelimitly/__init__.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""RateLimitly low-level Python client library."""
|
|
2
|
+
|
|
3
|
+
from .auth import parse_auth_key, AuthKeyInfo
|
|
4
|
+
from .policy import (
|
|
5
|
+
RequestPolicy,
|
|
6
|
+
Schedule,
|
|
7
|
+
FixedSchedule,
|
|
8
|
+
LinearSchedule,
|
|
9
|
+
ExponentialSchedule,
|
|
10
|
+
default_request_policy,
|
|
11
|
+
)
|
|
12
|
+
from .types import (
|
|
13
|
+
ResourceRequest,
|
|
14
|
+
LatencyGuard,
|
|
15
|
+
ServiceLatencyReport,
|
|
16
|
+
GuardResult,
|
|
17
|
+
ResourceResult,
|
|
18
|
+
RateLimitResult,
|
|
19
|
+
RCLIENT_OK,
|
|
20
|
+
RCLIENT_ERR_IO,
|
|
21
|
+
RCLIENT_ERR_TIMEOUT,
|
|
22
|
+
RCLIENT_ERR_PROTOCOL,
|
|
23
|
+
RCLIENT_ERR_AUTH,
|
|
24
|
+
RCLIENT_ERR_DNS,
|
|
25
|
+
RCLIENT_ERR_CONFIG,
|
|
26
|
+
RCLIENT_ERR_NOMEM,
|
|
27
|
+
)
|
|
28
|
+
from .protocol import (
|
|
29
|
+
r_client_derive_bucket_id,
|
|
30
|
+
r_client_derive_latency_tracker_id,
|
|
31
|
+
)
|
|
32
|
+
from .client import RateLimitlyClient, AsyncRateLimitlyClient
|
|
33
|
+
|
|
34
|
+
__version__ = "2.0.0"
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"RateLimitlyClient",
|
|
38
|
+
"AsyncRateLimitlyClient",
|
|
39
|
+
"ResourceRequest",
|
|
40
|
+
"LatencyGuard",
|
|
41
|
+
"ServiceLatencyReport",
|
|
42
|
+
"GuardResult",
|
|
43
|
+
"ResourceResult",
|
|
44
|
+
"RateLimitResult",
|
|
45
|
+
"RCLIENT_OK",
|
|
46
|
+
"RCLIENT_ERR_IO",
|
|
47
|
+
"RCLIENT_ERR_TIMEOUT",
|
|
48
|
+
"RCLIENT_ERR_PROTOCOL",
|
|
49
|
+
"RCLIENT_ERR_AUTH",
|
|
50
|
+
"RCLIENT_ERR_DNS",
|
|
51
|
+
"RCLIENT_ERR_CONFIG",
|
|
52
|
+
"RCLIENT_ERR_NOMEM",
|
|
53
|
+
"RequestPolicy",
|
|
54
|
+
"Schedule",
|
|
55
|
+
"FixedSchedule",
|
|
56
|
+
"LinearSchedule",
|
|
57
|
+
"ExponentialSchedule",
|
|
58
|
+
"default_request_policy",
|
|
59
|
+
"parse_auth_key",
|
|
60
|
+
"AuthKeyInfo",
|
|
61
|
+
"r_client_derive_bucket_id",
|
|
62
|
+
"r_client_derive_latency_tracker_id",
|
|
63
|
+
]
|
ratelimitly/auth.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Bech32 authentication key parser for RateLimitly credentials."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import List, Literal, Tuple
|
|
5
|
+
|
|
6
|
+
BECH32_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
|
7
|
+
BECH32_REV = {c: i for i, c in enumerate(BECH32_CHARSET)}
|
|
8
|
+
TENANT_KEY_FORMAT_VERSION = 1
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class AuthKeyInfo:
|
|
13
|
+
auth_type: Literal["aes", "cookie"]
|
|
14
|
+
format_version: int
|
|
15
|
+
key_id: int
|
|
16
|
+
secret: bytes
|
|
17
|
+
rate_buckets_max: int
|
|
18
|
+
latency_services_max: int
|
|
19
|
+
metrics_labels_max: int
|
|
20
|
+
latency_buffer_size_max: int
|
|
21
|
+
dedup_ttl_ms_max: int
|
|
22
|
+
rate_window_size_ms_max: int
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def default_dns_srv(self) -> str:
|
|
26
|
+
"""Constructs default tenant SRV domain string: c-${key_id}.p0.ratelimitly.com"""
|
|
27
|
+
return f"c-{self.key_id}.p0.ratelimitly.com"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _bech32_polymod(values: List[int]) -> int:
|
|
31
|
+
generators = (0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3)
|
|
32
|
+
checksum = 1
|
|
33
|
+
for value in values:
|
|
34
|
+
top = checksum >> 25
|
|
35
|
+
checksum = ((checksum & 0x1FFFFFF) << 5) ^ value
|
|
36
|
+
for index, generator in enumerate(generators):
|
|
37
|
+
if (top >> index) & 1:
|
|
38
|
+
checksum ^= generator
|
|
39
|
+
return checksum
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _bech32_hrp_expand(hrp: str) -> List[int]:
|
|
43
|
+
return [ord(char) >> 5 for char in hrp] + [0] + [ord(char) & 31 for char in hrp]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _bech32_decode(bech_str: str) -> Tuple[str, List[int]]:
|
|
47
|
+
"""Decodes a Bech32 string into HRP and 5-bit data values."""
|
|
48
|
+
if not bech_str or any(ord(char) < 33 or ord(char) > 126 for char in bech_str):
|
|
49
|
+
raise ValueError("Invalid Bech32 length")
|
|
50
|
+
if bech_str.lower() != bech_str and bech_str.upper() != bech_str:
|
|
51
|
+
raise ValueError("Mixed case Bech32 string")
|
|
52
|
+
|
|
53
|
+
bech_str = bech_str.lower()
|
|
54
|
+
pos = bech_str.rfind("1")
|
|
55
|
+
if pos < 1 or pos + 7 > len(bech_str):
|
|
56
|
+
raise ValueError("Invalid separator position in Bech32 string")
|
|
57
|
+
|
|
58
|
+
hrp = bech_str[:pos]
|
|
59
|
+
data = []
|
|
60
|
+
for c in bech_str[pos + 1:]:
|
|
61
|
+
if c not in BECH32_REV:
|
|
62
|
+
raise ValueError(f"Invalid character in Bech32 string: '{c}'")
|
|
63
|
+
data.append(BECH32_REV[c])
|
|
64
|
+
|
|
65
|
+
if _bech32_polymod(_bech32_hrp_expand(hrp) + data) != 1:
|
|
66
|
+
raise ValueError("Invalid Bech32 checksum")
|
|
67
|
+
|
|
68
|
+
return hrp, data
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _convertbits(data: List[int], frombits: int, tobits: int, pad: bool = True) -> bytes:
|
|
72
|
+
"""Converts a bit array from one representation to another (5-bit to 8-bit)."""
|
|
73
|
+
acc = 0
|
|
74
|
+
bits = 0
|
|
75
|
+
ret = bytearray()
|
|
76
|
+
maxv = (1 << tobits) - 1
|
|
77
|
+
max_acc = (1 << (frombits + tobits - 1)) - 1
|
|
78
|
+
for value in data:
|
|
79
|
+
if value < 0 or (value >> frombits):
|
|
80
|
+
raise ValueError("Invalid bit value")
|
|
81
|
+
acc = ((acc << frombits) | value) & max_acc
|
|
82
|
+
bits += frombits
|
|
83
|
+
while bits >= tobits:
|
|
84
|
+
bits -= tobits
|
|
85
|
+
ret.append((acc >> bits) & maxv)
|
|
86
|
+
if pad:
|
|
87
|
+
if bits:
|
|
88
|
+
ret.append((acc << (tobits - bits)) & maxv)
|
|
89
|
+
elif bits >= frombits or ((acc << (tobits - bits)) & maxv):
|
|
90
|
+
raise ValueError("Invalid padding in bit conversion")
|
|
91
|
+
return bytes(ret)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _decode_quota_word(word: int) -> Tuple[int, int, int, int, int, int]:
|
|
95
|
+
rate_exp = word & 0x1F
|
|
96
|
+
latency_exp = (word >> 5) & 0x1F
|
|
97
|
+
labels_exp = (word >> 10) & 0x1F
|
|
98
|
+
buffer_exp = (word >> 15) & 0x0F
|
|
99
|
+
dedup_units = (word >> 19) & 0xFF
|
|
100
|
+
window_exp = (word >> 27) & 0x1F
|
|
101
|
+
|
|
102
|
+
if rate_exp > 24:
|
|
103
|
+
raise ValueError("Invalid packed rate_buckets_max quota")
|
|
104
|
+
if latency_exp > 24:
|
|
105
|
+
raise ValueError("Invalid packed latency_services_max quota")
|
|
106
|
+
if not 1 <= dedup_units <= 200:
|
|
107
|
+
raise ValueError("Invalid packed dedup_ttl_ms_max quota")
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
1 << rate_exp,
|
|
111
|
+
1 << latency_exp,
|
|
112
|
+
1 << labels_exp,
|
|
113
|
+
1 << buffer_exp,
|
|
114
|
+
dedup_units * 10,
|
|
115
|
+
0xFFFFFFFF if window_exp == 31 else 1 << window_exp,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def parse_auth_key(key_str: str) -> AuthKeyInfo:
|
|
120
|
+
"""
|
|
121
|
+
Parses a RateLimitly Bech32 authentication key (rl-aes1... or rl-cookie1...).
|
|
122
|
+
|
|
123
|
+
Extracts:
|
|
124
|
+
- Auth Type ('aes' or 'cookie')
|
|
125
|
+
- Key ID (uint64)
|
|
126
|
+
- Secret payload bytes (32 bytes)
|
|
127
|
+
"""
|
|
128
|
+
if not isinstance(key_str, str):
|
|
129
|
+
raise TypeError("Authentication key must be a string")
|
|
130
|
+
|
|
131
|
+
hrp, data_5bit = _bech32_decode(key_str)
|
|
132
|
+
if hrp == "rl-aes":
|
|
133
|
+
auth_type = "aes"
|
|
134
|
+
elif hrp == "rl-cookie":
|
|
135
|
+
auth_type = "cookie"
|
|
136
|
+
else:
|
|
137
|
+
raise ValueError("Invalid auth key HRP; expected 'rl-aes' or 'rl-cookie'")
|
|
138
|
+
|
|
139
|
+
# Remove 6-character checksum at the end
|
|
140
|
+
payload_5bit = data_5bit[:-6]
|
|
141
|
+
raw_bytes = _convertbits(payload_5bit, 5, 8, pad=False)
|
|
142
|
+
|
|
143
|
+
if len(raw_bytes) != 45:
|
|
144
|
+
raise ValueError(f"Invalid auth key payload length: {len(raw_bytes)} bytes (expected 45)")
|
|
145
|
+
|
|
146
|
+
format_version = raw_bytes[0]
|
|
147
|
+
if format_version != TENANT_KEY_FORMAT_VERSION:
|
|
148
|
+
raise ValueError(f"Unsupported auth key format version: {format_version}")
|
|
149
|
+
|
|
150
|
+
key_id = int.from_bytes(raw_bytes[1:9], byteorder="little")
|
|
151
|
+
secret = raw_bytes[9:41]
|
|
152
|
+
quotas = _decode_quota_word(int.from_bytes(raw_bytes[41:45], byteorder="little"))
|
|
153
|
+
|
|
154
|
+
return AuthKeyInfo(
|
|
155
|
+
auth_type=auth_type,
|
|
156
|
+
format_version=format_version,
|
|
157
|
+
key_id=key_id,
|
|
158
|
+
secret=secret,
|
|
159
|
+
rate_buckets_max=quotas[0],
|
|
160
|
+
latency_services_max=quotas[1],
|
|
161
|
+
metrics_labels_max=quotas[2],
|
|
162
|
+
latency_buffer_size_max=quotas[3],
|
|
163
|
+
dedup_ttl_ms_max=quotas[4],
|
|
164
|
+
rate_window_size_ms_max=quotas[5],
|
|
165
|
+
)
|