weco 0.1.3__py3-none-any.whl → 0.1.5__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.
- weco/client.py +275 -58
- weco/constants.py +4 -0
- weco/functional.py +24 -11
- weco/utils.py +180 -0
- {weco-0.1.3.dist-info → weco-0.1.5.dist-info}/METADATA +48 -7
- weco-0.1.5.dist-info/RECORD +10 -0
- {weco-0.1.3.dist-info → weco-0.1.5.dist-info}/WHEEL +1 -1
- weco-0.1.3.dist-info/RECORD +0 -8
- {weco-0.1.3.dist-info → weco-0.1.5.dist-info}/LICENSE +0 -0
- {weco-0.1.3.dist-info → weco-0.1.5.dist-info}/top_level.txt +0 -0
weco/client.py
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import base64
|
|
2
3
|
import os
|
|
3
4
|
import warnings
|
|
4
|
-
from
|
|
5
|
+
from io import BytesIO
|
|
6
|
+
from typing import Any, Callable, Coroutine, Dict, List, Optional, Tuple, Union
|
|
5
7
|
|
|
6
8
|
import httpx
|
|
9
|
+
import requests
|
|
10
|
+
from httpx import HTTPStatusError
|
|
11
|
+
from PIL import Image
|
|
12
|
+
|
|
13
|
+
from .constants import MAX_IMAGE_SIZE_MB, MAX_IMAGE_UPLOADS, MAX_TEXT_LENGTH, SUPPORTED_IMAGE_EXTENSIONS
|
|
14
|
+
from .utils import (
|
|
15
|
+
generate_random_base16_code,
|
|
16
|
+
get_image_size,
|
|
17
|
+
is_base64_image,
|
|
18
|
+
is_local_image,
|
|
19
|
+
is_public_url_image,
|
|
20
|
+
preprocess_image,
|
|
21
|
+
)
|
|
7
22
|
|
|
8
23
|
|
|
9
24
|
class WecoAI:
|
|
@@ -17,7 +32,7 @@ class WecoAI:
|
|
|
17
32
|
The API key used for authentication.
|
|
18
33
|
"""
|
|
19
34
|
|
|
20
|
-
def __init__(self, api_key: str = None, timeout: float =
|
|
35
|
+
def __init__(self, api_key: str = None, timeout: float = 120.0, http2: bool = True) -> None:
|
|
21
36
|
"""Initializes the WecoAI client with the provided API key and base URL.
|
|
22
37
|
|
|
23
38
|
Parameters
|
|
@@ -25,6 +40,12 @@ class WecoAI:
|
|
|
25
40
|
api_key : str, optional
|
|
26
41
|
The API key used for authentication. If not provided, the client will attempt to read it from the environment variable - WECO_API_KEY.
|
|
27
42
|
|
|
43
|
+
timeout : float, optional
|
|
44
|
+
The timeout for the HTTP requests in seconds (default is 30.0).
|
|
45
|
+
|
|
46
|
+
http2 : bool, optional
|
|
47
|
+
Whether to use HTTP/2 protocol for the HTTP requests (default is True).
|
|
48
|
+
|
|
28
49
|
Raises
|
|
29
50
|
------
|
|
30
51
|
ValueError
|
|
@@ -37,30 +58,17 @@ class WecoAI:
|
|
|
37
58
|
except KeyError:
|
|
38
59
|
raise ValueError("WECO_API_KEY must be passed to client or set as an environment variable")
|
|
39
60
|
self.api_key = api_key
|
|
40
|
-
|
|
41
|
-
self.
|
|
42
|
-
|
|
61
|
+
self.http2 = http2
|
|
62
|
+
self.timeout = timeout
|
|
63
|
+
self.base_url = "https://function.api.weco.ai"
|
|
43
64
|
# Setup clients
|
|
44
|
-
self.client = httpx.Client(http2=
|
|
45
|
-
self.async_client = httpx.AsyncClient(http2=
|
|
46
|
-
|
|
47
|
-
def
|
|
48
|
-
"""
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if not self.async_client.is_closed:
|
|
52
|
-
try:
|
|
53
|
-
loop = asyncio.get_event_loop()
|
|
54
|
-
if loop.is_running():
|
|
55
|
-
loop.create_task(self.async_client.aclose())
|
|
56
|
-
else:
|
|
57
|
-
loop.run_until_complete(self.async_client.aclose())
|
|
58
|
-
except RuntimeError:
|
|
59
|
-
# If the event loop is closed, we can't do anything about it
|
|
60
|
-
pass
|
|
61
|
-
except AttributeError:
|
|
62
|
-
# If the client is not initialized, we can't do anything about it
|
|
63
|
-
pass
|
|
65
|
+
self.client = httpx.Client(http2=http2, timeout=timeout)
|
|
66
|
+
self.async_client = httpx.AsyncClient(http2=http2, timeout=timeout)
|
|
67
|
+
|
|
68
|
+
def close(self):
|
|
69
|
+
"""Close both synchronous and asynchronous clients."""
|
|
70
|
+
self.client.close()
|
|
71
|
+
asyncio.run(self.async_client.aclose())
|
|
64
72
|
|
|
65
73
|
def _headers(self) -> Dict[str, str]:
|
|
66
74
|
"""Constructs the headers for the API requests."""
|
|
@@ -92,22 +100,38 @@ class WecoAI:
|
|
|
92
100
|
if is_async:
|
|
93
101
|
|
|
94
102
|
async def _request():
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
103
|
+
try:
|
|
104
|
+
response = await self.async_client.post(url, json=data, headers=headers)
|
|
105
|
+
response.raise_for_status()
|
|
106
|
+
return response.json()
|
|
107
|
+
except HTTPStatusError as e:
|
|
108
|
+
# Handle HTTP errors (4xx and 5xx status codes)
|
|
109
|
+
error_message = f"HTTP error occurred: {e.response.status_code} - {e.response.text}"
|
|
110
|
+
raise ValueError(error_message) from e
|
|
111
|
+
except Exception as e:
|
|
112
|
+
# Handle other exceptions
|
|
113
|
+
raise ValueError(f"An error occurred: {str(e)}") from e
|
|
98
114
|
|
|
99
115
|
return _request()
|
|
100
116
|
else:
|
|
101
117
|
|
|
102
118
|
def _request():
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
119
|
+
try:
|
|
120
|
+
response = self.client.post(url, json=data, headers=headers)
|
|
121
|
+
response.raise_for_status()
|
|
122
|
+
return response.json()
|
|
123
|
+
except HTTPStatusError as e:
|
|
124
|
+
# Handle HTTP errors (4xx and 5xx status codes)
|
|
125
|
+
error_message = f"HTTP error occurred: {e.response.status_code} - {e.response.text}"
|
|
126
|
+
raise ValueError(error_message) from e
|
|
127
|
+
except Exception as e:
|
|
128
|
+
# Handle other exceptions
|
|
129
|
+
raise ValueError(f"An error occurred: {str(e)}") from e
|
|
106
130
|
|
|
107
131
|
return _request()
|
|
108
132
|
|
|
109
|
-
def
|
|
110
|
-
"""Processes the
|
|
133
|
+
def _process_query_response(self, response: Dict[str, Any]) -> Dict[str, Any]:
|
|
134
|
+
"""Processes the query response and handles warnings.
|
|
111
135
|
|
|
112
136
|
Parameters
|
|
113
137
|
----------
|
|
@@ -134,7 +158,7 @@ class WecoAI:
|
|
|
134
158
|
"latency_ms": response["latency_ms"],
|
|
135
159
|
}
|
|
136
160
|
|
|
137
|
-
def _build(self, task_description: str, is_async: bool) -> Tuple[str, str]
|
|
161
|
+
def _build(self, task_description: str, is_async: bool) -> Union[Tuple[str, str], Coroutine[Any, Any, Tuple[str, str]]]:
|
|
138
162
|
"""Internal method to handle both synchronous and asynchronous build requests.
|
|
139
163
|
|
|
140
164
|
Parameters
|
|
@@ -146,9 +170,20 @@ class WecoAI:
|
|
|
146
170
|
|
|
147
171
|
Returns
|
|
148
172
|
-------
|
|
149
|
-
tuple[str, str]
|
|
173
|
+
Union[tuple[str, str], Coroutine[Any, Any, tuple[str, str]]]
|
|
150
174
|
A tuple containing the name and description of the function, or a coroutine that returns such a tuple.
|
|
175
|
+
|
|
176
|
+
Raises
|
|
177
|
+
------
|
|
178
|
+
ValueError
|
|
179
|
+
If the task description is empty or exceeds the maximum length.
|
|
151
180
|
"""
|
|
181
|
+
# Validate the input
|
|
182
|
+
if len(task_description) == 0:
|
|
183
|
+
raise ValueError("Task description must be provided.")
|
|
184
|
+
if len(task_description) > MAX_TEXT_LENGTH:
|
|
185
|
+
raise ValueError(f"Task description must be less than {MAX_TEXT_LENGTH} characters.")
|
|
186
|
+
|
|
152
187
|
endpoint = "build"
|
|
153
188
|
data = {"request": task_description}
|
|
154
189
|
request = self._make_request(endpoint=endpoint, data=data, is_async=is_async)
|
|
@@ -194,47 +229,216 @@ class WecoAI:
|
|
|
194
229
|
"""
|
|
195
230
|
return self._build(task_description=task_description, is_async=False)
|
|
196
231
|
|
|
197
|
-
def
|
|
198
|
-
"""
|
|
232
|
+
def _upload_image(self, fn_name: str, upload_id: str, image_info: Dict[str, Any]) -> str:
|
|
233
|
+
"""
|
|
234
|
+
Uploads an image to an S3 bucket and returns the URL of the uploaded image.
|
|
199
235
|
|
|
200
236
|
Parameters
|
|
201
237
|
----------
|
|
202
238
|
fn_name : str
|
|
203
|
-
The name of the function
|
|
204
|
-
|
|
205
|
-
|
|
239
|
+
The name of the function for which the image is being uploaded.
|
|
240
|
+
upload_id: str
|
|
241
|
+
A unique identifier for the image upload.
|
|
242
|
+
image_info : Dict[str, Any]
|
|
243
|
+
A dictionary containing the image metadata.
|
|
244
|
+
|
|
245
|
+
Returns
|
|
246
|
+
-------
|
|
247
|
+
str
|
|
248
|
+
The URL of the uploaded image.
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
if image_info["source"] == "base64":
|
|
252
|
+
_, base64_info = is_base64_image(maybe_base64=image_info["image"])
|
|
253
|
+
img_data = base64.b64decode(base64_info["encoding"])
|
|
254
|
+
elif image_info["source"] == "url":
|
|
255
|
+
response = requests.get(image_info["image"])
|
|
256
|
+
response.raise_for_status()
|
|
257
|
+
img_data = response.content
|
|
258
|
+
elif image_info["source"] == "local":
|
|
259
|
+
with open(image_info["image"], "rb") as f:
|
|
260
|
+
img_data = f.read()
|
|
261
|
+
else:
|
|
262
|
+
raise ValueError("Invalid image input")
|
|
263
|
+
|
|
264
|
+
# Preprocess the image
|
|
265
|
+
img = Image.open(BytesIO(img_data))
|
|
266
|
+
file_type = image_info["file_type"]
|
|
267
|
+
processed_img, file_type = preprocess_image(image=img, file_type=file_type)
|
|
268
|
+
upload_data = BytesIO()
|
|
269
|
+
processed_img.save(upload_data, format=file_type)
|
|
270
|
+
upload_data = upload_data.getvalue()
|
|
271
|
+
|
|
272
|
+
# Request a presigned URL from the server
|
|
273
|
+
endpoint = "upload_link"
|
|
274
|
+
request_data = {"fn_name": fn_name, "upload_id": upload_id, "file_type": file_type}
|
|
275
|
+
# This needs to be a synchronous request since we need the presigned URL to upload the image
|
|
276
|
+
response = self._make_request(endpoint=endpoint, data=request_data, is_async=False)
|
|
277
|
+
|
|
278
|
+
# Upload the image to the S3 bucket
|
|
279
|
+
image_name = generate_random_base16_code()
|
|
280
|
+
files = {"file": (f"{image_name}.{file_type}", upload_data)}
|
|
281
|
+
http_response = requests.post(response["url"], data=response["fields"], files=files)
|
|
282
|
+
if http_response.status_code == 204:
|
|
283
|
+
pass
|
|
284
|
+
else:
|
|
285
|
+
raise ValueError("Image upload failed")
|
|
286
|
+
|
|
287
|
+
# Return the URL of the uploaded image
|
|
288
|
+
upload_link = f"{response['url']}{response['fields']['key']}"
|
|
289
|
+
return upload_link
|
|
290
|
+
|
|
291
|
+
def _validate_query(self, text_input: str, images_input: List[str]) -> List[Dict[str, Any]]:
|
|
292
|
+
"""
|
|
293
|
+
Validate the input for the query method.
|
|
294
|
+
|
|
295
|
+
Parameters
|
|
296
|
+
----------
|
|
297
|
+
text_input : str
|
|
298
|
+
The text input to the function.
|
|
299
|
+
images_input : List[str]
|
|
300
|
+
A list of image URLs or images encoded in base64 with their metadata to be sent as input to the function.
|
|
301
|
+
|
|
302
|
+
Returns
|
|
303
|
+
-------
|
|
304
|
+
List[Dict[str, Any]]
|
|
305
|
+
A list of dictionaries containing the image metadata.
|
|
306
|
+
|
|
307
|
+
Raises
|
|
308
|
+
------
|
|
309
|
+
ValueError
|
|
310
|
+
If the input is invalid.
|
|
311
|
+
"""
|
|
312
|
+
if not isinstance(text_input, str) or not isinstance(images_input, list):
|
|
313
|
+
raise ValueError("Text input must be a string and images input must be a list of strings.")
|
|
314
|
+
for image in images_input:
|
|
315
|
+
if not isinstance(image, str):
|
|
316
|
+
raise ValueError("Images input must be a list of strings.")
|
|
317
|
+
|
|
318
|
+
# Assert that either text or images or both must be provded
|
|
319
|
+
if len(text_input) == 0 and len(images_input) == 0:
|
|
320
|
+
raise ValueError("Either text or images or both must be provided as input.")
|
|
321
|
+
|
|
322
|
+
# Check if the text input is within the limit
|
|
323
|
+
if len(text_input) > MAX_TEXT_LENGTH:
|
|
324
|
+
raise ValueError(f"Text input must be less than {MAX_TEXT_LENGTH} characters.")
|
|
325
|
+
|
|
326
|
+
# Check if the images input is within the limit
|
|
327
|
+
if len(images_input) > MAX_IMAGE_UPLOADS:
|
|
328
|
+
raise ValueError(f"Number of images must be less than {MAX_IMAGE_UPLOADS}.")
|
|
329
|
+
|
|
330
|
+
# Check if input is an valid image
|
|
331
|
+
image_info = []
|
|
332
|
+
for image in images_input:
|
|
333
|
+
is_base64, base64_info = is_base64_image(maybe_base64=image)
|
|
334
|
+
if is_base64:
|
|
335
|
+
file_type = base64_info["media_type"].split("/")[1]
|
|
336
|
+
|
|
337
|
+
is_public_url = is_public_url_image(maybe_url_image=image)
|
|
338
|
+
if is_public_url:
|
|
339
|
+
response = requests.get(image)
|
|
340
|
+
response.raise_for_status()
|
|
341
|
+
file_type = response.headers["content-type"].split("/")[1]
|
|
342
|
+
|
|
343
|
+
is_local = is_local_image(maybe_local_image=image)
|
|
344
|
+
if is_local:
|
|
345
|
+
file_type = os.path.splitext(image)[1][1:]
|
|
346
|
+
|
|
347
|
+
if not (is_base64 or is_public_url or is_local):
|
|
348
|
+
raise ValueError("Images must be local paths, public URLs or base64 encoded strings.")
|
|
349
|
+
|
|
350
|
+
# Determine the source of image
|
|
351
|
+
if is_base64:
|
|
352
|
+
source = "base64"
|
|
353
|
+
elif is_public_url:
|
|
354
|
+
source = "url"
|
|
355
|
+
elif is_local:
|
|
356
|
+
source = "local"
|
|
357
|
+
|
|
358
|
+
# Check if the image type is supported
|
|
359
|
+
file_type = file_type.lower()
|
|
360
|
+
if file_type not in SUPPORTED_IMAGE_EXTENSIONS:
|
|
361
|
+
raise ValueError(
|
|
362
|
+
f"Image file type {file_type} is not supported. Supported types are {SUPPORTED_IMAGE_EXTENSIONS}."
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
# Check if the image size is within the limit
|
|
366
|
+
size = get_image_size(image=image, source=source)
|
|
367
|
+
if size > MAX_IMAGE_SIZE_MB:
|
|
368
|
+
raise ValueError(f"Individual image sizes must be less than {MAX_IMAGE_SIZE_MB} MB each.")
|
|
369
|
+
|
|
370
|
+
image_info.append({"image": image, "file_type": file_type, "size": size, "source": source})
|
|
371
|
+
|
|
372
|
+
return image_info
|
|
373
|
+
|
|
374
|
+
def _query(
|
|
375
|
+
self, is_async: bool, fn_name: str, text_input: Optional[str], images_input: Optional[List[str]]
|
|
376
|
+
) -> Union[Dict[str, Any], Coroutine[Any, Any, Dict[str, Any]]]:
|
|
377
|
+
"""Internal method to handle both synchronous and asynchronous query requests.
|
|
378
|
+
|
|
379
|
+
Parameters
|
|
380
|
+
----------
|
|
206
381
|
is_async : bool
|
|
207
382
|
Whether to perform an asynchronous request.
|
|
383
|
+
fn_name : str
|
|
384
|
+
The name of the function to query.
|
|
385
|
+
text_input : str, optional
|
|
386
|
+
The text input to the function.
|
|
387
|
+
images_input : List[str], optional
|
|
388
|
+
A list of image URLs or images encoded in base64 with their metadata to be sent as input to the function.
|
|
208
389
|
|
|
209
390
|
Returns
|
|
210
391
|
-------
|
|
211
|
-
|
|
392
|
+
Union[Dict[str, Any], Coroutine[Any, Any, dict]]
|
|
212
393
|
A dictionary containing the query results, or a coroutine that returns such a dictionary.
|
|
394
|
+
|
|
395
|
+
Raises
|
|
396
|
+
------
|
|
397
|
+
ValueError
|
|
398
|
+
If the input is invalid.
|
|
213
399
|
"""
|
|
400
|
+
# Validate the input
|
|
401
|
+
image_info = self._validate_query(text_input=text_input, images_input=images_input)
|
|
402
|
+
|
|
403
|
+
# Create links for all images that are not public URLs and upload images
|
|
404
|
+
image_urls = []
|
|
405
|
+
upload_id = generate_random_base16_code()
|
|
406
|
+
for i, info in enumerate(image_info):
|
|
407
|
+
if info["source"] == "url" or info["source"] == "base64" or info["source"] == "local":
|
|
408
|
+
url = self._upload_image(fn_name=fn_name, upload_id=upload_id, image_info=info)
|
|
409
|
+
else:
|
|
410
|
+
raise ValueError(f"Image at index {i} must be a public URL or a path to a local image file.")
|
|
411
|
+
image_urls.append(url)
|
|
412
|
+
|
|
413
|
+
# Make the request
|
|
214
414
|
endpoint = "query"
|
|
215
|
-
data = {"name": fn_name, "
|
|
415
|
+
data = {"name": fn_name, "text": text_input, "images": image_urls}
|
|
216
416
|
request = self._make_request(endpoint=endpoint, data=data, is_async=is_async)
|
|
217
417
|
|
|
218
418
|
if is_async:
|
|
219
419
|
|
|
220
420
|
async def _async_query():
|
|
221
421
|
response = await request
|
|
222
|
-
return self.
|
|
422
|
+
return self._process_query_response(response=response)
|
|
223
423
|
|
|
224
424
|
return _async_query()
|
|
225
425
|
else:
|
|
226
426
|
response = request # the request has already been made and the response is available
|
|
227
|
-
return self.
|
|
427
|
+
return self._process_query_response(response=response)
|
|
228
428
|
|
|
229
|
-
async def aquery(
|
|
429
|
+
async def aquery(
|
|
430
|
+
self, fn_name: str, text_input: Optional[str] = "", images_input: Optional[List[str]] = []
|
|
431
|
+
) -> Dict[str, Any]:
|
|
230
432
|
"""Asynchronously queries a function with the given function ID and input.
|
|
231
433
|
|
|
232
434
|
Parameters
|
|
233
435
|
----------
|
|
234
436
|
fn_name : str
|
|
235
437
|
The name of the function to query.
|
|
236
|
-
|
|
237
|
-
The input to the function.
|
|
438
|
+
text_input : str, optional
|
|
439
|
+
The text input to the function.
|
|
440
|
+
images_input : List[str], optional
|
|
441
|
+
A list of image URLs or images encoded in base64 with their metadata to be sent as input to the function.
|
|
238
442
|
|
|
239
443
|
Returns
|
|
240
444
|
-------
|
|
@@ -242,17 +446,19 @@ class WecoAI:
|
|
|
242
446
|
A dictionary containing the output of the function, the number of input tokens, the number of output tokens,
|
|
243
447
|
and the latency in milliseconds.
|
|
244
448
|
"""
|
|
245
|
-
return await self._query(fn_name=fn_name,
|
|
449
|
+
return await self._query(fn_name=fn_name, text_input=text_input, images_input=images_input, is_async=True)
|
|
246
450
|
|
|
247
|
-
def query(self, fn_name: str,
|
|
451
|
+
def query(self, fn_name: str, text_input: Optional[str] = "", images_input: Optional[List[str]] = []) -> Dict[str, Any]:
|
|
248
452
|
"""Synchronously queries a function with the given function ID and input.
|
|
249
453
|
|
|
250
454
|
Parameters
|
|
251
455
|
----------
|
|
252
456
|
fn_name : str
|
|
253
457
|
The name of the function to query.
|
|
254
|
-
|
|
255
|
-
The input to the function.
|
|
458
|
+
text_input : str, optional
|
|
459
|
+
The text input to the function.
|
|
460
|
+
images_input : List[str], optional
|
|
461
|
+
A list of image URLs or images encoded in base64 with their metadata to be sent as input to the function.
|
|
256
462
|
|
|
257
463
|
Returns
|
|
258
464
|
-------
|
|
@@ -260,9 +466,9 @@ class WecoAI:
|
|
|
260
466
|
A dictionary containing the output of the function, the number of input tokens, the number of output tokens,
|
|
261
467
|
and the latency in milliseconds.
|
|
262
468
|
"""
|
|
263
|
-
return self._query(fn_name=fn_name,
|
|
469
|
+
return self._query(fn_name=fn_name, text_input=text_input, images_input=images_input, is_async=False)
|
|
264
470
|
|
|
265
|
-
def batch_query(self, fn_names: str
|
|
471
|
+
def batch_query(self, fn_names: Union[str, List[str]], batch_inputs: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
266
472
|
"""Synchronously queries multiple functions using asynchronous calls internally.
|
|
267
473
|
|
|
268
474
|
This method uses the asynchronous queries to submit all queries concurrently
|
|
@@ -270,20 +476,28 @@ class WecoAI:
|
|
|
270
476
|
|
|
271
477
|
Parameters
|
|
272
478
|
----------
|
|
273
|
-
fn_name : str
|
|
479
|
+
fn_name : Union[str, List[str]]
|
|
274
480
|
The name of the function or a list of function names to query.
|
|
275
481
|
Note that if a single function name is provided, it will be used for all queries.
|
|
276
482
|
If a list of function names is provided, the length must match the number of queries.
|
|
277
483
|
|
|
278
|
-
batch_inputs : List[str]
|
|
279
|
-
A list of inputs for the functions to query.
|
|
280
|
-
|
|
484
|
+
batch_inputs : List[Dict[str, Any]]
|
|
485
|
+
A list of inputs for the functions to query. The input must be a dictionary containing the data to be processed. e.g.,
|
|
486
|
+
when providing for a text input, the dictionary should be {"text_input": "input text"}, for an image input, the dictionary should be {"images_input": ["url1", "url2", ...]}
|
|
487
|
+
and for a combination of text and image inputs, the dictionary should be {"text_input": "input text", "images_input": ["url1", "url2", ...]}.
|
|
488
|
+
Note that the index of each input must correspond to the index of the function name when both inputs are lists.
|
|
281
489
|
|
|
282
490
|
Returns
|
|
283
491
|
-------
|
|
284
492
|
List[Dict[str, Any]]
|
|
285
493
|
A list of dictionaries, each containing the output of a function query,
|
|
286
494
|
in the same order as the input queries.
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
Raises
|
|
498
|
+
------
|
|
499
|
+
ValueError
|
|
500
|
+
If the number of function names (when provided as a list) does not match the number of inputs.
|
|
287
501
|
"""
|
|
288
502
|
if isinstance(fn_names, str):
|
|
289
503
|
fn_names = [fn_names] * len(batch_inputs)
|
|
@@ -291,7 +505,10 @@ class WecoAI:
|
|
|
291
505
|
raise ValueError("The number of function names must match the number of inputs.")
|
|
292
506
|
|
|
293
507
|
async def run_queries():
|
|
294
|
-
tasks = [
|
|
508
|
+
tasks = [
|
|
509
|
+
self.aquery(fn_name=fn_name, **fn_input) # unpack the input kwargs
|
|
510
|
+
for fn_name, fn_input in zip(fn_names, batch_inputs)
|
|
511
|
+
]
|
|
295
512
|
return await asyncio.gather(*tasks)
|
|
296
513
|
|
|
297
514
|
return asyncio.run(run_queries())
|
weco/constants.py
ADDED
weco/functional.py
CHANGED
|
@@ -3,6 +3,7 @@ from typing import Any, Dict, List, Optional
|
|
|
3
3
|
from .client import WecoAI
|
|
4
4
|
|
|
5
5
|
|
|
6
|
+
# TODO: Implement the closing stuff for the client
|
|
6
7
|
def build(task_description: str, api_key: str = None) -> tuple[str, str]:
|
|
7
8
|
"""Builds a specialized function synchronously given a task description.
|
|
8
9
|
|
|
@@ -43,15 +44,19 @@ async def abuild(task_description: str, api_key: str = None) -> tuple[str, str]:
|
|
|
43
44
|
return response
|
|
44
45
|
|
|
45
46
|
|
|
46
|
-
def query(
|
|
47
|
+
def query(
|
|
48
|
+
fn_name: str, text_input: Optional[str] = "", images_input: Optional[List[str]] = [], api_key: Optional[str] = None
|
|
49
|
+
) -> Dict[str, Any]:
|
|
47
50
|
"""Queries a function synchronously with the given function ID and input.
|
|
48
51
|
|
|
49
52
|
Parameters
|
|
50
53
|
----------
|
|
51
54
|
fn_name : str
|
|
52
55
|
The name of the function to query.
|
|
53
|
-
|
|
54
|
-
The input to the function.
|
|
56
|
+
text_input : str, optional
|
|
57
|
+
The text input to the function.
|
|
58
|
+
images_input : List[str], optional
|
|
59
|
+
A list of image URLs or base64 encoded images to be used as input to the function.
|
|
55
60
|
api_key : str
|
|
56
61
|
The API key for the WecoAI service. If not provided, the API key must be set using the environment variable - WECO_API_KEY.
|
|
57
62
|
|
|
@@ -62,19 +67,23 @@ def query(fn_name: str, fn_input: str, api_key: Optional[str] = None) -> Dict[st
|
|
|
62
67
|
and the latency in milliseconds.
|
|
63
68
|
"""
|
|
64
69
|
client = WecoAI(api_key=api_key)
|
|
65
|
-
response = client.query(fn_name=fn_name,
|
|
70
|
+
response = client.query(fn_name=fn_name, text_input=text_input, images_input=images_input)
|
|
66
71
|
return response
|
|
67
72
|
|
|
68
73
|
|
|
69
|
-
async def aquery(
|
|
74
|
+
async def aquery(
|
|
75
|
+
fn_name: str, text_input: Optional[str] = "", images_input: Optional[List[str]] = [], api_key: Optional[str] = None
|
|
76
|
+
) -> Dict[str, Any]:
|
|
70
77
|
"""Queries a function asynchronously with the given function ID and input.
|
|
71
78
|
|
|
72
79
|
Parameters
|
|
73
80
|
----------
|
|
74
81
|
fn_name : str
|
|
75
82
|
The name of the function to query.
|
|
76
|
-
|
|
77
|
-
The input to the function.
|
|
83
|
+
text_input : str, optional
|
|
84
|
+
The text input to the function.
|
|
85
|
+
images_input : List[str], optional
|
|
86
|
+
A list of image URLs to be used as input to the function.
|
|
78
87
|
api_key : str
|
|
79
88
|
The API key for the WecoAI service. If not provided, the API key must be set using the environment variable - WECO_API_KEY.
|
|
80
89
|
|
|
@@ -85,11 +94,13 @@ async def aquery(fn_name: str, fn_input: str, api_key: Optional[str] = None) ->
|
|
|
85
94
|
and the latency in milliseconds.
|
|
86
95
|
"""
|
|
87
96
|
client = WecoAI(api_key=api_key)
|
|
88
|
-
response = await client.aquery(fn_name=fn_name,
|
|
97
|
+
response = await client.aquery(fn_name=fn_name, text_input=text_input, images_input=images_input)
|
|
89
98
|
return response
|
|
90
99
|
|
|
91
100
|
|
|
92
|
-
def batch_query(
|
|
101
|
+
def batch_query(
|
|
102
|
+
fn_names: str | List[str], batch_inputs: List[Dict[str, Any]], api_key: Optional[str] = None
|
|
103
|
+
) -> List[Dict[str, Any]]:
|
|
93
104
|
"""Synchronously queries multiple functions using asynchronous calls internally.
|
|
94
105
|
|
|
95
106
|
This method uses the asynchronous queries to submit all queries concurrently
|
|
@@ -103,8 +114,10 @@ def batch_query(fn_names: str | List[str], batch_inputs: List[str], api_key: Opt
|
|
|
103
114
|
If a list of function names is provided, the length must match the number of queries.
|
|
104
115
|
|
|
105
116
|
batch_inputs : List[str]
|
|
106
|
-
|
|
107
|
-
|
|
117
|
+
A list of inputs for the functions to query. The input must be a dictionary containing the data to be processed. e.g.,
|
|
118
|
+
when providing for a text input, the dictionary should be {"text_input": "input text"}, for an image input, the dictionary should be {"images_input": ["url1", "url2", ...]}
|
|
119
|
+
and for a combination of text and image inputs, the dictionary should be {"text_input": "input text", "images_input": ["url1", "url2", ...]}.
|
|
120
|
+
Note that the index of each input must correspond to the index of the function name when both inputs are lists.
|
|
108
121
|
|
|
109
122
|
api_key : str, optional
|
|
110
123
|
The API key for the WecoAI service. If not provided, the API key must be set using the environment variable - WECO_API_KEY.
|
weco/utils.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import os
|
|
3
|
+
import random
|
|
4
|
+
import re
|
|
5
|
+
import string
|
|
6
|
+
from io import BytesIO
|
|
7
|
+
from typing import Dict, Optional, Tuple
|
|
8
|
+
from urllib.parse import urlparse
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
from PIL import Image
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def is_local_image(maybe_local_image: str) -> bool:
|
|
15
|
+
"""
|
|
16
|
+
Check if the file is a local image.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
maybe_local_image : str
|
|
21
|
+
The file path.
|
|
22
|
+
|
|
23
|
+
Returns
|
|
24
|
+
-------
|
|
25
|
+
bool
|
|
26
|
+
True if the file is a local image, False otherwise.
|
|
27
|
+
"""
|
|
28
|
+
if not os.path.exists(maybe_local_image): # Check if the file exists
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
try: # Check if the file is an image
|
|
32
|
+
Image.open(maybe_local_image)
|
|
33
|
+
except IOError:
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
return True
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def is_base64_image(maybe_base64: str) -> Tuple[bool, Optional[Dict[str, str]]]:
|
|
40
|
+
"""
|
|
41
|
+
Check if the image is a base64 encoded image and if so, extract the image information from the encoded data or URL provided.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
data : str
|
|
46
|
+
The image data or URL.
|
|
47
|
+
|
|
48
|
+
Returns
|
|
49
|
+
-------
|
|
50
|
+
Tuple[bool, Optional[Dict[str, str]]]
|
|
51
|
+
"""
|
|
52
|
+
pattern = r"data:(?P<media_type>[\w/]+);(?P<source_type>\w+),(?P<encoding>.*)"
|
|
53
|
+
match = re.match(pattern, maybe_base64)
|
|
54
|
+
if match:
|
|
55
|
+
return True, match.groupdict()
|
|
56
|
+
|
|
57
|
+
return False, None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def is_public_url_image(maybe_url_image: str) -> bool:
|
|
61
|
+
"""
|
|
62
|
+
Check if the string is a publicly accessible URL
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
maybe_url_image : str
|
|
67
|
+
The URL to check.
|
|
68
|
+
|
|
69
|
+
Returns
|
|
70
|
+
-------
|
|
71
|
+
bool
|
|
72
|
+
True if the URL is publicly accessible, False otherwise.
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
# Check if it is a valid URL
|
|
76
|
+
if not urlparse(maybe_url_image).scheme:
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
# Check if the URL is publicly accessible
|
|
80
|
+
response = requests.head(maybe_url_image)
|
|
81
|
+
if response.status_code != 200:
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
# Check if the URL is an image
|
|
85
|
+
content_type = response.headers.get("content-type")
|
|
86
|
+
if not content_type:
|
|
87
|
+
return False
|
|
88
|
+
if not content_type.startswith("image"):
|
|
89
|
+
return False
|
|
90
|
+
except Exception:
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_image_size(image: str, source: str) -> float:
|
|
97
|
+
"""
|
|
98
|
+
Get the size of the image in MB.
|
|
99
|
+
|
|
100
|
+
Parameters
|
|
101
|
+
----------
|
|
102
|
+
image : str
|
|
103
|
+
The image data or URL.
|
|
104
|
+
|
|
105
|
+
source : str
|
|
106
|
+
The source of the image. It can be 'base64', 'url', or 'local'.
|
|
107
|
+
|
|
108
|
+
Returns
|
|
109
|
+
-------
|
|
110
|
+
float
|
|
111
|
+
The size of the image in MB.
|
|
112
|
+
|
|
113
|
+
Raises
|
|
114
|
+
------
|
|
115
|
+
ValueError
|
|
116
|
+
If the image is not a valid image.
|
|
117
|
+
"""
|
|
118
|
+
if source == "base64":
|
|
119
|
+
_, base64_info = is_base64_image(maybe_base64=image)
|
|
120
|
+
img_data = base64.b64decode(base64_info["encoding"])
|
|
121
|
+
elif source == "url":
|
|
122
|
+
response = requests.get(image)
|
|
123
|
+
response.raise_for_status()
|
|
124
|
+
img_data = response.content
|
|
125
|
+
elif source == "local":
|
|
126
|
+
with open(image, "rb") as f:
|
|
127
|
+
img_data = f.read()
|
|
128
|
+
else:
|
|
129
|
+
raise ValueError("Invalid image input")
|
|
130
|
+
|
|
131
|
+
img = Image.open(BytesIO(img_data))
|
|
132
|
+
img_byte_arr = BytesIO()
|
|
133
|
+
img.save(img_byte_arr, format=img.format)
|
|
134
|
+
return img_byte_arr.tell() / 1000000 # MB
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def preprocess_image(image: Image, file_type: str) -> Tuple:
|
|
138
|
+
"""
|
|
139
|
+
Preprocess the image by converting it to RGB if it has an alpha channel.
|
|
140
|
+
|
|
141
|
+
Parameters
|
|
142
|
+
----------
|
|
143
|
+
image : Image
|
|
144
|
+
The image to preprocess.
|
|
145
|
+
file_type : str
|
|
146
|
+
The file type of the image.
|
|
147
|
+
|
|
148
|
+
Returns
|
|
149
|
+
-------
|
|
150
|
+
Image
|
|
151
|
+
The preprocessed image.
|
|
152
|
+
file_type : str
|
|
153
|
+
The file type of the image.
|
|
154
|
+
"""
|
|
155
|
+
# Do not rescale or resize. Only do this if latency becomes an issue.
|
|
156
|
+
# Remove the alpha channel for PNG and WEBP images if it exists.
|
|
157
|
+
if image.mode in ("RGBA", "LA") or (image.mode == "P" and "transparency" in image.info):
|
|
158
|
+
image = image.convert("RGB")
|
|
159
|
+
|
|
160
|
+
# If the image file type is JPG, convert to JPEG for PIL compatibility.
|
|
161
|
+
if file_type == "jpg":
|
|
162
|
+
file_type = "jpeg"
|
|
163
|
+
return image, file_type
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def generate_random_base16_code(length: int = 5):
|
|
167
|
+
"""
|
|
168
|
+
Generate a random base16 code.
|
|
169
|
+
|
|
170
|
+
Parameters
|
|
171
|
+
----------
|
|
172
|
+
length : int
|
|
173
|
+
The length of the code.
|
|
174
|
+
|
|
175
|
+
Returns
|
|
176
|
+
-------
|
|
177
|
+
str
|
|
178
|
+
The random base16 code.
|
|
179
|
+
"""
|
|
180
|
+
return "".join(random.choices(string.hexdigits, k=length))
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: weco
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: A client facing API for interacting with the WeCo AI function builder service.
|
|
5
5
|
Author-email: WeCo AI Team <dhruv@weco.ai>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/WecoAI/weco-python
|
|
8
|
-
Keywords: AI,LLM,machine learning,data science,function builder
|
|
8
|
+
Keywords: AI,LLM,machine learning,data science,function builder,AI function
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
10
10
|
Classifier: Operating System :: OS Independent
|
|
11
11
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -14,21 +14,31 @@ Description-Content-Type: text/markdown
|
|
|
14
14
|
License-File: LICENSE
|
|
15
15
|
Requires-Dist: asyncio
|
|
16
16
|
Requires-Dist: httpx[http2]
|
|
17
|
+
Requires-Dist: pillow
|
|
17
18
|
Provides-Extra: dev
|
|
18
|
-
Requires-Dist: build ; extra == 'dev'
|
|
19
|
-
Requires-Dist: setuptools-scm ; extra == 'dev'
|
|
20
19
|
Requires-Dist: flake8 ; extra == 'dev'
|
|
21
20
|
Requires-Dist: flake8-pyproject ; extra == 'dev'
|
|
22
21
|
Requires-Dist: black ; extra == 'dev'
|
|
23
22
|
Requires-Dist: isort ; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest-asyncio ; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest-xdist ; extra == 'dev'
|
|
25
|
+
Requires-Dist: build ; extra == 'dev'
|
|
26
|
+
Requires-Dist: setuptools-scm ; extra == 'dev'
|
|
24
27
|
|
|
25
28
|
<div align="center" style="display: flex; align-items: center; justify-content: center;">
|
|
26
29
|
<img src="assets/weco.svg" alt="WeCo AI" style="height: 50px; margin-right: 10px;">
|
|
27
30
|
<a href="https://git.io/typing-svg"><img src="https://readme-typing-svg.demolab.com?font=Georgia&size=32&duration=4000&pause=400&color=FD4578&vCenter=true&multiline=false&width=200&height=50&lines=WeCo+Client" alt="Typing SVG" /></a>
|
|
28
31
|
</div>
|
|
29
32
|
|
|
33
|
+

|
|
34
|
+
[](https://opensource.org/licenses/MIT)
|
|
35
|
+
|
|
36
|
+
<!-- TODO: Update examples -->
|
|
30
37
|
# $f$(👷♂️)
|
|
31
38
|
|
|
39
|
+
<a href="https://colab.research.google.com/github/WecoAI/weco-python/blob/main/examples/cookbook.ipynb" target="_parent"><img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open In Colab" width=110 height=20/></a>
|
|
40
|
+
<a target="_blank" href="https://lightning.ai/new?repo_url=https%3A%2F%2Fgithub.com%2FWecoAI%2Fweco-python%2Fblob%2Fmain%2Fexamples%2Fcookbook.ipynb"><img src="https://pl-bolts-doc-images.s3.us-east-2.amazonaws.com/app-2/studio-badge.svg" alt="Open in Studio" width=100 height=20/></a>
|
|
41
|
+
|
|
32
42
|
A client facing API for interacting with the [WeCo AI](https://www.weco.ai/) function builder [service](https://weco-app.vercel.app/function)!
|
|
33
43
|
|
|
34
44
|
|
|
@@ -47,6 +57,7 @@ pip install weco
|
|
|
47
57
|
- The **query** function allows you to test and use the newly created function in your own code.
|
|
48
58
|
- We offer asynchronous versions of the above clients.
|
|
49
59
|
- We provide a **batch_query** functions that allows users to batch functions for various inputs as well as multiple inputs for the same function in a query. This is helpful to make a large number of queries more efficiently.
|
|
60
|
+
- We also offer multimodality capabilities. You can now query our client with both **language** AND **vision** inputs!
|
|
50
61
|
|
|
51
62
|
We provide both services in two ways:
|
|
52
63
|
- `weco.WecoAI` client to be used when you want to maintain the same client service across a portion of code. This is better for dense service usage.
|
|
@@ -63,18 +74,48 @@ export WECO_API_KEY=<YOUR_WECO_API_KEY>
|
|
|
63
74
|
## Example
|
|
64
75
|
|
|
65
76
|
We create a function on the [web console](https://weco-app.vercel.app/function) for the following task:
|
|
66
|
-
> "
|
|
77
|
+
> "Analyze a business idea and provide a structured evaluation. Output a JSON with 'viability_score' (0-100), 'strengths' (list), 'weaknesses' (list), and 'next_steps' (list)."
|
|
67
78
|
|
|
68
79
|
Now, you're ready to query this function anywhere in your code!
|
|
69
80
|
|
|
70
81
|
```python
|
|
71
82
|
from weco import query
|
|
72
83
|
response = query(
|
|
73
|
-
fn_name=
|
|
74
|
-
|
|
84
|
+
fn_name="BusinessIdeaAnalyzer-XYZ123", # Replace with your actual function name
|
|
85
|
+
text_input="A subscription service for personalized, AI-generated bedtime stories for children."
|
|
75
86
|
)
|
|
76
87
|
```
|
|
77
88
|
|
|
78
89
|
For more examples and an advanced user guide, check out our function builder [cookbook](examples/cookbook.ipynb).
|
|
79
90
|
|
|
80
91
|
## Happy building $f$(👷♂️)!
|
|
92
|
+
|
|
93
|
+
## Contributing
|
|
94
|
+
|
|
95
|
+
We value your contributions! If you believe you can help to improve our package enabling people to build AI with AI, please contribute!
|
|
96
|
+
|
|
97
|
+
Use the following steps as a guideline to help you make contributions:
|
|
98
|
+
|
|
99
|
+
1. Download and install package from source:
|
|
100
|
+
```bash
|
|
101
|
+
git clone https://github.com/WecoAI/weco-python.git
|
|
102
|
+
cd weco-python
|
|
103
|
+
pip install -e ".[dev]"
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
2. Create a new branch for your feature or bugfix:
|
|
107
|
+
```bash
|
|
108
|
+
git checkout -b feature/your-feature-name
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
3. Make your changes and run tests to ensure everything is working:
|
|
112
|
+
|
|
113
|
+
> **Tests can be expensive to run as they make LLM requests with the API key being used so it is the developers best interests to write small and simple tests that adds coverage for a large portion of the package.**
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
pytest -n auto tests
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
4. Commit and push your changes, then open a PR for us to view 😁
|
|
120
|
+
|
|
121
|
+
Please ensure your code follows our style guidelines (Numpy docstrings) and includes appropriate tests. We appreciate your contributions!
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
weco/__init__.py,sha256=qiKpnrm6t0n0bpAtXEKJO1Yz2xYXnJJRZBWt-cH7DdU,168
|
|
2
|
+
weco/client.py,sha256=MGwLxV0pgZ_dighM5tvE32VbM1lspppTq8EmdZnXCfE,20608
|
|
3
|
+
weco/constants.py,sha256=eoAq-9qN2aZrqyIWdrb3V1zomV5kp80PfxxoPoQNMNI,167
|
|
4
|
+
weco/functional.py,sha256=MpyFaREjoPBWqY0iLgsN0JovL6cyCyNf5SzYI0i1b5w,5328
|
|
5
|
+
weco/utils.py,sha256=UUSw6ocqWdlSmIXVcH66DAL4NuLU2rFOyviD8aTWsv0,4371
|
|
6
|
+
weco-0.1.5.dist-info/LICENSE,sha256=NvpxfBuSajszAczWBGKxhHe4gsvil1H63zmu8xXZdL0,1064
|
|
7
|
+
weco-0.1.5.dist-info/METADATA,sha256=tHQEA41we-S2LExGrjzUQbmBJ-NGQpCT64bEIZE6wd0,5957
|
|
8
|
+
weco-0.1.5.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
|
|
9
|
+
weco-0.1.5.dist-info/top_level.txt,sha256=F0N7v6e2zBSlsorFv-arAq2yDxQbzX3KVO8GxYhPUeE,5
|
|
10
|
+
weco-0.1.5.dist-info/RECORD,,
|
weco-0.1.3.dist-info/RECORD
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
weco/__init__.py,sha256=qiKpnrm6t0n0bpAtXEKJO1Yz2xYXnJJRZBWt-cH7DdU,168
|
|
2
|
-
weco/client.py,sha256=RZ6wsFohWhcJSz8HIGTIfVpXozUE_bYK7K037GSlZdA,10991
|
|
3
|
-
weco/functional.py,sha256=oZljY8enxw8nPKP1TGBc8AxERt3Y9ukpV1_40vIJ8oE,4375
|
|
4
|
-
weco-0.1.3.dist-info/LICENSE,sha256=NvpxfBuSajszAczWBGKxhHe4gsvil1H63zmu8xXZdL0,1064
|
|
5
|
-
weco-0.1.3.dist-info/METADATA,sha256=vqzoh9iOLs7ZbSVjQwZz1jiLmSdEITITT0A7FWAdz0c,3876
|
|
6
|
-
weco-0.1.3.dist-info/WHEEL,sha256=mguMlWGMX-VHnMpKOjjQidIo1ssRlCFu4a4mBpz1s2M,91
|
|
7
|
-
weco-0.1.3.dist-info/top_level.txt,sha256=F0N7v6e2zBSlsorFv-arAq2yDxQbzX3KVO8GxYhPUeE,5
|
|
8
|
-
weco-0.1.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|