ratecap 0.1.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.
- ratecap/__init__.py +4 -0
- ratecap/client.py +153 -0
- ratecap/cost.py +3 -0
- ratecap-0.1.0.dist-info/METADATA +9 -0
- ratecap-0.1.0.dist-info/RECORD +7 -0
- ratecap-0.1.0.dist-info/WHEEL +5 -0
- ratecap-0.1.0.dist-info/top_level.txt +1 -0
ratecap/__init__.py
ADDED
ratecap/client.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import ssl
|
|
2
|
+
import time
|
|
3
|
+
import urllib.parse
|
|
4
|
+
import urllib.request
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class AllowResult:
|
|
10
|
+
allowed: bool
|
|
11
|
+
retry_after_ms: int = 0
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class _Reservation:
|
|
16
|
+
key: str
|
|
17
|
+
token: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Ticket:
|
|
21
|
+
def __init__(self, client, key, allowed, retry_after_ms=0, reservations=None):
|
|
22
|
+
self.allowed = allowed
|
|
23
|
+
self.retry_after_ms = retry_after_ms
|
|
24
|
+
self._client = client
|
|
25
|
+
self._key = key
|
|
26
|
+
self._reservations = reservations or []
|
|
27
|
+
|
|
28
|
+
def release(self):
|
|
29
|
+
errors = []
|
|
30
|
+
for reservation in self._reservations:
|
|
31
|
+
try:
|
|
32
|
+
self._client._release_one(reservation)
|
|
33
|
+
except Exception as exc:
|
|
34
|
+
errors.append(f"{reservation.key}: {exc}")
|
|
35
|
+
if errors:
|
|
36
|
+
raise RuntimeError("failed to release reservation(s): " + "; ".join(errors))
|
|
37
|
+
|
|
38
|
+
def refund(self, refund_amount):
|
|
39
|
+
url = f"{self._client._sidecar_addr}/release"
|
|
40
|
+
req = urllib.request.Request(
|
|
41
|
+
url,
|
|
42
|
+
method="POST",
|
|
43
|
+
headers={
|
|
44
|
+
"X-RateCap-Refund-Key": self._key,
|
|
45
|
+
"X-RateCap-Refund-Amount": str(refund_amount),
|
|
46
|
+
},
|
|
47
|
+
)
|
|
48
|
+
try:
|
|
49
|
+
with self._client._urlopen(req) as resp:
|
|
50
|
+
if resp.status != 200:
|
|
51
|
+
raise RuntimeError(f"refund failed with status {resp.status}")
|
|
52
|
+
except urllib.error.HTTPError as err:
|
|
53
|
+
raise RuntimeError(f"refund failed with status {err.code}") from err
|
|
54
|
+
|
|
55
|
+
def __enter__(self):
|
|
56
|
+
return self
|
|
57
|
+
|
|
58
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
59
|
+
self.release()
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Client:
|
|
64
|
+
def __init__(
|
|
65
|
+
self, sidecar_addr, timeout=5.0, max_retries=0, backoff_base=0.1, ca_file=None
|
|
66
|
+
):
|
|
67
|
+
self._sidecar_addr = sidecar_addr.rstrip("/")
|
|
68
|
+
self._timeout = timeout
|
|
69
|
+
self._max_retries = max_retries
|
|
70
|
+
self._backoff_base = backoff_base
|
|
71
|
+
self._ssl_context = None
|
|
72
|
+
if ca_file is not None:
|
|
73
|
+
self._ssl_context = ssl.create_default_context(cafile=ca_file)
|
|
74
|
+
|
|
75
|
+
def _urlopen(self, req):
|
|
76
|
+
attempt = 0
|
|
77
|
+
while True:
|
|
78
|
+
try:
|
|
79
|
+
kwargs = {"timeout": self._timeout}
|
|
80
|
+
if self._ssl_context is not None:
|
|
81
|
+
kwargs["context"] = self._ssl_context
|
|
82
|
+
return urllib.request.urlopen(req, **kwargs)
|
|
83
|
+
except urllib.error.HTTPError:
|
|
84
|
+
raise
|
|
85
|
+
except Exception:
|
|
86
|
+
if attempt >= self._max_retries:
|
|
87
|
+
raise
|
|
88
|
+
time.sleep(self._backoff_base * (2**attempt))
|
|
89
|
+
attempt += 1
|
|
90
|
+
|
|
91
|
+
def allow(self, key, cost=None, priority=None):
|
|
92
|
+
params = {"key": key, "skip_reservations": "true"}
|
|
93
|
+
if cost is not None:
|
|
94
|
+
params["cost"] = str(cost)
|
|
95
|
+
query = urllib.parse.urlencode(params)
|
|
96
|
+
url = f"{self._sidecar_addr}/check?{query}"
|
|
97
|
+
headers = {"x-ratecap-priority": priority} if priority else {}
|
|
98
|
+
req = urllib.request.Request(url, method="GET", headers=headers)
|
|
99
|
+
try:
|
|
100
|
+
with self._urlopen(req) as resp:
|
|
101
|
+
return AllowResult(allowed=True)
|
|
102
|
+
except urllib.error.HTTPError as err:
|
|
103
|
+
retry_after_ms = int(err.headers.get("Retry-After-Ms", 0) or 0)
|
|
104
|
+
return AllowResult(allowed=False, retry_after_ms=retry_after_ms)
|
|
105
|
+
|
|
106
|
+
def acquire(self, key, cost=None, priority=None):
|
|
107
|
+
params = {"key": key}
|
|
108
|
+
if cost is not None:
|
|
109
|
+
params["cost"] = str(cost)
|
|
110
|
+
query = urllib.parse.urlencode(params)
|
|
111
|
+
url = f"{self._sidecar_addr}/check?{query}"
|
|
112
|
+
headers = {"x-ratecap-priority": priority} if priority else {}
|
|
113
|
+
req = urllib.request.Request(url, method="GET", headers=headers)
|
|
114
|
+
try:
|
|
115
|
+
with self._urlopen(req) as resp:
|
|
116
|
+
reservations = self._parse_reservations(resp.headers)
|
|
117
|
+
return Ticket(self, key, allowed=True, reservations=reservations)
|
|
118
|
+
except urllib.error.HTTPError as err:
|
|
119
|
+
reservations = self._parse_reservations(err.headers)
|
|
120
|
+
retry_after_ms = int(err.headers.get("Retry-After-Ms", 0) or 0)
|
|
121
|
+
return Ticket(
|
|
122
|
+
self,
|
|
123
|
+
key,
|
|
124
|
+
allowed=False,
|
|
125
|
+
retry_after_ms=retry_after_ms,
|
|
126
|
+
reservations=reservations,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def _parse_reservations(self, headers):
|
|
130
|
+
reservations = []
|
|
131
|
+
i = 0
|
|
132
|
+
while True:
|
|
133
|
+
token = headers.get(f"Concurrency-Token-{i}")
|
|
134
|
+
if not token:
|
|
135
|
+
break
|
|
136
|
+
key = headers.get(f"Concurrency-Key-{i}", "")
|
|
137
|
+
reservations.append(_Reservation(key=key, token=token))
|
|
138
|
+
i += 1
|
|
139
|
+
return reservations
|
|
140
|
+
|
|
141
|
+
def _release_one(self, reservation):
|
|
142
|
+
url = f"{self._sidecar_addr}/release"
|
|
143
|
+
req = urllib.request.Request(
|
|
144
|
+
url,
|
|
145
|
+
method="POST",
|
|
146
|
+
headers={
|
|
147
|
+
"X-RateCap-Concurrency-Key": reservation.key,
|
|
148
|
+
"X-RateCap-Concurrency-Token": reservation.token,
|
|
149
|
+
},
|
|
150
|
+
)
|
|
151
|
+
with self._urlopen(req) as resp:
|
|
152
|
+
if resp.status != 200:
|
|
153
|
+
raise RuntimeError(f"release failed with status {resp.status}")
|
ratecap/cost.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ratecap
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Thin Python client for the RateCap sidecar
|
|
5
|
+
Project-URL: Homepage, https://github.com/sairam0424/RateCap
|
|
6
|
+
Project-URL: Repository, https://github.com/sairam0424/RateCap
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Requires-Python: >=3.10
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
ratecap/__init__.py,sha256=aw7Xl4bx1EzbDR7APDaNkVWqD0j2Hp3cTvwOeOueqj8,166
|
|
2
|
+
ratecap/client.py,sha256=LV-SzJphgzoqDxuKireuPMGs-gLcbe7CevCOuezG5DI,5298
|
|
3
|
+
ratecap/cost.py,sha256=7s7sEkrnk0nRB0og_UwuwwA6-GlFqlCE7xLiN4-FeHg,110
|
|
4
|
+
ratecap-0.1.0.dist-info/METADATA,sha256=YC02E7QB9OiZ3miKoNQLA4fQdCjOB1kiAiIZX0KNSu0,350
|
|
5
|
+
ratecap-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
6
|
+
ratecap-0.1.0.dist-info/top_level.txt,sha256=Iev0w-CZJjvnt3tcpvMjD2H86vVZCbd4aRuZPwb_Sjk,8
|
|
7
|
+
ratecap-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ratecap
|