stores 0.1.8.dev1__tar.gz → 0.1.8.dev3__tar.gz

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.
Files changed (55) hide show
  1. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/PKG-INFO +1 -1
  2. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/pyproject.toml +1 -1
  3. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/stores/indexes/remote_index.py +5 -1
  4. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/stores/indexes/venv_utils.py +140 -116
  5. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/tests/test_indexes/test_venv_utils.py +1 -1
  6. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/uv.lock +1 -1
  7. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/.gitignore +0 -0
  8. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/.python-version +0 -0
  9. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/LICENSE +0 -0
  10. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/README.md +0 -0
  11. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/examples/README.md +0 -0
  12. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/examples/quickstarts/anthropic_api.py +0 -0
  13. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/examples/quickstarts/google_gemini_auto_call.py +0 -0
  14. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/examples/quickstarts/google_gemini_manual_call.py +0 -0
  15. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/examples/quickstarts/langchain_w_tool_calling.py +0 -0
  16. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/examples/quickstarts/langgraph_agent.py +0 -0
  17. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/examples/quickstarts/litellm_w_tool_calling.py +0 -0
  18. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/examples/quickstarts/llamaindex_agent.py +0 -0
  19. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/examples/quickstarts/openai_agent.py +0 -0
  20. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/examples/quickstarts/openai_chat_completions.py +0 -0
  21. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/examples/quickstarts/openai_responses.py +0 -0
  22. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/local_generator.py +0 -0
  23. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/run_browser_use.py +0 -0
  24. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/run_remote_tool.py +0 -0
  25. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/run_sandbox.py +0 -0
  26. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/stores/__init__.py +0 -0
  27. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/stores/constants.py +0 -0
  28. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/stores/format.py +0 -0
  29. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/stores/indexes/__init__.py +0 -0
  30. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/stores/indexes/base_index.py +0 -0
  31. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/stores/indexes/index.py +0 -0
  32. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/stores/indexes/local_index.py +0 -0
  33. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/stores/parse.py +0 -0
  34. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/stores/utils.py +0 -0
  35. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/tests/README.md +0 -0
  36. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/tests/mock_index/hello/__init__.py +0 -0
  37. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/tests/mock_index/tools.py +0 -0
  38. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/tests/mock_index/tools.toml +0 -0
  39. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/tests/mock_index_custom_class/foo.py +0 -0
  40. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/tests/mock_index_custom_class/tools.toml +0 -0
  41. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/tests/mock_index_function_error/foo.py +0 -0
  42. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/tests/mock_index_function_error/tools.toml +0 -0
  43. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/tests/mock_index_w_deps/mock_index/__init__.py +0 -0
  44. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/tests/mock_index_w_deps/pyproject.toml +0 -0
  45. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/tests/mock_index_w_deps/requirements.txt +0 -0
  46. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/tests/mock_index_w_deps/tools.toml +0 -0
  47. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/tests/test_format/conftest.py +0 -0
  48. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/tests/test_format/test_format.py +0 -0
  49. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/tests/test_indexes/conftest.py +0 -0
  50. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/tests/test_indexes/test_base_index.py +0 -0
  51. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/tests/test_indexes/test_index.py +0 -0
  52. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/tests/test_indexes/test_local_index.py +0 -0
  53. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/tests/test_indexes/test_remote_index.py +0 -0
  54. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/tests/test_parse/conftest.py +0 -0
  55. {stores-0.1.8.dev1 → stores-0.1.8.dev3}/tests/test_parse/test_parse.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stores
3
- Version: 0.1.8.dev1
3
+ Version: 0.1.8.dev3
4
4
  Summary: Repository of Python functions and tools for LLMs
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.10
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "stores"
3
- version = "0.1.8dev1"
3
+ version = "0.1.8dev3"
4
4
  description = "Repository of Python functions and tools for LLMs"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -8,7 +8,6 @@ from pathlib import Path
8
8
  from typing import Optional
9
9
 
10
10
  import requests
11
- from git import GitCommandError, Repo
12
11
 
13
12
  from stores.constants import VENV_NAME
14
13
  from stores.indexes.base_index import BaseIndex
@@ -18,6 +17,11 @@ logging.basicConfig()
18
17
  logger = logging.getLogger("stores.indexes.remote_index")
19
18
  logger.setLevel(logging.INFO)
20
19
 
20
+ try:
21
+ from git import GitCommandError, Repo
22
+ except Exception:
23
+ logger.warning("Failed to import git")
24
+
21
25
  # TODO: CACHE_DIR might resolve differently
22
26
  CACHE_DIR = Path(".tools")
23
27
  INDEX_LOOKUP_URL = (
@@ -327,8 +327,7 @@ def parse_tool_signature(
327
327
  if signature_dict.get("isasyncgenfunction"):
328
328
 
329
329
  async def func_handler(*args, **kwargs):
330
- # TODO: Make this truly async
331
- async for value in run_remote_tool(
330
+ async for value in run_remote_tool_async(
332
331
  tool_id=signature_dict["tool_id"],
333
332
  index_folder=index_folder,
334
333
  args=args,
@@ -338,6 +337,7 @@ def parse_tool_signature(
338
337
  stream=True,
339
338
  ):
340
339
  yield value
340
+
341
341
  elif signature_dict.get("isgeneratorfunction"):
342
342
 
343
343
  def func_handler(*args, **kwargs):
@@ -347,7 +347,7 @@ def parse_tool_signature(
347
347
  def run():
348
348
  async def runner():
349
349
  try:
350
- async for item in run_remote_tool(
350
+ async for item in run_remote_tool_async(
351
351
  tool_id=signature_dict["tool_id"],
352
352
  index_folder=index_folder,
353
353
  args=args,
@@ -381,26 +381,63 @@ def parse_tool_signature(
381
381
  elif signature_dict.get("iscoroutinefunction"):
382
382
 
383
383
  async def func_handler(*args, **kwargs):
384
- # TODO: Make this truly async
385
- return run_remote_tool(
384
+ result = []
385
+ async for item in run_remote_tool_async(
386
386
  tool_id=signature_dict["tool_id"],
387
387
  index_folder=index_folder,
388
388
  args=args,
389
389
  kwargs=kwargs,
390
390
  venv=venv,
391
391
  env_var=env_var,
392
- )
392
+ stream=True,
393
+ ):
394
+ result.append(item)
395
+ return result[-1] if result else None
393
396
  else:
394
397
 
395
- def func_handler(*args, **kwargs):
396
- return run_remote_tool(
398
+ async def func_handler_async_fallback(*args, **kwargs):
399
+ result = []
400
+ async for item in run_remote_tool_async(
397
401
  tool_id=signature_dict["tool_id"],
398
402
  index_folder=index_folder,
399
403
  args=args,
400
404
  kwargs=kwargs,
401
405
  venv=venv,
402
406
  env_var=env_var,
403
- )
407
+ stream=True,
408
+ ):
409
+ result.append(item)
410
+ return result[-1] if result else None
411
+
412
+ def func_handler(*args, **kwargs):
413
+ coro = func_handler_async_fallback(*args, **kwargs)
414
+ try:
415
+ # Check if we're in an async context
416
+ asyncio.get_running_loop()
417
+ in_async = True
418
+ except RuntimeError:
419
+ in_async = False
420
+
421
+ if not in_async:
422
+ # Safe to run directly
423
+ return asyncio.run(coro)
424
+
425
+ q = queue.Queue()
426
+
427
+ def runner():
428
+ try:
429
+ result = asyncio.run(coro)
430
+ q.put(result)
431
+ except Exception as e:
432
+ q.put(e)
433
+
434
+ t = threading.Thread(target=runner)
435
+ t.start()
436
+ result = q.get()
437
+ t.join()
438
+ if isinstance(result, Exception):
439
+ raise result
440
+ return result
404
441
 
405
442
  # Reconstruct signature from list of args
406
443
  params = []
@@ -426,92 +463,7 @@ def parse_tool_signature(
426
463
  return func
427
464
 
428
465
 
429
- # TODO: Sanitize tool_id, args, and kwargs
430
- def run_remote_tool(
431
- tool_id: str,
432
- index_folder: os.PathLike,
433
- args: list | None = None,
434
- kwargs: dict | None = None,
435
- venv: str = VENV_NAME,
436
- env_var: dict | None = None,
437
- stream: bool = False,
438
- ):
439
- args = args or []
440
- kwargs = kwargs or {}
441
- env_var = env_var or {}
442
-
443
- module_name = ".".join(tool_id.split(".")[:-1])
444
- tool_name = tool_id.split(".")[-1]
445
- payload = json.dumps(
446
- {
447
- "args": args,
448
- "kwargs": kwargs,
449
- }
450
- ).encode("utf-8")
451
-
452
- # We use sockets to pass function output
453
- listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
454
- listener.bind(("localhost", 0))
455
- listener.listen(1)
456
- _, port = listener.getsockname()
457
-
458
- result_data = {}
459
-
460
- def handle_connection_sync():
461
- conn, _ = listener.accept()
462
- with conn:
463
- buffer = ""
464
- while True:
465
- chunk = conn.recv(4096).decode("utf-8")
466
- if not chunk:
467
- break
468
- buffer += chunk
469
- while "\n" in buffer:
470
- line, buffer = buffer.split("\n", 1)
471
- if not line.strip():
472
- continue
473
- msg = json.loads(line)
474
- if msg.get("ok") and "stream" in msg:
475
- result_data.setdefault("stream", []).append(msg["stream"])
476
- elif msg.get("ok") and "result" in msg:
477
- result_data["result"] = msg["result"]
478
- elif "error" in msg:
479
- result_data["error"] = msg["error"]
480
- elif msg.get("done"):
481
- return
482
-
483
- async def handle_connection_async():
484
- loop = asyncio.get_running_loop()
485
- conn, _ = await loop.sock_accept(listener)
486
- conn.setblocking(False)
487
- buffer = ""
488
- try:
489
- while True:
490
- chunk = await loop.sock_recv(conn, 4096)
491
- if not chunk:
492
- break
493
- buffer += chunk.decode("utf-8")
494
- while "\n" in buffer:
495
- line, buffer = buffer.split("\n", 1)
496
- if not line.strip():
497
- continue
498
- msg = json.loads(line)
499
- if msg.get("ok") and "stream" in msg:
500
- yield msg["stream"]
501
- elif msg.get("ok") and "result" in msg:
502
- yield msg["result"]
503
- elif "error" in msg:
504
- raise RuntimeError(f"Subprocess error:\n{msg['error']}")
505
- elif msg.get("done"):
506
- return
507
- finally:
508
- conn.close()
509
-
510
- if not stream:
511
- thread = threading.Thread(target=lambda: handle_connection_sync())
512
- thread.start()
513
-
514
- runner = f"""
466
+ tool_runner = """
515
467
  import asyncio, inspect, json, socket, sys, traceback
516
468
  sys.path.insert(0, "{index_folder}")
517
469
 
@@ -560,26 +512,98 @@ finally:
560
512
  pass
561
513
  """
562
514
 
563
- proc = subprocess.Popen(
564
- [get_python_command(Path(index_folder) / venv), "-c", runner],
565
- stdin=subprocess.PIPE,
566
- stdout=subprocess.DEVNULL,
567
- stderr=subprocess.PIPE,
515
+
516
+ # TODO: Sanitize tool_id, args, and kwargs
517
+ async def run_remote_tool_async(
518
+ tool_id: str,
519
+ index_folder: os.PathLike,
520
+ args: list | None = None,
521
+ kwargs: dict | None = None,
522
+ venv: str = VENV_NAME,
523
+ env_var: dict | None = None,
524
+ stream: bool = True,
525
+ ):
526
+ args = args or []
527
+ kwargs = kwargs or {}
528
+ env_var = env_var or {}
529
+
530
+ module_name = ".".join(tool_id.split(".")[:-1])
531
+ tool_name = tool_id.split(".")[-1]
532
+ payload = json.dumps({"args": args, "kwargs": kwargs}).encode("utf-8")
533
+
534
+ listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
535
+ listener.bind(("localhost", 0))
536
+ listener.listen(1)
537
+ listener.setblocking(False)
538
+ _, port = listener.getsockname()
539
+
540
+ loop = asyncio.get_running_loop()
541
+ conn_task = loop.create_task(loop.sock_accept(listener))
542
+
543
+ runner = tool_runner.format(
544
+ index_folder=index_folder,
545
+ port=port,
546
+ module_name=module_name,
547
+ tool_name=tool_name,
548
+ )
549
+
550
+ proc = await asyncio.create_subprocess_exec(
551
+ get_python_command(Path(index_folder) / venv),
552
+ "-c",
553
+ runner,
554
+ stdin=asyncio.subprocess.PIPE,
555
+ stdout=asyncio.subprocess.DEVNULL,
556
+ stderr=asyncio.subprocess.PIPE,
568
557
  env=env_var or None,
569
558
  )
570
- proc.stdin.write(payload)
571
- proc.stdin.close()
572
-
573
- if not stream:
574
- thread.join()
575
-
576
- if "error" in result_data:
577
- raise RuntimeError(f"Subprocess failed with error:\n{result_data['error']}")
578
- elif "result" in result_data:
579
- return result_data["result"]
580
- elif "stream" in result_data:
581
- return result_data["stream"]
582
- else:
583
- raise RuntimeError("Subprocess completed without returning data.")
584
- else:
585
- return handle_connection_async()
559
+
560
+ try:
561
+ proc.stdin.write(payload)
562
+ await proc.stdin.drain()
563
+ proc.stdin.close()
564
+
565
+ conn, _ = await conn_task
566
+ conn.setblocking(False)
567
+
568
+ buffer = ""
569
+ result = None
570
+ while True:
571
+ chunk = await loop.sock_recv(conn, 4096)
572
+ if not chunk:
573
+ break
574
+ buffer += chunk.decode("utf-8")
575
+ while "\n" in buffer:
576
+ line, buffer = buffer.split("\n", 1)
577
+ if not line.strip():
578
+ continue
579
+ msg = json.loads(line)
580
+
581
+ if msg.get("ok") and "stream" in msg:
582
+ if stream:
583
+ yield msg["stream"]
584
+ else:
585
+ result = msg["stream"]
586
+ elif msg.get("ok") and "result" in msg:
587
+ result = msg["result"]
588
+ elif "error" in msg:
589
+ raise RuntimeError(f"Subprocess error:\n{msg['error']}")
590
+ elif "done" in msg and result is not None:
591
+ yield result
592
+ return
593
+
594
+ except asyncio.CancelledError:
595
+ proc.kill()
596
+ await proc.wait()
597
+ raise
598
+ finally:
599
+ try:
600
+ conn.close()
601
+ except Exception:
602
+ pass
603
+ try:
604
+ listener.close()
605
+ except Exception:
606
+ pass
607
+ if proc.returncode is None:
608
+ proc.kill()
609
+ await proc.wait()
@@ -134,7 +134,7 @@ def test_index_with_invalid_tool(index_folder_custom_class):
134
134
 
135
135
  def test_index_with_tool_error(index_folder_function_error):
136
136
  tools = venv_utils.init_venv_tools(index_folder_function_error)
137
- with pytest.raises(RuntimeError, match="Subprocess failed with error"):
137
+ with pytest.raises(RuntimeError, match="ZeroDivisionError"):
138
138
  tools[0]()
139
139
 
140
140
 
@@ -2963,7 +2963,7 @@ wheels = [
2963
2963
 
2964
2964
  [[package]]
2965
2965
  name = "stores"
2966
- version = "0.1.7.dev6"
2966
+ version = "0.1.8.dev3"
2967
2967
  source = { editable = "." }
2968
2968
  dependencies = [
2969
2969
  { name = "dirtyjson" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes