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 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
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
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,9 @@
1
+ from typing import Any, Dict
2
+
3
+ from vincta.execution.engine import execute
4
+
5
+
6
+ def run_function(function_name: str, inputs: Dict[str, Any]) -> Dict[str, Any]:
7
+ return execute(function_name, inputs)
8
+
9
+
@@ -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,2 @@
1
+ [console_scripts]
2
+ vincta = vincta.cli:main
@@ -0,0 +1,13 @@
1
+ fastapi>=0.111.0
2
+ uvicorn[standard]>=0.29.0
3
+ pydantic>=2.0.0
4
+
5
+ [all]
6
+ pandas>=2.0.0
7
+ numpy>=1.24.0
8
+
9
+ [numpy]
10
+ numpy>=1.24.0
11
+
12
+ [pandas]
13
+ pandas>=2.0.0
@@ -0,0 +1 @@
1
+ vincta