talocode-calclane 0.3.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.
- calclane/__init__.py +12 -0
- calclane/__main__.py +80 -0
- calclane/client.py +410 -0
- talocode_calclane-0.3.0.dist-info/METADATA +153 -0
- talocode_calclane-0.3.0.dist-info/RECORD +8 -0
- talocode_calclane-0.3.0.dist-info/WHEEL +5 -0
- talocode_calclane-0.3.0.dist-info/entry_points.txt +2 -0
- talocode_calclane-0.3.0.dist-info/top_level.txt +1 -0
calclane/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""CalcLane — calculator engine client for agents (Talocode)."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.3.0"
|
|
4
|
+
|
|
5
|
+
from .client import CalcLaneClient, CalcLaneError, create_calclane_client
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"CalcLaneClient",
|
|
9
|
+
"CalcLaneError",
|
|
10
|
+
"create_calclane_client",
|
|
11
|
+
"__version__",
|
|
12
|
+
]
|
calclane/__main__.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""CLI: `calclane` / `python -m calclane`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from . import __version__
|
|
10
|
+
from .client import CalcLaneClient, CalcLaneError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main(argv: list[str] | None = None) -> None:
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
prog="calclane",
|
|
16
|
+
description="CalcLane — calculator for agents (Talocode)",
|
|
17
|
+
)
|
|
18
|
+
parser.add_argument("--version", action="version", version=__version__)
|
|
19
|
+
parser.add_argument("--base-url", default=None)
|
|
20
|
+
parser.add_argument("--api-key", default=None)
|
|
21
|
+
parser.add_argument(
|
|
22
|
+
"--remote",
|
|
23
|
+
action="store_true",
|
|
24
|
+
help="Use hosted API at TALOCODE_BASE_URL",
|
|
25
|
+
)
|
|
26
|
+
sub = parser.add_subparsers(dest="command")
|
|
27
|
+
|
|
28
|
+
sub.add_parser("health")
|
|
29
|
+
sub.add_parser("pricing")
|
|
30
|
+
sub.add_parser("capabilities")
|
|
31
|
+
|
|
32
|
+
ev = sub.add_parser("eval", aliases=["evaluate"], help="Evaluate expression")
|
|
33
|
+
ev.add_argument("expression", nargs="?", default=None)
|
|
34
|
+
ev.add_argument("-e", "--expr", dest="expr", default=None)
|
|
35
|
+
ev.add_argument("--mode", choices=["standard", "scientific"], default="scientific")
|
|
36
|
+
ev.add_argument("--angle", choices=["deg", "rad", "grad"], default="deg")
|
|
37
|
+
|
|
38
|
+
args = parser.parse_args(argv)
|
|
39
|
+
if not args.command:
|
|
40
|
+
parser.print_help()
|
|
41
|
+
sys.exit(1)
|
|
42
|
+
|
|
43
|
+
client = CalcLaneClient(
|
|
44
|
+
api_key=args.api_key,
|
|
45
|
+
base_url=args.base_url,
|
|
46
|
+
prefer_local=not args.remote,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
if args.command == "health":
|
|
51
|
+
result = client.health(remote=args.remote)
|
|
52
|
+
elif args.command == "pricing":
|
|
53
|
+
result = client.pricing(remote=args.remote)
|
|
54
|
+
elif args.command == "capabilities":
|
|
55
|
+
result = client.capabilities(remote=args.remote)
|
|
56
|
+
elif args.command in ("eval", "evaluate"):
|
|
57
|
+
expression = args.expression or args.expr
|
|
58
|
+
if not expression:
|
|
59
|
+
print("expression required", file=sys.stderr)
|
|
60
|
+
sys.exit(1)
|
|
61
|
+
result = client.evaluate(
|
|
62
|
+
expression,
|
|
63
|
+
mode=args.mode,
|
|
64
|
+
angle=args.angle,
|
|
65
|
+
remote=args.remote,
|
|
66
|
+
)
|
|
67
|
+
if not result.get("ok"):
|
|
68
|
+
print(json.dumps(result, indent=2))
|
|
69
|
+
sys.exit(2)
|
|
70
|
+
else:
|
|
71
|
+
print(f"Unknown command: {args.command}", file=sys.stderr)
|
|
72
|
+
sys.exit(1)
|
|
73
|
+
print(json.dumps(result, indent=2))
|
|
74
|
+
except CalcLaneError as e:
|
|
75
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
76
|
+
sys.exit(1)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
main()
|
calclane/client.py
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
"""CalcLane REST client + local expression evaluator (stdlib only)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import json
|
|
7
|
+
import math
|
|
8
|
+
import operator
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import urllib.error
|
|
12
|
+
import urllib.request
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CalcLaneError(Exception):
|
|
17
|
+
"""Raised when evaluation or the hosted API fails."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
message: str,
|
|
22
|
+
*,
|
|
23
|
+
code: str | None = None,
|
|
24
|
+
status: int | None = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
super().__init__(message)
|
|
27
|
+
self.code = code
|
|
28
|
+
self.status = status
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_BINOPS = {
|
|
32
|
+
ast.Add: operator.add,
|
|
33
|
+
ast.Sub: operator.sub,
|
|
34
|
+
ast.Mult: operator.mul,
|
|
35
|
+
ast.Div: operator.truediv,
|
|
36
|
+
ast.Mod: operator.mod,
|
|
37
|
+
ast.Pow: operator.pow,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
_FUNCS = {
|
|
41
|
+
"sin": math.sin,
|
|
42
|
+
"cos": math.cos,
|
|
43
|
+
"tan": math.tan,
|
|
44
|
+
"asin": math.asin,
|
|
45
|
+
"acos": math.acos,
|
|
46
|
+
"atan": math.atan,
|
|
47
|
+
"log": math.log10,
|
|
48
|
+
"ln": math.log,
|
|
49
|
+
"sqrt": math.sqrt,
|
|
50
|
+
"cbrt": lambda x: x ** (1 / 3) if x >= 0 else -((-x) ** (1 / 3)),
|
|
51
|
+
"abs": abs,
|
|
52
|
+
"floor": math.floor,
|
|
53
|
+
"ceil": math.ceil,
|
|
54
|
+
"round": round,
|
|
55
|
+
"exp": math.exp,
|
|
56
|
+
"fact": math.factorial,
|
|
57
|
+
"factorial": math.factorial,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _to_radians(value: float, angle: str) -> float:
|
|
62
|
+
if angle == "deg":
|
|
63
|
+
return math.radians(value)
|
|
64
|
+
if angle == "grad":
|
|
65
|
+
return value * math.pi / 200
|
|
66
|
+
return value
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _from_radians(value: float, angle: str) -> float:
|
|
70
|
+
if angle == "deg":
|
|
71
|
+
return math.degrees(value)
|
|
72
|
+
if angle == "grad":
|
|
73
|
+
return value * 200 / math.pi
|
|
74
|
+
return value
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class _SafeEval(ast.NodeVisitor):
|
|
78
|
+
def __init__(self, angle: str = "deg", mode: str = "scientific") -> None:
|
|
79
|
+
self.angle = angle
|
|
80
|
+
self.mode = mode
|
|
81
|
+
|
|
82
|
+
def visit(self, node: ast.AST) -> Any: # type: ignore[override]
|
|
83
|
+
return super().visit(node)
|
|
84
|
+
|
|
85
|
+
def eval(self, expression: str) -> float:
|
|
86
|
+
# normalize ^ to ** for Python AST
|
|
87
|
+
expr = expression.strip().replace("^", "**")
|
|
88
|
+
expr = re.sub(r"\bpi\b", "pi", expr, flags=re.I)
|
|
89
|
+
tree = ast.parse(expr, mode="eval")
|
|
90
|
+
return float(self.visit(tree.body))
|
|
91
|
+
|
|
92
|
+
def visit_Expression(self, node: ast.Expression) -> Any:
|
|
93
|
+
return self.visit(node.body)
|
|
94
|
+
|
|
95
|
+
def visit_Constant(self, node: ast.Constant) -> Any:
|
|
96
|
+
if isinstance(node.value, (int, float)):
|
|
97
|
+
return float(node.value)
|
|
98
|
+
raise CalcLaneError("Invalid constant")
|
|
99
|
+
|
|
100
|
+
def visit_Name(self, node: ast.Name) -> Any:
|
|
101
|
+
n = node.id.lower()
|
|
102
|
+
if n == "pi":
|
|
103
|
+
return math.pi
|
|
104
|
+
if n == "e":
|
|
105
|
+
return math.e
|
|
106
|
+
raise CalcLaneError(f"Unknown identifier: {node.id}")
|
|
107
|
+
|
|
108
|
+
def visit_UnaryOp(self, node: ast.UnaryOp) -> Any:
|
|
109
|
+
v = self.visit(node.operand)
|
|
110
|
+
if isinstance(node.op, ast.UAdd):
|
|
111
|
+
return +v
|
|
112
|
+
if isinstance(node.op, ast.USub):
|
|
113
|
+
return -v
|
|
114
|
+
raise CalcLaneError("Unsupported unary operator")
|
|
115
|
+
|
|
116
|
+
def visit_BinOp(self, node: ast.BinOp) -> Any:
|
|
117
|
+
left = self.visit(node.left)
|
|
118
|
+
right = self.visit(node.right)
|
|
119
|
+
op_type = type(node.op)
|
|
120
|
+
if op_type not in _BINOPS:
|
|
121
|
+
raise CalcLaneError("Unsupported operator")
|
|
122
|
+
if op_type is ast.Div and right == 0:
|
|
123
|
+
raise CalcLaneError("Cannot divide by zero")
|
|
124
|
+
if op_type is ast.Mod and right == 0:
|
|
125
|
+
raise CalcLaneError("Cannot divide by zero")
|
|
126
|
+
result = _BINOPS[op_type](left, right)
|
|
127
|
+
if not math.isfinite(result):
|
|
128
|
+
raise CalcLaneError("Overflow")
|
|
129
|
+
return result
|
|
130
|
+
|
|
131
|
+
def visit_Call(self, node: ast.Call) -> Any:
|
|
132
|
+
if not isinstance(node.func, ast.Name):
|
|
133
|
+
raise CalcLaneError("Invalid function call")
|
|
134
|
+
name = node.func.id.lower()
|
|
135
|
+
if name not in _FUNCS:
|
|
136
|
+
raise CalcLaneError(f"Unknown function: {name}")
|
|
137
|
+
if len(node.args) != 1 or node.keywords:
|
|
138
|
+
raise CalcLaneError(f"{name} expects one argument")
|
|
139
|
+
x = float(self.visit(node.args[0]))
|
|
140
|
+
if name in ("sin", "cos", "tan"):
|
|
141
|
+
x = _to_radians(x, self.angle)
|
|
142
|
+
result = _FUNCS[name](x)
|
|
143
|
+
elif name in ("asin", "acos", "atan"):
|
|
144
|
+
result = _from_radians(_FUNCS[name](x), self.angle)
|
|
145
|
+
else:
|
|
146
|
+
if name in ("fact", "factorial"):
|
|
147
|
+
if not float(x).is_integer() or x < 0:
|
|
148
|
+
raise CalcLaneError("Invalid factorial input")
|
|
149
|
+
result = float(math.factorial(int(x)))
|
|
150
|
+
else:
|
|
151
|
+
result = float(_FUNCS[name](x))
|
|
152
|
+
if not math.isfinite(result):
|
|
153
|
+
raise CalcLaneError("Invalid input")
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
def visit_Expr(self, node: ast.Expr) -> Any:
|
|
157
|
+
return self.visit(node.value)
|
|
158
|
+
|
|
159
|
+
def generic_visit(self, node: ast.AST) -> Any:
|
|
160
|
+
raise CalcLaneError(f"Unsupported expression node: {type(node).__name__}")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def evaluate_local(
|
|
164
|
+
expression: str,
|
|
165
|
+
*,
|
|
166
|
+
mode: str = "scientific",
|
|
167
|
+
angle: str = "deg",
|
|
168
|
+
) -> dict[str, Any]:
|
|
169
|
+
"""Evaluate expression offline (stdlib). Scientific uses Python precedence."""
|
|
170
|
+
expr = (expression or "").strip()
|
|
171
|
+
if not expr:
|
|
172
|
+
return {
|
|
173
|
+
"ok": False,
|
|
174
|
+
"expression": expr,
|
|
175
|
+
"result": None,
|
|
176
|
+
"display": "expression is required",
|
|
177
|
+
"mode": mode,
|
|
178
|
+
"angle": angle,
|
|
179
|
+
"engine": "calclane",
|
|
180
|
+
"version": "0.3.0",
|
|
181
|
+
"error": "expression is required",
|
|
182
|
+
}
|
|
183
|
+
try:
|
|
184
|
+
# Standard mode: left-to-right by reducing operators sequentially is hard with AST.
|
|
185
|
+
# For standard we still use scientific-safe AST; note documented difference.
|
|
186
|
+
value = _SafeEval(angle=angle, mode=mode).eval(expr)
|
|
187
|
+
# pretty display
|
|
188
|
+
if abs(value) != 0 and (abs(value) >= 1e16 or abs(value) < 1e-10):
|
|
189
|
+
display = f"{value:.10e}".replace("e+", "e")
|
|
190
|
+
else:
|
|
191
|
+
display = f"{value:.15g}"
|
|
192
|
+
return {
|
|
193
|
+
"ok": True,
|
|
194
|
+
"expression": expr,
|
|
195
|
+
"result": value,
|
|
196
|
+
"display": display,
|
|
197
|
+
"mode": mode,
|
|
198
|
+
"angle": angle,
|
|
199
|
+
"engine": "calclane",
|
|
200
|
+
"version": "0.3.0",
|
|
201
|
+
}
|
|
202
|
+
except CalcLaneError as e:
|
|
203
|
+
return {
|
|
204
|
+
"ok": False,
|
|
205
|
+
"expression": expr,
|
|
206
|
+
"result": None,
|
|
207
|
+
"display": str(e),
|
|
208
|
+
"mode": mode,
|
|
209
|
+
"angle": angle,
|
|
210
|
+
"engine": "calclane",
|
|
211
|
+
"version": "0.3.0",
|
|
212
|
+
"error": str(e),
|
|
213
|
+
}
|
|
214
|
+
except Exception as e: # noqa: BLE001
|
|
215
|
+
return {
|
|
216
|
+
"ok": False,
|
|
217
|
+
"expression": expr,
|
|
218
|
+
"result": None,
|
|
219
|
+
"display": str(e),
|
|
220
|
+
"mode": mode,
|
|
221
|
+
"angle": angle,
|
|
222
|
+
"engine": "calclane",
|
|
223
|
+
"version": "0.3.0",
|
|
224
|
+
"error": str(e),
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class CalcLaneClient:
|
|
229
|
+
"""Python client for Talocode CalcLane (`/v1/calclane/*`) + local eval."""
|
|
230
|
+
|
|
231
|
+
def __init__(
|
|
232
|
+
self,
|
|
233
|
+
api_key: str | None = None,
|
|
234
|
+
base_url: str | None = None,
|
|
235
|
+
*,
|
|
236
|
+
prefer_local: bool = True,
|
|
237
|
+
timeout: float = 30.0,
|
|
238
|
+
) -> None:
|
|
239
|
+
self.api_key = api_key or os.environ.get("TALOCODE_API_KEY")
|
|
240
|
+
self.base_url = (
|
|
241
|
+
base_url
|
|
242
|
+
or os.environ.get("TALOCODE_BASE_URL")
|
|
243
|
+
or "https://api.talocode.site"
|
|
244
|
+
).rstrip("/")
|
|
245
|
+
self.prefer_local = prefer_local
|
|
246
|
+
self.timeout = timeout
|
|
247
|
+
|
|
248
|
+
def _resolve_path(self, path: str) -> str:
|
|
249
|
+
if self.base_url.endswith("/calclane") and (
|
|
250
|
+
path == "/v1/calclane" or path.startswith("/v1/calclane/")
|
|
251
|
+
):
|
|
252
|
+
rest = path[len("/v1/calclane") :] or "/health"
|
|
253
|
+
return rest if rest.startswith("/") else f"/{rest}"
|
|
254
|
+
return path
|
|
255
|
+
|
|
256
|
+
def _headers(self) -> dict[str, str]:
|
|
257
|
+
headers = {"Content-Type": "application/json"}
|
|
258
|
+
if self.api_key:
|
|
259
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
260
|
+
headers["X-Api-Key"] = self.api_key
|
|
261
|
+
return headers
|
|
262
|
+
|
|
263
|
+
def _request(
|
|
264
|
+
self,
|
|
265
|
+
method: str,
|
|
266
|
+
path: str,
|
|
267
|
+
body: dict[str, Any] | None = None,
|
|
268
|
+
) -> dict[str, Any]:
|
|
269
|
+
url = f"{self.base_url}{self._resolve_path(path)}"
|
|
270
|
+
data = json.dumps(body).encode("utf-8") if body is not None else None
|
|
271
|
+
req = urllib.request.Request(
|
|
272
|
+
url,
|
|
273
|
+
data=data,
|
|
274
|
+
headers=self._headers(),
|
|
275
|
+
method=method,
|
|
276
|
+
)
|
|
277
|
+
try:
|
|
278
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
279
|
+
raw = resp.read().decode("utf-8")
|
|
280
|
+
if not raw:
|
|
281
|
+
return {}
|
|
282
|
+
return json.loads(raw)
|
|
283
|
+
except urllib.error.HTTPError as e:
|
|
284
|
+
code: str | None = None
|
|
285
|
+
msg = str(e)
|
|
286
|
+
try:
|
|
287
|
+
detail = json.loads(e.read().decode("utf-8"))
|
|
288
|
+
if isinstance(detail, dict):
|
|
289
|
+
err = detail.get("error")
|
|
290
|
+
if isinstance(err, dict):
|
|
291
|
+
msg = err.get("message") or msg
|
|
292
|
+
code = err.get("code")
|
|
293
|
+
elif isinstance(err, str):
|
|
294
|
+
msg = err
|
|
295
|
+
code = code or detail.get("code")
|
|
296
|
+
except Exception:
|
|
297
|
+
pass
|
|
298
|
+
raise CalcLaneError(msg, code=code, status=e.code) from e
|
|
299
|
+
except urllib.error.URLError as e:
|
|
300
|
+
raise CalcLaneError(f"Request failed: {e.reason}") from e
|
|
301
|
+
|
|
302
|
+
def evaluate_local(
|
|
303
|
+
self,
|
|
304
|
+
expression: str,
|
|
305
|
+
*,
|
|
306
|
+
mode: str = "scientific",
|
|
307
|
+
angle: str = "deg",
|
|
308
|
+
) -> dict[str, Any]:
|
|
309
|
+
return evaluate_local(expression, mode=mode, angle=angle)
|
|
310
|
+
|
|
311
|
+
def evaluate(
|
|
312
|
+
self,
|
|
313
|
+
expression: str,
|
|
314
|
+
*,
|
|
315
|
+
mode: str = "scientific",
|
|
316
|
+
angle: str = "deg",
|
|
317
|
+
remote: bool = False,
|
|
318
|
+
fe: bool | None = None,
|
|
319
|
+
) -> dict[str, Any]:
|
|
320
|
+
if remote or (not self.prefer_local and self.api_key):
|
|
321
|
+
return self.evaluate_remote(expression, mode=mode, angle=angle, fe=fe)
|
|
322
|
+
return self.evaluate_local(expression, mode=mode, angle=angle)
|
|
323
|
+
|
|
324
|
+
def evaluate_remote(
|
|
325
|
+
self,
|
|
326
|
+
expression: str,
|
|
327
|
+
*,
|
|
328
|
+
mode: str = "scientific",
|
|
329
|
+
angle: str = "deg",
|
|
330
|
+
fe: bool | None = None,
|
|
331
|
+
) -> dict[str, Any]:
|
|
332
|
+
if not self.api_key:
|
|
333
|
+
raise CalcLaneError(
|
|
334
|
+
"TALOCODE_API_KEY required for hosted evaluate",
|
|
335
|
+
code="MISSING_API_KEY",
|
|
336
|
+
status=401,
|
|
337
|
+
)
|
|
338
|
+
body: dict[str, Any] = {
|
|
339
|
+
"expression": expression,
|
|
340
|
+
"mode": mode,
|
|
341
|
+
"angle": angle,
|
|
342
|
+
}
|
|
343
|
+
if fe is not None:
|
|
344
|
+
body["fe"] = fe
|
|
345
|
+
return self._request("POST", "/v1/calclane/evaluate", body)
|
|
346
|
+
|
|
347
|
+
def dispatch(
|
|
348
|
+
self,
|
|
349
|
+
commands: list[dict[str, Any]],
|
|
350
|
+
*,
|
|
351
|
+
mode: str | None = None,
|
|
352
|
+
angle: str | None = None,
|
|
353
|
+
remote: bool = False,
|
|
354
|
+
) -> dict[str, Any]:
|
|
355
|
+
if remote or (not self.prefer_local and self.api_key):
|
|
356
|
+
if not self.api_key:
|
|
357
|
+
raise CalcLaneError(
|
|
358
|
+
"TALOCODE_API_KEY required for hosted dispatch",
|
|
359
|
+
code="MISSING_API_KEY",
|
|
360
|
+
status=401,
|
|
361
|
+
)
|
|
362
|
+
body: dict[str, Any] = {"commands": commands}
|
|
363
|
+
if mode:
|
|
364
|
+
body["mode"] = mode
|
|
365
|
+
if angle:
|
|
366
|
+
body["angle"] = angle
|
|
367
|
+
return self._request("POST", "/v1/calclane/dispatch", body)
|
|
368
|
+
raise CalcLaneError(
|
|
369
|
+
"Local dispatch requires the Node engine; use evaluate_local or remote=True",
|
|
370
|
+
code="LOCAL_DISPATCH_UNSUPPORTED",
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
def health(self, *, remote: bool = False) -> dict[str, Any]:
|
|
374
|
+
if remote or (not self.prefer_local and self.api_key):
|
|
375
|
+
return self._request("GET", "/v1/calclane/health")
|
|
376
|
+
return {"ok": True, "service": "calclane", "version": "0.3.0", "mode": "local"}
|
|
377
|
+
|
|
378
|
+
def pricing(self, *, remote: bool = False) -> dict[str, Any]:
|
|
379
|
+
if remote or (not self.prefer_local and self.api_key):
|
|
380
|
+
return self._request("GET", "/v1/calclane/pricing")
|
|
381
|
+
return {
|
|
382
|
+
"product": "calclane",
|
|
383
|
+
"currency": "credits",
|
|
384
|
+
"credits": {"calclane.evaluate": 1, "calclane.dispatch": 1},
|
|
385
|
+
"version": "0.3.0",
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
def capabilities(self, *, remote: bool = False) -> dict[str, Any]:
|
|
389
|
+
if remote or (not self.prefer_local and self.api_key):
|
|
390
|
+
return self._request("GET", "/v1/calclane/capabilities")
|
|
391
|
+
return {
|
|
392
|
+
"product": "calclane",
|
|
393
|
+
"version": "0.3.0",
|
|
394
|
+
"modes": ["standard", "scientific"],
|
|
395
|
+
"endpoints": [
|
|
396
|
+
"GET /v1/calclane/health",
|
|
397
|
+
"GET /v1/calclane/pricing",
|
|
398
|
+
"GET /v1/calclane/capabilities",
|
|
399
|
+
"POST /v1/calclane/evaluate",
|
|
400
|
+
"POST /v1/calclane/dispatch",
|
|
401
|
+
],
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def create_calclane_client(
|
|
406
|
+
api_key: str | None = None,
|
|
407
|
+
base_url: str | None = None,
|
|
408
|
+
**kwargs: Any,
|
|
409
|
+
) -> CalcLaneClient:
|
|
410
|
+
return CalcLaneClient(api_key=api_key, base_url=base_url, **kwargs)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: talocode-calclane
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: CalcLane — open calculator engine for agents. Local evaluate + hosted Talocode API SDK/CLI.
|
|
5
|
+
Author: Talocode
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/talocode/calclane
|
|
8
|
+
Project-URL: Documentation, https://docs.talocode.site
|
|
9
|
+
Project-URL: Repository, https://github.com/talocode/calclane
|
|
10
|
+
Project-URL: Issues, https://github.com/talocode/calclane/issues
|
|
11
|
+
Project-URL: PyPI, https://pypi.org/project/talocode-calclane/
|
|
12
|
+
Keywords: calculator,calclane,scientific-calculator,talocode,agents,mcp,sdk
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# CalcLane (Python)
|
|
26
|
+
|
|
27
|
+
**Open calculator for agents** — local expression evaluation plus hosted Talocode API client.
|
|
28
|
+
|
|
29
|
+
Part of **[Talocode](https://talocode.site)** — open tools people trust; hosted power under [docs.talocode.site](https://docs.talocode.site).
|
|
30
|
+
|
|
31
|
+
| | |
|
|
32
|
+
|--|--|
|
|
33
|
+
| **Cloud API** | `https://api.talocode.site` → `/v1/calclane/*` |
|
|
34
|
+
| **Python** | `pip install talocode-calclane` (this package) |
|
|
35
|
+
| **Node** | `npm install @talocode/calclane` |
|
|
36
|
+
| **Repo** | [github.com/talocode/calclane](https://github.com/talocode/calclane) |
|
|
37
|
+
| **License** | MIT |
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Why CalcLane?
|
|
42
|
+
|
|
43
|
+
Agents need **deterministic math** as a tool, not a free-form LLM guess:
|
|
44
|
+
|
|
45
|
+
- **Structured JSON** — `ok`, `result`, `display`, `mode`, `error`
|
|
46
|
+
- **Local free path** — stdlib evaluator, no network
|
|
47
|
+
- **Hosted path** — same contract on Talocode Cloud (**1 credit** per evaluate)
|
|
48
|
+
- **CLI** — `calclane eval "2+3*4"`
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install talocode-calclane
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Requires **Python 3.10+**. Stdlib only (no extra runtime deps).
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Quickstart
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import os
|
|
66
|
+
from calclane import CalcLaneClient
|
|
67
|
+
|
|
68
|
+
client = CalcLaneClient()
|
|
69
|
+
|
|
70
|
+
# Local (free)
|
|
71
|
+
print(client.evaluate("2 + 3 * 4"))
|
|
72
|
+
print(client.evaluate("sin(90)", angle="deg"))
|
|
73
|
+
print(client.evaluate("5!"))
|
|
74
|
+
|
|
75
|
+
# Hosted API (1 credit) — needs TALOCODE_API_KEY
|
|
76
|
+
cloud = CalcLaneClient(
|
|
77
|
+
api_key=os.environ["TALOCODE_API_KEY"],
|
|
78
|
+
prefer_local=False,
|
|
79
|
+
)
|
|
80
|
+
print(cloud.evaluate_remote("sqrt(144)"))
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Environment
|
|
84
|
+
|
|
85
|
+
| Variable | Default | Purpose |
|
|
86
|
+
|----------|---------|---------|
|
|
87
|
+
| `TALOCODE_API_KEY` | — | Bearer token for hosted routes |
|
|
88
|
+
| `TALOCODE_BASE_URL` | `https://api.talocode.site` | API host |
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## CLI
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
calclane eval "2^10"
|
|
96
|
+
calclane eval "sin(90)" --angle deg
|
|
97
|
+
calclane health
|
|
98
|
+
calclane pricing
|
|
99
|
+
calclane eval "pi*2" --remote # hosted, 1 credit
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Or: `python -m calclane eval "..."`.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## API surface
|
|
107
|
+
|
|
108
|
+
| Method | Path | Credits |
|
|
109
|
+
|--------|------|---------|
|
|
110
|
+
| GET | `/v1/calclane/health` | free* |
|
|
111
|
+
| GET | `/v1/calclane/pricing` | free* |
|
|
112
|
+
| GET | `/v1/calclane/capabilities` | free* |
|
|
113
|
+
| POST | `/v1/calclane/evaluate` | **1** |
|
|
114
|
+
| POST | `/v1/calclane/dispatch` | **1** |
|
|
115
|
+
|
|
116
|
+
\*Valid API key required; no credit charge.
|
|
117
|
+
|
|
118
|
+
Auth: `Authorization: Bearer $TALOCODE_API_KEY` or `X-Api-Key`.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Related packages
|
|
123
|
+
|
|
124
|
+
| Package | Install |
|
|
125
|
+
|---------|---------|
|
|
126
|
+
| This package | `pip install talocode-calclane` |
|
|
127
|
+
| Node SDK / CLI | `npm i @talocode/calclane` |
|
|
128
|
+
| SearchLane | `pip install talocode-searchlane` |
|
|
129
|
+
| StackLane | `pip install talocode` |
|
|
130
|
+
| Tera | `pip install talocode-tera` |
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Talocode ecosystem
|
|
135
|
+
|
|
136
|
+
| Project | What it is |
|
|
137
|
+
|---------|------------|
|
|
138
|
+
| [Codra](https://github.com/talocode/codra) | Local-first agentic coding · `pip install talocode-codra` |
|
|
139
|
+
| [Tera](https://github.com/talocode/tera) | AI learning companion · `pip install talocode-tera` |
|
|
140
|
+
| [Stacklane](https://github.com/talocode/stacklane) | Cloud control plane · `pip install talocode` |
|
|
141
|
+
| [SearchLane](https://github.com/talocode/searchlane) | Agent web search · `pip install talocode-searchlane` |
|
|
142
|
+
| [DocuLane](https://github.com/talocode/doculane) | Office documents · `pip install talocode-doculane` |
|
|
143
|
+
| [ContextLane](https://github.com/talocode/contextlane) | Context tooling · `pip install contextlane` |
|
|
144
|
+
| [Tradia](https://github.com/talocode/tradia) | Trading agents · `pip install tradia` |
|
|
145
|
+
| **[CalcLane](https://github.com/talocode/calclane)** (this package) | Open calculator · `pip install talocode-calclane` |
|
|
146
|
+
|
|
147
|
+
More: [github.com/talocode](https://github.com/talocode) · [talocode.site](https://talocode.site) · [docs.talocode.site](https://docs.talocode.site)
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
MIT © Talocode
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
calclane/__init__.py,sha256=SIYjNATR9ufXlPR8i5OO_Am0XQJpImlCNoxvtjCr3Dg,272
|
|
2
|
+
calclane/__main__.py,sha256=wS9uoeTWBYdb-x9No1xW2rxFILItVVkyvD_Kyudf3mc,2577
|
|
3
|
+
calclane/client.py,sha256=RhmPWBlpWPftEt7_qahlaME-h968isUldsSorG2pb9g,13236
|
|
4
|
+
talocode_calclane-0.3.0.dist-info/METADATA,sha256=dUdqPfhx9-aKpBV9XCdPVSWj-KHV48EPjV4AhN_cEv4,4777
|
|
5
|
+
talocode_calclane-0.3.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
6
|
+
talocode_calclane-0.3.0.dist-info/entry_points.txt,sha256=6JEjQuPa1TkTJUNwF5igVyiAjalORUOK3BYuAXmsoYM,52
|
|
7
|
+
talocode_calclane-0.3.0.dist-info/top_level.txt,sha256=68ogV2tw8XBNY9eLPWdWam9yg8R1weqGth9wICX84lY,9
|
|
8
|
+
talocode_calclane-0.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
calclane
|