ratelimitly 2.0.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.
@@ -0,0 +1,165 @@
1
+ Metadata-Version: 2.4
2
+ Name: ratelimitly
3
+ Version: 2.0.0
4
+ Summary: Official Python client library for RateLimitly high-performance rate limiting.
5
+ Author-email: RateLimitly Team <support@ratelimitly.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://ratelimitly.com
8
+ Project-URL: Repository, https://github.com/ratelimitly-com/rl-python-client
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: cryptography>=3.4
14
+ Requires-Dist: dnspython>=2.0.0
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
17
+ Requires-Dist: pytest-asyncio>=0.20.0; extra == "dev"
18
+
19
+ # RateLimitly Python client
20
+
21
+ `ratelimitly` is the official Python client for [RateLimitly](https://ratelimitly.com/), a distributed admission-control service. An application asks whether it may begin work that consumes rate-limited resources. The decision can also depend on recent latency observations for services used by that work.
22
+
23
+ The library exposes two independent operations:
24
+
25
+ - A **resource request** atomically asks to consume quantities from zero or more rate buckets, subject to zero or more latency guards. A successful decision represents consumption of every requested quantity and authorizes the work to proceed.
26
+ - A **latency report** contributes measured service latencies to one or more trackers. It does not request resources or make an admission decision.
27
+
28
+ An empty resource request succeeds locally. A guard-only request is sent to RateLimitly and evaluated normally.
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install ratelimitly
34
+ ```
35
+
36
+ ## Request one token
37
+
38
+ In English, this request means: “Give me one token from the `checkout` bucket whose definition is 100 tokens per 1,000 ms.”
39
+
40
+ ```python
41
+ from ratelimitly import (
42
+ RCLIENT_OK,
43
+ RateLimitlyClient,
44
+ ResourceRequest,
45
+ r_client_derive_bucket_id,
46
+ )
47
+
48
+ window_size_ms = 1_000
49
+ rate_limit = 100
50
+
51
+ resource = ResourceRequest(
52
+ bucket_id=r_client_derive_bucket_id(
53
+ "checkout", # Exact application-defined bucket name.
54
+ window_size_ms, # Bucket window in milliseconds.
55
+ rate_limit, # Tokens available per window.
56
+ ),
57
+ window_size_ms=window_size_ms,
58
+ rate_limit=rate_limit,
59
+ tokens_requested=1,
60
+ )
61
+
62
+ with RateLimitlyClient("rl-aes1...") as client:
63
+ status, result = client.check_rate_limit([resource])
64
+
65
+ if status != RCLIENT_OK:
66
+ print(f"No decision: client status {status}")
67
+ elif result.success:
68
+ print("Granted; perform the protected work")
69
+ else:
70
+ print("Denied; do not perform the protected work")
71
+ ```
72
+
73
+ `RCLIENT_OK` means a valid RateLimitly decision was received; it does not mean the request was granted. Check `result.success` for the admission decision.
74
+
75
+ ## Report one measured latency
76
+
77
+ In English: “Add a 25 ms observation to the `inventory-backend` tracker defined by these storage settings.”
78
+
79
+ ```python
80
+ from ratelimitly import (
81
+ RateLimitlyClient,
82
+ ServiceLatencyReport,
83
+ r_client_derive_latency_tracker_id,
84
+ )
85
+
86
+ ttl_ms = 10_000
87
+ max_samples = 100
88
+ min_sample_threshold = 5
89
+
90
+ tracker_id = r_client_derive_latency_tracker_id(
91
+ "inventory-backend", # Exact application-defined tracker name.
92
+ ttl_ms, # Maximum sample lifetime.
93
+ max_samples, # Samples considered by the tracker.
94
+ min_sample_threshold, # Warm-up samples before guards take effect.
95
+ )
96
+
97
+ report = ServiceLatencyReport(
98
+ latency_tracker_id=tracker_id,
99
+ observed_latency_ms=25,
100
+ ttl_ms=ttl_ms,
101
+ max_samples=max_samples,
102
+ min_sample_threshold=min_sample_threshold,
103
+ )
104
+
105
+ with RateLimitlyClient("rl-aes1...") as client:
106
+ status = client.report_latency([report])
107
+ ```
108
+
109
+ Measure the service operation, not the RateLimitly request. Reports are independent of resource requests and may be sent by a different process.
110
+
111
+ ## Add a latency guard
112
+
113
+ This request asks for the same token only when the tracker’s current latency is below 50 ms:
114
+
115
+ ```python
116
+ from ratelimitly import LatencyGuard
117
+
118
+ guard = LatencyGuard(
119
+ latency_tracker_id=tracker_id, # Same tracker definition as the report.
120
+ threshold_ms=50, # Admission requires current latency < 50 ms.
121
+ ttl_ms=ttl_ms,
122
+ max_samples=max_samples,
123
+ min_sample_threshold=min_sample_threshold,
124
+ )
125
+
126
+ with RateLimitlyClient("rl-aes1...") as client:
127
+ status, result = client.check_rate_limit(
128
+ resources=[resource],
129
+ guards=[guard],
130
+ )
131
+ ```
132
+
133
+ ## Canonical IDs must agree across clients
134
+
135
+ Bucket IDs include the exact bucket-name bytes, `window_size_ms`, and `rate_limit`. Latency-tracker IDs include the exact tracker-name bytes, `ttl_ms`, `max_samples`, and `min_sample_threshold`; a guard threshold is deliberately not part of the tracker ID.
136
+
137
+ The Python helpers implement the same domain-separated binary preimage,
138
+ little-endian integer encoding, BLAKE2s-256 digest, and 16-byte truncation as
139
+ the coordinated wire-v2 C client. Do not replace them with hashing of formatted
140
+ text, and do not use `hashlib.blake2s(..., digest_size=16)`: digest length is a
141
+ BLAKE2 parameter, so that produces a different ID.
142
+
143
+ See [API reference](docs/api.md#canonical-content-defined-identifiers) for the exact formula and cross-client known-answer vectors.
144
+
145
+ ## High-availability policy
146
+
147
+ The default policy matches `rl-c-client`: `unit_ms=20`, one replay, a fixed one-unit round duration, one final receive-only unit, and completion delivery enabled. Its deduplication TTL and maximum decision horizon are 60 ms. The initial transmission goes to every discovered r-server, and the oldest known server’s response wins when it arrives within the first round.
148
+
149
+ See [configuration](docs/configuration.md) for the complete parametrized policy and [architecture](docs/architecture.md) for wire and selection semantics.
150
+
151
+ ## API layers
152
+
153
+ `RateLimitlyClient` is blocking. `AsyncRateLimitlyClient` provides the same serialized state machine through `asyncio`. A client preserves DNS results and UDP sockets across calls; call `close()` or use the context-manager forms to release them.
154
+
155
+ The client returns the same status-code family as the C library. It does not choose an application fail-open or fail-closed policy: the caller decides what to do when no RateLimitly decision is available.
156
+
157
+ ## Documentation
158
+
159
+ - [API reference](docs/api.md)
160
+ - [Configuration and request policy](docs/configuration.md)
161
+ - [Architecture, wire format, and conformance](docs/architecture.md)
162
+
163
+ ## License
164
+
165
+ MIT
@@ -0,0 +1,147 @@
1
+ # RateLimitly Python client
2
+
3
+ `ratelimitly` is the official Python client for [RateLimitly](https://ratelimitly.com/), a distributed admission-control service. An application asks whether it may begin work that consumes rate-limited resources. The decision can also depend on recent latency observations for services used by that work.
4
+
5
+ The library exposes two independent operations:
6
+
7
+ - A **resource request** atomically asks to consume quantities from zero or more rate buckets, subject to zero or more latency guards. A successful decision represents consumption of every requested quantity and authorizes the work to proceed.
8
+ - A **latency report** contributes measured service latencies to one or more trackers. It does not request resources or make an admission decision.
9
+
10
+ An empty resource request succeeds locally. A guard-only request is sent to RateLimitly and evaluated normally.
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ pip install ratelimitly
16
+ ```
17
+
18
+ ## Request one token
19
+
20
+ In English, this request means: “Give me one token from the `checkout` bucket whose definition is 100 tokens per 1,000 ms.”
21
+
22
+ ```python
23
+ from ratelimitly import (
24
+ RCLIENT_OK,
25
+ RateLimitlyClient,
26
+ ResourceRequest,
27
+ r_client_derive_bucket_id,
28
+ )
29
+
30
+ window_size_ms = 1_000
31
+ rate_limit = 100
32
+
33
+ resource = ResourceRequest(
34
+ bucket_id=r_client_derive_bucket_id(
35
+ "checkout", # Exact application-defined bucket name.
36
+ window_size_ms, # Bucket window in milliseconds.
37
+ rate_limit, # Tokens available per window.
38
+ ),
39
+ window_size_ms=window_size_ms,
40
+ rate_limit=rate_limit,
41
+ tokens_requested=1,
42
+ )
43
+
44
+ with RateLimitlyClient("rl-aes1...") as client:
45
+ status, result = client.check_rate_limit([resource])
46
+
47
+ if status != RCLIENT_OK:
48
+ print(f"No decision: client status {status}")
49
+ elif result.success:
50
+ print("Granted; perform the protected work")
51
+ else:
52
+ print("Denied; do not perform the protected work")
53
+ ```
54
+
55
+ `RCLIENT_OK` means a valid RateLimitly decision was received; it does not mean the request was granted. Check `result.success` for the admission decision.
56
+
57
+ ## Report one measured latency
58
+
59
+ In English: “Add a 25 ms observation to the `inventory-backend` tracker defined by these storage settings.”
60
+
61
+ ```python
62
+ from ratelimitly import (
63
+ RateLimitlyClient,
64
+ ServiceLatencyReport,
65
+ r_client_derive_latency_tracker_id,
66
+ )
67
+
68
+ ttl_ms = 10_000
69
+ max_samples = 100
70
+ min_sample_threshold = 5
71
+
72
+ tracker_id = r_client_derive_latency_tracker_id(
73
+ "inventory-backend", # Exact application-defined tracker name.
74
+ ttl_ms, # Maximum sample lifetime.
75
+ max_samples, # Samples considered by the tracker.
76
+ min_sample_threshold, # Warm-up samples before guards take effect.
77
+ )
78
+
79
+ report = ServiceLatencyReport(
80
+ latency_tracker_id=tracker_id,
81
+ observed_latency_ms=25,
82
+ ttl_ms=ttl_ms,
83
+ max_samples=max_samples,
84
+ min_sample_threshold=min_sample_threshold,
85
+ )
86
+
87
+ with RateLimitlyClient("rl-aes1...") as client:
88
+ status = client.report_latency([report])
89
+ ```
90
+
91
+ Measure the service operation, not the RateLimitly request. Reports are independent of resource requests and may be sent by a different process.
92
+
93
+ ## Add a latency guard
94
+
95
+ This request asks for the same token only when the tracker’s current latency is below 50 ms:
96
+
97
+ ```python
98
+ from ratelimitly import LatencyGuard
99
+
100
+ guard = LatencyGuard(
101
+ latency_tracker_id=tracker_id, # Same tracker definition as the report.
102
+ threshold_ms=50, # Admission requires current latency < 50 ms.
103
+ ttl_ms=ttl_ms,
104
+ max_samples=max_samples,
105
+ min_sample_threshold=min_sample_threshold,
106
+ )
107
+
108
+ with RateLimitlyClient("rl-aes1...") as client:
109
+ status, result = client.check_rate_limit(
110
+ resources=[resource],
111
+ guards=[guard],
112
+ )
113
+ ```
114
+
115
+ ## Canonical IDs must agree across clients
116
+
117
+ Bucket IDs include the exact bucket-name bytes, `window_size_ms`, and `rate_limit`. Latency-tracker IDs include the exact tracker-name bytes, `ttl_ms`, `max_samples`, and `min_sample_threshold`; a guard threshold is deliberately not part of the tracker ID.
118
+
119
+ The Python helpers implement the same domain-separated binary preimage,
120
+ little-endian integer encoding, BLAKE2s-256 digest, and 16-byte truncation as
121
+ the coordinated wire-v2 C client. Do not replace them with hashing of formatted
122
+ text, and do not use `hashlib.blake2s(..., digest_size=16)`: digest length is a
123
+ BLAKE2 parameter, so that produces a different ID.
124
+
125
+ See [API reference](docs/api.md#canonical-content-defined-identifiers) for the exact formula and cross-client known-answer vectors.
126
+
127
+ ## High-availability policy
128
+
129
+ The default policy matches `rl-c-client`: `unit_ms=20`, one replay, a fixed one-unit round duration, one final receive-only unit, and completion delivery enabled. Its deduplication TTL and maximum decision horizon are 60 ms. The initial transmission goes to every discovered r-server, and the oldest known server’s response wins when it arrives within the first round.
130
+
131
+ See [configuration](docs/configuration.md) for the complete parametrized policy and [architecture](docs/architecture.md) for wire and selection semantics.
132
+
133
+ ## API layers
134
+
135
+ `RateLimitlyClient` is blocking. `AsyncRateLimitlyClient` provides the same serialized state machine through `asyncio`. A client preserves DNS results and UDP sockets across calls; call `close()` or use the context-manager forms to release them.
136
+
137
+ The client returns the same status-code family as the C library. It does not choose an application fail-open or fail-closed policy: the caller decides what to do when no RateLimitly decision is available.
138
+
139
+ ## Documentation
140
+
141
+ - [API reference](docs/api.md)
142
+ - [Configuration and request policy](docs/configuration.md)
143
+ - [Architecture, wire format, and conformance](docs/architecture.md)
144
+
145
+ ## License
146
+
147
+ MIT
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ratelimitly"
7
+ version = "2.0.0"
8
+ description = "Official Python client library for RateLimitly high-performance rate limiting."
9
+ readme = "README.md"
10
+ authors = [
11
+ { name = "RateLimitly Team", email = "support@ratelimitly.com" }
12
+ ]
13
+ license = { text = "MIT" }
14
+ requires-python = ">=3.8"
15
+ dependencies = [
16
+ "cryptography>=3.4",
17
+ "dnspython>=2.0.0",
18
+ ]
19
+ classifiers = [
20
+ "Programming Language :: Python :: 3",
21
+ "Operating System :: OS Independent",
22
+ ]
23
+
24
+ [project.urls]
25
+ Homepage = "https://ratelimitly.com"
26
+ Repository = "https://github.com/ratelimitly-com/rl-python-client"
27
+
28
+ [project.optional-dependencies]
29
+ dev = [
30
+ "pytest>=7.0.0",
31
+ "pytest-asyncio>=0.20.0",
32
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ """Compatibility entry point; package metadata lives in pyproject.toml."""
2
+
3
+ from setuptools import setup
4
+
5
+
6
+ setup()
@@ -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
+ ]
@@ -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
+ )