libentry 1.22.3__py3-none-any.whl → 1.22.4__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/json.py +4 -0
- libentry/service/flask.py +15 -2
- libentry/service/flask_mcp.py +337 -0
- {libentry-1.22.3.dist-info → libentry-1.22.4.dist-info}/METADATA +17 -8
- {libentry-1.22.3.dist-info → libentry-1.22.4.dist-info}/RECORD +10 -9
- {libentry-1.22.3.dist-info → libentry-1.22.4.dist-info}/WHEEL +1 -1
- {libentry-1.22.3.dist-info → libentry-1.22.4.dist-info}/entry_points.txt +0 -1
- {libentry-1.22.3.dist-info → libentry-1.22.4.dist-info}/LICENSE +0 -0
- {libentry-1.22.3.dist-info → libentry-1.22.4.dist-info}/top_level.txt +0 -0
- {libentry-1.22.3.dist-info → libentry-1.22.4.dist-info}/zip-safe +0 -0
libentry/json.py
CHANGED
@@ -13,6 +13,7 @@ from base64 import b64decode, b64encode
|
|
13
13
|
from functools import partial
|
14
14
|
|
15
15
|
import numpy as np
|
16
|
+
from pydantic import BaseModel
|
16
17
|
|
17
18
|
_BINDINGS = []
|
18
19
|
|
@@ -26,6 +27,9 @@ def bind(name, type_):
|
|
26
27
|
|
27
28
|
|
28
29
|
def custom_encode(o) -> dict:
|
30
|
+
if isinstance(o, BaseModel):
|
31
|
+
return o.model_dump()
|
32
|
+
|
29
33
|
for name, type_, support in _BINDINGS:
|
30
34
|
if isinstance(o, type_):
|
31
35
|
doc = support.encode(o)
|
libentry/service/flask.py
CHANGED
@@ -371,6 +371,7 @@ def run_service(
|
|
371
371
|
worker_class: str = "gthread",
|
372
372
|
timeout: int = 60,
|
373
373
|
keyfile: Optional[str] = None,
|
374
|
+
keyfile_password: Optional[str] = None,
|
374
375
|
certfile: Optional[str] = None
|
375
376
|
):
|
376
377
|
logger.info("Starting gunicorn server.")
|
@@ -378,6 +379,18 @@ def run_service(
|
|
378
379
|
num_connections = num_threads * 2
|
379
380
|
if backlog is None or backlog < num_threads * 2:
|
380
381
|
backlog = num_threads * 2
|
382
|
+
|
383
|
+
def ssl_context(config, default_ssl_context_factory):
|
384
|
+
import ssl
|
385
|
+
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
386
|
+
context.load_cert_chain(
|
387
|
+
certfile=config.certfile,
|
388
|
+
keyfile=config.keyfile,
|
389
|
+
password=keyfile_password
|
390
|
+
)
|
391
|
+
context.minimum_version = ssl.TLSVersion.TLSv1_3
|
392
|
+
return context
|
393
|
+
|
381
394
|
options = {
|
382
395
|
"bind": f"{host}:{port}",
|
383
396
|
"workers": num_workers,
|
@@ -387,9 +400,9 @@ def run_service(
|
|
387
400
|
"backlog": backlog,
|
388
401
|
"keyfile": keyfile,
|
389
402
|
"certfile": certfile,
|
390
|
-
"worker_class": worker_class
|
403
|
+
"worker_class": worker_class,
|
404
|
+
"ssl_context": ssl_context
|
391
405
|
}
|
392
406
|
for name, value in options.items():
|
393
407
|
logger.info(f"Option {name}: {value}")
|
394
408
|
GunicornApplication(service_type, service_config, options).run()
|
395
|
-
|
@@ -0,0 +1,337 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
|
3
|
+
__author__ = "xi"
|
4
|
+
__all__ = [
|
5
|
+
"MCPMethod",
|
6
|
+
]
|
7
|
+
|
8
|
+
import asyncio
|
9
|
+
import traceback
|
10
|
+
from inspect import signature
|
11
|
+
from types import GeneratorType
|
12
|
+
from typing import Callable, Dict, Iterable, Optional, Type, Union
|
13
|
+
|
14
|
+
from flask import Flask, request as flask_request
|
15
|
+
from pydantic import BaseModel
|
16
|
+
|
17
|
+
from libentry import api, json, logger
|
18
|
+
from libentry.api import list_api_info
|
19
|
+
from libentry.schema import query_api
|
20
|
+
|
21
|
+
try:
|
22
|
+
from gunicorn.app.base import BaseApplication
|
23
|
+
except ImportError:
|
24
|
+
class BaseApplication:
|
25
|
+
|
26
|
+
def load(self) -> Flask:
|
27
|
+
pass
|
28
|
+
|
29
|
+
def run(self):
|
30
|
+
flask_server = self.load()
|
31
|
+
assert hasattr(self, "options")
|
32
|
+
bind = getattr(self, "options")["bind"]
|
33
|
+
pos = bind.rfind(":")
|
34
|
+
host = bind[:pos]
|
35
|
+
port = int(bind[pos + 1:])
|
36
|
+
logger.warn("Your system doesn't support gunicorn.")
|
37
|
+
logger.warn("Use Flask directly.")
|
38
|
+
logger.warn("Options like \"num_threads\", \"num_workers\" are ignored.")
|
39
|
+
return flask_server.run(host=host, port=port)
|
40
|
+
|
41
|
+
|
42
|
+
class MCPMethod:
|
43
|
+
|
44
|
+
def __init__(self, fn: Callable, method: str = None):
|
45
|
+
self.fn = fn
|
46
|
+
assert hasattr(fn, "__name__")
|
47
|
+
self.__name__ = fn.__name__
|
48
|
+
self.method = self.__name__ if method is None else method
|
49
|
+
|
50
|
+
self.input_schema = None
|
51
|
+
params = signature(fn).parameters
|
52
|
+
if len(params) == 1:
|
53
|
+
for name, value in params.items():
|
54
|
+
annotation = value.annotation
|
55
|
+
if isinstance(annotation, type) and issubclass(annotation, BaseModel):
|
56
|
+
self.input_schema = annotation
|
57
|
+
|
58
|
+
def __call__(self, request: dict) -> Union[dict, Iterable[dict]]:
|
59
|
+
try:
|
60
|
+
jsonrpc_version = request["jsonrpc"]
|
61
|
+
request_id = request["id"]
|
62
|
+
method = request["method"]
|
63
|
+
except KeyError:
|
64
|
+
raise RuntimeError("Invalid JSON-RPC specification.")
|
65
|
+
|
66
|
+
if not isinstance(request_id, (str, int)):
|
67
|
+
raise RuntimeError(
|
68
|
+
f"Request ID should be an integer or string. "
|
69
|
+
f"Got {type(request_id)}."
|
70
|
+
)
|
71
|
+
|
72
|
+
if method != self.method:
|
73
|
+
raise RuntimeError(
|
74
|
+
f"Method missmatch."
|
75
|
+
f"Expect {self.method}, got {method}."
|
76
|
+
)
|
77
|
+
|
78
|
+
params = request.get("params", {})
|
79
|
+
|
80
|
+
try:
|
81
|
+
if self.input_schema is not None:
|
82
|
+
# Note that "input_schema is not None" means:
|
83
|
+
# (1) The function has only one argument;
|
84
|
+
# (2) The arguments is a BaseModel.
|
85
|
+
# In this case, the request data can be directly validated as a "BaseModel" and
|
86
|
+
# subsequently passed to the function as a single object.
|
87
|
+
pydantic_params = self.input_schema.model_validate(params)
|
88
|
+
result = self.fn(pydantic_params)
|
89
|
+
else:
|
90
|
+
# The function has multiple arguments, and the request data bundle them as a single object.
|
91
|
+
# So, they should be unpacked before pass to the function.
|
92
|
+
result = self.fn(**params)
|
93
|
+
except Exception as e:
|
94
|
+
if isinstance(e, (SystemExit, KeyboardInterrupt)):
|
95
|
+
raise e
|
96
|
+
return {
|
97
|
+
"jsonrpc": jsonrpc_version,
|
98
|
+
"id": request_id,
|
99
|
+
"error": self._make_error(e)
|
100
|
+
}
|
101
|
+
|
102
|
+
if not isinstance(result, (GeneratorType, range)):
|
103
|
+
return {
|
104
|
+
"jsonrpc": jsonrpc_version,
|
105
|
+
"id": request_id,
|
106
|
+
"result": result
|
107
|
+
}
|
108
|
+
|
109
|
+
return ({
|
110
|
+
"jsonrpc": jsonrpc_version,
|
111
|
+
"id": request_id,
|
112
|
+
"result": item
|
113
|
+
} for item in result)
|
114
|
+
|
115
|
+
@staticmethod
|
116
|
+
def _make_error(e):
|
117
|
+
err_cls = e.__class__
|
118
|
+
err_name = err_cls.__name__
|
119
|
+
module = err_cls.__module__
|
120
|
+
if module != "builtins":
|
121
|
+
err_name = f"{module}.{err_name}"
|
122
|
+
return {
|
123
|
+
"code": 1,
|
124
|
+
"message": f"{err_name}: {str(e)}",
|
125
|
+
"data": traceback.format_exc()
|
126
|
+
}
|
127
|
+
|
128
|
+
|
129
|
+
class FlaskMethod:
|
130
|
+
|
131
|
+
def __init__(self, method, api_info, app):
|
132
|
+
self.method = MCPMethod(method)
|
133
|
+
self.api_info = api_info
|
134
|
+
self.app = app
|
135
|
+
assert hasattr(method, "__name__")
|
136
|
+
self.__name__ = method.__name__
|
137
|
+
|
138
|
+
CONTENT_TYPE_JSON = "application/json"
|
139
|
+
CONTENT_TYPE_SSE = "text/event-stream"
|
140
|
+
|
141
|
+
def __call__(self):
|
142
|
+
args = flask_request.args
|
143
|
+
data = flask_request.data
|
144
|
+
content_type = flask_request.content_type
|
145
|
+
accepts = flask_request.accept_mimetypes
|
146
|
+
|
147
|
+
json_from_url = {**args}
|
148
|
+
if data:
|
149
|
+
if (not content_type) or content_type == self.CONTENT_TYPE_JSON:
|
150
|
+
json_from_data = json.loads(data)
|
151
|
+
else:
|
152
|
+
return self.app.error(f"Unsupported Content-Type: \"{content_type}\".")
|
153
|
+
else:
|
154
|
+
json_from_data = {}
|
155
|
+
|
156
|
+
conflicts = json_from_url.keys() & json_from_data.keys()
|
157
|
+
if len(conflicts) > 0:
|
158
|
+
return self.app.error(f"Duplicated fields: \"{conflicts}\".")
|
159
|
+
|
160
|
+
input_json = {**json_from_url, **json_from_data}
|
161
|
+
print(input_json)
|
162
|
+
|
163
|
+
try:
|
164
|
+
output_json = self.method(input_json)
|
165
|
+
except Exception as e:
|
166
|
+
return self.app.error(str(e))
|
167
|
+
|
168
|
+
if isinstance(output_json, Dict):
|
169
|
+
if self.CONTENT_TYPE_JSON in accepts:
|
170
|
+
return self.app.ok(json.dumps(output_json), mimetype=self.CONTENT_TYPE_JSON)
|
171
|
+
else:
|
172
|
+
return self.app.error(f"Unsupported Accept: \"{[*accepts]}\".")
|
173
|
+
elif isinstance(output_json, (GeneratorType, range)):
|
174
|
+
if self.CONTENT_TYPE_SSE in accepts:
|
175
|
+
# todo
|
176
|
+
return self.app.ok(json.dumps(output_json), mimetype=self.CONTENT_TYPE_SSE)
|
177
|
+
else:
|
178
|
+
return self.app.error(f"Unsupported Accept: \"{[*accepts]}\".")
|
179
|
+
|
180
|
+
|
181
|
+
class FlaskServer(Flask):
|
182
|
+
|
183
|
+
def __init__(self, service):
|
184
|
+
super().__init__(__name__)
|
185
|
+
self.service = service
|
186
|
+
|
187
|
+
logger.info("Initializing Flask application.")
|
188
|
+
self.api_info_list = list_api_info(service)
|
189
|
+
if len(self.api_info_list) == 0:
|
190
|
+
logger.error("No API found, nothing to serve.")
|
191
|
+
return
|
192
|
+
|
193
|
+
for fn, api_info in self.api_info_list:
|
194
|
+
method = api_info.method
|
195
|
+
path = api_info.path
|
196
|
+
if asyncio.iscoroutinefunction(fn):
|
197
|
+
logger.error(f"Async function \"{fn.__name__}\" is not supported.")
|
198
|
+
continue
|
199
|
+
logger.info(f"Serving {method}-API for {path}")
|
200
|
+
|
201
|
+
wrapped_fn = FlaskMethod(fn, api_info, self)
|
202
|
+
if method == "GET":
|
203
|
+
self.get(path)(wrapped_fn)
|
204
|
+
elif method == "POST":
|
205
|
+
self.post(path)(wrapped_fn)
|
206
|
+
else:
|
207
|
+
raise RuntimeError(f"Unsupported method \"{method}\" for ")
|
208
|
+
|
209
|
+
for fn, api_info in list_api_info(self):
|
210
|
+
method = api_info.method
|
211
|
+
path = api_info.path
|
212
|
+
|
213
|
+
if any(api_info.path == a.path for _, a in self.api_info_list):
|
214
|
+
logger.info(f"Use custom implementation of {path}.")
|
215
|
+
continue
|
216
|
+
|
217
|
+
if asyncio.iscoroutinefunction(fn):
|
218
|
+
logger.error(f"Async function \"{fn.__name__}\" is not supported.")
|
219
|
+
continue
|
220
|
+
logger.info(f"Serving {method}-API for {path}")
|
221
|
+
|
222
|
+
wrapped_fn = FlaskMethod(fn, api_info, self)
|
223
|
+
if method == "GET":
|
224
|
+
self.get(path)(wrapped_fn)
|
225
|
+
elif method == "POST":
|
226
|
+
self.post(path)(wrapped_fn)
|
227
|
+
else:
|
228
|
+
raise RuntimeError(f"Unsupported method \"{method}\" for ")
|
229
|
+
|
230
|
+
logger.info("Flask application initialized.")
|
231
|
+
|
232
|
+
@api.get("/")
|
233
|
+
def index(self, name: str = None):
|
234
|
+
if name is None:
|
235
|
+
all_api = []
|
236
|
+
for _, api_info in self.api_info_list:
|
237
|
+
all_api.append({"path": api_info.path})
|
238
|
+
return all_api
|
239
|
+
|
240
|
+
for fn, api_info in self.api_info_list:
|
241
|
+
if api_info.path == "/" + name:
|
242
|
+
return query_api(fn).model_dump()
|
243
|
+
|
244
|
+
return f"No API named \"{name}\""
|
245
|
+
|
246
|
+
@api.get()
|
247
|
+
def live(self):
|
248
|
+
return "OK"
|
249
|
+
|
250
|
+
def ok(self, body: Union[str, Iterable[str]], mimetype: str):
|
251
|
+
return self.response_class(body, status=200, mimetype=mimetype)
|
252
|
+
|
253
|
+
def error(self, body: str, mimetype="text"):
|
254
|
+
return self.response_class(body, status=500, mimetype=mimetype)
|
255
|
+
|
256
|
+
|
257
|
+
class GunicornApplication(BaseApplication):
|
258
|
+
|
259
|
+
def __init__(self, service_type, service_config=None, options=None):
|
260
|
+
self.service_type = service_type
|
261
|
+
self.service_config = service_config
|
262
|
+
self.options = options or {}
|
263
|
+
super().__init__()
|
264
|
+
|
265
|
+
def load_config(self):
|
266
|
+
config = {
|
267
|
+
key: value
|
268
|
+
for key, value in self.options.items()
|
269
|
+
if key in self.cfg.settings and value is not None
|
270
|
+
}
|
271
|
+
for key, value in config.items():
|
272
|
+
self.cfg.set(key.lower(), value)
|
273
|
+
|
274
|
+
def load(self):
|
275
|
+
logger.info("Initializing the service.")
|
276
|
+
if isinstance(self.service_type, type) or callable(self.service_type):
|
277
|
+
service = self.service_type(self.service_config) if self.service_config else self.service_type()
|
278
|
+
elif self.service_config is None:
|
279
|
+
logger.warning(
|
280
|
+
"Be careful! It is not recommended to start the server from a service instance. "
|
281
|
+
"Use service_type and service_config instead."
|
282
|
+
)
|
283
|
+
service = self.service_type
|
284
|
+
else:
|
285
|
+
raise TypeError(f"Invalid service type \"{type(self.service_type)}\".")
|
286
|
+
logger.info("Service initialized.")
|
287
|
+
|
288
|
+
return FlaskServer(service)
|
289
|
+
|
290
|
+
|
291
|
+
def run_service(
|
292
|
+
service_type: Union[Type, Callable],
|
293
|
+
service_config=None,
|
294
|
+
host: str = "0.0.0.0",
|
295
|
+
port: int = 8888,
|
296
|
+
num_workers: int = 1,
|
297
|
+
num_threads: int = 20,
|
298
|
+
num_connections: Optional[int] = 1000,
|
299
|
+
backlog: Optional[int] = 1000,
|
300
|
+
worker_class: str = "gthread",
|
301
|
+
timeout: int = 60,
|
302
|
+
keyfile: Optional[str] = None,
|
303
|
+
keyfile_password: Optional[str] = None,
|
304
|
+
certfile: Optional[str] = None
|
305
|
+
):
|
306
|
+
logger.info("Starting gunicorn server.")
|
307
|
+
if num_connections is None or num_connections < num_threads * 2:
|
308
|
+
num_connections = num_threads * 2
|
309
|
+
if backlog is None or backlog < num_threads * 2:
|
310
|
+
backlog = num_threads * 2
|
311
|
+
|
312
|
+
def ssl_context(config, default_ssl_context_factory):
|
313
|
+
import ssl
|
314
|
+
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
315
|
+
context.load_cert_chain(
|
316
|
+
certfile=config.certfile,
|
317
|
+
keyfile=config.keyfile,
|
318
|
+
password=keyfile_password
|
319
|
+
)
|
320
|
+
context.minimum_version = ssl.TLSVersion.TLSv1_3
|
321
|
+
return context
|
322
|
+
|
323
|
+
options = {
|
324
|
+
"bind": f"{host}:{port}",
|
325
|
+
"workers": num_workers,
|
326
|
+
"threads": num_threads,
|
327
|
+
"timeout": timeout,
|
328
|
+
"worker_connections": num_connections,
|
329
|
+
"backlog": backlog,
|
330
|
+
"keyfile": keyfile,
|
331
|
+
"certfile": certfile,
|
332
|
+
"worker_class": worker_class,
|
333
|
+
"ssl_context": ssl_context
|
334
|
+
}
|
335
|
+
for name, value in options.items():
|
336
|
+
logger.info(f"Option {name}: {value}")
|
337
|
+
GunicornApplication(service_type, service_config, options).run()
|
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.2
|
2
2
|
Name: libentry
|
3
|
-
Version: 1.22.
|
3
|
+
Version: 1.22.4
|
4
4
|
Summary: Entries for experimental utilities.
|
5
5
|
Home-page: https://github.com/XoriieInpottn/libentry
|
6
6
|
Author: xi
|
@@ -9,13 +9,24 @@ License: Apache-2.0 license
|
|
9
9
|
Platform: any
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
11
11
|
Description-Content-Type: text/markdown
|
12
|
-
|
12
|
+
License-File: LICENSE
|
13
|
+
Requires-Dist: pydantic
|
13
14
|
Requires-Dist: PyYAML
|
14
|
-
Requires-Dist: gunicorn
|
15
|
-
Requires-Dist: httpx
|
16
15
|
Requires-Dist: numpy
|
17
|
-
Requires-Dist: pydantic
|
18
16
|
Requires-Dist: urllib3
|
17
|
+
Requires-Dist: httpx
|
18
|
+
Requires-Dist: Flask
|
19
|
+
Requires-Dist: gunicorn
|
20
|
+
Dynamic: author
|
21
|
+
Dynamic: author-email
|
22
|
+
Dynamic: classifier
|
23
|
+
Dynamic: description
|
24
|
+
Dynamic: description-content-type
|
25
|
+
Dynamic: home-page
|
26
|
+
Dynamic: license
|
27
|
+
Dynamic: platform
|
28
|
+
Dynamic: requires-dist
|
29
|
+
Dynamic: summary
|
19
30
|
|
20
31
|
# libentry
|
21
32
|
|
@@ -29,5 +40,3 @@ Requires-Dist: urllib3
|
|
29
40
|
2. Use its post() or get() method to send the request.
|
30
41
|
|
31
42
|
|
32
|
-
|
33
|
-
|
@@ -4,22 +4,23 @@ 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
|
6
6
|
libentry/experiment.py,sha256=ejgAHDXWIe9x4haUzIFuz1WasLY0_aD1z_vyEVGjTu8,4922
|
7
|
-
libentry/json.py,sha256=
|
7
|
+
libentry/json.py,sha256=CubUUu29h7idLaC4d66vKhjBgVHKN1rZOv-Tw2qM17k,1916
|
8
8
|
libentry/logging.py,sha256=IiYoCUzm8XTK1fduA-NA0FI2Qz_m81NEPV3d3tEfgdI,1349
|
9
9
|
libentry/schema.py,sha256=o6JcdR00Yj4_Qjmlo100OlQpMVnl0PgvvwVVrL9limw,8268
|
10
10
|
libentry/test_api.py,sha256=Xw7B7sH6g1iCTV5sFzyBF3JAJzeOr9xg0AyezTNsnIk,4452
|
11
11
|
libentry/utils.py,sha256=O7P6GadtUIjq0N2IZH7PhHZDUM3NebzcqyDqytet7CM,683
|
12
12
|
libentry/service/__init__.py,sha256=1oLL20yLB1GL9IbFiZD8OReDqiCpFr-yetIR6x1cNkI,23
|
13
13
|
libentry/service/common.py,sha256=OVaW2afgKA6YqstJmtnprBCqQEUZEWotZ6tHavmJJeU,42
|
14
|
-
libentry/service/flask.py,sha256=
|
14
|
+
libentry/service/flask.py,sha256=SDaZnhkS3Zk6y8CytVO_awwQ3RUiY7qSuMkYAgTu_SU,13816
|
15
|
+
libentry/service/flask_mcp.py,sha256=guzDVVT4gfjhFhnLbMSTWYARyxqbEv1gDaI6SLKurdU,11540
|
15
16
|
libentry/service/list.py,sha256=ElHWhTgShGOhaxMUEwVbMXos0NQKjHsODboiQ-3AMwE,1397
|
16
17
|
libentry/service/running.py,sha256=FrPJoJX6wYxcHIysoatAxhW3LajCCm0Gx6l7__6sULQ,5105
|
17
18
|
libentry/service/start.py,sha256=mZT7b9rVULvzy9GTZwxWnciCHgv9dbGN2JbxM60OMn4,1270
|
18
19
|
libentry/service/stop.py,sha256=wOpwZgrEJ7QirntfvibGq-XsTC6b3ELhzRW2zezh-0s,1187
|
19
|
-
libentry-1.22.
|
20
|
-
libentry-1.22.
|
21
|
-
libentry-1.22.
|
22
|
-
libentry-1.22.
|
23
|
-
libentry-1.22.
|
24
|
-
libentry-1.22.
|
25
|
-
libentry-1.22.
|
20
|
+
libentry-1.22.4.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
21
|
+
libentry-1.22.4.dist-info/METADATA,sha256=B9WWlSfqYclWBLafid59zTg2GMh78j5cp7uOGTKznUo,1040
|
22
|
+
libentry-1.22.4.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
23
|
+
libentry-1.22.4.dist-info/entry_points.txt,sha256=1v_nLVDsjvVJp9SWhl4ef2zZrsLTBtFWgrYFgqvQBgc,61
|
24
|
+
libentry-1.22.4.dist-info/top_level.txt,sha256=u2uF6-X5fn2Erf9PYXOg_6tntPqTpyT-yzUZrltEd6I,9
|
25
|
+
libentry-1.22.4.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
26
|
+
libentry-1.22.4.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|