vincta 0.1.0__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.
- vincta-0.1.0/PKG-INFO +26 -0
- vincta-0.1.0/pyproject.toml +39 -0
- vincta-0.1.0/setup.cfg +4 -0
- vincta-0.1.0/vincta/__init__.py +0 -0
- vincta-0.1.0/vincta/cli.py +67 -0
- vincta-0.1.0/vincta/discovery/__init__.py +0 -0
- vincta-0.1.0/vincta/discovery/autodiscover.py +106 -0
- vincta-0.1.0/vincta/execution/__init__.py +0 -0
- vincta-0.1.0/vincta/execution/engine.py +92 -0
- vincta-0.1.0/vincta/object_store/__init__.py +0 -0
- vincta-0.1.0/vincta/object_store/store.py +104 -0
- vincta-0.1.0/vincta/registry/__init__.py +0 -0
- vincta-0.1.0/vincta/registry/decorator.py +41 -0
- vincta-0.1.0/vincta/registry/registry.py +126 -0
- vincta-0.1.0/vincta/routers/__init__.py +0 -0
- vincta-0.1.0/vincta/routers/execution.py +31 -0
- vincta-0.1.0/vincta/routers/functions.py +26 -0
- vincta-0.1.0/vincta/routers/objects.py +50 -0
- vincta-0.1.0/vincta/schemas/__init__.py +0 -0
- vincta-0.1.0/vincta/schemas/models.py +44 -0
- vincta-0.1.0/vincta/server.py +60 -0
- vincta-0.1.0/vincta/services/__init__.py +0 -0
- vincta-0.1.0/vincta/services/execution_service.py +9 -0
- vincta-0.1.0/vincta.egg-info/PKG-INFO +26 -0
- vincta-0.1.0/vincta.egg-info/SOURCES.txt +27 -0
- vincta-0.1.0/vincta.egg-info/dependency_links.txt +1 -0
- vincta-0.1.0/vincta.egg-info/entry_points.txt +2 -0
- vincta-0.1.0/vincta.egg-info/requires.txt +13 -0
- vincta-0.1.0/vincta.egg-info/top_level.txt +1 -0
vincta-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vincta
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Auto-discover and serve Python functions over HTTP with a built-in RAM object store
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Keywords: fastapi,functions,orchestration,http,object-store,autodiscovery,api
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Framework :: FastAPI
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: fastapi>=0.111.0
|
|
18
|
+
Requires-Dist: uvicorn[standard]>=0.29.0
|
|
19
|
+
Requires-Dist: pydantic>=2.0.0
|
|
20
|
+
Provides-Extra: pandas
|
|
21
|
+
Requires-Dist: pandas>=2.0.0; extra == "pandas"
|
|
22
|
+
Provides-Extra: numpy
|
|
23
|
+
Requires-Dist: numpy>=1.24.0; extra == "numpy"
|
|
24
|
+
Provides-Extra: all
|
|
25
|
+
Requires-Dist: pandas>=2.0.0; extra == "all"
|
|
26
|
+
Requires-Dist: numpy>=1.24.0; extra == "all"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vincta"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Auto-discover and serve Python functions over HTTP with a built-in RAM object store"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
keywords = ["fastapi", "functions", "orchestration", "http", "object-store", "autodiscovery", "api"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Framework :: FastAPI",
|
|
20
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
21
|
+
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"fastapi>=0.111.0",
|
|
25
|
+
"uvicorn[standard]>=0.29.0",
|
|
26
|
+
"pydantic>=2.0.0",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
pandas = ["pandas>=2.0.0"]
|
|
31
|
+
numpy = ["numpy>=1.24.0"]
|
|
32
|
+
all = ["pandas>=2.0.0", "numpy>=1.24.0"]
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
vincta = "vincta.cli:main"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["."]
|
|
39
|
+
include = ["vincta*"]
|
vincta-0.1.0/setup.cfg
ADDED
|
File without changes
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main() -> None:
|
|
9
|
+
parser = argparse.ArgumentParser(
|
|
10
|
+
prog="relay",
|
|
11
|
+
description="Pure function orchestration engine — serve Python functions over HTTP.",
|
|
12
|
+
)
|
|
13
|
+
parser.add_argument("--host", default="127.0.0.1", help="Bind host (default: 127.0.0.1)")
|
|
14
|
+
parser.add_argument("--port", type=int, default=8000, help="Bind port (default: 8000)")
|
|
15
|
+
parser.add_argument(
|
|
16
|
+
"--dir",
|
|
17
|
+
action="append",
|
|
18
|
+
dest="dirs",
|
|
19
|
+
default=[],
|
|
20
|
+
metavar="DIR",
|
|
21
|
+
help="Directory to discover functions from (repeatable)",
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"--sys-path",
|
|
25
|
+
action="append",
|
|
26
|
+
dest="sys_paths",
|
|
27
|
+
default=[],
|
|
28
|
+
metavar="PATH",
|
|
29
|
+
help="Extra path prepended to sys.path before discovery (repeatable)",
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"--exclude",
|
|
33
|
+
default="test_*,*.test.py",
|
|
34
|
+
help="Comma-separated glob patterns to exclude from discovery",
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument("--reload", action="store_true", help="Enable auto-reload (development)")
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"--workers", type=int, default=1, help="Number of worker processes (ignored with --reload)"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
args = parser.parse_args()
|
|
42
|
+
|
|
43
|
+
if args.dirs:
|
|
44
|
+
os.environ["RELAY_DIRS"] = os.pathsep.join(args.dirs)
|
|
45
|
+
if args.sys_paths:
|
|
46
|
+
os.environ["RELAY_SYS_PATHS"] = os.pathsep.join(args.sys_paths)
|
|
47
|
+
os.environ["RELAY_EXCLUDE"] = args.exclude
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
import uvicorn
|
|
51
|
+
except ImportError:
|
|
52
|
+
print("uvicorn is required: pip install uvicorn[standard]", file=sys.stderr)
|
|
53
|
+
sys.exit(1)
|
|
54
|
+
|
|
55
|
+
uvicorn.run(
|
|
56
|
+
"vincta.server:app",
|
|
57
|
+
host=args.host,
|
|
58
|
+
port=args.port,
|
|
59
|
+
reload=args.reload,
|
|
60
|
+
workers=1 if args.reload else args.workers,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
main()
|
|
66
|
+
|
|
67
|
+
|
|
File without changes
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import inspect
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
|
|
9
|
+
_ALWAYS_EXCLUDE = {"__pycache__", ".git", ".venv", "venv", "node_modules", ".mypy_cache"}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _should_exclude(path: Path, patterns: List[str]) -> bool:
|
|
13
|
+
for part in path.parts:
|
|
14
|
+
if part in _ALWAYS_EXCLUDE:
|
|
15
|
+
return True
|
|
16
|
+
for pattern in patterns:
|
|
17
|
+
if path.match(pattern):
|
|
18
|
+
return True
|
|
19
|
+
return False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _path_to_module_name(file: Path, base: Path) -> str:
|
|
23
|
+
rel = file.relative_to(base)
|
|
24
|
+
parts = list(rel.with_suffix("").parts)
|
|
25
|
+
return ".".join(parts)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _auto_register_module_functions(module) -> int:
|
|
29
|
+
from vincta.registry.registry import is_registered, register
|
|
30
|
+
|
|
31
|
+
count = 0
|
|
32
|
+
for attr_name, obj in inspect.getmembers(module, inspect.isfunction):
|
|
33
|
+
if attr_name.startswith("_"):
|
|
34
|
+
continue
|
|
35
|
+
# Only register functions defined in this module (not imported ones)
|
|
36
|
+
if getattr(obj, "__module__", None) != module.__name__:
|
|
37
|
+
continue
|
|
38
|
+
if is_registered(obj):
|
|
39
|
+
continue
|
|
40
|
+
register(obj, name=attr_name, module=module.__name__)
|
|
41
|
+
count += 1
|
|
42
|
+
return count
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def discover_and_import(
|
|
46
|
+
directories: List[Path],
|
|
47
|
+
exclude_patterns: Optional[List[str]] = None,
|
|
48
|
+
extra_sys_paths: Optional[List[Path]] = None,
|
|
49
|
+
) -> dict:
|
|
50
|
+
exclude_patterns = exclude_patterns or []
|
|
51
|
+
results = {"imported": [], "auto_registered": 0, "errors": []}
|
|
52
|
+
|
|
53
|
+
# Prepend caller-supplied paths first so they win over anything already
|
|
54
|
+
# on sys.path. Typical use: dependency directories for codebases that
|
|
55
|
+
# use flat (non-package) imports.
|
|
56
|
+
for p in reversed(extra_sys_paths or []):
|
|
57
|
+
s = str(p)
|
|
58
|
+
if p.exists() and s not in sys.path:
|
|
59
|
+
sys.path.insert(0, s)
|
|
60
|
+
|
|
61
|
+
for directory in directories:
|
|
62
|
+
if not directory.exists():
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
# Add both the directory and its parent to sys.path.
|
|
66
|
+
# - parent → enables `import <dirname>.<module>` style
|
|
67
|
+
# - directory itself → enables flat `from <module> import ...` style
|
|
68
|
+
# (used by MarketSentiment and other codebases that were written to
|
|
69
|
+
# be run from inside their own folder)
|
|
70
|
+
for p in (directory.parent, directory):
|
|
71
|
+
s = str(p)
|
|
72
|
+
if s not in sys.path:
|
|
73
|
+
sys.path.insert(0, s)
|
|
74
|
+
|
|
75
|
+
for py_file in sorted(directory.rglob("*.py")):
|
|
76
|
+
if _should_exclude(py_file, exclude_patterns):
|
|
77
|
+
continue
|
|
78
|
+
if py_file.name == "__init__.py":
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
module_name = _path_to_module_name(py_file, directory.parent)
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
if module_name in sys.modules:
|
|
85
|
+
module = sys.modules[module_name]
|
|
86
|
+
else:
|
|
87
|
+
spec = importlib.util.spec_from_file_location(module_name, py_file)
|
|
88
|
+
if spec is None or spec.loader is None:
|
|
89
|
+
continue
|
|
90
|
+
module = importlib.util.module_from_spec(spec)
|
|
91
|
+
module.__name__ = module_name
|
|
92
|
+
sys.modules[module_name] = module
|
|
93
|
+
spec.loader.exec_module(module)
|
|
94
|
+
|
|
95
|
+
count = _auto_register_module_functions(module)
|
|
96
|
+
results["imported"].append(module_name)
|
|
97
|
+
results["auto_registered"] += count
|
|
98
|
+
|
|
99
|
+
except Exception as e:
|
|
100
|
+
msg = f"{py_file}: {e}"
|
|
101
|
+
results["errors"].append(msg)
|
|
102
|
+
print(f"[discovery] Failed to import {msg}")
|
|
103
|
+
|
|
104
|
+
return results
|
|
105
|
+
|
|
106
|
+
|
|
File without changes
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Tuple
|
|
4
|
+
|
|
5
|
+
from vincta.object_store.store import retrieve_object, store_object
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
import pandas as pd
|
|
9
|
+
_HAS_PANDAS = True
|
|
10
|
+
except ImportError:
|
|
11
|
+
_HAS_PANDAS = False
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
import numpy as np
|
|
15
|
+
_HAS_NUMPY = True
|
|
16
|
+
except ImportError:
|
|
17
|
+
_HAS_NUMPY = False
|
|
18
|
+
|
|
19
|
+
_PRIMITIVES = (int, float, str, bool, type(None))
|
|
20
|
+
|
|
21
|
+
# Collections smaller than this length are returned directly
|
|
22
|
+
_COLLECTION_THRESHOLD = 100
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _is_obj_ref(value: Any) -> bool:
|
|
26
|
+
return isinstance(value, str) and value.startswith("obj_")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _resolve_inputs(raw_inputs: Dict[str, Any]) -> Dict[str, Any]:
|
|
30
|
+
resolved: Dict[str, Any] = {}
|
|
31
|
+
for key, value in raw_inputs.items():
|
|
32
|
+
if _is_obj_ref(value):
|
|
33
|
+
obj = retrieve_object(value)
|
|
34
|
+
if obj is None:
|
|
35
|
+
raise KeyError(f"Object ref '{value}' not found in store")
|
|
36
|
+
resolved[key] = obj
|
|
37
|
+
else:
|
|
38
|
+
resolved[key] = value
|
|
39
|
+
return resolved
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _should_store(obj: Any) -> bool:
|
|
43
|
+
if isinstance(obj, _PRIMITIVES):
|
|
44
|
+
return False
|
|
45
|
+
if _HAS_PANDAS and isinstance(obj, pd.DataFrame):
|
|
46
|
+
return True
|
|
47
|
+
if _HAS_NUMPY and isinstance(obj, np.ndarray):
|
|
48
|
+
return True
|
|
49
|
+
if isinstance(obj, (dict, list)) and len(obj) > _COLLECTION_THRESHOLD:
|
|
50
|
+
return True
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _package_value(value: Any, key: str) -> Tuple[str, Any]:
|
|
55
|
+
if _should_store(value):
|
|
56
|
+
ref = store_object(value)
|
|
57
|
+
return key, ref
|
|
58
|
+
return key, value
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def execute(function_name: str, raw_inputs: Dict[str, Any]) -> Dict[str, Any]:
|
|
62
|
+
from vincta.registry.registry import get_function
|
|
63
|
+
|
|
64
|
+
entry = get_function(function_name)
|
|
65
|
+
if entry is None:
|
|
66
|
+
raise ValueError(f"Function '{function_name}' not found in registry")
|
|
67
|
+
|
|
68
|
+
fn = entry["fn"]
|
|
69
|
+
declared_outputs: Dict[str, str] = entry.get("outputs", {})
|
|
70
|
+
|
|
71
|
+
resolved = _resolve_inputs(raw_inputs)
|
|
72
|
+
result = fn(**resolved)
|
|
73
|
+
|
|
74
|
+
if result is None:
|
|
75
|
+
return {}
|
|
76
|
+
|
|
77
|
+
# Tuple return → zip with declared output keys
|
|
78
|
+
if isinstance(result, tuple):
|
|
79
|
+
output_keys = list(declared_outputs.keys())
|
|
80
|
+
packaged: Dict[str, Any] = {}
|
|
81
|
+
for i, val in enumerate(result):
|
|
82
|
+
key = output_keys[i] if i < len(output_keys) else f"result_{i}"
|
|
83
|
+
k, v = _package_value(val, key)
|
|
84
|
+
packaged[k] = v
|
|
85
|
+
return packaged
|
|
86
|
+
|
|
87
|
+
# Single return value
|
|
88
|
+
key = next(iter(declared_outputs), "result")
|
|
89
|
+
k, v = _package_value(result, key)
|
|
90
|
+
return {k: v}
|
|
91
|
+
|
|
92
|
+
|
|
File without changes
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import uuid
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
import pandas as pd
|
|
9
|
+
_HAS_PANDAS = True
|
|
10
|
+
except ImportError:
|
|
11
|
+
_HAS_PANDAS = False
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
import numpy as np
|
|
15
|
+
_HAS_NUMPY = True
|
|
16
|
+
except ImportError:
|
|
17
|
+
_HAS_NUMPY = False
|
|
18
|
+
|
|
19
|
+
_STORE: Dict[str, Any] = {}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def store_object(obj: Any) -> str:
|
|
23
|
+
ref = f"obj_{uuid.uuid4().hex[:12]}"
|
|
24
|
+
_STORE[ref] = obj
|
|
25
|
+
return ref
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def retrieve_object(ref: str) -> Optional[Any]:
|
|
29
|
+
return _STORE.get(ref)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def delete_object(ref: str) -> bool:
|
|
33
|
+
if ref in _STORE:
|
|
34
|
+
del _STORE[ref]
|
|
35
|
+
return True
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def list_refs() -> List[str]:
|
|
40
|
+
return list(_STORE.keys())
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def object_count() -> int:
|
|
44
|
+
return len(_STORE)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _object_size(obj: Any) -> int:
|
|
48
|
+
try:
|
|
49
|
+
if _HAS_PANDAS and isinstance(obj, pd.DataFrame):
|
|
50
|
+
return int(obj.memory_usage(deep=True).sum())
|
|
51
|
+
if _HAS_NUMPY and isinstance(obj, np.ndarray):
|
|
52
|
+
return obj.nbytes
|
|
53
|
+
return sys.getsizeof(obj)
|
|
54
|
+
except Exception:
|
|
55
|
+
return 0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def summarize_object(ref: str) -> Optional[dict]:
|
|
59
|
+
obj = _STORE.get(ref)
|
|
60
|
+
if obj is None:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
type_name = type(obj).__name__
|
|
64
|
+
summary: dict = {"type": type_name}
|
|
65
|
+
|
|
66
|
+
if _HAS_PANDAS and isinstance(obj, pd.DataFrame):
|
|
67
|
+
summary["shape"] = list(obj.shape)
|
|
68
|
+
summary["columns"] = list(obj.columns)
|
|
69
|
+
summary["dtypes"] = {str(k): str(v) for k, v in obj.dtypes.items()}
|
|
70
|
+
summary["memory_bytes"] = _object_size(obj)
|
|
71
|
+
try:
|
|
72
|
+
summary["preview"] = obj.head(5).to_dict(orient="records")
|
|
73
|
+
except Exception:
|
|
74
|
+
summary["preview"] = []
|
|
75
|
+
|
|
76
|
+
elif _HAS_NUMPY and isinstance(obj, np.ndarray):
|
|
77
|
+
summary["shape"] = list(obj.shape)
|
|
78
|
+
summary["dtype"] = str(obj.dtype)
|
|
79
|
+
summary["memory_bytes"] = obj.nbytes
|
|
80
|
+
|
|
81
|
+
elif isinstance(obj, dict):
|
|
82
|
+
summary["size"] = len(obj)
|
|
83
|
+
summary["keys_preview"] = list(obj.keys())[:20]
|
|
84
|
+
summary["memory_bytes"] = _object_size(obj)
|
|
85
|
+
|
|
86
|
+
elif isinstance(obj, list):
|
|
87
|
+
summary["length"] = len(obj)
|
|
88
|
+
summary["memory_bytes"] = _object_size(obj)
|
|
89
|
+
|
|
90
|
+
else:
|
|
91
|
+
summary["repr"] = repr(obj)[:300]
|
|
92
|
+
summary["memory_bytes"] = _object_size(obj)
|
|
93
|
+
|
|
94
|
+
return summary
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def get_memory_usage() -> dict:
|
|
98
|
+
total = 0
|
|
99
|
+
breakdown: Dict[str, int] = {}
|
|
100
|
+
for ref, obj in _STORE.items():
|
|
101
|
+
size = _object_size(obj)
|
|
102
|
+
total += size
|
|
103
|
+
breakdown[ref] = size
|
|
104
|
+
return {"total_bytes": total, "objects": breakdown}
|
|
File without changes
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Callable, Dict, Optional
|
|
4
|
+
|
|
5
|
+
from vincta.registry.registry import register
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def register_function(
|
|
9
|
+
name: Optional[str] = None,
|
|
10
|
+
inputs: Optional[Dict[str, str]] = None,
|
|
11
|
+
outputs: Optional[Dict[str, str]] = None,
|
|
12
|
+
description: Optional[str] = None,
|
|
13
|
+
):
|
|
14
|
+
"""Decorator for explicit function registration with schema override.
|
|
15
|
+
|
|
16
|
+
Usage::
|
|
17
|
+
|
|
18
|
+
@register_function(
|
|
19
|
+
name="build_features",
|
|
20
|
+
inputs={"df_ref": "str"},
|
|
21
|
+
outputs={"feature_ref": "str"},
|
|
22
|
+
)
|
|
23
|
+
def build_features(df):
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
Decorator schema takes precedence over inferred type hints.
|
|
27
|
+
"""
|
|
28
|
+
def decorator(fn: Callable) -> Callable:
|
|
29
|
+
register(
|
|
30
|
+
fn,
|
|
31
|
+
name=name,
|
|
32
|
+
inputs=inputs,
|
|
33
|
+
outputs=outputs,
|
|
34
|
+
description=description,
|
|
35
|
+
explicit=True,
|
|
36
|
+
)
|
|
37
|
+
return fn
|
|
38
|
+
|
|
39
|
+
return decorator
|
|
40
|
+
|
|
41
|
+
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import typing
|
|
5
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
import pandas as pd
|
|
9
|
+
_PD_DATAFRAME = pd.DataFrame
|
|
10
|
+
except ImportError:
|
|
11
|
+
_PD_DATAFRAME = None
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
import numpy as np
|
|
15
|
+
_NP_NDARRAY = np.ndarray
|
|
16
|
+
except ImportError:
|
|
17
|
+
_NP_NDARRAY = None
|
|
18
|
+
|
|
19
|
+
# Keyed by function name. Each entry is:
|
|
20
|
+
# { fn, name, module, inputs, outputs, description }
|
|
21
|
+
_REGISTRY: Dict[str, dict] = {}
|
|
22
|
+
|
|
23
|
+
# Tracks function object ids that were explicitly registered via decorator,
|
|
24
|
+
# so auto-discovery skips them.
|
|
25
|
+
_REGISTERED_FN_IDS: set = set()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _type_to_str(t: Any) -> str:
|
|
29
|
+
if t is inspect.Parameter.empty or t is type(None):
|
|
30
|
+
return "Any"
|
|
31
|
+
if _PD_DATAFRAME is not None and t is _PD_DATAFRAME:
|
|
32
|
+
return "DataFrame"
|
|
33
|
+
if _NP_NDARRAY is not None and t is _NP_NDARRAY:
|
|
34
|
+
return "ndarray"
|
|
35
|
+
# Handle Optional[X], List[X], Dict[K,V], etc.
|
|
36
|
+
origin = getattr(t, "__origin__", None)
|
|
37
|
+
if origin is not None:
|
|
38
|
+
args = getattr(t, "__args__", ())
|
|
39
|
+
origin_name = getattr(origin, "__name__", str(origin))
|
|
40
|
+
if args:
|
|
41
|
+
arg_strs = ", ".join(_type_to_str(a) for a in args)
|
|
42
|
+
return f"{origin_name}[{arg_strs}]"
|
|
43
|
+
return origin_name
|
|
44
|
+
if hasattr(t, "__name__"):
|
|
45
|
+
return t.__name__
|
|
46
|
+
return str(t)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _extract_schema(fn: Callable) -> dict:
|
|
50
|
+
try:
|
|
51
|
+
hints = typing.get_type_hints(fn)
|
|
52
|
+
except Exception:
|
|
53
|
+
hints = {}
|
|
54
|
+
|
|
55
|
+
sig = inspect.signature(fn)
|
|
56
|
+
inputs: Dict[str, str] = {}
|
|
57
|
+
|
|
58
|
+
for param_name, param in sig.parameters.items():
|
|
59
|
+
if param_name == "self":
|
|
60
|
+
continue
|
|
61
|
+
hint = hints.get(param_name, inspect.Parameter.empty)
|
|
62
|
+
inputs[param_name] = _type_to_str(hint)
|
|
63
|
+
|
|
64
|
+
return_hint = hints.get("return", inspect.Parameter.empty)
|
|
65
|
+
if return_hint is type(None) or return_hint is inspect.Parameter.empty:
|
|
66
|
+
outputs: Dict[str, str] = {}
|
|
67
|
+
else:
|
|
68
|
+
outputs = {"result": _type_to_str(return_hint)}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
"inputs": inputs,
|
|
72
|
+
"outputs": outputs,
|
|
73
|
+
"description": inspect.getdoc(fn),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def register(
|
|
78
|
+
fn: Callable,
|
|
79
|
+
*,
|
|
80
|
+
name: Optional[str] = None,
|
|
81
|
+
inputs: Optional[Dict[str, str]] = None,
|
|
82
|
+
outputs: Optional[Dict[str, str]] = None,
|
|
83
|
+
description: Optional[str] = None,
|
|
84
|
+
module: Optional[str] = None,
|
|
85
|
+
explicit: bool = False,
|
|
86
|
+
) -> None:
|
|
87
|
+
fn_name = name or fn.__qualname__
|
|
88
|
+
schema = _extract_schema(fn)
|
|
89
|
+
|
|
90
|
+
if inputs is not None:
|
|
91
|
+
schema["inputs"] = inputs
|
|
92
|
+
if outputs is not None:
|
|
93
|
+
schema["outputs"] = outputs
|
|
94
|
+
if description is not None:
|
|
95
|
+
schema["description"] = description
|
|
96
|
+
|
|
97
|
+
_REGISTRY[fn_name] = {
|
|
98
|
+
"fn": fn,
|
|
99
|
+
"name": fn_name,
|
|
100
|
+
"module": module or getattr(fn, "__module__", "unknown"),
|
|
101
|
+
"inputs": schema["inputs"],
|
|
102
|
+
"outputs": schema["outputs"],
|
|
103
|
+
"description": schema["description"],
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if explicit:
|
|
107
|
+
_REGISTERED_FN_IDS.add(id(fn))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def is_registered(fn: Callable) -> bool:
|
|
111
|
+
return id(fn) in _REGISTERED_FN_IDS
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def get_function(name: str) -> Optional[dict]:
|
|
115
|
+
return _REGISTRY.get(name)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def list_functions() -> List[dict]:
|
|
119
|
+
return [
|
|
120
|
+
{k: v for k, v in entry.items() if k != "fn"}
|
|
121
|
+
for entry in _REGISTRY.values()
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def function_count() -> int:
|
|
126
|
+
return len(_REGISTRY)
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from typing import Any, Dict
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, HTTPException
|
|
4
|
+
|
|
5
|
+
from vincta.schemas.models import RunRequest
|
|
6
|
+
from vincta.services.execution_service import run_function
|
|
7
|
+
|
|
8
|
+
router = APIRouter(prefix="/function", tags=["execution"])
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@router.post("/run", response_model=Dict[str, Any])
|
|
12
|
+
def run_function_endpoint(req: RunRequest) -> Dict[str, Any]:
|
|
13
|
+
"""Execute a registered function.
|
|
14
|
+
|
|
15
|
+
Inputs that are object refs (``obj_*``) are automatically resolved
|
|
16
|
+
from the object store before calling the function. Heavy outputs
|
|
17
|
+
(DataFrames, arrays, large collections) are stored automatically and
|
|
18
|
+
returned as lightweight refs.
|
|
19
|
+
"""
|
|
20
|
+
try:
|
|
21
|
+
return run_function(req.function, req.inputs)
|
|
22
|
+
except ValueError as e:
|
|
23
|
+
raise HTTPException(status_code=404, detail=str(e))
|
|
24
|
+
except KeyError as e:
|
|
25
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
26
|
+
except TypeError as e:
|
|
27
|
+
raise HTTPException(status_code=422, detail=f"Argument error: {e}")
|
|
28
|
+
except Exception as e:
|
|
29
|
+
raise HTTPException(status_code=500, detail=f"Execution error: {e}")
|
|
30
|
+
|
|
31
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from fastapi import APIRouter
|
|
2
|
+
|
|
3
|
+
from vincta.registry.registry import list_functions
|
|
4
|
+
from vincta.schemas.models import FunctionListResponse, FunctionSchema
|
|
5
|
+
|
|
6
|
+
router = APIRouter(prefix="/functions", tags=["functions"])
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@router.get("", response_model=FunctionListResponse)
|
|
10
|
+
def get_functions():
|
|
11
|
+
"""Return all registered functions with their schemas and descriptions."""
|
|
12
|
+
entries = list_functions()
|
|
13
|
+
return FunctionListResponse(
|
|
14
|
+
functions=[
|
|
15
|
+
FunctionSchema(
|
|
16
|
+
name=e["name"],
|
|
17
|
+
module=e["module"],
|
|
18
|
+
description=e.get("description"),
|
|
19
|
+
inputs=e["inputs"],
|
|
20
|
+
outputs=e["outputs"],
|
|
21
|
+
)
|
|
22
|
+
for e in entries
|
|
23
|
+
]
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from fastapi import APIRouter, HTTPException
|
|
2
|
+
|
|
3
|
+
from vincta.object_store.store import (
|
|
4
|
+
delete_object,
|
|
5
|
+
get_memory_usage,
|
|
6
|
+
list_refs,
|
|
7
|
+
store_object,
|
|
8
|
+
summarize_object,
|
|
9
|
+
)
|
|
10
|
+
from vincta.schemas.models import (
|
|
11
|
+
DeleteResponse,
|
|
12
|
+
MemoryStats,
|
|
13
|
+
ObjectListResponse,
|
|
14
|
+
ObjectSummary,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
router = APIRouter(prefix="/object", tags=["objects"])
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@router.get("/list", response_model=ObjectListResponse)
|
|
21
|
+
def list_objects():
|
|
22
|
+
"""List all object refs currently in the store."""
|
|
23
|
+
return ObjectListResponse(refs=list_refs())
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@router.get("/memory", response_model=MemoryStats)
|
|
27
|
+
def memory_stats():
|
|
28
|
+
"""Return memory usage breakdown for all stored objects."""
|
|
29
|
+
usage = get_memory_usage()
|
|
30
|
+
return MemoryStats(total_bytes=usage["total_bytes"], objects=usage["objects"])
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@router.get("/{ref}", response_model=ObjectSummary)
|
|
34
|
+
def get_object_summary(ref: str):
|
|
35
|
+
"""Preview a stored object — returns metadata/head, never the full payload."""
|
|
36
|
+
summary = summarize_object(ref)
|
|
37
|
+
if summary is None:
|
|
38
|
+
raise HTTPException(status_code=404, detail=f"Object '{ref}' not found")
|
|
39
|
+
obj_type = summary.pop("type")
|
|
40
|
+
return ObjectSummary(ref=ref, type=obj_type, summary=summary)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@router.delete("/{ref}", response_model=DeleteResponse)
|
|
44
|
+
def delete_object_endpoint(ref: str):
|
|
45
|
+
"""Remove an object from the store and free its memory."""
|
|
46
|
+
if not delete_object(ref):
|
|
47
|
+
raise HTTPException(status_code=404, detail=f"Object '{ref}' not found")
|
|
48
|
+
return DeleteResponse(deleted=ref)
|
|
49
|
+
|
|
50
|
+
|
|
File without changes
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class FunctionSchema(BaseModel):
|
|
6
|
+
name: str
|
|
7
|
+
module: str
|
|
8
|
+
description: Optional[str] = None
|
|
9
|
+
inputs: Dict[str, str]
|
|
10
|
+
outputs: Dict[str, str]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FunctionListResponse(BaseModel):
|
|
14
|
+
functions: List[FunctionSchema]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RunRequest(BaseModel):
|
|
18
|
+
function: str
|
|
19
|
+
inputs: Dict[str, Any] = {}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ObjectSummary(BaseModel):
|
|
23
|
+
ref: str
|
|
24
|
+
type: str
|
|
25
|
+
summary: Dict[str, Any]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class MemoryStats(BaseModel):
|
|
29
|
+
total_bytes: int
|
|
30
|
+
objects: Dict[str, int]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ObjectListResponse(BaseModel):
|
|
34
|
+
refs: List[str]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class DeleteResponse(BaseModel):
|
|
38
|
+
deleted: str
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class HealthResponse(BaseModel):
|
|
42
|
+
status: str
|
|
43
|
+
registered_functions: int
|
|
44
|
+
stored_objects: int
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from fastapi import FastAPI
|
|
8
|
+
|
|
9
|
+
from vincta.routers import execution, functions, objects
|
|
10
|
+
from vincta.registry.registry import function_count
|
|
11
|
+
from vincta.object_store.store import object_count
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _discovery_config():
|
|
15
|
+
dirs_raw = os.environ.get("RELAY_DIRS", "")
|
|
16
|
+
paths_raw = os.environ.get("RELAY_SYS_PATHS", "")
|
|
17
|
+
exclude_raw = os.environ.get("RELAY_EXCLUDE", "test_*,*.test.py")
|
|
18
|
+
dirs = [Path(d) for d in dirs_raw.split(os.pathsep) if d]
|
|
19
|
+
sys_paths = [Path(p) for p in paths_raw.split(os.pathsep) if p]
|
|
20
|
+
exclude = [p.strip() for p in exclude_raw.split(",") if p.strip()]
|
|
21
|
+
return dirs, sys_paths, exclude
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@asynccontextmanager
|
|
25
|
+
async def lifespan(app: FastAPI):
|
|
26
|
+
from vincta.discovery.autodiscover import discover_and_import
|
|
27
|
+
|
|
28
|
+
dirs, sys_paths, exclude = _discovery_config()
|
|
29
|
+
results = discover_and_import(dirs, exclude, sys_paths)
|
|
30
|
+
print(f"[startup] Imported modules: {results['imported']}")
|
|
31
|
+
print(f"[startup] Auto-registered: {results['auto_registered']} functions")
|
|
32
|
+
if results["errors"]:
|
|
33
|
+
print(f"[startup] Import errors: {results['errors']}")
|
|
34
|
+
yield
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
app = FastAPI(
|
|
38
|
+
title="relay",
|
|
39
|
+
description=(
|
|
40
|
+
"Pure function orchestration engine — auto-discover and serve Python functions "
|
|
41
|
+
"over HTTP with a built-in RAM object store."
|
|
42
|
+
),
|
|
43
|
+
version="0.1.0",
|
|
44
|
+
lifespan=lifespan,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
app.include_router(functions.router)
|
|
48
|
+
app.include_router(execution.router)
|
|
49
|
+
app.include_router(objects.router)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@app.get("/health", tags=["meta"])
|
|
53
|
+
def health():
|
|
54
|
+
return {
|
|
55
|
+
"status": "ok",
|
|
56
|
+
"registered_functions": function_count(),
|
|
57
|
+
"stored_objects": object_count(),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vincta
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Auto-discover and serve Python functions over HTTP with a built-in RAM object store
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Keywords: fastapi,functions,orchestration,http,object-store,autodiscovery,api
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Framework :: FastAPI
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: fastapi>=0.111.0
|
|
18
|
+
Requires-Dist: uvicorn[standard]>=0.29.0
|
|
19
|
+
Requires-Dist: pydantic>=2.0.0
|
|
20
|
+
Provides-Extra: pandas
|
|
21
|
+
Requires-Dist: pandas>=2.0.0; extra == "pandas"
|
|
22
|
+
Provides-Extra: numpy
|
|
23
|
+
Requires-Dist: numpy>=1.24.0; extra == "numpy"
|
|
24
|
+
Provides-Extra: all
|
|
25
|
+
Requires-Dist: pandas>=2.0.0; extra == "all"
|
|
26
|
+
Requires-Dist: numpy>=1.24.0; extra == "all"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
vincta/__init__.py
|
|
3
|
+
vincta/cli.py
|
|
4
|
+
vincta/server.py
|
|
5
|
+
vincta.egg-info/PKG-INFO
|
|
6
|
+
vincta.egg-info/SOURCES.txt
|
|
7
|
+
vincta.egg-info/dependency_links.txt
|
|
8
|
+
vincta.egg-info/entry_points.txt
|
|
9
|
+
vincta.egg-info/requires.txt
|
|
10
|
+
vincta.egg-info/top_level.txt
|
|
11
|
+
vincta/discovery/__init__.py
|
|
12
|
+
vincta/discovery/autodiscover.py
|
|
13
|
+
vincta/execution/__init__.py
|
|
14
|
+
vincta/execution/engine.py
|
|
15
|
+
vincta/object_store/__init__.py
|
|
16
|
+
vincta/object_store/store.py
|
|
17
|
+
vincta/registry/__init__.py
|
|
18
|
+
vincta/registry/decorator.py
|
|
19
|
+
vincta/registry/registry.py
|
|
20
|
+
vincta/routers/__init__.py
|
|
21
|
+
vincta/routers/execution.py
|
|
22
|
+
vincta/routers/functions.py
|
|
23
|
+
vincta/routers/objects.py
|
|
24
|
+
vincta/schemas/__init__.py
|
|
25
|
+
vincta/schemas/models.py
|
|
26
|
+
vincta/services/__init__.py
|
|
27
|
+
vincta/services/execution_service.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
vincta
|