libentry 1.19__py3-none-any.whl → 1.20__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.
- libentry/api.py +107 -80
- libentry/test_api.py +23 -5
- {libentry-1.19.dist-info → libentry-1.20.dist-info}/METADATA +3 -3
- {libentry-1.19.dist-info → libentry-1.20.dist-info}/RECORD +9 -9
- {libentry-1.19.dist-info → libentry-1.20.dist-info}/LICENSE +0 -0
- {libentry-1.19.dist-info → libentry-1.20.dist-info}/WHEEL +0 -0
- {libentry-1.19.dist-info → libentry-1.20.dist-info}/entry_points.txt +0 -0
- {libentry-1.19.dist-info → libentry-1.20.dist-info}/top_level.txt +0 -0
- {libentry-1.19.dist-info → libentry-1.20.dist-info}/zip-safe +0 -0
libentry/api.py
CHANGED
@@ -15,7 +15,8 @@ from time import sleep
|
|
15
15
|
from typing import Any, Callable, Iterable, List, Literal, Mapping, Optional, Tuple
|
16
16
|
from urllib.parse import urljoin
|
17
17
|
|
18
|
-
import
|
18
|
+
from urllib3 import PoolManager
|
19
|
+
from urllib3.exceptions import HTTPError, TimeoutError
|
19
20
|
|
20
21
|
from libentry import json
|
21
22
|
|
@@ -193,32 +194,49 @@ class APIClient:
|
|
193
194
|
if api_key is not None:
|
194
195
|
self.headers["Authorization"] = f"Bearer {api_key}"
|
195
196
|
self.verify = verify
|
197
|
+
self.charset = "UTF-8"
|
198
|
+
|
199
|
+
DEFAULT_CONN_POOL_SIZE = 10
|
200
|
+
CONN_POOL = (
|
201
|
+
PoolManager(DEFAULT_CONN_POOL_SIZE),
|
202
|
+
PoolManager(DEFAULT_CONN_POOL_SIZE, cert_reqs='CERT_NONE')
|
203
|
+
)
|
196
204
|
|
197
205
|
def _request(
|
198
206
|
self,
|
199
|
-
method:
|
207
|
+
method: Literal["GET", "POST"],
|
200
208
|
url: str,
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
209
|
+
body: Optional[str] = None,
|
210
|
+
headers: Optional[Mapping[str, str]] = None,
|
211
|
+
stream: bool = False,
|
212
|
+
num_trials: int = 5,
|
213
|
+
timeout: float = 15,
|
214
|
+
interval: float = 1,
|
215
|
+
retry_factor: float = 0.5,
|
205
216
|
on_error: Optional[ErrorCallback] = None,
|
206
|
-
|
217
|
+
verify: Optional[bool] = None,
|
207
218
|
):
|
219
|
+
headers = self.headers if headers is None else headers
|
220
|
+
verify = self.verify if verify is None else verify
|
221
|
+
preload_content = not stream
|
222
|
+
|
223
|
+
pool = self.CONN_POOL[int(not verify)]
|
208
224
|
err = None
|
209
225
|
for i in range(num_trials):
|
210
226
|
try:
|
211
|
-
return
|
227
|
+
return pool.request(
|
212
228
|
method=method,
|
213
229
|
url=url,
|
230
|
+
body=body,
|
231
|
+
headers=headers,
|
214
232
|
timeout=timeout * (1 + i * retry_factor),
|
215
|
-
|
233
|
+
preload_content=preload_content
|
216
234
|
)
|
217
|
-
except
|
235
|
+
except TimeoutError as e:
|
218
236
|
err = e
|
219
237
|
if callable(on_error):
|
220
238
|
on_error(e)
|
221
|
-
except
|
239
|
+
except HTTPError as e:
|
222
240
|
err = e
|
223
241
|
if callable(on_error):
|
224
242
|
on_error(e)
|
@@ -236,10 +254,8 @@ class APIClient:
|
|
236
254
|
):
|
237
255
|
full_url = urljoin(self.base_url, path)
|
238
256
|
response = self._request(
|
239
|
-
"
|
257
|
+
method="GET",
|
240
258
|
url=full_url,
|
241
|
-
headers=self.headers,
|
242
|
-
verify=self.verify,
|
243
259
|
num_trials=num_trials,
|
244
260
|
timeout=timeout,
|
245
261
|
interval=interval,
|
@@ -247,15 +263,15 @@ class APIClient:
|
|
247
263
|
on_error=on_error
|
248
264
|
)
|
249
265
|
|
250
|
-
if response.
|
251
|
-
text = response.
|
252
|
-
response.
|
266
|
+
if response.status != 200:
|
267
|
+
text = response.data.decode(self.charset)
|
268
|
+
response.release_conn()
|
253
269
|
raise ServiceError(text)
|
254
270
|
|
255
271
|
try:
|
256
|
-
return _load_json_or_str(response.
|
272
|
+
return _load_json_or_str(response.data.decode(self.charset))
|
257
273
|
finally:
|
258
|
-
response.
|
274
|
+
response.release_conn()
|
259
275
|
|
260
276
|
def post(
|
261
277
|
self,
|
@@ -271,19 +287,19 @@ class APIClient:
|
|
271
287
|
chunk_delimiter: str = "\n\n",
|
272
288
|
chunk_prefix: str = None,
|
273
289
|
chunk_suffix: str = None,
|
274
|
-
error_prefix: str = "ERROR: "
|
290
|
+
error_prefix: str = "ERROR: ",
|
291
|
+
stream_read_size: int = 512
|
275
292
|
):
|
276
293
|
full_url = urljoin(self.base_url, path)
|
277
294
|
|
278
295
|
headers = {**self.headers}
|
279
296
|
headers["Accept"] = headers["Accept"] + f"; stream={int(stream)}"
|
280
|
-
|
297
|
+
body = json.dumps(json_data) if json_data is not None else None
|
281
298
|
response = self._request(
|
282
|
-
"
|
299
|
+
"POST",
|
283
300
|
url=full_url,
|
301
|
+
body=body,
|
284
302
|
headers=headers,
|
285
|
-
data=data,
|
286
|
-
verify=self.verify,
|
287
303
|
stream=stream,
|
288
304
|
num_trials=num_trials,
|
289
305
|
timeout=timeout,
|
@@ -291,70 +307,81 @@ class APIClient:
|
|
291
307
|
retry_factor=retry_factor,
|
292
308
|
on_error=on_error
|
293
309
|
)
|
294
|
-
if response.
|
295
|
-
text = response.
|
296
|
-
response.
|
310
|
+
if response.status != 200:
|
311
|
+
text = response.data.decode(self.charset)
|
312
|
+
response.release_conn()
|
297
313
|
raise ServiceError(text)
|
298
314
|
|
299
|
-
if stream:
|
300
|
-
if chunk_delimiter is None:
|
301
|
-
# TODO: this branch is not tested yet!
|
302
|
-
return response.iter_content(decode_unicode=True)
|
303
|
-
else:
|
304
|
-
gen = self._iter_chunks(
|
305
|
-
response=response,
|
306
|
-
chunk_delimiter=chunk_delimiter.encode() if chunk_delimiter else None,
|
307
|
-
chunk_prefix=chunk_prefix.encode() if chunk_prefix else None,
|
308
|
-
chunk_suffix=chunk_suffix.encode() if chunk_suffix else None,
|
309
|
-
error_prefix=error_prefix.encode() if error_prefix else None,
|
310
|
-
)
|
311
|
-
return gen if not exhaust_stream else [*gen]
|
312
|
-
else:
|
315
|
+
if not stream:
|
313
316
|
try:
|
314
|
-
return _load_json_or_str(response.
|
317
|
+
return _load_json_or_str(response.data.decode(self.charset))
|
315
318
|
finally:
|
316
|
-
response.
|
319
|
+
response.release_conn()
|
320
|
+
else:
|
321
|
+
def iter_content():
|
322
|
+
try:
|
323
|
+
if hasattr(response, "stream"):
|
324
|
+
yield from response.stream(stream_read_size, decode_content=True)
|
325
|
+
else:
|
326
|
+
while True:
|
327
|
+
data = response.read(stream_read_size)
|
328
|
+
if not data:
|
329
|
+
break
|
330
|
+
yield data
|
331
|
+
finally:
|
332
|
+
response.release_conn()
|
333
|
+
|
334
|
+
def iter_lines(contents: Iterable[bytes]) -> Iterable[str]:
|
335
|
+
delimiter = chunk_delimiter.encode(self.charset) if chunk_delimiter is not None else None
|
336
|
+
pending = None
|
337
|
+
for data in contents:
|
338
|
+
if pending is not None:
|
339
|
+
data = pending + data
|
340
|
+
|
341
|
+
lines = data.split(delimiter) if delimiter else data.splitlines()
|
342
|
+
pending = lines.pop() if lines and lines[-1] and lines[-1][-1] == data[-1] else None
|
343
|
+
|
344
|
+
for line in lines:
|
345
|
+
yield line.decode(self.charset)
|
346
|
+
|
347
|
+
if pending is not None:
|
348
|
+
yield pending.decode(self.charset)
|
349
|
+
|
350
|
+
def iter_chunks(lines: Iterable[str]):
|
351
|
+
error = None
|
352
|
+
for chunk in lines:
|
353
|
+
if error is not None:
|
354
|
+
# error is not None means there is a fatal exception raised from the server side.
|
355
|
+
# The client should just complete the stream and then raise the error to the upper.
|
356
|
+
continue
|
317
357
|
|
318
|
-
|
319
|
-
|
320
|
-
response: requests.Response,
|
321
|
-
chunk_delimiter: bytes,
|
322
|
-
chunk_prefix: bytes,
|
323
|
-
chunk_suffix: bytes,
|
324
|
-
error_prefix: bytes
|
325
|
-
) -> Iterable:
|
326
|
-
try:
|
327
|
-
error = None
|
328
|
-
for chunk in response.iter_lines(decode_unicode=False, delimiter=chunk_delimiter):
|
329
|
-
if error is not None:
|
330
|
-
# error is not None means there is a fatal exception raised from the server side.
|
331
|
-
# The client should just complete the stream and then raise the error to the upper.
|
332
|
-
continue
|
358
|
+
if not chunk:
|
359
|
+
continue
|
333
360
|
|
334
|
-
|
335
|
-
|
361
|
+
if error_prefix is not None:
|
362
|
+
if chunk.startswith(error_prefix):
|
363
|
+
chunk = chunk[len(error_prefix):]
|
364
|
+
error = ServiceError(chunk)
|
365
|
+
continue
|
336
366
|
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
367
|
+
if chunk_prefix is not None:
|
368
|
+
if chunk.startswith(chunk_prefix):
|
369
|
+
chunk = chunk[len(chunk_prefix):]
|
370
|
+
else:
|
371
|
+
continue
|
342
372
|
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
373
|
+
if chunk_suffix is not None:
|
374
|
+
if chunk.endswith(chunk_suffix):
|
375
|
+
chunk = chunk[:-len(chunk_suffix)]
|
376
|
+
else:
|
377
|
+
continue
|
348
378
|
|
349
|
-
|
350
|
-
if chunk.endswith(chunk_suffix):
|
351
|
-
chunk = chunk[:-len(chunk_suffix)]
|
352
|
-
else:
|
353
|
-
continue
|
379
|
+
yield _load_json_or_str(chunk)
|
354
380
|
|
355
|
-
|
381
|
+
if error is not None:
|
382
|
+
raise error
|
356
383
|
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
384
|
+
gen = iter_content()
|
385
|
+
gen = iter_lines(gen)
|
386
|
+
gen = iter_chunks(gen)
|
387
|
+
return gen if not exhaust_stream else [*gen]
|
libentry/test_api.py
CHANGED
@@ -21,6 +21,7 @@ class TestRequest(BaseModel):
|
|
21
21
|
timeout: float = 15
|
22
22
|
num_threads: int = 1
|
23
23
|
num_calls: int = 1
|
24
|
+
stream: bool = False
|
24
25
|
quiet: bool = False
|
25
26
|
max_resp_len: int = 100
|
26
27
|
|
@@ -42,7 +43,8 @@ def test(request: TestRequest):
|
|
42
43
|
try:
|
43
44
|
kwargs = dict(
|
44
45
|
on_error=lambda err: print(f"[{tid}:{cid}:RETRY] {err}"),
|
45
|
-
timeout=request.timeout
|
46
|
+
timeout=request.timeout,
|
47
|
+
stream=request.stream
|
46
48
|
)
|
47
49
|
t = time()
|
48
50
|
if request.method == "GET":
|
@@ -56,12 +58,26 @@ def test(request: TestRequest):
|
|
56
58
|
request.data,
|
57
59
|
**kwargs
|
58
60
|
)
|
59
|
-
|
61
|
+
if not request.stream:
|
62
|
+
t = time() - t
|
63
|
+
if not request.quiet:
|
64
|
+
content = str(response).replace("\n", "\\n")
|
65
|
+
print(f"[{tid}:{cid}:SUCCESS] Response={content:.{request.max_resp_len}} Time={t:.04f}")
|
66
|
+
else:
|
67
|
+
if not request.quiet and request.num_threads == 1:
|
68
|
+
print(f"[{tid}:{cid}:SUCCESS] Response=[", end="", flush=True)
|
69
|
+
for chunk in response:
|
70
|
+
print(f"'{chunk}', ", end="", flush=True)
|
71
|
+
t = time() - t
|
72
|
+
print(f"] Time={t:.04f}", flush=True)
|
73
|
+
else:
|
74
|
+
content = [*response]
|
75
|
+
t = time() - t
|
76
|
+
if not request.quiet:
|
77
|
+
content = str(content).replace("\n", "\\n")
|
78
|
+
print(f"[{tid}:{cid}:SUCCESS] Response={content:.{request.max_resp_len}} Time={t:.04f}")
|
60
79
|
with lock:
|
61
80
|
time_list.append(t)
|
62
|
-
if not request.quiet:
|
63
|
-
content = str(response).replace("\n", " ")
|
64
|
-
print(f"[{tid}:{cid}:SUCCESS] Time={t:.04f} Response={content:.{request.max_resp_len}}")
|
65
81
|
except Exception as e:
|
66
82
|
print(f"[{tid}:{cid}:FAILED] {e}")
|
67
83
|
|
@@ -93,6 +109,7 @@ def main():
|
|
93
109
|
parser.add_argument("--timeout", type=float, default=15)
|
94
110
|
parser.add_argument("--num_threads", "-t", type=int, default=1)
|
95
111
|
parser.add_argument("--num_calls", "-c", type=int, default=1)
|
112
|
+
parser.add_argument("--stream", "-s", action="store_true")
|
96
113
|
parser.add_argument("--quiet", "-q", action="store_true")
|
97
114
|
args = parser.parse_args()
|
98
115
|
|
@@ -110,6 +127,7 @@ def main():
|
|
110
127
|
timeout=args.timeout,
|
111
128
|
num_threads=args.num_threads,
|
112
129
|
num_calls=args.num_calls,
|
130
|
+
stream=args.stream,
|
113
131
|
quiet=args.quiet
|
114
132
|
))
|
115
133
|
print(response.model_dump_json(indent=4))
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: libentry
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.20
|
4
4
|
Summary: Entries for experimental utilities.
|
5
5
|
Home-page: https://github.com/XoriieInpottn/libentry
|
6
6
|
Author: xi
|
@@ -12,10 +12,10 @@ Description-Content-Type: text/markdown
|
|
12
12
|
Requires-Dist: Flask
|
13
13
|
Requires-Dist: PyYAML
|
14
14
|
Requires-Dist: gunicorn
|
15
|
-
Requires-Dist: json5
|
16
15
|
Requires-Dist: numpy
|
16
|
+
Requires-Dist: psutil
|
17
17
|
Requires-Dist: pydantic
|
18
|
-
Requires-Dist:
|
18
|
+
Requires-Dist: urllib3
|
19
19
|
|
20
20
|
# libentry
|
21
21
|
|
@@ -1,5 +1,5 @@
|
|
1
1
|
libentry/__init__.py,sha256=rDBip9M1Xb1N4wMKE1ni_DldrQbkRjp8DxPkTp3K2qo,170
|
2
|
-
libentry/api.py,sha256=
|
2
|
+
libentry/api.py,sha256=bs4mpOLRA6i9gAWPGzeNarpKssZs2UlaP2xuQbAi9NQ,12063
|
3
3
|
libentry/argparse.py,sha256=NxzXV-jBN51ReZsNs5aeyOfzwYQ5A5nJ95rWoa-FYCs,10415
|
4
4
|
libentry/dataclasses.py,sha256=AQV2PuxplJCwGZ5HKX72U-z-POUhTdy3XtpEK9KNIGQ,4541
|
5
5
|
libentry/executor.py,sha256=cTV0WxJi0nU1TP-cOwmeodN8DD6L1691M2HIQsJtGrU,6582
|
@@ -8,7 +8,7 @@ libentry/json.py,sha256=1-Kv5ZRb5dBrOTU84n6sZtYZV3xE-O6wEt_--ynbSaU,1209
|
|
8
8
|
libentry/logging.py,sha256=IiYoCUzm8XTK1fduA-NA0FI2Qz_m81NEPV3d3tEfgdI,1349
|
9
9
|
libentry/schema.py,sha256=BO7EE7i43Cb4xn_OLsBM-HDwWOT6V5fPs91Am3QqnNQ,8178
|
10
10
|
libentry/server.py,sha256=gYPoZXd0umlDYZf-6ZV0_vJadg3YQvnLDc6JFDJh9jc,1503
|
11
|
-
libentry/test_api.py,sha256=
|
11
|
+
libentry/test_api.py,sha256=GFmirvxfCAQ-nhh6QXyB5EpF4WaENCc3Eoymuk5JxBQ,4448
|
12
12
|
libentry/service/__init__.py,sha256=1oLL20yLB1GL9IbFiZD8OReDqiCpFr-yetIR6x1cNkI,23
|
13
13
|
libentry/service/common.py,sha256=OVaW2afgKA6YqstJmtnprBCqQEUZEWotZ6tHavmJJeU,42
|
14
14
|
libentry/service/flask.py,sha256=Alpsix01ROqI28k-dabD8wJlWAWs-ObNc1nJ-LxOXn4,13349
|
@@ -16,10 +16,10 @@ libentry/service/list.py,sha256=ElHWhTgShGOhaxMUEwVbMXos0NQKjHsODboiQ-3AMwE,1397
|
|
16
16
|
libentry/service/running.py,sha256=FrPJoJX6wYxcHIysoatAxhW3LajCCm0Gx6l7__6sULQ,5105
|
17
17
|
libentry/service/start.py,sha256=mZT7b9rVULvzy9GTZwxWnciCHgv9dbGN2JbxM60OMn4,1270
|
18
18
|
libentry/service/stop.py,sha256=wOpwZgrEJ7QirntfvibGq-XsTC6b3ELhzRW2zezh-0s,1187
|
19
|
-
libentry-1.
|
20
|
-
libentry-1.
|
21
|
-
libentry-1.
|
22
|
-
libentry-1.
|
23
|
-
libentry-1.
|
24
|
-
libentry-1.
|
25
|
-
libentry-1.
|
19
|
+
libentry-1.20.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
20
|
+
libentry-1.20.dist-info/METADATA,sha256=lW-aRHt0LPfumZXzccvJazTcOXkTCFhNMUrzGiU90-w,812
|
21
|
+
libentry-1.20.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
22
|
+
libentry-1.20.dist-info/entry_points.txt,sha256=vgHmJZhM-kqM7U9S179UwDD3pM232tpzJ5NntncXi_8,62
|
23
|
+
libentry-1.20.dist-info/top_level.txt,sha256=u2uF6-X5fn2Erf9PYXOg_6tntPqTpyT-yzUZrltEd6I,9
|
24
|
+
libentry-1.20.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
25
|
+
libentry-1.20.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|