abstractgateway 0.1.0__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.
- abstractgateway/__init__.py +11 -0
- abstractgateway/app.py +55 -0
- abstractgateway/cli.py +30 -0
- abstractgateway/config.py +94 -0
- abstractgateway/hosts/__init__.py +6 -0
- abstractgateway/hosts/bundle_host.py +626 -0
- abstractgateway/hosts/visualflow_host.py +213 -0
- abstractgateway/routes/__init__.py +5 -0
- abstractgateway/routes/gateway.py +393 -0
- abstractgateway/runner.py +429 -0
- abstractgateway/security/__init__.py +5 -0
- abstractgateway/security/gateway_security.py +504 -0
- abstractgateway/service.py +134 -0
- abstractgateway/stores.py +34 -0
- abstractgateway-0.1.0.dist-info/METADATA +101 -0
- abstractgateway-0.1.0.dist-info/RECORD +18 -0
- abstractgateway-0.1.0.dist-info/WHEEL +4 -0
- abstractgateway-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""AbstractGateway.
|
|
2
|
+
|
|
3
|
+
AbstractGateway is a deployable Run Gateway host for AbstractRuntime:
|
|
4
|
+
- durable command inbox (start/resume/pause/cancel/emit_event)
|
|
5
|
+
- ledger replay + SSE streaming (replay-first)
|
|
6
|
+
- security middleware for network-safe deployments
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
|
|
11
|
+
|
abstractgateway/app.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""AbstractGateway FastAPI application."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
6
|
+
|
|
7
|
+
from fastapi import FastAPI
|
|
8
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
9
|
+
|
|
10
|
+
from .routes import gateway_router
|
|
11
|
+
from .security import GatewaySecurityMiddleware, load_gateway_auth_policy_from_env
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@asynccontextmanager
|
|
15
|
+
async def _lifespan(_app: FastAPI):
|
|
16
|
+
# Start the background worker that polls the durable command inbox and ticks runs.
|
|
17
|
+
from .service import start_gateway_runner, stop_gateway_runner
|
|
18
|
+
|
|
19
|
+
start_gateway_runner()
|
|
20
|
+
try:
|
|
21
|
+
yield
|
|
22
|
+
finally:
|
|
23
|
+
stop_gateway_runner()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
app = FastAPI(
|
|
27
|
+
title="AbstractGateway",
|
|
28
|
+
description="Durable Run Gateway for AbstractRuntime (commands + ledger replay/stream).",
|
|
29
|
+
version="0.1.0",
|
|
30
|
+
lifespan=_lifespan,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Gateway security (backlog 309).
|
|
34
|
+
app.add_middleware(GatewaySecurityMiddleware, policy=load_gateway_auth_policy_from_env())
|
|
35
|
+
|
|
36
|
+
# CORS for browser clients. In production, prefer configuring exact origins and terminating TLS at a reverse proxy.
|
|
37
|
+
#
|
|
38
|
+
# IMPORTANT: add after GatewaySecurityMiddleware so CORS headers are present even on early security rejections
|
|
39
|
+
# (otherwise browsers surface a generic "NetworkError").
|
|
40
|
+
app.add_middleware(
|
|
41
|
+
CORSMiddleware,
|
|
42
|
+
allow_origins=["*"],
|
|
43
|
+
allow_credentials=True,
|
|
44
|
+
allow_methods=["*"],
|
|
45
|
+
allow_headers=["*"],
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
app.include_router(gateway_router, prefix="/api")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@app.get("/api/health")
|
|
52
|
+
async def health_check():
|
|
53
|
+
return {"status": "healthy", "service": "abstractgateway"}
|
|
54
|
+
|
|
55
|
+
|
abstractgateway/cli.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main(argv: list[str] | None = None) -> None:
|
|
7
|
+
parser = argparse.ArgumentParser(prog="abstractgateway", description="AbstractGateway (Run Gateway host)")
|
|
8
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
9
|
+
|
|
10
|
+
serve = sub.add_parser("serve", help="Run the AbstractGateway HTTP/SSE server")
|
|
11
|
+
serve.add_argument("--host", default="127.0.0.1", help="Bind host (default: 127.0.0.1)")
|
|
12
|
+
serve.add_argument("--port", type=int, default=8080, help="Bind port (default: 8080)")
|
|
13
|
+
serve.add_argument("--reload", action="store_true", help="Enable auto-reload (dev only)")
|
|
14
|
+
|
|
15
|
+
args = parser.parse_args(argv)
|
|
16
|
+
|
|
17
|
+
if args.cmd == "serve":
|
|
18
|
+
import uvicorn
|
|
19
|
+
|
|
20
|
+
uvicorn.run(
|
|
21
|
+
"abstractgateway.app:app",
|
|
22
|
+
host=str(args.host),
|
|
23
|
+
port=int(args.port),
|
|
24
|
+
reload=bool(args.reload),
|
|
25
|
+
)
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
raise SystemExit(2)
|
|
29
|
+
|
|
30
|
+
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _as_bool(raw: Any, default: bool) -> bool:
|
|
10
|
+
if raw is None:
|
|
11
|
+
return default
|
|
12
|
+
if isinstance(raw, bool):
|
|
13
|
+
return raw
|
|
14
|
+
s = str(raw).strip().lower()
|
|
15
|
+
if not s:
|
|
16
|
+
return default
|
|
17
|
+
if s in {"1", "true", "yes", "on"}:
|
|
18
|
+
return True
|
|
19
|
+
if s in {"0", "false", "no", "off"}:
|
|
20
|
+
return False
|
|
21
|
+
return default
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _as_int(raw: Optional[str], default: int) -> int:
|
|
25
|
+
if raw is None or not str(raw).strip():
|
|
26
|
+
return default
|
|
27
|
+
try:
|
|
28
|
+
return int(str(raw).strip())
|
|
29
|
+
except Exception:
|
|
30
|
+
return default
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _as_float(raw: Optional[str], default: float) -> float:
|
|
34
|
+
if raw is None or not str(raw).strip():
|
|
35
|
+
return default
|
|
36
|
+
try:
|
|
37
|
+
return float(str(raw).strip())
|
|
38
|
+
except Exception:
|
|
39
|
+
return default
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _env(name: str, fallback: Optional[str] = None) -> Optional[str]:
|
|
43
|
+
v = os.getenv(name)
|
|
44
|
+
if v is not None and str(v).strip():
|
|
45
|
+
return v
|
|
46
|
+
if fallback:
|
|
47
|
+
v2 = os.getenv(fallback)
|
|
48
|
+
if v2 is not None and str(v2).strip():
|
|
49
|
+
return v2
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class GatewayHostConfig:
|
|
55
|
+
"""Process-level configuration for the AbstractGateway host."""
|
|
56
|
+
|
|
57
|
+
data_dir: Path
|
|
58
|
+
flows_dir: Path
|
|
59
|
+
|
|
60
|
+
runner_enabled: bool = True
|
|
61
|
+
poll_interval_s: float = 0.25
|
|
62
|
+
command_batch_limit: int = 200
|
|
63
|
+
tick_max_steps: int = 100
|
|
64
|
+
tick_workers: int = 2
|
|
65
|
+
run_scan_limit: int = 200
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def from_env() -> "GatewayHostConfig":
|
|
69
|
+
# NOTE: We intentionally use ABSTRACTGATEWAY_* as the canonical namespace.
|
|
70
|
+
# For a transition period, we accept legacy ABSTRACTFLOW_* names as fallbacks.
|
|
71
|
+
data_dir_raw = _env("ABSTRACTGATEWAY_DATA_DIR", "ABSTRACTFLOW_RUNTIME_DIR") or "./runtime"
|
|
72
|
+
flows_dir_raw = _env("ABSTRACTGATEWAY_FLOWS_DIR", "ABSTRACTFLOW_FLOWS_DIR") or "./flows"
|
|
73
|
+
|
|
74
|
+
enabled_raw = _env("ABSTRACTGATEWAY_RUNNER", "ABSTRACTFLOW_GATEWAY_RUNNER") or "1"
|
|
75
|
+
runner_enabled = _as_bool(enabled_raw, True)
|
|
76
|
+
|
|
77
|
+
poll_s = _as_float(_env("ABSTRACTGATEWAY_POLL_S", "ABSTRACTFLOW_GATEWAY_POLL_S"), 0.25)
|
|
78
|
+
tick_workers = _as_int(_env("ABSTRACTGATEWAY_TICK_WORKERS", "ABSTRACTFLOW_GATEWAY_TICK_WORKERS"), 2)
|
|
79
|
+
tick_steps = _as_int(_env("ABSTRACTGATEWAY_TICK_MAX_STEPS", "ABSTRACTFLOW_GATEWAY_TICK_MAX_STEPS"), 100)
|
|
80
|
+
batch = _as_int(_env("ABSTRACTGATEWAY_COMMAND_BATCH_LIMIT", "ABSTRACTFLOW_GATEWAY_COMMAND_BATCH_LIMIT"), 200)
|
|
81
|
+
scan = _as_int(_env("ABSTRACTGATEWAY_RUN_SCAN_LIMIT", "ABSTRACTFLOW_GATEWAY_RUN_SCAN_LIMIT"), 200)
|
|
82
|
+
|
|
83
|
+
return GatewayHostConfig(
|
|
84
|
+
data_dir=Path(data_dir_raw).expanduser().resolve(),
|
|
85
|
+
flows_dir=Path(flows_dir_raw).expanduser().resolve(),
|
|
86
|
+
runner_enabled=bool(runner_enabled),
|
|
87
|
+
poll_interval_s=float(poll_s),
|
|
88
|
+
command_batch_limit=max(1, int(batch)),
|
|
89
|
+
tick_max_steps=max(1, int(tick_steps)),
|
|
90
|
+
tick_workers=max(1, int(tick_workers)),
|
|
91
|
+
run_scan_limit=max(1, int(scan)),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|