sunholo 0.74.7__py3-none-any.whl → 0.74.9__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.
- sunholo/tools/web_browser.py +143 -47
- sunholo/utils/parsers.py +1 -1
- {sunholo-0.74.7.dist-info → sunholo-0.74.9.dist-info}/METADATA +2 -2
- {sunholo-0.74.7.dist-info → sunholo-0.74.9.dist-info}/RECORD +8 -8
- {sunholo-0.74.7.dist-info → sunholo-0.74.9.dist-info}/LICENSE.txt +0 -0
- {sunholo-0.74.7.dist-info → sunholo-0.74.9.dist-info}/WHEEL +0 -0
- {sunholo-0.74.7.dist-info → sunholo-0.74.9.dist-info}/entry_points.txt +0 -0
- {sunholo-0.74.7.dist-info → sunholo-0.74.9.dist-info}/top_level.txt +0 -0
sunholo/tools/web_browser.py
CHANGED
|
@@ -4,6 +4,7 @@ import json
|
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
import urllib.parse
|
|
6
6
|
import time
|
|
7
|
+
from io import BytesIO
|
|
7
8
|
|
|
8
9
|
from ..logging import log
|
|
9
10
|
|
|
@@ -115,7 +116,14 @@ class BrowseWebWithImagePromptsBot:
|
|
|
115
116
|
```
|
|
116
117
|
"""
|
|
117
118
|
#class BrowseWebWithImagePromptsBot:
|
|
118
|
-
def __init__(
|
|
119
|
+
def __init__(
|
|
120
|
+
self,
|
|
121
|
+
website_name:str,
|
|
122
|
+
session_id: str=None,
|
|
123
|
+
browser_type:str='chromium',
|
|
124
|
+
headless:bool=True,
|
|
125
|
+
max_steps:int=10
|
|
126
|
+
):
|
|
119
127
|
try:
|
|
120
128
|
from playwright.sync_api import sync_playwright
|
|
121
129
|
except ImportError as err:
|
|
@@ -125,12 +133,19 @@ class BrowseWebWithImagePromptsBot:
|
|
|
125
133
|
if not sync_playwright:
|
|
126
134
|
raise ImportError("playright needed for BrowseWebWithImagePromptsBot class - install via `pip install sunholo[tools]`")
|
|
127
135
|
|
|
128
|
-
|
|
136
|
+
log.info(f"Starting BrowseWebWithImagePromptsBot with {website_name=}, {session_id=}, {browser_type=}, {headless=}, {max_steps=}")
|
|
137
|
+
|
|
138
|
+
# Assign session_id if it is None or 'None'
|
|
139
|
+
if not session_id or session_id == 'None':
|
|
140
|
+
self.session_id = datetime.now().strftime("%Y%m%d%H%M%S")
|
|
141
|
+
else:
|
|
142
|
+
self.session_id = session_id
|
|
143
|
+
|
|
129
144
|
self.website_name = website_name
|
|
130
|
-
self.browser_type = browser_type
|
|
131
|
-
self.max_steps = max_steps
|
|
145
|
+
self.browser_type = browser_type or 'chromium'
|
|
146
|
+
self.max_steps = int(max_steps)
|
|
132
147
|
self.steps = 0
|
|
133
|
-
self.screenshot_dir = f"browser_tool/{get_clean_website_name(website_name)}/{session_id}"
|
|
148
|
+
self.screenshot_dir = f"browser_tool/{get_clean_website_name(website_name)}/{self.session_id}"
|
|
134
149
|
os.makedirs(self.screenshot_dir, exist_ok=True)
|
|
135
150
|
self.cookie_file = os.path.join(self.screenshot_dir, "cookies.json")
|
|
136
151
|
self.action_log_file = os.path.join(self.screenshot_dir, "action_log.json")
|
|
@@ -199,21 +214,81 @@ class BrowseWebWithImagePromptsBot:
|
|
|
199
214
|
log.warning(f"navigate failed with {str(err)}")
|
|
200
215
|
self.action_log.append(f"Tried to navigate to {url} but got an error")
|
|
201
216
|
|
|
217
|
+
def get_locator_via_roles_and_placeholder(self, selector: str):
|
|
218
|
+
interactive_roles = ["button", "link", "menuitem", "menuitemcheckbox", "menuitemradio", "tab", "option"]
|
|
219
|
+
|
|
220
|
+
for role in interactive_roles:
|
|
221
|
+
log.info(f'Trying role {role} for selector {selector}')
|
|
222
|
+
elements = self.page.get_by_role(role).get_by_placeholder(selector).locator("visible=true").all()
|
|
223
|
+
if elements:
|
|
224
|
+
log.info(f"Got {len(elements)} elements for selector {selector} with role {role}")
|
|
225
|
+
for element in elements:
|
|
226
|
+
try:
|
|
227
|
+
log.info(f"Trying {selector} with element.hover locator: {element}")
|
|
228
|
+
try:
|
|
229
|
+
element.hover(timeout=10000, trial=True)
|
|
230
|
+
self.action_log.append(f"Successfully found element via selector: {selector}")
|
|
231
|
+
|
|
232
|
+
return element
|
|
233
|
+
|
|
234
|
+
except Exception as err:
|
|
235
|
+
log.warning(f"Could not hover over element: {element} {str(err)} - trying next element")
|
|
236
|
+
except Exception as e:
|
|
237
|
+
log.error(f"Failed to get locator for selector '{selector}' with role {role}: {str(e)}")
|
|
238
|
+
|
|
239
|
+
time.sleep(0.5) # Wait for a bit before retrying
|
|
240
|
+
|
|
241
|
+
log.info(f"No elements for '{selector}' within role '{role}'")
|
|
242
|
+
|
|
243
|
+
self.action_log.append(f"FAILED: Using page.get_by_role('role').get_by_placeholder('{selector}').locator('visible=true') could not find any valid element. Try something else.")
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
def get_locator_via_roles_and_text(self, selector: str):
|
|
247
|
+
interactive_roles = ["button", "link", "menuitem", "menuitemcheckbox", "menuitemradio", "tab", "option"]
|
|
248
|
+
|
|
249
|
+
for role in interactive_roles:
|
|
250
|
+
log.info(f'Trying role {role} for selector {selector}')
|
|
251
|
+
elements = self.page.get_by_role(role).get_by_text(selector).locator("visible=true").all()
|
|
252
|
+
if elements:
|
|
253
|
+
log.info(f"Got {len(elements)} elements for selector {selector} with role {role}")
|
|
254
|
+
for element in elements:
|
|
255
|
+
try:
|
|
256
|
+
log.info(f"Trying {selector} with element.hover locator: {element}")
|
|
257
|
+
try:
|
|
258
|
+
element.hover(timeout=10000, trial=True)
|
|
259
|
+
self.action_log.append(f"Successfully found element via selector: {selector}")
|
|
260
|
+
|
|
261
|
+
return element
|
|
262
|
+
|
|
263
|
+
except Exception as err:
|
|
264
|
+
log.warning(f"Could not hover over element: {element} {str(err)} - trying next element")
|
|
265
|
+
except Exception as e:
|
|
266
|
+
log.error(f"Failed to get locator for selector '{selector}' with role {role}: {str(e)}")
|
|
267
|
+
|
|
268
|
+
time.sleep(0.5) # Wait for a bit before retrying
|
|
269
|
+
|
|
270
|
+
log.info(f"No elements for '{selector}' within role '{role}'")
|
|
271
|
+
|
|
272
|
+
self.action_log.append(f"FAILED: Using page.get_by_role('role').get_by_text('{selector}').locator('visible=true') could not find any valid element. Try something else.")
|
|
273
|
+
return None
|
|
274
|
+
|
|
202
275
|
def get_locator(self, selector, by_text=True):
|
|
203
276
|
if by_text:
|
|
204
|
-
|
|
205
|
-
if elements:
|
|
206
|
-
return elements[0]
|
|
207
|
-
else:
|
|
208
|
-
log.warning(f"No elements found with text: {selector}")
|
|
209
|
-
return None
|
|
277
|
+
element = self.get_locator_via_roles_and_text(selector)
|
|
210
278
|
else:
|
|
211
|
-
|
|
279
|
+
element = self.page.locator(selector)
|
|
280
|
+
|
|
281
|
+
if element:
|
|
282
|
+
return element
|
|
283
|
+
|
|
284
|
+
log.error(f"Failed to get locator for selector {selector}")
|
|
285
|
+
|
|
286
|
+
return None
|
|
212
287
|
|
|
213
|
-
def click(self, selector
|
|
288
|
+
def click(self, selector):
|
|
214
289
|
(x,y)=(0,0)
|
|
215
290
|
|
|
216
|
-
element = self.
|
|
291
|
+
element = self.get_locator_via_roles_and_text(selector)
|
|
217
292
|
if element is None:
|
|
218
293
|
self.action_log.append(f"Tried to click on text {selector} but it was not a valid location to click")
|
|
219
294
|
return (x,y)
|
|
@@ -229,14 +304,14 @@ class BrowseWebWithImagePromptsBot:
|
|
|
229
304
|
try:
|
|
230
305
|
element.click()
|
|
231
306
|
self.page.wait_for_load_state()
|
|
232
|
-
log.info(f"Clicked on element with
|
|
233
|
-
self.action_log.append(f"Clicked on element with
|
|
307
|
+
log.info(f"Clicked on {element=} with {selector=} at {x=},{y=}")
|
|
308
|
+
self.action_log.append(f"Clicked on {element=} with {selector=} at {x=},{y=}")
|
|
234
309
|
|
|
235
310
|
return (x,y)
|
|
236
311
|
|
|
237
312
|
except Exception as err:
|
|
238
313
|
log.warning(f"click failed with {str(err)}")
|
|
239
|
-
self.action_log.append(f"Tried to click on element with
|
|
314
|
+
self.action_log.append(f"Tried to click on {element=} with {selector=} at {x=},{y=} but got an error")
|
|
240
315
|
|
|
241
316
|
return (x,y)
|
|
242
317
|
|
|
@@ -257,11 +332,11 @@ class BrowseWebWithImagePromptsBot:
|
|
|
257
332
|
log.warning(f"Scrolled failed with {str(err)}")
|
|
258
333
|
self.action_log.append(f"Tried to scroll {direction} by {amount} pixels but got an error")
|
|
259
334
|
|
|
260
|
-
def type_text(self, selector, text
|
|
335
|
+
def type_text(self, selector, text):
|
|
261
336
|
(x,y)=(0,0)
|
|
262
|
-
element = self.
|
|
337
|
+
element = self.get_locator_via_roles_and_placeholder(selector)
|
|
263
338
|
if element is None:
|
|
264
|
-
self.action_log.append(f"Tried to type {text} via website text: {selector} but it was not a valid
|
|
339
|
+
self.action_log.append(f"Tried to type {text} via website text: {selector} but it was not a valid element to add text")
|
|
265
340
|
return (x,y)
|
|
266
341
|
|
|
267
342
|
try:
|
|
@@ -275,50 +350,60 @@ class BrowseWebWithImagePromptsBot:
|
|
|
275
350
|
try:
|
|
276
351
|
element.fill(text)
|
|
277
352
|
self.page.wait_for_load_state()
|
|
278
|
-
log.info(f"Typed text '{text}' into element with
|
|
279
|
-
self.action_log.append(f"Typed text '{text}' into element with
|
|
353
|
+
log.info(f"Typed text '{text}' into {element=} with {selector=} at {x=},{y=}")
|
|
354
|
+
self.action_log.append(f"Typed text '{text}' into {element=} with {selector=} at {x=},{y=}")
|
|
280
355
|
|
|
281
356
|
return (x, y)
|
|
282
357
|
|
|
283
358
|
except Exception as err:
|
|
284
359
|
log.warning(f"Typed text failed with {str(err)}")
|
|
285
|
-
self.action_log.append(f"Tried to type text '{text}' into element with
|
|
360
|
+
self.action_log.append(f"Tried to type text '{text}' into {element=} with {selector=} at {x=},{y=} but got an error")
|
|
286
361
|
|
|
287
362
|
return (x, y)
|
|
288
363
|
|
|
289
|
-
def take_screenshot(self,
|
|
364
|
+
def take_screenshot(self, full_page=False, mark_action=None):
|
|
365
|
+
|
|
366
|
+
from PIL import Image
|
|
367
|
+
|
|
290
368
|
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
|
|
291
369
|
parsed_url = urllib.parse.urlparse(self.page.url)
|
|
292
370
|
|
|
293
371
|
url_path = parsed_url.path
|
|
294
372
|
if url_path == "/":
|
|
295
373
|
url_path = "index.html"
|
|
296
|
-
if final:
|
|
297
|
-
screenshot_path = os.path.join(self.screenshot_dir, f"final/{timestamp}_{url_path}.png")
|
|
298
374
|
else:
|
|
299
|
-
|
|
300
|
-
|
|
375
|
+
url_path = url_path.replace("/","_")
|
|
376
|
+
|
|
377
|
+
if get_clean_website_name(url_path) != self.website_name:
|
|
378
|
+
url_path = f"{get_clean_website_name(url_path)}_{url_path}"
|
|
379
|
+
|
|
380
|
+
screenshot_path = os.path.join(self.screenshot_dir, f"{timestamp}_{url_path}.png")
|
|
381
|
+
screenshot_bytes = self.page.screenshot(full_page=full_page, scale='css')
|
|
301
382
|
|
|
302
383
|
if mark_action:
|
|
303
|
-
self.mark_screenshot(
|
|
384
|
+
image = self.mark_screenshot(screenshot_bytes, mark_action)
|
|
385
|
+
else:
|
|
386
|
+
image = Image.open(BytesIO(screenshot_bytes))
|
|
387
|
+
|
|
388
|
+
image.save(screenshot_path, format='png')
|
|
304
389
|
|
|
305
390
|
log.info(f"Screenshot {self.page.url} taken and saved to {screenshot_path}")
|
|
306
391
|
#self.action_log.append(f"Screenshot {self.page.url} taken and saved to {screenshot_path}")
|
|
307
392
|
self.session_screenshots.append(screenshot_path)
|
|
308
393
|
|
|
309
|
-
return
|
|
394
|
+
return screenshot_bytes
|
|
310
395
|
|
|
311
|
-
def mark_screenshot(self,
|
|
396
|
+
def mark_screenshot(self, screenshot_bytes, mark_action):
|
|
312
397
|
"""
|
|
313
398
|
Marks the screenshot with the specified action.
|
|
314
399
|
|
|
315
400
|
Parameters:
|
|
316
|
-
|
|
401
|
+
screenshot_bytes (bytes): The bytes of the screenshot.
|
|
317
402
|
mark_action (dict): Action details for marking the screenshot.
|
|
318
403
|
"""
|
|
319
404
|
from PIL import Image, ImageDraw
|
|
320
|
-
|
|
321
|
-
image = Image.open(
|
|
405
|
+
time.sleep(1) # maybe pass in bytes to avoid waiting for file to be written
|
|
406
|
+
image = Image.open(BytesIO(screenshot_bytes))
|
|
322
407
|
draw = ImageDraw.Draw(image)
|
|
323
408
|
|
|
324
409
|
if mark_action['type'] == 'click':
|
|
@@ -329,7 +414,7 @@ class BrowseWebWithImagePromptsBot:
|
|
|
329
414
|
x, y = mark_action['position']
|
|
330
415
|
draw.rectangle((x-5, y-5, x+5, y+5), outline='blue', width=3)
|
|
331
416
|
|
|
332
|
-
image
|
|
417
|
+
return image
|
|
333
418
|
|
|
334
419
|
def get_latest_screenshot_path(self):
|
|
335
420
|
screenshots = sorted(
|
|
@@ -364,6 +449,10 @@ class BrowseWebWithImagePromptsBot:
|
|
|
364
449
|
else:
|
|
365
450
|
log.warning(f'Unknown response: {response=} {type(response)}')
|
|
366
451
|
output = None
|
|
452
|
+
|
|
453
|
+
if not output:
|
|
454
|
+
log.error(f'Got no output from response: {response=}')
|
|
455
|
+
return None
|
|
367
456
|
|
|
368
457
|
if 'status' not in output:
|
|
369
458
|
log.error(f'Response did not contain status')
|
|
@@ -377,9 +466,9 @@ class BrowseWebWithImagePromptsBot:
|
|
|
377
466
|
|
|
378
467
|
return output
|
|
379
468
|
|
|
380
|
-
def send_screenshot_to_llm(self,
|
|
381
|
-
|
|
382
|
-
|
|
469
|
+
def send_screenshot_to_llm(self, screenshot_bytes, last_message):
|
|
470
|
+
|
|
471
|
+
encoded_image = base64.b64encode(screenshot_bytes).decode('utf-8')
|
|
383
472
|
|
|
384
473
|
prompt_vars = self.create_prompt_vars(last_message)
|
|
385
474
|
response = self.send_prompt_to_llm(prompt_vars, encoded_image) # Sending prompt and image separately
|
|
@@ -396,6 +485,8 @@ This method should be implemented by subclasses: `def send_prompt_to_llm(self, p
|
|
|
396
485
|
""")
|
|
397
486
|
|
|
398
487
|
def close(self):
|
|
488
|
+
log.info(f"Session {self.session_id} finished")
|
|
489
|
+
self.take_screenshot()
|
|
399
490
|
self.save_cookies()
|
|
400
491
|
self.browser.close()
|
|
401
492
|
self.playwright.stop()
|
|
@@ -437,14 +528,14 @@ This method should be implemented by subclasses: `def send_prompt_to_llm(self, p
|
|
|
437
528
|
log.warning(f"Reached the maximum number of steps: {self.max_steps}")
|
|
438
529
|
return
|
|
439
530
|
time.sleep(2)
|
|
440
|
-
|
|
531
|
+
screenshot_bytes = self.take_screenshot(mark_action=mark_action)
|
|
441
532
|
next_browser_instructions = self.send_screenshot_to_llm(
|
|
442
|
-
|
|
533
|
+
screenshot_bytes,
|
|
443
534
|
last_message=last_message)
|
|
444
535
|
|
|
445
536
|
return next_browser_instructions
|
|
446
537
|
|
|
447
|
-
def create_gif_from_pngs(self, frame_duration=
|
|
538
|
+
def create_gif_from_pngs(self, frame_duration=300):
|
|
448
539
|
"""
|
|
449
540
|
Creates a GIF from a folder of PNG images.
|
|
450
541
|
|
|
@@ -501,9 +592,17 @@ This method should be implemented by subclasses: `def send_prompt_to_llm(self, p
|
|
|
501
592
|
log.error('Browser status: "in-progress" but no new_instructions')
|
|
502
593
|
last_message = next_instructions['message']
|
|
503
594
|
self.action_log.append(last_message)
|
|
504
|
-
|
|
505
|
-
next_instructions
|
|
506
|
-
|
|
595
|
+
try:
|
|
596
|
+
next_instructions = self.execute_instructions(
|
|
597
|
+
next_instructions['new_instructions'],
|
|
598
|
+
last_message=last_message)
|
|
599
|
+
except Exception as err:
|
|
600
|
+
log.error(f'session aborted due to: {str(err)}')
|
|
601
|
+
next_instructions = {
|
|
602
|
+
'status': 'error',
|
|
603
|
+
'message': f'session errored with: {str(err)}'
|
|
604
|
+
}
|
|
605
|
+
break
|
|
507
606
|
else:
|
|
508
607
|
log.info(f'Session finished due to status={next_instructions["status"]}')
|
|
509
608
|
in_session=False
|
|
@@ -513,9 +612,6 @@ This method should be implemented by subclasses: `def send_prompt_to_llm(self, p
|
|
|
513
612
|
in_session=False
|
|
514
613
|
break
|
|
515
614
|
|
|
516
|
-
log.info("Session finished")
|
|
517
|
-
self.take_screenshot()
|
|
518
|
-
|
|
519
615
|
self.close()
|
|
520
616
|
|
|
521
617
|
answer = None
|
sunholo/utils/parsers.py
CHANGED
|
@@ -186,6 +186,6 @@ def escape_braces(text):
|
|
|
186
186
|
text = re.sub(r'(?<!})}(?!})', '}}', text) # Replace '}' with '}}' if not already double braced
|
|
187
187
|
return text
|
|
188
188
|
|
|
189
|
-
def get_clean_website_name(url):
|
|
189
|
+
def get_clean_website_name(url: str):
|
|
190
190
|
parsed_url = urllib.parse.urlparse(url)
|
|
191
191
|
return parsed_url.netloc
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: sunholo
|
|
3
|
-
Version: 0.74.
|
|
3
|
+
Version: 0.74.9
|
|
4
4
|
Summary: Large Language Model DevOps - a package to help deploy LLMs to the Cloud.
|
|
5
5
|
Home-page: https://github.com/sunholo-data/sunholo-py
|
|
6
|
-
Download-URL: https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.74.
|
|
6
|
+
Download-URL: https://github.com/sunholo-data/sunholo-py/archive/refs/tags/v0.74.9.tar.gz
|
|
7
7
|
Author: Holosun ApS
|
|
8
8
|
Author-email: multivac@sunholo.com
|
|
9
9
|
License: Apache License, Version 2.0
|
|
@@ -99,7 +99,7 @@ sunholo/streaming/streaming.py,sha256=9z6pXINEopuL_Z1RnmgXAoZJum9dzyuOxqYtEYnjf8
|
|
|
99
99
|
sunholo/summarise/__init__.py,sha256=MZk3dblUMODcPb1crq4v-Z508NrFIpkSWNf9FIO8BcU,38
|
|
100
100
|
sunholo/summarise/summarise.py,sha256=C3HhjepTjUhUC8FLk4jMQIBvq1BcORniwuTFHjPVhVo,3784
|
|
101
101
|
sunholo/tools/__init__.py,sha256=5NuYpwwTX81qGUWvgwfItoSLXteNnp7KjgD7IPZUFjI,53
|
|
102
|
-
sunholo/tools/web_browser.py,sha256=
|
|
102
|
+
sunholo/tools/web_browser.py,sha256=wms9vHndrbD2NaAvNpYG291YiGP8wVHJ6iljsMVZc5w,26292
|
|
103
103
|
sunholo/utils/__init__.py,sha256=Hv02T5L2zYWvCso5hzzwm8FQogwBq0OgtUbN_7Quzqc,89
|
|
104
104
|
sunholo/utils/api_key.py,sha256=Ct4bIAQZxzPEw14hP586LpVxBAVi_W9Serpy0BK-7KI,244
|
|
105
105
|
sunholo/utils/big_context.py,sha256=gJIP7_ZL-YSLhOMq8jmFTMqH1wq8eB1NK7oKPeZAq2s,5578
|
|
@@ -108,7 +108,7 @@ sunholo/utils/config_class.py,sha256=4fm2Bwn_zFhVJBiUnMBzfCA5LKhTcBMU3mzhf5seXrw
|
|
|
108
108
|
sunholo/utils/config_schema.py,sha256=Wv-ncitzljOhgbDaq9qnFqH5LCuxNv59dTGDWgd1qdk,4189
|
|
109
109
|
sunholo/utils/gcp.py,sha256=uueODEpA-P6O15-t0hmcGC9dONLO_hLfzSsSoQnkUss,4854
|
|
110
110
|
sunholo/utils/gcp_project.py,sha256=0ozs6tzI4qEvEeXb8MxLnCdEVoWKxlM6OH05htj7_tc,1325
|
|
111
|
-
sunholo/utils/parsers.py,sha256=
|
|
111
|
+
sunholo/utils/parsers.py,sha256=akLSZLdvHf5T9OKVj5C3bo4g3Y8puATd8rxnbB4tWDs,5419
|
|
112
112
|
sunholo/utils/timedelta.py,sha256=BbLabEx7_rbErj_YbNM0MBcaFN76DC4PTe4zD2ucezg,493
|
|
113
113
|
sunholo/utils/user_ids.py,sha256=SQd5_H7FE7vcTZp9AQuQDWBXd4FEEd7TeVMQe1H4Ny8,292
|
|
114
114
|
sunholo/utils/version.py,sha256=P1QAJQdZfT2cMqdTSmXmcxrD2PssMPEGM-WI6083Fck,237
|
|
@@ -117,9 +117,9 @@ sunholo/vertex/extensions_class.py,sha256=4PsUM9dSYrIPpq9bZ3K2rL9MRb_rlqAgnMsW0o
|
|
|
117
117
|
sunholo/vertex/init.py,sha256=-w7b9GKsyJnAJpYHYz6_zBUtmeJeLXlEkgOfwoe4DEI,2715
|
|
118
118
|
sunholo/vertex/memory_tools.py,sha256=pomHrDKqvY8MZxfUqoEwhdlpCvSGP6KmFJMVKOimXjs,6842
|
|
119
119
|
sunholo/vertex/safety.py,sha256=S9PgQT1O_BQAkcqauWncRJaydiP8Q_Jzmu9gxYfy1VA,2482
|
|
120
|
-
sunholo-0.74.
|
|
121
|
-
sunholo-0.74.
|
|
122
|
-
sunholo-0.74.
|
|
123
|
-
sunholo-0.74.
|
|
124
|
-
sunholo-0.74.
|
|
125
|
-
sunholo-0.74.
|
|
120
|
+
sunholo-0.74.9.dist-info/LICENSE.txt,sha256=SdE3QjnD3GEmqqg9EX3TM9f7WmtOzqS1KJve8rhbYmU,11345
|
|
121
|
+
sunholo-0.74.9.dist-info/METADATA,sha256=tf6DzIGnmFeb47QUwocgunQWJD2WEmYI3Kv73pcCNy4,7010
|
|
122
|
+
sunholo-0.74.9.dist-info/WHEEL,sha256=y4mX-SOX4fYIkonsAGA5N0Oy-8_gI4FXw5HNI1xqvWg,91
|
|
123
|
+
sunholo-0.74.9.dist-info/entry_points.txt,sha256=bZuN5AIHingMPt4Ro1b_T-FnQvZ3teBes-3OyO0asl4,49
|
|
124
|
+
sunholo-0.74.9.dist-info/top_level.txt,sha256=wt5tadn5--5JrZsjJz2LceoUvcrIvxjHJe-RxuudxAk,8
|
|
125
|
+
sunholo-0.74.9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|