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.
@@ -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
+
@@ -0,0 +1,6 @@
1
+ from .visualflow_host import VisualFlowGatewayHost
2
+ from .bundle_host import WorkflowBundleGatewayHost
3
+
4
+ __all__ = ["VisualFlowGatewayHost", "WorkflowBundleGatewayHost"]
5
+
6
+