kalibr 1.0.22__tar.gz → 1.0.24__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 (33) hide show
  1. {kalibr-1.0.22/kalibr.egg-info → kalibr-1.0.24}/PKG-INFO +6 -32
  2. kalibr-1.0.24/kalibr/__init__.py +1 -0
  3. kalibr-1.0.24/kalibr/__main__.py +206 -0
  4. kalibr-1.0.24/kalibr/deployment.py +41 -0
  5. {kalibr-1.0.22 → kalibr-1.0.24}/kalibr/kalibr_app.py +32 -11
  6. kalibr-1.0.24/kalibr/packager.py +43 -0
  7. kalibr-1.0.24/kalibr/runtime_router.py +138 -0
  8. {kalibr-1.0.22 → kalibr-1.0.24}/kalibr/schema_generators.py +21 -74
  9. kalibr-1.0.24/kalibr/validator.py +70 -0
  10. {kalibr-1.0.22 → kalibr-1.0.24/kalibr.egg-info}/PKG-INFO +6 -32
  11. {kalibr-1.0.22 → kalibr-1.0.24}/kalibr.egg-info/SOURCES.txt +3 -0
  12. kalibr-1.0.24/kalibr.egg-info/entry_points.txt +2 -0
  13. kalibr-1.0.24/kalibr.egg-info/requires.txt +7 -0
  14. kalibr-1.0.24/pyproject.toml +22 -0
  15. {kalibr-1.0.22 → kalibr-1.0.24}/setup.py +1 -1
  16. kalibr-1.0.22/kalibr/__init__.py +0 -7
  17. kalibr-1.0.22/kalibr/__main__.py +0 -677
  18. kalibr-1.0.22/kalibr/deployment.py +0 -26
  19. kalibr-1.0.22/kalibr.egg-info/entry_points.txt +0 -2
  20. kalibr-1.0.22/kalibr.egg-info/requires.txt +0 -19
  21. kalibr-1.0.22/pyproject.toml +0 -76
  22. {kalibr-1.0.22 → kalibr-1.0.24}/LICENSE +0 -0
  23. {kalibr-1.0.22 → kalibr-1.0.24}/MANIFEST.in +0 -0
  24. {kalibr-1.0.22 → kalibr-1.0.24}/README.md +0 -0
  25. {kalibr-1.0.22 → kalibr-1.0.24}/examples/README.md +0 -0
  26. {kalibr-1.0.22 → kalibr-1.0.24}/examples/__init__.py +0 -0
  27. {kalibr-1.0.22 → kalibr-1.0.24}/examples/basic_kalibr_example.py +0 -0
  28. {kalibr-1.0.22 → kalibr-1.0.24}/examples/enhanced_kalibr_example.py +0 -0
  29. {kalibr-1.0.22 → kalibr-1.0.24}/kalibr/kalibr.py +0 -0
  30. {kalibr-1.0.22 → kalibr-1.0.24}/kalibr/types.py +0 -0
  31. {kalibr-1.0.22 → kalibr-1.0.24}/kalibr.egg-info/dependency_links.txt +0 -0
  32. {kalibr-1.0.22 → kalibr-1.0.24}/kalibr.egg-info/top_level.txt +0 -0
  33. {kalibr-1.0.22 → kalibr-1.0.24}/setup.cfg +0 -0
@@ -1,48 +1,22 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kalibr
3
- Version: 1.0.22
4
- Summary: Multi-Model AI Integration Framework
3
+ Version: 1.0.24
4
+ Summary: Multi-Model MCP SDK — deploy to GPT, Claude, Gemini, Copilot from one codebase.
5
5
  Home-page: https://github.com/devonakelley/kalibr-sdk
6
6
  Author: Kalibr Team
7
- Author-email: Kalibr Team <team@kalibr.dev>
8
- License: MIT
9
- Project-URL: Homepage, https://kalibr.dev
10
- Project-URL: Documentation, https://kalibr.dev/docs
11
- Project-URL: Repository, https://github.com/devonakelley/kalibr-sdk
12
- Project-URL: Bug Reports, https://github.com/devonakelley/kalibr-sdk/issues
13
- Keywords: ai,api,framework,gpt,claude,gemini,copilot,multi-model,sdk
14
- Classifier: Development Status :: 4 - Beta
15
- Classifier: Intended Audience :: Developers
16
- Classifier: License :: OSI Approved :: MIT License
17
- Classifier: Operating System :: OS Independent
18
- Classifier: Programming Language :: Python :: 3
19
- Classifier: Programming Language :: Python :: 3.11
20
- Classifier: Programming Language :: Python :: 3.12
21
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
- Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
23
- Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
7
+ Author-email: team@kalibr.dev
24
8
  Requires-Python: >=3.11
25
9
  Description-Content-Type: text/markdown
26
10
  License-File: LICENSE
27
11
  Requires-Dist: fastapi>=0.110.1
28
12
  Requires-Dist: uvicorn>=0.25.0
29
- Requires-Dist: pydantic>=2.6.4
30
13
  Requires-Dist: typer>=0.9.0
14
+ Requires-Dist: pydantic>=2.6.4
31
15
  Requires-Dist: requests>=2.31.0
32
- Requires-Dist: python-jose[cryptography]>=3.3.0
33
- Requires-Dist: passlib[bcrypt]>=1.7.4
34
- Requires-Dist: python-multipart>=0.0.9
35
- Requires-Dist: motor>=3.3.1
36
- Requires-Dist: pymongo>=4.5.0
37
- Requires-Dist: boto3>=1.34.129
38
16
  Requires-Dist: aiofiles>=23.2.1
39
- Provides-Extra: dev
40
- Requires-Dist: pytest>=8.0.0; extra == "dev"
41
- Requires-Dist: black>=24.1.1; extra == "dev"
42
- Requires-Dist: isort>=5.13.2; extra == "dev"
43
- Requires-Dist: flake8>=7.0.0; extra == "dev"
44
- Requires-Dist: mypy>=1.8.0; extra == "dev"
17
+ Requires-Dist: jsonschema>=4.21.1
45
18
  Dynamic: author
19
+ Dynamic: author-email
46
20
  Dynamic: home-page
47
21
  Dynamic: license-file
48
22
  Dynamic: requires-python
@@ -0,0 +1 @@
1
+ __version__ = "1.0.24"
@@ -0,0 +1,206 @@
1
+ import typer
2
+ import uvicorn
3
+ import sys
4
+ import importlib.util
5
+ from pathlib import Path
6
+ import os
7
+ import requests
8
+ import json
9
+
10
+ app = typer.Typer()
11
+
12
+ def _load_user_module(file: str):
13
+ file_path = Path(file).resolve()
14
+ if not file_path.exists():
15
+ print(f"❌ Error: {file} not found")
16
+ raise typer.Exit(1)
17
+ spec = importlib.util.spec_from_file_location("user_app", file_path)
18
+ if not spec or not spec.loader:
19
+ print(f"❌ Error: Could not load {file}")
20
+ raise typer.Exit(1)
21
+ module = importlib.util.module_from_spec(spec)
22
+ sys.modules["user_app"] = module
23
+ try:
24
+ spec.loader.exec_module(module)
25
+ except Exception as e:
26
+ print(f"❌ Error loading {file}: {e}")
27
+ raise typer.Exit(1)
28
+ return module
29
+
30
+ @app.command()
31
+ def serve(
32
+ file: str = typer.Argument("kalibr_app.py", help="Python file with Kalibr app"),
33
+ host: str = typer.Option("0.0.0.0", "--host", "-h"),
34
+ port: int = typer.Option(8000, "--port", "-p"),
35
+ base_url: str = typer.Option("http://localhost:8000", "--base-url", "-b"),
36
+ ):
37
+ """Serve a Kalibr-powered API locally."""
38
+ module = _load_user_module(file)
39
+
40
+ # Import Kalibr classes
41
+ from kalibr import Kalibr, KalibrApp
42
+ kalibr_instance = None
43
+ for attr_name in dir(module):
44
+ attr = getattr(module, attr_name)
45
+ if isinstance(attr, Kalibr) or (KalibrApp and isinstance(attr, KalibrApp)):
46
+ kalibr_instance = attr
47
+ kalibr_instance.base_url = base_url
48
+ break
49
+ if not kalibr_instance:
50
+ print(f"❌ Error: No Kalibr/KalibrApp instance found in {file}")
51
+ raise typer.Exit(1)
52
+
53
+ if hasattr(kalibr_instance, 'get_app'):
54
+ fastapi_app = kalibr_instance.get_app()
55
+ elif hasattr(kalibr_instance, 'app'):
56
+ fastapi_app = kalibr_instance.app
57
+ else:
58
+ print(f"❌ Error: Kalibr instance has no get_app() method or app attribute")
59
+ raise typer.Exit(1)
60
+
61
+ is_enhanced = 'KalibrApp' in str(type(kalibr_instance))
62
+ print(f"🚀 Starting {'Enhanced ' if is_enhanced else ''}Kalibr server from {file}")
63
+ print(f"📍 GPT (OpenAPI): {base_url}/openapi.json")
64
+ print(f"📍 Claude (MCP): {base_url}/mcp.json")
65
+ if is_enhanced:
66
+ print(f"📍 Gemini: {base_url}/schemas/gemini")
67
+ print(f"📍 Copilot: {base_url}/schemas/copilot")
68
+ print(f"📍 Supported Models: {base_url}/models/supported")
69
+ print(f"📍 Health Check: {base_url}/health")
70
+ print(f"📍 Swagger UI: {base_url}/docs")
71
+ print(f"🔌 Actions registered: {list(kalibr_instance.actions.keys())}")
72
+
73
+ uvicorn.run(fastapi_app, host=host, port=port)
74
+
75
+ @app.command()
76
+ def package(
77
+ app_dir: str = typer.Option(".", "--app-dir", "-d", help="Directory containing your app"),
78
+ output: str = typer.Option("kalibr_bundle.zip", "--output", "-o", help="Bundle file"),
79
+ models: str = typer.Option("mcp,gpt-actions,gemini,copilot", "--models", "-m", help="Comma-separated models supported")
80
+ ):
81
+ """Create a deployable MCP bundle (code + schemas + metadata)."""
82
+ from kalibr.packager import package_app
83
+ try:
84
+ models_supported = [x.strip() for x in models.split(",") if x.strip()]
85
+ # Version best-effort
86
+ try:
87
+ from kalibr import __version__ as kalibr_version
88
+ except Exception:
89
+ kalibr_version = "unknown"
90
+ bundle_path = package_app(app_dir=app_dir, output=output, models_supported=models_supported, kalibr_version=kalibr_version)
91
+ print(f"📦 Bundle created: {bundle_path}")
92
+ except Exception as e:
93
+ print(f"❌ Packaging error: {e}")
94
+ raise typer.Exit(1)
95
+
96
+ @app.command()
97
+ def deploy(
98
+ file: str = typer.Argument(..., help="Python file to serve/deploy (e.g., kalibr_app.py)"),
99
+ name: str = typer.Option("", "--name", "-n", help="App name (defaults to filename)"),
100
+ runtime: str = typer.Option("local", "--runtime", "-r", help="Runtime: local|fly|render"),
101
+ host: str = typer.Option("0.0.0.0", "--host"),
102
+ port: int = typer.Option(8000, "--port"),
103
+ base_url: str = typer.Option("http://localhost:8000", "--base-url"),
104
+ ):
105
+ """Deploy via runtime router (no hosting burden on Kalibr)."""
106
+ from kalibr.runtime_router import deploy as router_deploy
107
+ file_path = Path(file)
108
+ if not file_path.exists():
109
+ print(f"❌ Error: {file} not found")
110
+ raise typer.Exit(1)
111
+ if not name:
112
+ name = file_path.stem.replace('_', '-').replace('.', '-')
113
+ try:
114
+ result = router_deploy(runtime=runtime, app_name=name, app_file=str(file_path), host=host, port=port, base_url=base_url)
115
+ if result.get("status") in ("success", "started"):
116
+ print("🎉 Deploy OK")
117
+ eps = result.get("endpoints", {})
118
+ if eps:
119
+ print("📍 Endpoints:")
120
+ for k, v in eps.items():
121
+ print(f" - {k}: {v}")
122
+ else:
123
+ print("⚠️ Deploy finished with unknown status:", result)
124
+ except Exception as e:
125
+ print(f"❌ Deployment error: {e}")
126
+ raise typer.Exit(1)
127
+
128
+ @app.command()
129
+ def validate(
130
+ url: str = typer.Option("http://localhost:8000/mcp.json", "--mcp-url", help="URL to MCP manifest"),
131
+ ):
132
+ """Validate MCP manifest against minimal JSON schema & version hint."""
133
+ from kalibr.validator import validate_mcp_manifest
134
+ try:
135
+ resp = requests.get(url, timeout=10)
136
+ resp.raise_for_status()
137
+ manifest = resp.json()
138
+ validate_mcp_manifest(manifest)
139
+ print("✅ MCP manifest looks structurally valid.")
140
+ except Exception as e:
141
+ print(f"❌ Validation failed: {e}")
142
+ raise typer.Exit(1)
143
+
144
+ @app.command()
145
+ def update_schemas():
146
+ """Stub: instruct users to upgrade SDK and regenerate manifests."""
147
+ from kalibr.validator import update_schemas as _upd
148
+ _upd()
149
+
150
+ @app.command()
151
+ def status(
152
+ app_url: str = typer.Argument(..., help="URL of deployed Kalibr app"),
153
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed information")
154
+ ):
155
+ """Check status of a deployed Kalibr app."""
156
+ try:
157
+ health_url = f"{app_url.rstrip('/')}/health"
158
+ response = requests.get(health_url, timeout=10)
159
+ if response.status_code == 200:
160
+ health_data = response.json()
161
+ print(f"✅ App is healthy at {app_url}")
162
+ print(f" Version: {health_data.get('version', 'unknown')}")
163
+ print(f" Features: {health_data.get('features', {})}")
164
+ if verbose:
165
+ schemas = ["mcp.json", "openapi.json", "schemas/gemini", "schemas/copilot"]
166
+ print(f"\n📊 Available AI model schemas:")
167
+ for schema in schemas:
168
+ schema_url = f"{app_url.rstrip('/')}/{schema}"
169
+ try:
170
+ schema_response = requests.get(schema_url, timeout=5)
171
+ status_emoji = "✅" if schema_response.status_code == 200 else "❌"
172
+ model_name = schema.replace(".json", "").replace("schemas/", "")
173
+ print(f" {status_emoji} {model_name.upper()}: {schema_url}")
174
+ except:
175
+ print(f" ❌ {schema}: Connection failed")
176
+ else:
177
+ print(f"❌ App at {app_url} is not responding (HTTP {response.status_code})")
178
+ raise typer.Exit(1)
179
+ except requests.exceptions.RequestException as e:
180
+ print(f"❌ Cannot connect to {app_url}: {e}")
181
+ raise typer.Exit(1)
182
+
183
+ @app.command()
184
+ def version():
185
+ try:
186
+ from kalibr import __version__
187
+ except (ImportError, AttributeError):
188
+ __version__ = "unknown"
189
+ print(f"Kalibr SDK version: {__version__}")
190
+ print("Enhanced multi-model AI integration framework")
191
+ print("Supports: GPT Actions, Claude MCP, Gemini Extensions, Copilot Plugins")
192
+ print("GitHub: https://github.com/devonakelley/kalibr-sdk")
193
+
194
+ def main():
195
+ config_file = Path.home() / ".kalibr" / "config.json"
196
+ if config_file.exists():
197
+ try:
198
+ config = json.loads(config_file.read_text())
199
+ if "api_token" in config and not os.environ.get("KALIBR_TOKEN"):
200
+ os.environ["KALIBR_TOKEN"] = config["api_token"]
201
+ except:
202
+ pass
203
+ app()
204
+
205
+ if __name__ == "__main__":
206
+ main()
@@ -0,0 +1,41 @@
1
+ """
2
+ Kalibr Deployment
3
+ -----------------
4
+ Thin wrapper that forwards to the runtime router.
5
+ Keeps a simple API surface for backwards-compat commands.
6
+ """
7
+
8
+ from __future__ import annotations
9
+ from dataclasses import dataclass, field
10
+ from typing import Dict, Any
11
+ from kalibr.runtime_router import deploy as router_deploy
12
+
13
+ @dataclass
14
+ class DeploymentConfig:
15
+ app_name: str
16
+ memory_mb: int = 512
17
+ timeout_seconds: int = 30
18
+ environment_vars: Dict[str, str] = field(default_factory=dict)
19
+
20
+ def deploy_app(file_path: str, config: DeploymentConfig, platform: str = "local") -> Dict[str, Any]:
21
+ # Map older "platform" to runtime names used by router
22
+ runtime = {
23
+ "local": "local",
24
+ "fly": "fly",
25
+ "aws-lambda": "local", # not supported; punt to local
26
+ "render": "render",
27
+ }.get(platform, platform)
28
+
29
+ result = router_deploy(runtime=runtime, app_name=config.app_name, app_file=file_path)
30
+ if result.get("status") in ("success", "started"):
31
+ eps = result.get("endpoints", {})
32
+ return {
33
+ "status": "success",
34
+ "endpoints": {
35
+ "root": eps.get("root", ""),
36
+ "mcp": eps.get("mcp", ""),
37
+ "openapi": eps.get("openapi", ""),
38
+ "health": eps.get("health", ""),
39
+ }
40
+ }
41
+ return {"status": "error", "error": "unknown deploy outcome", "raw": result}
@@ -9,7 +9,6 @@ import os
9
9
 
10
10
  from kalibr.types import FileUpload, Session, WorkflowState
11
11
 
12
-
13
12
  class KalibrApp:
14
13
  """
15
14
  Enhanced app-level Kalibr framework with advanced capabilities:
@@ -20,7 +19,7 @@ class KalibrApp:
20
19
  - Multi-model schema generation
21
20
  """
22
21
 
23
- def __init__(self, title="Kalibr Enhanced API", version="2.0.0", base_url: Optional[str] = None):
22
+ def __init__(self, title="Kalibr Enhanced API", version="2.0.0", base_url: Optional[str] = None, models: Optional[List[str]] = None):
24
23
  """
25
24
  Initialize the Kalibr enhanced app.
26
25
  Automatically determines correct base URL for deployed environments.
@@ -30,6 +29,9 @@ class KalibrApp:
30
29
  2. Env var `KALIBR_BASE_URL`
31
30
  3. Env var `FLY_APP_NAME` -> https://<fly_app_name>.fly.dev
32
31
  4. Default localhost for dev
32
+
33
+ `models`: optional list like ["mcp"] or ["mcp","gpt-actions","gemini","copilot"]
34
+ controls which schema endpoints are advertised.
33
35
  """
34
36
  self.app = FastAPI(title=title, version=version)
35
37
 
@@ -42,6 +44,8 @@ class KalibrApp:
42
44
  else:
43
45
  self.base_url = "http://localhost:8000"
44
46
 
47
+ self.models_supported = models or ["mcp", "gpt-actions", "gemini", "copilot"]
48
+
45
49
  # Storage for different action types
46
50
  self.actions: Dict[str, Any] = {}
47
51
  self.file_handlers: Dict[str, Any] = {}
@@ -221,10 +225,10 @@ class KalibrApp:
221
225
  import json
222
226
  if inspect.isasyncgen(result):
223
227
  async for item in result:
224
- yield json.dumps(item) + "\n"
228
+ yield json.dumps(item) + "\\n"
225
229
  elif inspect.isgenerator(result):
226
230
  for item in result:
227
- yield json.dumps(item) + "\n"
231
+ yield json.dumps(item) + "\\n"
228
232
 
229
233
  return FastAPIStreamingResponse(generate(), media_type="application/x-ndjson")
230
234
  except Exception as e:
@@ -255,17 +259,25 @@ class KalibrApp:
255
259
 
256
260
  @self.app.get("/")
257
261
  def root():
262
+ schemas = {}
263
+ if "gpt-actions" in self.models_supported:
264
+ schemas["gpt_actions"] = f"{self.base_url}/gpt-actions.json"
265
+ schemas["claude_mcp"] = f"{self.base_url}/mcp.json" # MCP is the default lingua franca
266
+ if "gemini" in self.models_supported:
267
+ schemas["gemini"] = f"{self.base_url}/schemas/gemini"
268
+ if "copilot" in self.models_supported:
269
+ schemas["copilot"] = f"{self.base_url}/schemas/copilot"
270
+
258
271
  return {
259
272
  "message": "Kalibr Enhanced API is running",
260
273
  "actions": list(self.actions.keys()),
261
- "schemas": {
262
- "gpt_actions": f"{self.base_url}/gpt-actions.json",
263
- "claude_mcp": f"{self.base_url}/mcp.json",
264
- "gemini": f"{self.base_url}/schemas/gemini",
265
- "copilot": f"{self.base_url}/schemas/copilot",
266
- },
274
+ "schemas": schemas,
267
275
  }
268
276
 
277
+ @self.app.get("/models/supported")
278
+ def supported_models():
279
+ return {"models": self.models_supported}
280
+
269
281
  @self.app.get("/gpt-actions.json")
270
282
  def gpt_actions_schema():
271
283
  all_actions = {**self.actions, **self.file_handlers, **self.session_actions}
@@ -288,7 +300,16 @@ class KalibrApp:
288
300
 
289
301
  @self.app.get("/health")
290
302
  def health_check():
291
- return {"status": "healthy", "service": "Kalibr Enhanced API"}
303
+ return {
304
+ "status": "healthy",
305
+ "service": "Kalibr Enhanced API",
306
+ "version": self.app.version,
307
+ "features": {
308
+ "file_handlers": bool(self.file_handlers),
309
+ "sessions": True,
310
+ "streams": bool(self.stream_actions),
311
+ },
312
+ }
292
313
 
293
314
  # -------------------------------------------------------------------------
294
315
  # Helpers
@@ -0,0 +1,43 @@
1
+ """
2
+ Packager
3
+ --------
4
+ Create a deployable MCP bundle (code + manifests + metadata).
5
+ This does not host anything; it prepares artifacts for any runtime.
6
+ """
7
+
8
+ from __future__ import annotations
9
+ from pathlib import Path
10
+ import shutil
11
+ import json
12
+ import tempfile
13
+ from typing import Dict, Any, Optional
14
+
15
+ DEFAULT_BUNDLE = "kalibr_bundle.zip"
16
+
17
+ def package_app(app_dir: str = ".", output: str = DEFAULT_BUNDLE, models_supported: Optional[list] = None, kalibr_version: str = "unknown") -> str:
18
+ app_dir = Path(app_dir).resolve()
19
+ out_path = Path(output).resolve()
20
+
21
+ # Assemble temp dir with metadata
22
+ with tempfile.TemporaryDirectory() as tmp:
23
+ tmpdir = Path(tmp)
24
+ # Copy source tree
25
+ for item in app_dir.iterdir():
26
+ if item.name == out_path.name:
27
+ continue
28
+ dest = tmpdir / item.name
29
+ if item.is_dir():
30
+ shutil.copytree(item, dest)
31
+ else:
32
+ shutil.copy2(item, dest)
33
+
34
+ # Write bundle metadata
35
+ (tmpdir / "kalibr_manifest.json").write_text(json.dumps({
36
+ "kalibr_version": kalibr_version,
37
+ "models_supported": models_supported or ["mcp", "gpt-actions", "gemini", "copilot"],
38
+ }, indent=2))
39
+
40
+ # Zip
41
+ shutil.make_archive(out_path.with_suffix(""), "zip", tmpdir)
42
+
43
+ return str(out_path)
@@ -0,0 +1,138 @@
1
+ """
2
+ Runtime Router
3
+ --------------
4
+ Abstraction over deployment targets without hosting them ourselves.
5
+ Generates minimal configs and invokes the target's CLI/API where possible.
6
+
7
+ Supported:
8
+ - local (uvicorn)
9
+ - fly (fly.io) -> generates fly.toml and basic Dockerfile
10
+ - render -> generates render.yaml
11
+
12
+ Note: We do not ship vendor SDKs. We shell out to their CLIs if present.
13
+ """
14
+
15
+ from __future__ import annotations
16
+ from pathlib import Path
17
+ import subprocess
18
+ import shutil
19
+ import os
20
+ import json
21
+ from typing import Dict, Any, Optional, Tuple
22
+
23
+ HERE = Path(__file__).parent
24
+
25
+ def which(cmd: str) -> Optional[str]:
26
+ return shutil.which(cmd)
27
+
28
+ def ensure_file(path: Path, content: str):
29
+ path.parent.mkdir(parents=True, exist_ok=True)
30
+ if not path.exists():
31
+ path.write_text(content)
32
+
33
+ def generate_fly_files(app_name: str) -> Tuple[Path, Path]:
34
+ fly_toml = Path("fly.toml")
35
+ dockerfile = Path("Dockerfile")
36
+ ensure_file(fly_toml, f"""# fly.toml
37
+ app = "{app_name}"
38
+ primary_region = "iad"
39
+
40
+ [build]
41
+ dockerfile = "Dockerfile"
42
+
43
+ [http_service]
44
+ internal_port = 8000
45
+ force_https = true
46
+ auto_stop_machines = "off"
47
+ auto_start_machines = true
48
+ min_machines_running = 1
49
+ """)
50
+ ensure_file(dockerfile, """# Dockerfile
51
+ FROM python:3.11-slim
52
+ WORKDIR /app
53
+ COPY . /app
54
+ RUN pip install --no-cache-dir -U pip && \
55
+ pip install --no-cache-dir fastapi uvicorn typer pydantic requests
56
+ EXPOSE 8000
57
+ CMD ["python", "-m", "kalibr", "serve", "kalibr_app.py", "--host", "0.0.0.0", "--port", "8000", "--base-url", "http://0.0.0.0:8000"]
58
+ """)
59
+ return fly_toml, dockerfile
60
+
61
+ def generate_render_file(service_name: str) -> Path:
62
+ render_yaml = Path("render.yaml")
63
+ ensure_file(render_yaml, f"""# render.yaml
64
+ services:
65
+ - type: web
66
+ name: {service_name}
67
+ env: docker
68
+ plan: free
69
+ dockerfilePath: ./Dockerfile
70
+ autoDeploy: true
71
+ """)
72
+ return render_yaml
73
+
74
+ def deploy_local(app_file: str, host: str = "0.0.0.0", port: int = 8000, base_url: str = "http://localhost:8000") -> Dict[str, Any]:
75
+ # Run uvicorn inline (non-blocking not handled here - CLI uses this to print guidance)
76
+ cmd = ["python", "-m", "kalibr", "serve", app_file, "--host", host, "--port", str(port), "--base-url", base_url]
77
+ print("▶︎", " ".join(cmd))
78
+ subprocess.run(cmd, check=False)
79
+ return {
80
+ "status": "started",
81
+ "endpoints": {
82
+ "root": f"{base_url}/",
83
+ "mcp": f"{base_url}/mcp.json",
84
+ "openapi": f"{base_url}/openapi.json",
85
+ "health": f"{base_url}/health"
86
+ }
87
+ }
88
+
89
+ def deploy_fly(app_name: str) -> Dict[str, Any]:
90
+ if not which("flyctl"):
91
+ raise RuntimeError("flyctl is not installed. See https://fly.io/docs/flyctl/install/")
92
+ # Ensure files exist
93
+ generate_fly_files(app_name)
94
+ # Launch or deploy
95
+ print("▶︎ flyctl apps list")
96
+ subprocess.run(["flyctl", "apps", "list"], check=False)
97
+ print(f"▶︎ flyctl deploy --app {app_name}")
98
+ subprocess.run(["flyctl", "deploy", "--app", app_name], check=False)
99
+ url = f"https://{app_name}.fly.dev"
100
+ return {
101
+ "status": "success",
102
+ "endpoints": {
103
+ "root": f"{url}/",
104
+ "mcp": f"{url}/mcp.json",
105
+ "openapi": f"{url}/openapi.json",
106
+ "health": f"{url}/health"
107
+ }
108
+ }
109
+
110
+ def deploy_render(service_name: str) -> Dict[str, Any]:
111
+ # We just generate render.yaml and Dockerfile. User connects repo in Render UI.
112
+ generate_render_file(service_name)
113
+ ensure_file(Path("Dockerfile"), """# Dockerfile for Render
114
+ FROM python:3.11-slim
115
+ WORKDIR /app
116
+ COPY . /app
117
+ RUN pip install --no-cache-dir -U pip && \
118
+ pip install --no-cache-dir fastapi uvicorn typer pydantic requests
119
+ EXPOSE 8000
120
+ CMD ["python", "-m", "kalibr", "serve", "kalibr_app.py", "--host", "0.0.0.0", "--port", "8000", "--base-url", "https://$RENDER_EXTERNAL_URL"]
121
+ """)
122
+ print("📄 Generated render.yaml and Dockerfile. Connect your repo in Render.com and auto-deploy.")
123
+ return {
124
+ "status": "success",
125
+ "endpoints": {},
126
+ "note": "Connect this repository to Render; it will build from render.yaml."
127
+ }
128
+
129
+ def deploy(runtime: str, app_name: str, app_file: str, host: str = "0.0.0.0", port: int = 8000, base_url: str = "http://localhost:8000") -> Dict[str, Any]:
130
+ runtime = runtime.lower()
131
+ if runtime in ("local", "dev"):
132
+ return deploy_local(app_file, host, port, base_url)
133
+ if runtime in ("fly", "flyio"):
134
+ return deploy_fly(app_name)
135
+ if runtime == "render":
136
+ return deploy_render(app_name)
137
+ raise ValueError(f"Unknown runtime: {runtime}")
138
+