tatvastack-codeforge 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.
- tatvastack_codeforge-0.1.0/LICENSE +21 -0
- tatvastack_codeforge-0.1.0/PKG-INFO +62 -0
- tatvastack_codeforge-0.1.0/README.md +40 -0
- tatvastack_codeforge-0.1.0/codeforge/__init__.py +169 -0
- tatvastack_codeforge-0.1.0/codeforge/py.typed +0 -0
- tatvastack_codeforge-0.1.0/pyproject.toml +38 -0
- tatvastack_codeforge-0.1.0/setup.cfg +4 -0
- tatvastack_codeforge-0.1.0/tatvastack_codeforge.egg-info/PKG-INFO +62 -0
- tatvastack_codeforge-0.1.0/tatvastack_codeforge.egg-info/SOURCES.txt +11 -0
- tatvastack_codeforge-0.1.0/tatvastack_codeforge.egg-info/dependency_links.txt +1 -0
- tatvastack_codeforge-0.1.0/tatvastack_codeforge.egg-info/requires.txt +3 -0
- tatvastack_codeforge-0.1.0/tatvastack_codeforge.egg-info/top_level.txt +1 -0
- tatvastack_codeforge-0.1.0/tests/test_sdk.py +126 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TatvaStack
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tatvastack-codeforge
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official CodeForge SDK: run Python, JavaScript, Java and C++ in isolated, network-free sandboxes with one API.
|
|
5
|
+
Author-email: TatvaStack <bhaskar@tatvastack.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://codeforge.tatvastack.com
|
|
8
|
+
Project-URL: Documentation, https://codeforge.tatvastack.com/docs/
|
|
9
|
+
Keywords: codeforge,code execution,code execution api,sandbox,compiler api,online compiler,code runner,judge
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Python: >=3.8
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Provides-Extra: certifi
|
|
20
|
+
Requires-Dist: certifi; extra == "certifi"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# CodeForge SDK for Python
|
|
24
|
+
|
|
25
|
+
Run Python, JavaScript, Java and C++ in isolated, network-free sandboxes. Python 3.8+, standard library only.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install tatvastack-codeforge
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The package installs as `tatvastack-codeforge` and imports as `codeforge`.
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import os
|
|
35
|
+
from codeforge import CodeForge
|
|
36
|
+
|
|
37
|
+
cf = CodeForge(api_key=os.environ["CODEFORGE_API_KEY"]) # keep keys server-side
|
|
38
|
+
|
|
39
|
+
result = cf.run("python", "print(input()[::-1])", stdin="hello")
|
|
40
|
+
print(result["status"], result["stdout"]) # SUCCEEDED olleh
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- `run(...)` submits and waits; `submit(...)` + `wait(id)` / `get(id)` for your own flow; `usage()` for quota.
|
|
44
|
+
- Every `submit` sends an `Idempotency-Key` (a UUID, or pass `idempotency_key=`). Network errors, 5xx and `CONCURRENCY_LIMIT` are retried with the **same** key, so a retry never runs code twice or charges twice.
|
|
45
|
+
- A compile error or non-zero exit is a normal result: check `result["status"]`. Exceptions are `CodeForgeError` with `code`, `status`, `request_id` and `retryable`.
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from codeforge import CodeForgeError
|
|
49
|
+
try:
|
|
50
|
+
cf.run("cpp", source, time_ms=2000)
|
|
51
|
+
except CodeForgeError as e:
|
|
52
|
+
if e.code == "QUOTA_EXCEEDED":
|
|
53
|
+
print("Resets at", e.details["resetsAt"])
|
|
54
|
+
else:
|
|
55
|
+
raise
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**TLS on macOS:** python.org builds ship without CA certificates. If you see `TLS_ERROR`, run `pip install "tatvastack-codeforge[certifi]"` (the SDK uses it automatically) or *Install Certificates.command*. Behind a corporate proxy, pass `ssl_context=ssl.create_default_context(cafile=...)`.
|
|
59
|
+
|
|
60
|
+
Get an API key from the [CodeForge dashboard](https://codeforge.tatvastack.com/app/) (private beta). Full API reference: https://codeforge.tatvastack.com/docs/
|
|
61
|
+
|
|
62
|
+
MIT licensed. Made by [TatvaStack](https://tatvastack.com).
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# CodeForge SDK for Python
|
|
2
|
+
|
|
3
|
+
Run Python, JavaScript, Java and C++ in isolated, network-free sandboxes. Python 3.8+, standard library only.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install tatvastack-codeforge
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The package installs as `tatvastack-codeforge` and imports as `codeforge`.
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
import os
|
|
13
|
+
from codeforge import CodeForge
|
|
14
|
+
|
|
15
|
+
cf = CodeForge(api_key=os.environ["CODEFORGE_API_KEY"]) # keep keys server-side
|
|
16
|
+
|
|
17
|
+
result = cf.run("python", "print(input()[::-1])", stdin="hello")
|
|
18
|
+
print(result["status"], result["stdout"]) # SUCCEEDED olleh
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- `run(...)` submits and waits; `submit(...)` + `wait(id)` / `get(id)` for your own flow; `usage()` for quota.
|
|
22
|
+
- Every `submit` sends an `Idempotency-Key` (a UUID, or pass `idempotency_key=`). Network errors, 5xx and `CONCURRENCY_LIMIT` are retried with the **same** key, so a retry never runs code twice or charges twice.
|
|
23
|
+
- A compile error or non-zero exit is a normal result: check `result["status"]`. Exceptions are `CodeForgeError` with `code`, `status`, `request_id` and `retryable`.
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from codeforge import CodeForgeError
|
|
27
|
+
try:
|
|
28
|
+
cf.run("cpp", source, time_ms=2000)
|
|
29
|
+
except CodeForgeError as e:
|
|
30
|
+
if e.code == "QUOTA_EXCEEDED":
|
|
31
|
+
print("Resets at", e.details["resetsAt"])
|
|
32
|
+
else:
|
|
33
|
+
raise
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**TLS on macOS:** python.org builds ship without CA certificates. If you see `TLS_ERROR`, run `pip install "tatvastack-codeforge[certifi]"` (the SDK uses it automatically) or *Install Certificates.command*. Behind a corporate proxy, pass `ssl_context=ssl.create_default_context(cafile=...)`.
|
|
37
|
+
|
|
38
|
+
Get an API key from the [CodeForge dashboard](https://codeforge.tatvastack.com/app/) (private beta). Full API reference: https://codeforge.tatvastack.com/docs/
|
|
39
|
+
|
|
40
|
+
MIT licensed. Made by [TatvaStack](https://tatvastack.com).
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""CodeForge SDK for Python 3.8+. Standard library only.
|
|
2
|
+
|
|
3
|
+
from codeforge import CodeForge
|
|
4
|
+
cf = CodeForge(api_key=os.environ["CODEFORGE_API_KEY"])
|
|
5
|
+
result = cf.run("python", "print(6 * 7)")
|
|
6
|
+
print(result["status"], result["stdout"]) # SUCCEEDED 42
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import random
|
|
13
|
+
import ssl
|
|
14
|
+
import time
|
|
15
|
+
import urllib.error
|
|
16
|
+
import urllib.request
|
|
17
|
+
import uuid
|
|
18
|
+
from typing import Any, Dict, Optional
|
|
19
|
+
|
|
20
|
+
__all__ = ["CodeForge", "CodeForgeError", "DEFAULT_BASE_URL", "TERMINAL_STATUSES"]
|
|
21
|
+
__version__ = "0.1.0"
|
|
22
|
+
|
|
23
|
+
DEFAULT_BASE_URL = "https://api.tatvastack.com"
|
|
24
|
+
TERMINAL_STATUSES = frozenset({"SUCCEEDED", "COMPILE_ERROR", "RUNTIME_ERROR", "TIMED_OUT", "PLATFORM_ERROR"})
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CodeForgeError(Exception):
|
|
28
|
+
"""An API error, or a client-side failure (NETWORK_ERROR, WAIT_TIMEOUT)."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, message: str, code: str = "UNKNOWN", status: Optional[int] = None,
|
|
31
|
+
request_id: Optional[str] = None, retryable: bool = False, details: Optional[dict] = None):
|
|
32
|
+
super().__init__(message)
|
|
33
|
+
self.message = message
|
|
34
|
+
self.code = code
|
|
35
|
+
self.status = status
|
|
36
|
+
self.request_id = request_id
|
|
37
|
+
self.retryable = retryable
|
|
38
|
+
self.details = details or {}
|
|
39
|
+
|
|
40
|
+
def __repr__(self) -> str:
|
|
41
|
+
return f"CodeForgeError(code={self.code!r}, status={self.status}, request_id={self.request_id!r}, message={self.message!r})"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _default_ssl_context() -> ssl.SSLContext:
|
|
45
|
+
"""System CA store, or certifi's when installed (fixes python.org builds on macOS)."""
|
|
46
|
+
try:
|
|
47
|
+
import certifi # type: ignore
|
|
48
|
+
return ssl.create_default_context(cafile=certifi.where())
|
|
49
|
+
except ImportError:
|
|
50
|
+
return ssl.create_default_context()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
_TLS_HINT = (
|
|
54
|
+
"TLS certificate verification failed. Your Python has no trusted CA certificates. "
|
|
55
|
+
"Fix: `pip install certifi` (the SDK uses it automatically), or on macOS run "
|
|
56
|
+
"'Install Certificates.command' from your Python folder, or pass ssl_context=..."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _backoff(attempt: int) -> float:
|
|
61
|
+
return min(8.0, 0.3 * 2 ** (attempt - 1)) * (0.5 + random.random() / 2)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class CodeForge:
|
|
65
|
+
"""Client for the CodeForge execution API.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
api_key: Your ``cf_live_`` key. Keep it on your server.
|
|
69
|
+
base_url: API origin.
|
|
70
|
+
max_retries: Retries for network errors, 5xx responses and concurrency limits.
|
|
71
|
+
request_timeout: Seconds per HTTP request.
|
|
72
|
+
ssl_context: Custom TLS settings (e.g. a corporate CA bundle). Defaults to the system
|
|
73
|
+
store, or certifi's if installed.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(self, api_key: str, base_url: str = DEFAULT_BASE_URL, max_retries: int = 3, request_timeout: float = 15.0,
|
|
77
|
+
ssl_context: Optional[ssl.SSLContext] = None):
|
|
78
|
+
if not api_key:
|
|
79
|
+
raise CodeForgeError("api_key is required", code="CONFIGURATION")
|
|
80
|
+
self.api_key = api_key
|
|
81
|
+
self.base_url = base_url.rstrip("/")
|
|
82
|
+
self.max_retries = max_retries
|
|
83
|
+
self.request_timeout = request_timeout
|
|
84
|
+
self.ssl_context = ssl_context or _default_ssl_context()
|
|
85
|
+
|
|
86
|
+
def _request(self, method: str, path: str, body: Optional[dict] = None, headers: Optional[Dict[str, str]] = None) -> Any:
|
|
87
|
+
data = json.dumps(body).encode() if body is not None else None
|
|
88
|
+
all_headers = {"Authorization": f"Bearer {self.api_key}", "User-Agent": f"codeforge-python/{__version__}", "Accept": "application/json"}
|
|
89
|
+
if data is not None:
|
|
90
|
+
all_headers["Content-Type"] = "application/json"
|
|
91
|
+
all_headers.update(headers or {})
|
|
92
|
+
|
|
93
|
+
attempt = 0
|
|
94
|
+
while True:
|
|
95
|
+
attempt += 1
|
|
96
|
+
req = urllib.request.Request(self.base_url + path, data=data, method=method, headers=all_headers)
|
|
97
|
+
try:
|
|
98
|
+
with urllib.request.urlopen(req, timeout=self.request_timeout, context=self.ssl_context) as res:
|
|
99
|
+
text = res.read().decode("utf-8")
|
|
100
|
+
return json.loads(text) if text else None
|
|
101
|
+
except urllib.error.HTTPError as http_err:
|
|
102
|
+
text = http_err.read().decode("utf-8", "replace")
|
|
103
|
+
try:
|
|
104
|
+
payload = json.loads(text) if text else {}
|
|
105
|
+
except ValueError:
|
|
106
|
+
payload = {}
|
|
107
|
+
e = payload.get("error", {}) if isinstance(payload, dict) else {}
|
|
108
|
+
error = CodeForgeError(
|
|
109
|
+
e.get("message") or f"HTTP {http_err.code}",
|
|
110
|
+
code=e.get("code") or f"HTTP_{http_err.code}",
|
|
111
|
+
status=http_err.code,
|
|
112
|
+
request_id=e.get("requestId") or http_err.headers.get("x-request-id"),
|
|
113
|
+
retryable=e.get("retryable", http_err.code >= 500),
|
|
114
|
+
details=e,
|
|
115
|
+
)
|
|
116
|
+
if error.retryable and attempt <= self.max_retries:
|
|
117
|
+
time.sleep(_backoff(attempt))
|
|
118
|
+
continue
|
|
119
|
+
raise error from None
|
|
120
|
+
except (urllib.error.URLError, TimeoutError, ConnectionError) as net_err:
|
|
121
|
+
reason = getattr(net_err, "reason", net_err)
|
|
122
|
+
if isinstance(reason, ssl.SSLCertVerificationError):
|
|
123
|
+
# Retrying can't fix a local trust-store problem.
|
|
124
|
+
raise CodeForgeError(_TLS_HINT, code="TLS_ERROR", retryable=False) from None
|
|
125
|
+
# Safe to retry: POSTs carry an Idempotency-Key, so a retry never runs code twice.
|
|
126
|
+
if attempt <= self.max_retries:
|
|
127
|
+
time.sleep(_backoff(attempt))
|
|
128
|
+
continue
|
|
129
|
+
raise CodeForgeError(f"Network error: {net_err}", code="NETWORK_ERROR", retryable=True) from None
|
|
130
|
+
|
|
131
|
+
def submit(self, language: str, source: str, stdin: Optional[str] = None, time_ms: Optional[int] = None,
|
|
132
|
+
idempotency_key: Optional[str] = None) -> dict:
|
|
133
|
+
"""Queue an execution and return immediately: ``{"executionId", "status": "QUEUED", ...}``."""
|
|
134
|
+
body: Dict[str, Any] = {"language": language, "source": source}
|
|
135
|
+
if stdin is not None:
|
|
136
|
+
body["stdin"] = stdin
|
|
137
|
+
if time_ms is not None:
|
|
138
|
+
body["limits"] = {"timeMs": time_ms}
|
|
139
|
+
return self._request("POST", "/v1/executions", body, {"Idempotency-Key": idempotency_key or str(uuid.uuid4())})
|
|
140
|
+
|
|
141
|
+
def get(self, execution_id: str) -> dict:
|
|
142
|
+
"""Fetch an execution's current state."""
|
|
143
|
+
return self._request("GET", f"/v1/executions/{urllib.request.quote(execution_id, safe='')}")
|
|
144
|
+
|
|
145
|
+
def wait(self, execution_id: str, poll_interval: float = 0.5, timeout: float = 120.0) -> dict:
|
|
146
|
+
"""Poll until the execution finishes and return it."""
|
|
147
|
+
deadline = time.monotonic() + timeout
|
|
148
|
+
while True:
|
|
149
|
+
execution = self.get(execution_id)
|
|
150
|
+
if execution.get("status") in TERMINAL_STATUSES:
|
|
151
|
+
return execution
|
|
152
|
+
if time.monotonic() + poll_interval > deadline:
|
|
153
|
+
raise CodeForgeError(f"Execution {execution_id} still {execution.get('status')} after {timeout}s",
|
|
154
|
+
code="WAIT_TIMEOUT", retryable=True, details={"executionId": execution_id})
|
|
155
|
+
time.sleep(poll_interval)
|
|
156
|
+
|
|
157
|
+
def run(self, language: str, source: str, stdin: Optional[str] = None, time_ms: Optional[int] = None,
|
|
158
|
+
idempotency_key: Optional[str] = None, poll_interval: float = 0.5, timeout: float = 120.0) -> dict:
|
|
159
|
+
"""Submit code and wait for the result.
|
|
160
|
+
|
|
161
|
+
A non-zero exit or compile error is a normal result (check ``status``), not an exception;
|
|
162
|
+
exceptions mean the request itself failed.
|
|
163
|
+
"""
|
|
164
|
+
accepted = self.submit(language, source, stdin=stdin, time_ms=time_ms, idempotency_key=idempotency_key)
|
|
165
|
+
return self.wait(accepted["executionId"], poll_interval=poll_interval, timeout=timeout)
|
|
166
|
+
|
|
167
|
+
def usage(self) -> dict:
|
|
168
|
+
"""This month's quota, current concurrency and 30-day daily usage."""
|
|
169
|
+
return self._request("GET", "/v1/usage")
|
|
File without changes
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
# PyPI's "codeforge" name belongs to an unrelated project; the import name is still `codeforge`.
|
|
7
|
+
name = "tatvastack-codeforge"
|
|
8
|
+
version = "0.1.0"
|
|
9
|
+
description = "Official CodeForge SDK: run Python, JavaScript, Java and C++ in isolated, network-free sandboxes with one API."
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
license = "MIT"
|
|
13
|
+
license-files = ["LICENSE"]
|
|
14
|
+
authors = [{ name = "TatvaStack", email = "bhaskar@tatvastack.com" }]
|
|
15
|
+
keywords = ["codeforge", "code execution", "code execution api", "sandbox", "compiler api", "online compiler", "code runner", "judge"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Topic :: Software Development :: Libraries",
|
|
22
|
+
"Typing :: Typed",
|
|
23
|
+
]
|
|
24
|
+
dependencies = []
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
# Fixes certificate errors on python.org builds for macOS; used automatically when installed.
|
|
28
|
+
certifi = ["certifi"]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://codeforge.tatvastack.com"
|
|
32
|
+
Documentation = "https://codeforge.tatvastack.com/docs/"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools]
|
|
35
|
+
packages = ["codeforge"]
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.package-data]
|
|
38
|
+
codeforge = ["py.typed"]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tatvastack-codeforge
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official CodeForge SDK: run Python, JavaScript, Java and C++ in isolated, network-free sandboxes with one API.
|
|
5
|
+
Author-email: TatvaStack <bhaskar@tatvastack.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://codeforge.tatvastack.com
|
|
8
|
+
Project-URL: Documentation, https://codeforge.tatvastack.com/docs/
|
|
9
|
+
Keywords: codeforge,code execution,code execution api,sandbox,compiler api,online compiler,code runner,judge
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Python: >=3.8
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Provides-Extra: certifi
|
|
20
|
+
Requires-Dist: certifi; extra == "certifi"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# CodeForge SDK for Python
|
|
24
|
+
|
|
25
|
+
Run Python, JavaScript, Java and C++ in isolated, network-free sandboxes. Python 3.8+, standard library only.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install tatvastack-codeforge
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The package installs as `tatvastack-codeforge` and imports as `codeforge`.
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import os
|
|
35
|
+
from codeforge import CodeForge
|
|
36
|
+
|
|
37
|
+
cf = CodeForge(api_key=os.environ["CODEFORGE_API_KEY"]) # keep keys server-side
|
|
38
|
+
|
|
39
|
+
result = cf.run("python", "print(input()[::-1])", stdin="hello")
|
|
40
|
+
print(result["status"], result["stdout"]) # SUCCEEDED olleh
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- `run(...)` submits and waits; `submit(...)` + `wait(id)` / `get(id)` for your own flow; `usage()` for quota.
|
|
44
|
+
- Every `submit` sends an `Idempotency-Key` (a UUID, or pass `idempotency_key=`). Network errors, 5xx and `CONCURRENCY_LIMIT` are retried with the **same** key, so a retry never runs code twice or charges twice.
|
|
45
|
+
- A compile error or non-zero exit is a normal result: check `result["status"]`. Exceptions are `CodeForgeError` with `code`, `status`, `request_id` and `retryable`.
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from codeforge import CodeForgeError
|
|
49
|
+
try:
|
|
50
|
+
cf.run("cpp", source, time_ms=2000)
|
|
51
|
+
except CodeForgeError as e:
|
|
52
|
+
if e.code == "QUOTA_EXCEEDED":
|
|
53
|
+
print("Resets at", e.details["resetsAt"])
|
|
54
|
+
else:
|
|
55
|
+
raise
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**TLS on macOS:** python.org builds ship without CA certificates. If you see `TLS_ERROR`, run `pip install "tatvastack-codeforge[certifi]"` (the SDK uses it automatically) or *Install Certificates.command*. Behind a corporate proxy, pass `ssl_context=ssl.create_default_context(cafile=...)`.
|
|
59
|
+
|
|
60
|
+
Get an API key from the [CodeForge dashboard](https://codeforge.tatvastack.com/app/) (private beta). Full API reference: https://codeforge.tatvastack.com/docs/
|
|
61
|
+
|
|
62
|
+
MIT licensed. Made by [TatvaStack](https://tatvastack.com).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
codeforge/__init__.py
|
|
5
|
+
codeforge/py.typed
|
|
6
|
+
tatvastack_codeforge.egg-info/PKG-INFO
|
|
7
|
+
tatvastack_codeforge.egg-info/SOURCES.txt
|
|
8
|
+
tatvastack_codeforge.egg-info/dependency_links.txt
|
|
9
|
+
tatvastack_codeforge.egg-info/requires.txt
|
|
10
|
+
tatvastack_codeforge.egg-info/top_level.txt
|
|
11
|
+
tests/test_sdk.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
codeforge
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Offline tests against a local fake API that behaves like CodeForge (no network, no AWS)."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import threading
|
|
7
|
+
import unittest
|
|
8
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
9
|
+
|
|
10
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
11
|
+
from codeforge import CodeForge, CodeForgeError # noqa: E402
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FakeApi:
|
|
15
|
+
def __init__(self, handler):
|
|
16
|
+
self.calls = []
|
|
17
|
+
outer = self
|
|
18
|
+
|
|
19
|
+
class Handler(BaseHTTPRequestHandler):
|
|
20
|
+
def log_message(self, *args):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
def _serve(self):
|
|
24
|
+
length = int(self.headers.get("Content-Length") or 0)
|
|
25
|
+
body = json.loads(self.rfile.read(length)) if length else None
|
|
26
|
+
outer.calls.append({"method": self.command, "path": self.path, "headers": dict(self.headers), "body": body})
|
|
27
|
+
status, payload = handler(self.command, len(outer.calls), body)
|
|
28
|
+
data = json.dumps(payload).encode()
|
|
29
|
+
self.send_response(status)
|
|
30
|
+
self.send_header("Content-Type", "application/json")
|
|
31
|
+
self.send_header("Content-Length", str(len(data)))
|
|
32
|
+
self.end_headers()
|
|
33
|
+
self.wfile.write(data)
|
|
34
|
+
|
|
35
|
+
do_GET = do_POST = _serve
|
|
36
|
+
|
|
37
|
+
self.server = HTTPServer(("127.0.0.1", 0), Handler)
|
|
38
|
+
self.url = f"http://127.0.0.1:{self.server.server_address[1]}"
|
|
39
|
+
threading.Thread(target=self.server.serve_forever, daemon=True).start()
|
|
40
|
+
|
|
41
|
+
def close(self):
|
|
42
|
+
self.server.shutdown()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def err(code, retryable, **extra):
|
|
46
|
+
return {"error": {"code": code, "message": code, "requestId": "req_test", "retryable": retryable, **extra}}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class SdkTest(unittest.TestCase):
|
|
50
|
+
def test_run_submits_with_idempotency_key_and_polls(self):
|
|
51
|
+
state = {"polls": 0}
|
|
52
|
+
|
|
53
|
+
def handler(method, n, body):
|
|
54
|
+
if method == "POST":
|
|
55
|
+
return 202, {"executionId": "exe_1", "status": "QUEUED"}
|
|
56
|
+
state["polls"] += 1
|
|
57
|
+
return 200, {"executionId": "exe_1", "status": "RUNNING" if state["polls"] < 3 else "SUCCEEDED", "stdout": "42\n", "exitCode": 0}
|
|
58
|
+
|
|
59
|
+
api = FakeApi(handler)
|
|
60
|
+
try:
|
|
61
|
+
cf = CodeForge(api_key="cf_live_x", base_url=api.url)
|
|
62
|
+
result = cf.run("python", "print(42)", time_ms=2000, poll_interval=0.01)
|
|
63
|
+
self.assertEqual(result["status"], "SUCCEEDED")
|
|
64
|
+
self.assertEqual(result["stdout"], "42\n")
|
|
65
|
+
post = api.calls[0]
|
|
66
|
+
self.assertEqual(post["headers"]["Authorization"], "Bearer cf_live_x")
|
|
67
|
+
self.assertRegex(post["headers"]["Idempotency-Key"], r"^[0-9a-f-]{36}$")
|
|
68
|
+
self.assertEqual(post["body"], {"language": "python", "source": "print(42)", "limits": {"timeMs": 2000}})
|
|
69
|
+
self.assertEqual(len(api.calls), 4)
|
|
70
|
+
finally:
|
|
71
|
+
api.close()
|
|
72
|
+
|
|
73
|
+
def test_retries_reuse_the_same_idempotency_key(self):
|
|
74
|
+
def handler(method, n, body):
|
|
75
|
+
if n == 1:
|
|
76
|
+
return 500, err("INTERNAL_ERROR", True)
|
|
77
|
+
if n == 2:
|
|
78
|
+
return 429, err("CONCURRENCY_LIMIT", True)
|
|
79
|
+
return 202, {"executionId": "exe_2", "status": "QUEUED"}
|
|
80
|
+
|
|
81
|
+
api = FakeApi(handler)
|
|
82
|
+
try:
|
|
83
|
+
accepted = CodeForge(api_key="k", base_url=api.url).submit("cpp", "int main(){}")
|
|
84
|
+
self.assertEqual(accepted["executionId"], "exe_2")
|
|
85
|
+
self.assertEqual(len(api.calls), 3)
|
|
86
|
+
self.assertEqual(len({c["headers"]["Idempotency-Key"] for c in api.calls}), 1)
|
|
87
|
+
finally:
|
|
88
|
+
api.close()
|
|
89
|
+
|
|
90
|
+
def test_non_retryable_error_is_raised_with_details(self):
|
|
91
|
+
api = FakeApi(lambda m, n, b: (429, err("QUOTA_EXCEEDED", False, resetsAt="2026-12-01T00:00:00Z")))
|
|
92
|
+
try:
|
|
93
|
+
with self.assertRaises(CodeForgeError) as ctx:
|
|
94
|
+
CodeForge(api_key="k", base_url=api.url).submit("python", "x")
|
|
95
|
+
e = ctx.exception
|
|
96
|
+
self.assertEqual((e.code, e.status, e.request_id, e.retryable), ("QUOTA_EXCEEDED", 429, "req_test", False))
|
|
97
|
+
self.assertEqual(e.details["resetsAt"], "2026-12-01T00:00:00Z")
|
|
98
|
+
self.assertEqual(len(api.calls), 1)
|
|
99
|
+
finally:
|
|
100
|
+
api.close()
|
|
101
|
+
|
|
102
|
+
def test_wait_times_out(self):
|
|
103
|
+
api = FakeApi(lambda m, n, b: (200, {"executionId": "exe_3", "status": "QUEUED"}))
|
|
104
|
+
try:
|
|
105
|
+
with self.assertRaises(CodeForgeError) as ctx:
|
|
106
|
+
CodeForge(api_key="k", base_url=api.url).wait("exe_3", poll_interval=0.01, timeout=0.05)
|
|
107
|
+
self.assertEqual(ctx.exception.code, "WAIT_TIMEOUT")
|
|
108
|
+
finally:
|
|
109
|
+
api.close()
|
|
110
|
+
|
|
111
|
+
def test_custom_idempotency_key(self):
|
|
112
|
+
api = FakeApi(lambda m, n, b: (202, {"executionId": "exe_4", "status": "QUEUED"}))
|
|
113
|
+
try:
|
|
114
|
+
CodeForge(api_key="k", base_url=api.url).submit("java", "x", idempotency_key="order-123")
|
|
115
|
+
self.assertEqual(api.calls[0]["headers"]["Idempotency-Key"], "order-123")
|
|
116
|
+
finally:
|
|
117
|
+
api.close()
|
|
118
|
+
|
|
119
|
+
def test_network_error(self):
|
|
120
|
+
with self.assertRaises(CodeForgeError) as ctx:
|
|
121
|
+
CodeForge(api_key="k", base_url="http://127.0.0.1:1", max_retries=1).get("exe_x")
|
|
122
|
+
self.assertEqual(ctx.exception.code, "NETWORK_ERROR")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
if __name__ == "__main__":
|
|
126
|
+
unittest.main()
|