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.
@@ -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
+ )