jaclang 0.8.9__py3-none-any.whl → 0.8.10__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.
Potentially problematic release.
This version of jaclang might be problematic. Click here for more details.
- jaclang/cli/cli.py +147 -25
- jaclang/cli/cmdreg.py +144 -8
- jaclang/compiler/__init__.py +6 -1
- jaclang/compiler/codeinfo.py +16 -1
- jaclang/compiler/constant.py +33 -13
- jaclang/compiler/jac.lark +130 -31
- jaclang/compiler/larkparse/jac_parser.py +2 -2
- jaclang/compiler/parser.py +567 -176
- jaclang/compiler/passes/__init__.py +2 -1
- jaclang/compiler/passes/ast_gen/__init__.py +5 -0
- jaclang/compiler/passes/ast_gen/base_ast_gen_pass.py +54 -0
- jaclang/compiler/passes/ast_gen/jsx_processor.py +344 -0
- jaclang/compiler/passes/ecmascript/__init__.py +25 -0
- jaclang/compiler/passes/ecmascript/es_unparse.py +576 -0
- jaclang/compiler/passes/ecmascript/esast_gen_pass.py +2068 -0
- jaclang/compiler/passes/ecmascript/estree.py +972 -0
- jaclang/compiler/passes/ecmascript/tests/__init__.py +1 -0
- jaclang/compiler/passes/ecmascript/tests/fixtures/advanced_language_features.jac +170 -0
- jaclang/compiler/passes/ecmascript/tests/fixtures/class_separate_impl.impl.jac +30 -0
- jaclang/compiler/passes/ecmascript/tests/fixtures/class_separate_impl.jac +14 -0
- jaclang/compiler/passes/ecmascript/tests/fixtures/client_jsx.jac +89 -0
- jaclang/compiler/passes/ecmascript/tests/fixtures/core_language_features.jac +195 -0
- jaclang/compiler/passes/ecmascript/tests/test_esast_gen_pass.py +167 -0
- jaclang/compiler/passes/ecmascript/tests/test_js_generation.py +239 -0
- jaclang/compiler/passes/main/__init__.py +0 -3
- jaclang/compiler/passes/main/annex_pass.py +23 -1
- jaclang/compiler/passes/main/pyast_gen_pass.py +324 -234
- jaclang/compiler/passes/main/pyast_load_pass.py +46 -11
- jaclang/compiler/passes/main/pyjac_ast_link_pass.py +2 -0
- jaclang/compiler/passes/main/sym_tab_build_pass.py +18 -1
- jaclang/compiler/passes/main/tests/fixtures/autoimpl.cl.jac +7 -0
- jaclang/compiler/passes/main/tests/fixtures/checker_arity.jac +3 -0
- jaclang/compiler/passes/main/tests/fixtures/checker_class_construct.jac +33 -0
- jaclang/compiler/passes/main/tests/fixtures/defuse_modpath.jac +7 -0
- jaclang/compiler/passes/main/tests/fixtures/member_access_type_resolve.jac +2 -1
- jaclang/compiler/passes/main/tests/test_checker_pass.py +31 -2
- jaclang/compiler/passes/main/tests/test_def_use_pass.py +12 -0
- jaclang/compiler/passes/main/tests/test_import_pass.py +23 -4
- jaclang/compiler/passes/main/tests/test_pyast_gen_pass.py +25 -0
- jaclang/compiler/passes/main/type_checker_pass.py +7 -0
- jaclang/compiler/passes/tool/doc_ir_gen_pass.py +115 -0
- jaclang/compiler/passes/tool/fuse_comments_pass.py +1 -10
- jaclang/compiler/passes/tool/tests/test_jac_format_pass.py +4 -1
- jaclang/compiler/passes/transform.py +9 -1
- jaclang/compiler/passes/uni_pass.py +5 -7
- jaclang/compiler/program.py +22 -25
- jaclang/compiler/tests/test_client_codegen.py +113 -0
- jaclang/compiler/tests/test_importer.py +12 -10
- jaclang/compiler/tests/test_parser.py +249 -3
- jaclang/compiler/type_system/type_evaluator.jac +169 -50
- jaclang/compiler/type_system/type_utils.py +1 -1
- jaclang/compiler/type_system/types.py +6 -0
- jaclang/compiler/unitree.py +430 -84
- jaclang/langserve/engine.jac +224 -288
- jaclang/langserve/sem_manager.jac +12 -8
- jaclang/langserve/server.jac +48 -48
- jaclang/langserve/tests/fixtures/greet.py +17 -0
- jaclang/langserve/tests/fixtures/md_path.jac +22 -0
- jaclang/langserve/tests/fixtures/user.jac +15 -0
- jaclang/langserve/tests/test_server.py +66 -371
- jaclang/lib.py +1 -1
- jaclang/runtimelib/client_bundle.py +169 -0
- jaclang/runtimelib/client_runtime.jac +586 -0
- jaclang/runtimelib/constructs.py +2 -0
- jaclang/runtimelib/machine.py +259 -100
- jaclang/runtimelib/meta_importer.py +111 -22
- jaclang/runtimelib/mtp.py +15 -0
- jaclang/runtimelib/server.py +1089 -0
- jaclang/runtimelib/tests/fixtures/client_app.jac +18 -0
- jaclang/runtimelib/tests/fixtures/custom_access_validation.jac +1 -1
- jaclang/runtimelib/tests/fixtures/savable_object.jac +4 -5
- jaclang/runtimelib/tests/fixtures/serve_api.jac +75 -0
- jaclang/runtimelib/tests/test_client_bundle.py +55 -0
- jaclang/runtimelib/tests/test_client_render.py +63 -0
- jaclang/runtimelib/tests/test_serve.py +1069 -0
- jaclang/settings.py +0 -2
- jaclang/tests/fixtures/iife_functions.jac +142 -0
- jaclang/tests/fixtures/iife_functions_client.jac +143 -0
- jaclang/tests/fixtures/multistatement_lambda.jac +116 -0
- jaclang/tests/fixtures/multistatement_lambda_client.jac +113 -0
- jaclang/tests/fixtures/needs_import_dup.jac +6 -4
- jaclang/tests/fixtures/py_run.py +7 -5
- jaclang/tests/fixtures/pyfunc_fstr.py +2 -2
- jaclang/tests/fixtures/simple_lambda_test.jac +12 -0
- jaclang/tests/test_cli.py +1 -1
- jaclang/tests/test_language.py +10 -39
- jaclang/tests/test_reference.py +17 -2
- jaclang/utils/NonGPT.py +375 -0
- jaclang/utils/helpers.py +44 -16
- jaclang/utils/lang_tools.py +31 -4
- jaclang/utils/tests/test_lang_tools.py +1 -1
- jaclang/utils/treeprinter.py +8 -3
- {jaclang-0.8.9.dist-info → jaclang-0.8.10.dist-info}/METADATA +3 -3
- {jaclang-0.8.9.dist-info → jaclang-0.8.10.dist-info}/RECORD +96 -66
- jaclang/compiler/passes/main/binder_pass.py +0 -594
- jaclang/compiler/passes/main/tests/fixtures/sym_binder.jac +0 -47
- jaclang/compiler/passes/main/tests/test_binder_pass.py +0 -111
- jaclang/langserve/tests/session.jac +0 -294
- jaclang/langserve/tests/test_dev_server.py +0 -80
- jaclang/runtimelib/importer.py +0 -351
- jaclang/tests/test_typecheck.py +0 -542
- {jaclang-0.8.9.dist-info → jaclang-0.8.10.dist-info}/WHEEL +0 -0
- {jaclang-0.8.9.dist-info → jaclang-0.8.10.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,1089 @@
|
|
|
1
|
+
"""REST API Server for Jac Programs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import html
|
|
7
|
+
import inspect
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import secrets
|
|
11
|
+
from contextlib import suppress
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
14
|
+
from typing import Any, Callable, Literal, TypeAlias, get_type_hints
|
|
15
|
+
from urllib.parse import parse_qs, urlparse
|
|
16
|
+
|
|
17
|
+
from jaclang.runtimelib.client_bundle import ClientBundleBuilder, ClientBundleError
|
|
18
|
+
from jaclang.runtimelib.constructs import (
|
|
19
|
+
Archetype,
|
|
20
|
+
NodeArchetype,
|
|
21
|
+
Root,
|
|
22
|
+
WalkerArchetype,
|
|
23
|
+
)
|
|
24
|
+
from jaclang.runtimelib.machine import ExecutionContext, JacMachine as Jac
|
|
25
|
+
|
|
26
|
+
# Type Aliases
|
|
27
|
+
JsonValue: TypeAlias = (
|
|
28
|
+
None | str | int | float | bool | list["JsonValue"] | dict[str, "JsonValue"]
|
|
29
|
+
)
|
|
30
|
+
StatusCode: TypeAlias = Literal[200, 201, 400, 401, 404, 503]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Response Models
|
|
34
|
+
@dataclass(frozen=True, slots=True)
|
|
35
|
+
class Response:
|
|
36
|
+
"""Base response container."""
|
|
37
|
+
|
|
38
|
+
status: StatusCode
|
|
39
|
+
body: JsonValue
|
|
40
|
+
content_type: str = "application/json"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True, slots=True)
|
|
44
|
+
class UserData:
|
|
45
|
+
"""User authentication data."""
|
|
46
|
+
|
|
47
|
+
username: str
|
|
48
|
+
token: str
|
|
49
|
+
root_id: str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Core Serializer
|
|
53
|
+
class JacSerializer:
|
|
54
|
+
"""Type-safe serializer for Jac objects."""
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def serialize(obj: object) -> JsonValue:
|
|
58
|
+
"""Serialize objects to JSON-compatible format."""
|
|
59
|
+
if obj is None or isinstance(obj, (str, int, float, bool)):
|
|
60
|
+
return obj
|
|
61
|
+
|
|
62
|
+
if isinstance(obj, (list, tuple)):
|
|
63
|
+
return [JacSerializer.serialize(item) for item in obj]
|
|
64
|
+
|
|
65
|
+
if isinstance(obj, dict):
|
|
66
|
+
return {key: JacSerializer.serialize(value) for key, value in obj.items()}
|
|
67
|
+
|
|
68
|
+
if isinstance(obj, Archetype):
|
|
69
|
+
return JacSerializer._serialize_archetype(obj)
|
|
70
|
+
|
|
71
|
+
if hasattr(obj, "__dict__"):
|
|
72
|
+
with suppress(Exception):
|
|
73
|
+
return JacSerializer.serialize(
|
|
74
|
+
{k: v for k, v in obj.__dict__.items() if not k.startswith("_")}
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return str(obj) if hasattr(obj, "__str__") else f"<{type(obj).__name__}>"
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def _serialize_archetype(arch: Archetype) -> dict[str, JsonValue]:
|
|
81
|
+
"""Serialize Archetype instances."""
|
|
82
|
+
result: dict[str, JsonValue] = {
|
|
83
|
+
"_jac_type": type(arch).__name__,
|
|
84
|
+
"_jac_id": arch.__jac__.id.hex,
|
|
85
|
+
"_jac_archetype": (
|
|
86
|
+
"node"
|
|
87
|
+
if isinstance(arch, NodeArchetype)
|
|
88
|
+
else "walker" if isinstance(arch, WalkerArchetype) else "archetype"
|
|
89
|
+
),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for attr_name in dir(arch):
|
|
93
|
+
if not attr_name.startswith("_") and attr_name != "__jac__":
|
|
94
|
+
with suppress(Exception):
|
|
95
|
+
attr_value = getattr(arch, attr_name)
|
|
96
|
+
if not callable(attr_value):
|
|
97
|
+
result[attr_name] = JacSerializer.serialize(attr_value)
|
|
98
|
+
|
|
99
|
+
return result
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# User Management
|
|
103
|
+
@dataclass(slots=True)
|
|
104
|
+
class UserManager:
|
|
105
|
+
"""Manage users and their persistent roots."""
|
|
106
|
+
|
|
107
|
+
session_path: str
|
|
108
|
+
_users: dict[str, dict[str, str]] = field(default_factory=dict, init=False)
|
|
109
|
+
_tokens: dict[str, str] = field(default_factory=dict, init=False)
|
|
110
|
+
_db_path: str = field(init=False)
|
|
111
|
+
|
|
112
|
+
def __post_init__(self) -> None:
|
|
113
|
+
"""Initialize user database."""
|
|
114
|
+
self._db_path = f"{self.session_path}.users.json"
|
|
115
|
+
self._load_db()
|
|
116
|
+
|
|
117
|
+
def _load_db(self) -> None:
|
|
118
|
+
"""Load user data from persistent storage."""
|
|
119
|
+
try:
|
|
120
|
+
with open(self._db_path, encoding="utf-8") as fh:
|
|
121
|
+
data = json.load(fh)
|
|
122
|
+
self._users = data.get("__jac_users__", {})
|
|
123
|
+
self._tokens = data.get("__jac_tokens__", {})
|
|
124
|
+
except Exception:
|
|
125
|
+
self._users, self._tokens = {}, {}
|
|
126
|
+
|
|
127
|
+
def _persist(self) -> None:
|
|
128
|
+
"""Save user data to persistent storage."""
|
|
129
|
+
with open(self._db_path, "w", encoding="utf-8") as fh:
|
|
130
|
+
json.dump(
|
|
131
|
+
{"__jac_users__": self._users, "__jac_tokens__": self._tokens}, fh
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def create_user(self, username: str, password: str) -> dict[str, str]:
|
|
135
|
+
"""Create a new user with their own root node. Returns dict with user data or error."""
|
|
136
|
+
if username in self._users:
|
|
137
|
+
return {"error": "User already exists"}
|
|
138
|
+
|
|
139
|
+
ctx = ExecutionContext(session=self.session_path)
|
|
140
|
+
Jac.set_context(ctx)
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
user_root = Root()
|
|
144
|
+
root_anchor = user_root.__jac__
|
|
145
|
+
Jac.save(root_anchor)
|
|
146
|
+
Jac.commit(root_anchor)
|
|
147
|
+
root_id = root_anchor.id.hex
|
|
148
|
+
finally:
|
|
149
|
+
ctx.mem.close()
|
|
150
|
+
Jac.set_context(ExecutionContext())
|
|
151
|
+
|
|
152
|
+
token = secrets.token_urlsafe(32)
|
|
153
|
+
password_hash = hashlib.sha256(password.encode()).hexdigest()
|
|
154
|
+
|
|
155
|
+
self._users[username] = {
|
|
156
|
+
"password_hash": password_hash,
|
|
157
|
+
"token": token,
|
|
158
|
+
"root_id": root_id,
|
|
159
|
+
}
|
|
160
|
+
self._tokens[token] = username
|
|
161
|
+
self._persist()
|
|
162
|
+
|
|
163
|
+
return {"username": username, "token": token, "root_id": root_id}
|
|
164
|
+
|
|
165
|
+
def authenticate(self, username: str, password: str) -> dict[str, str] | None:
|
|
166
|
+
"""Authenticate a user and return their data."""
|
|
167
|
+
if username not in self._users:
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
user = self._users[username]
|
|
171
|
+
password_hash = hashlib.sha256(password.encode()).hexdigest()
|
|
172
|
+
|
|
173
|
+
if user["password_hash"] == password_hash:
|
|
174
|
+
return {
|
|
175
|
+
"username": username,
|
|
176
|
+
"token": user["token"],
|
|
177
|
+
"root_id": user["root_id"],
|
|
178
|
+
}
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
def validate_token(self, token: str) -> str | None:
|
|
182
|
+
"""Validate token and return username."""
|
|
183
|
+
return self._tokens.get(token)
|
|
184
|
+
|
|
185
|
+
def get_root_id(self, username: str) -> str | None:
|
|
186
|
+
"""Get user's root node ID."""
|
|
187
|
+
return self._users[username]["root_id"] if username in self._users else None
|
|
188
|
+
|
|
189
|
+
def close(self) -> None:
|
|
190
|
+
"""Close and persist user data."""
|
|
191
|
+
self._persist()
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# Execution Context Manager
|
|
195
|
+
class ExecutionManager:
|
|
196
|
+
"""Manages execution contexts for user operations."""
|
|
197
|
+
|
|
198
|
+
def __init__(self, session_path: str, user_manager: UserManager) -> None:
|
|
199
|
+
"""Initialize execution manager."""
|
|
200
|
+
self.session_path = session_path
|
|
201
|
+
self.user_manager = user_manager
|
|
202
|
+
|
|
203
|
+
def execute_function(
|
|
204
|
+
self, func: Callable[..., Any], args: dict[str, Any], username: str
|
|
205
|
+
) -> dict[str, JsonValue]:
|
|
206
|
+
"""Execute a function in user's context."""
|
|
207
|
+
root_id = self.user_manager.get_root_id(username)
|
|
208
|
+
if not root_id:
|
|
209
|
+
return {"error": "User not found"}
|
|
210
|
+
|
|
211
|
+
ctx = ExecutionContext(session=self.session_path, root=root_id)
|
|
212
|
+
Jac.set_context(ctx)
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
result = func(**args)
|
|
216
|
+
Jac.commit()
|
|
217
|
+
return {
|
|
218
|
+
"result": JacSerializer.serialize(result),
|
|
219
|
+
"reports": JacSerializer.serialize(ctx.reports),
|
|
220
|
+
}
|
|
221
|
+
except Exception as e:
|
|
222
|
+
return {"error": str(e)}
|
|
223
|
+
finally:
|
|
224
|
+
ctx.mem.close()
|
|
225
|
+
Jac.set_context(ExecutionContext())
|
|
226
|
+
|
|
227
|
+
def spawn_walker(
|
|
228
|
+
self, walker_cls: type[WalkerArchetype], fields: dict[str, Any], username: str
|
|
229
|
+
) -> dict[str, JsonValue]:
|
|
230
|
+
"""Spawn a walker in user's context."""
|
|
231
|
+
root_id = self.user_manager.get_root_id(username)
|
|
232
|
+
if not root_id:
|
|
233
|
+
return {"error": "User not found"}
|
|
234
|
+
|
|
235
|
+
target_node_id = fields.pop("_jac_spawn_node", None)
|
|
236
|
+
ctx = ExecutionContext(session=self.session_path, root=root_id)
|
|
237
|
+
Jac.set_context(ctx)
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
walker = walker_cls(**fields)
|
|
241
|
+
|
|
242
|
+
if target_node_id:
|
|
243
|
+
target_node = Jac.get_object(target_node_id)
|
|
244
|
+
if not isinstance(target_node, NodeArchetype):
|
|
245
|
+
return {"error": f"Invalid target node: {target_node_id}"}
|
|
246
|
+
else:
|
|
247
|
+
target_node = ctx.get_root()
|
|
248
|
+
|
|
249
|
+
Jac.spawn(walker, target_node)
|
|
250
|
+
Jac.commit()
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
"result": JacSerializer.serialize(walker),
|
|
254
|
+
"reports": JacSerializer.serialize(ctx.reports),
|
|
255
|
+
}
|
|
256
|
+
except Exception as e:
|
|
257
|
+
import traceback
|
|
258
|
+
|
|
259
|
+
return {"error": str(e), "traceback": traceback.format_exc()}
|
|
260
|
+
finally:
|
|
261
|
+
ctx.mem.close()
|
|
262
|
+
Jac.set_context(ExecutionContext())
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# Module Introspector
|
|
266
|
+
@dataclass(slots=True)
|
|
267
|
+
class ModuleIntrospector:
|
|
268
|
+
"""Introspects and caches module metadata."""
|
|
269
|
+
|
|
270
|
+
module_name: str
|
|
271
|
+
base_path: str | None
|
|
272
|
+
_module: Any = field(default=None, init=False)
|
|
273
|
+
_functions: dict[str, Callable[..., Any]] = field(default_factory=dict, init=False)
|
|
274
|
+
_walkers: dict[str, type[WalkerArchetype]] = field(default_factory=dict, init=False)
|
|
275
|
+
_client_functions: dict[str, Callable[..., Any]] = field(
|
|
276
|
+
default_factory=dict, init=False
|
|
277
|
+
)
|
|
278
|
+
_client_manifest: dict[str, Any] = field(default_factory=dict, init=False)
|
|
279
|
+
_bundle: Any = field(default=None, init=False)
|
|
280
|
+
_bundle_error: str | None = field(default=None, init=False)
|
|
281
|
+
_bundle_builder: ClientBundleBuilder = field(
|
|
282
|
+
default_factory=ClientBundleBuilder, init=False
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
def load(self, force_reload: bool = False) -> None:
|
|
286
|
+
"""Load module and refresh caches."""
|
|
287
|
+
needs_import = force_reload or self.module_name not in Jac.loaded_modules
|
|
288
|
+
|
|
289
|
+
if needs_import and self.base_path:
|
|
290
|
+
Jac.jac_import(
|
|
291
|
+
target=self.module_name,
|
|
292
|
+
base_path=os.path.abspath(self.base_path),
|
|
293
|
+
lng="jac",
|
|
294
|
+
reload_module=force_reload,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
module = Jac.loaded_modules.get(self.module_name)
|
|
298
|
+
if not module or self._module is module and not needs_import:
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
self._module = module
|
|
302
|
+
self._load_manifest()
|
|
303
|
+
self._functions = self._collect_functions()
|
|
304
|
+
self._walkers = self._collect_walkers()
|
|
305
|
+
self._bundle = None
|
|
306
|
+
self._bundle_error = None
|
|
307
|
+
|
|
308
|
+
def _load_manifest(self) -> None:
|
|
309
|
+
"""Load client manifest from module."""
|
|
310
|
+
if not self._module:
|
|
311
|
+
return
|
|
312
|
+
|
|
313
|
+
mod_path = getattr(self._module, "__file__", None)
|
|
314
|
+
if mod_path:
|
|
315
|
+
mod = Jac.program.mod.hub.get(mod_path)
|
|
316
|
+
if mod and mod.gen.client_manifest:
|
|
317
|
+
manifest = mod.gen.client_manifest
|
|
318
|
+
self._client_manifest = {
|
|
319
|
+
"exports": manifest.exports,
|
|
320
|
+
"globals": manifest.globals,
|
|
321
|
+
"params": manifest.params,
|
|
322
|
+
"globals_values": manifest.globals_values,
|
|
323
|
+
"has_client": manifest.has_client,
|
|
324
|
+
}
|
|
325
|
+
return
|
|
326
|
+
self._client_manifest = {}
|
|
327
|
+
|
|
328
|
+
def _collect_functions(self) -> dict[str, Callable[..., Any]]:
|
|
329
|
+
"""Collect callable functions from module."""
|
|
330
|
+
if not self._module:
|
|
331
|
+
return {}
|
|
332
|
+
|
|
333
|
+
functions: dict[str, Callable[..., Any]] = {}
|
|
334
|
+
export_names = set(self._client_manifest.get("exports", []))
|
|
335
|
+
|
|
336
|
+
for name, obj in inspect.getmembers(self._module):
|
|
337
|
+
if (
|
|
338
|
+
inspect.isfunction(obj)
|
|
339
|
+
and not name.startswith("_")
|
|
340
|
+
and obj.__module__ == self._module.__name__
|
|
341
|
+
):
|
|
342
|
+
functions[name] = obj
|
|
343
|
+
if name in export_names:
|
|
344
|
+
self._client_functions[name] = obj
|
|
345
|
+
|
|
346
|
+
# Ensure all manifest exports are included
|
|
347
|
+
for name in export_names:
|
|
348
|
+
if name not in self._client_functions and hasattr(self._module, name):
|
|
349
|
+
attr = getattr(self._module, name)
|
|
350
|
+
if callable(attr):
|
|
351
|
+
self._client_functions[name] = attr
|
|
352
|
+
|
|
353
|
+
return functions
|
|
354
|
+
|
|
355
|
+
def _collect_walkers(self) -> dict[str, type[WalkerArchetype]]:
|
|
356
|
+
"""Collect walker classes from module."""
|
|
357
|
+
if not self._module:
|
|
358
|
+
return {}
|
|
359
|
+
|
|
360
|
+
walkers: dict[str, type[WalkerArchetype]] = {}
|
|
361
|
+
for name, obj in inspect.getmembers(self._module):
|
|
362
|
+
if (
|
|
363
|
+
isinstance(obj, type)
|
|
364
|
+
and issubclass(obj, WalkerArchetype)
|
|
365
|
+
and obj is not WalkerArchetype
|
|
366
|
+
and obj.__module__ == self._module.__name__
|
|
367
|
+
):
|
|
368
|
+
walkers[name] = obj
|
|
369
|
+
return walkers
|
|
370
|
+
|
|
371
|
+
def get_functions(self) -> dict[str, Callable[..., Any]]:
|
|
372
|
+
"""Get all functions."""
|
|
373
|
+
if not self._functions:
|
|
374
|
+
self.load()
|
|
375
|
+
return dict(self._functions)
|
|
376
|
+
|
|
377
|
+
def get_walkers(self) -> dict[str, type[WalkerArchetype]]:
|
|
378
|
+
"""Get all walkers."""
|
|
379
|
+
if not self._walkers:
|
|
380
|
+
self.load()
|
|
381
|
+
return dict(self._walkers)
|
|
382
|
+
|
|
383
|
+
def get_client_functions(self) -> dict[str, Callable[..., Any]]:
|
|
384
|
+
"""Get client-exportable functions."""
|
|
385
|
+
if not self._client_functions:
|
|
386
|
+
self.load()
|
|
387
|
+
return dict(self._client_functions)
|
|
388
|
+
|
|
389
|
+
def introspect_callable(self, func: Callable[..., Any]) -> dict[str, Any]:
|
|
390
|
+
"""Get callable signature information."""
|
|
391
|
+
try:
|
|
392
|
+
sig = inspect.signature(func)
|
|
393
|
+
type_hints = get_type_hints(func)
|
|
394
|
+
except Exception:
|
|
395
|
+
return {"parameters": {}, "return_type": "Any"}
|
|
396
|
+
|
|
397
|
+
params = {
|
|
398
|
+
name: {
|
|
399
|
+
"type": str(type_hints.get(name, Any)),
|
|
400
|
+
"required": param.default == inspect.Parameter.empty,
|
|
401
|
+
"default": (
|
|
402
|
+
None
|
|
403
|
+
if param.default == inspect.Parameter.empty
|
|
404
|
+
else str(param.default)
|
|
405
|
+
),
|
|
406
|
+
}
|
|
407
|
+
for name, param in sig.parameters.items()
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return {"parameters": params, "return_type": str(type_hints.get("return", Any))}
|
|
411
|
+
|
|
412
|
+
def introspect_walker(self, walker_cls: type[WalkerArchetype]) -> dict[str, Any]:
|
|
413
|
+
"""Get walker field information."""
|
|
414
|
+
try:
|
|
415
|
+
sig = inspect.signature(walker_cls.__init__)
|
|
416
|
+
type_hints = get_type_hints(walker_cls.__init__)
|
|
417
|
+
except Exception:
|
|
418
|
+
return {"fields": {}}
|
|
419
|
+
|
|
420
|
+
fields = {
|
|
421
|
+
name: {
|
|
422
|
+
"type": str(type_hints.get(name, Any)),
|
|
423
|
+
"required": param.default == inspect.Parameter.empty,
|
|
424
|
+
"default": (
|
|
425
|
+
None
|
|
426
|
+
if param.default == inspect.Parameter.empty
|
|
427
|
+
else str(param.default)
|
|
428
|
+
),
|
|
429
|
+
}
|
|
430
|
+
for name, param in sig.parameters.items()
|
|
431
|
+
if name not in ("self", "args", "kwargs")
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
fields["_jac_spawn_node"] = {
|
|
435
|
+
"type": "str (node ID, optional)",
|
|
436
|
+
"required": False,
|
|
437
|
+
"default": "root",
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return {"fields": fields}
|
|
441
|
+
|
|
442
|
+
def ensure_bundle(self) -> str:
|
|
443
|
+
"""Ensure client bundle is available and return hash."""
|
|
444
|
+
if not self._module:
|
|
445
|
+
raise RuntimeError("Module not loaded")
|
|
446
|
+
|
|
447
|
+
if self._bundle:
|
|
448
|
+
return self._bundle.hash
|
|
449
|
+
|
|
450
|
+
try:
|
|
451
|
+
self._bundle = self._bundle_builder.build(self._module)
|
|
452
|
+
self._bundle_error = None
|
|
453
|
+
return self._bundle.hash
|
|
454
|
+
except ClientBundleError as exc:
|
|
455
|
+
self._bundle = None
|
|
456
|
+
self._bundle_error = str(exc)
|
|
457
|
+
raise RuntimeError(self._bundle_error) from exc
|
|
458
|
+
|
|
459
|
+
def render_page(
|
|
460
|
+
self, function_name: str, args: dict[str, Any], username: str
|
|
461
|
+
) -> dict[str, Any]:
|
|
462
|
+
"""Render HTML page for client function."""
|
|
463
|
+
self.load()
|
|
464
|
+
|
|
465
|
+
available_exports = set(self._client_manifest.get("exports", [])) or set(
|
|
466
|
+
self.get_client_functions().keys()
|
|
467
|
+
)
|
|
468
|
+
if function_name not in available_exports:
|
|
469
|
+
raise ValueError(f"Client function '{function_name}' not found")
|
|
470
|
+
|
|
471
|
+
bundle_hash = self.ensure_bundle()
|
|
472
|
+
arg_order = list(self._client_manifest.get("params", {}).get(function_name, []))
|
|
473
|
+
|
|
474
|
+
globals_payload = {
|
|
475
|
+
name: JacSerializer.serialize(value)
|
|
476
|
+
for name, value in self._collect_client_globals().items()
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
initial_state = {
|
|
480
|
+
"module": self._module.__name__ if self._module else self.module_name,
|
|
481
|
+
"function": function_name,
|
|
482
|
+
"args": {
|
|
483
|
+
key: JacSerializer.serialize(value) for key, value in args.items()
|
|
484
|
+
},
|
|
485
|
+
"globals": globals_payload,
|
|
486
|
+
"argOrder": arg_order,
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
safe_initial_json = json.dumps(initial_state).replace("</", "<\\/")
|
|
490
|
+
|
|
491
|
+
page = (
|
|
492
|
+
"<!DOCTYPE html>"
|
|
493
|
+
'<html lang="en">'
|
|
494
|
+
"<head>"
|
|
495
|
+
'<meta charset="utf-8"/>'
|
|
496
|
+
f"<title>{html.escape(function_name)}</title>"
|
|
497
|
+
"</head>"
|
|
498
|
+
"<body>"
|
|
499
|
+
'<div id="__jac_root"></div>'
|
|
500
|
+
f'<script id="__jac_init__" type="application/json">{safe_initial_json}</script>'
|
|
501
|
+
f'<script src="/static/client.js?hash={bundle_hash}" defer></script>'
|
|
502
|
+
"</body>"
|
|
503
|
+
"</html>"
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
return {
|
|
507
|
+
"html": page,
|
|
508
|
+
"bundle_hash": bundle_hash,
|
|
509
|
+
"bundle_code": self._bundle.code,
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
def _collect_client_globals(self) -> dict[str, Any]:
|
|
513
|
+
"""Collect client-exposed global values."""
|
|
514
|
+
if not self._module:
|
|
515
|
+
return {}
|
|
516
|
+
|
|
517
|
+
result: dict[str, Any] = {}
|
|
518
|
+
names = self._client_manifest.get("globals", [])
|
|
519
|
+
values_map = self._client_manifest.get("globals_values", {})
|
|
520
|
+
|
|
521
|
+
for name in names:
|
|
522
|
+
if name in values_map:
|
|
523
|
+
result[name] = values_map[name]
|
|
524
|
+
elif hasattr(self._module, name):
|
|
525
|
+
result[name] = getattr(self._module, name)
|
|
526
|
+
else:
|
|
527
|
+
result[name] = None
|
|
528
|
+
|
|
529
|
+
return result
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
# HTTP Response Builder
|
|
533
|
+
class ResponseBuilder:
|
|
534
|
+
"""Build and send HTTP responses."""
|
|
535
|
+
|
|
536
|
+
@staticmethod
|
|
537
|
+
def send_json(
|
|
538
|
+
handler: BaseHTTPRequestHandler, status: StatusCode, data: dict[str, JsonValue]
|
|
539
|
+
) -> None:
|
|
540
|
+
"""Send JSON response with CORS headers."""
|
|
541
|
+
handler.send_response(status)
|
|
542
|
+
handler.send_header("Content-Type", "application/json")
|
|
543
|
+
ResponseBuilder._add_cors_headers(handler)
|
|
544
|
+
handler.end_headers()
|
|
545
|
+
handler.wfile.write(json.dumps(data).encode())
|
|
546
|
+
|
|
547
|
+
@staticmethod
|
|
548
|
+
def send_html(
|
|
549
|
+
handler: BaseHTTPRequestHandler, status: StatusCode, body: str
|
|
550
|
+
) -> None:
|
|
551
|
+
"""Send HTML response with CORS headers."""
|
|
552
|
+
payload = body.encode("utf-8")
|
|
553
|
+
handler.send_response(status)
|
|
554
|
+
handler.send_header("Content-Type", "text/html; charset=utf-8")
|
|
555
|
+
handler.send_header("Content-Length", str(len(payload)))
|
|
556
|
+
ResponseBuilder._add_cors_headers(handler)
|
|
557
|
+
handler.end_headers()
|
|
558
|
+
handler.wfile.write(payload)
|
|
559
|
+
|
|
560
|
+
@staticmethod
|
|
561
|
+
def send_javascript(handler: BaseHTTPRequestHandler, code: str) -> None:
|
|
562
|
+
"""Send JavaScript response."""
|
|
563
|
+
payload = code.encode("utf-8")
|
|
564
|
+
handler.send_response(200)
|
|
565
|
+
handler.send_header("Content-Type", "application/javascript; charset=utf-8")
|
|
566
|
+
handler.send_header("Content-Length", str(len(payload)))
|
|
567
|
+
handler.send_header("Cache-Control", "no-cache")
|
|
568
|
+
ResponseBuilder._add_cors_headers(handler)
|
|
569
|
+
handler.end_headers()
|
|
570
|
+
handler.wfile.write(payload)
|
|
571
|
+
|
|
572
|
+
@staticmethod
|
|
573
|
+
def _add_cors_headers(handler: BaseHTTPRequestHandler) -> None:
|
|
574
|
+
"""Add CORS headers to response."""
|
|
575
|
+
handler.send_header("Access-Control-Allow-Origin", "*")
|
|
576
|
+
handler.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
577
|
+
handler.send_header(
|
|
578
|
+
"Access-Control-Allow-Headers", "Content-Type, Authorization"
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
# Route Handlers
|
|
583
|
+
class RouteHandler:
|
|
584
|
+
"""Base route handler."""
|
|
585
|
+
|
|
586
|
+
def __init__(
|
|
587
|
+
self,
|
|
588
|
+
introspector: ModuleIntrospector,
|
|
589
|
+
execution_manager: ExecutionManager,
|
|
590
|
+
user_manager: UserManager,
|
|
591
|
+
) -> None:
|
|
592
|
+
"""Initialize route handler."""
|
|
593
|
+
self.introspector = introspector
|
|
594
|
+
self.execution_manager = execution_manager
|
|
595
|
+
self.user_manager = user_manager
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
class AuthHandler(RouteHandler):
|
|
599
|
+
"""Handle authentication routes."""
|
|
600
|
+
|
|
601
|
+
def create_user(self, username: str, password: str) -> Response:
|
|
602
|
+
"""Create new user."""
|
|
603
|
+
if not username or not password:
|
|
604
|
+
return Response(400, {"error": "Username and password required"})
|
|
605
|
+
|
|
606
|
+
result = self.user_manager.create_user(username, password)
|
|
607
|
+
if "error" in result:
|
|
608
|
+
return Response(400, dict[str, JsonValue](result))
|
|
609
|
+
|
|
610
|
+
return Response(201, dict[str, JsonValue](result))
|
|
611
|
+
|
|
612
|
+
def login(self, username: str, password: str) -> Response:
|
|
613
|
+
"""Authenticate user."""
|
|
614
|
+
if not username or not password:
|
|
615
|
+
return Response(400, {"error": "Username and password required"})
|
|
616
|
+
|
|
617
|
+
result = self.user_manager.authenticate(username, password)
|
|
618
|
+
if not result:
|
|
619
|
+
return Response(401, {"error": "Invalid credentials"})
|
|
620
|
+
|
|
621
|
+
return Response(200, dict[str, JsonValue](result))
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
class IntrospectionHandler(RouteHandler):
|
|
625
|
+
"""Handle introspection routes."""
|
|
626
|
+
|
|
627
|
+
def list_functions(self) -> Response:
|
|
628
|
+
"""List all functions."""
|
|
629
|
+
self.introspector.load()
|
|
630
|
+
return Response(
|
|
631
|
+
200, {"functions": list(self.introspector.get_functions().keys())}
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
def list_walkers(self) -> Response:
|
|
635
|
+
"""List all walkers."""
|
|
636
|
+
self.introspector.load()
|
|
637
|
+
return Response(200, {"walkers": list(self.introspector.get_walkers().keys())})
|
|
638
|
+
|
|
639
|
+
def get_function_info(self, name: str) -> Response:
|
|
640
|
+
"""Get function signature."""
|
|
641
|
+
self.introspector.load()
|
|
642
|
+
functions = self.introspector.get_functions()
|
|
643
|
+
|
|
644
|
+
if name not in functions:
|
|
645
|
+
return Response(404, {"error": f"Function '{name}' not found"})
|
|
646
|
+
|
|
647
|
+
signature = self.introspector.introspect_callable(functions[name])
|
|
648
|
+
return Response(200, {"name": name, "signature": signature})
|
|
649
|
+
|
|
650
|
+
def get_walker_info(self, name: str) -> Response:
|
|
651
|
+
"""Get walker info."""
|
|
652
|
+
self.introspector.load()
|
|
653
|
+
walkers = self.introspector.get_walkers()
|
|
654
|
+
|
|
655
|
+
if name not in walkers:
|
|
656
|
+
return Response(404, {"error": f"Walker '{name}' not found"})
|
|
657
|
+
|
|
658
|
+
info = self.introspector.introspect_walker(walkers[name])
|
|
659
|
+
return Response(200, {"name": name, "info": info})
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
class ExecutionHandler(RouteHandler):
|
|
663
|
+
"""Handle execution routes."""
|
|
664
|
+
|
|
665
|
+
def call_function(self, name: str, args: dict[str, Any], username: str) -> Response:
|
|
666
|
+
"""Call a function."""
|
|
667
|
+
self.introspector.load()
|
|
668
|
+
functions = self.introspector.get_functions()
|
|
669
|
+
|
|
670
|
+
if name not in functions:
|
|
671
|
+
return Response(404, {"error": f"Function '{name}' not found"})
|
|
672
|
+
|
|
673
|
+
result = self.execution_manager.execute_function(
|
|
674
|
+
functions[name], args, username
|
|
675
|
+
)
|
|
676
|
+
return Response(200, result)
|
|
677
|
+
|
|
678
|
+
def spawn_walker(
|
|
679
|
+
self, name: str, fields: dict[str, Any], username: str
|
|
680
|
+
) -> Response:
|
|
681
|
+
"""Spawn a walker."""
|
|
682
|
+
self.introspector.load()
|
|
683
|
+
walkers = self.introspector.get_walkers()
|
|
684
|
+
|
|
685
|
+
if name not in walkers:
|
|
686
|
+
return Response(404, {"error": f"Walker '{name}' not found"})
|
|
687
|
+
|
|
688
|
+
result = self.execution_manager.spawn_walker(walkers[name], fields, username)
|
|
689
|
+
return Response(200, result)
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
# Main Server
|
|
693
|
+
class JacAPIServer:
|
|
694
|
+
"""REST API Server for Jac programs."""
|
|
695
|
+
|
|
696
|
+
def __init__(
|
|
697
|
+
self,
|
|
698
|
+
module_name: str,
|
|
699
|
+
session_path: str,
|
|
700
|
+
port: int = 8000,
|
|
701
|
+
base_path: str | None = None,
|
|
702
|
+
) -> None:
|
|
703
|
+
"""Initialize the API server."""
|
|
704
|
+
self.module_name = module_name
|
|
705
|
+
self.session_path = session_path
|
|
706
|
+
self.port = port
|
|
707
|
+
self.base_path = base_path
|
|
708
|
+
|
|
709
|
+
# Core components
|
|
710
|
+
self.user_manager = UserManager(session_path)
|
|
711
|
+
self.introspector = ModuleIntrospector(module_name, base_path)
|
|
712
|
+
self.execution_manager = ExecutionManager(session_path, self.user_manager)
|
|
713
|
+
|
|
714
|
+
# Route handlers
|
|
715
|
+
self.auth_handler = AuthHandler(
|
|
716
|
+
self.introspector, self.execution_manager, self.user_manager
|
|
717
|
+
)
|
|
718
|
+
self.introspection_handler = IntrospectionHandler(
|
|
719
|
+
self.introspector, self.execution_manager, self.user_manager
|
|
720
|
+
)
|
|
721
|
+
self.execution_handler = ExecutionHandler(
|
|
722
|
+
self.introspector, self.execution_manager, self.user_manager
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
def create_handler(self) -> type[BaseHTTPRequestHandler]:
|
|
726
|
+
"""Create HTTP request handler."""
|
|
727
|
+
server = self
|
|
728
|
+
|
|
729
|
+
class JacRequestHandler(BaseHTTPRequestHandler):
|
|
730
|
+
"""Handle HTTP requests."""
|
|
731
|
+
|
|
732
|
+
def _get_auth_token(self) -> str | None:
|
|
733
|
+
"""Extract auth token from Authorization header."""
|
|
734
|
+
auth_header = self.headers.get("Authorization")
|
|
735
|
+
return (
|
|
736
|
+
auth_header[7:]
|
|
737
|
+
if auth_header and auth_header.startswith("Bearer ")
|
|
738
|
+
else None
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
def _authenticate(self) -> str | None:
|
|
742
|
+
"""Authenticate request and return username."""
|
|
743
|
+
token = self._get_auth_token()
|
|
744
|
+
return server.user_manager.validate_token(token) if token else None
|
|
745
|
+
|
|
746
|
+
def _read_json(self) -> dict[str, Any]:
|
|
747
|
+
"""Read and parse JSON from request body."""
|
|
748
|
+
content_length = int(self.headers.get("Content-Length", 0))
|
|
749
|
+
body = (
|
|
750
|
+
self.rfile.read(content_length).decode()
|
|
751
|
+
if content_length > 0
|
|
752
|
+
else "{}"
|
|
753
|
+
)
|
|
754
|
+
return json.loads(body)
|
|
755
|
+
|
|
756
|
+
def _send_response(self, response: Response) -> None:
|
|
757
|
+
"""Send response to client."""
|
|
758
|
+
ResponseBuilder.send_json(self, response.status, response.body) # type: ignore[arg-type]
|
|
759
|
+
|
|
760
|
+
def do_OPTIONS(self) -> None: # noqa: N802
|
|
761
|
+
"""Handle OPTIONS requests (CORS preflight)."""
|
|
762
|
+
self.send_response(200)
|
|
763
|
+
ResponseBuilder._add_cors_headers(self)
|
|
764
|
+
self.end_headers()
|
|
765
|
+
|
|
766
|
+
def do_GET(self) -> None: # noqa: N802
|
|
767
|
+
"""Handle GET requests."""
|
|
768
|
+
parsed_path = urlparse(self.path)
|
|
769
|
+
path = parsed_path.path
|
|
770
|
+
|
|
771
|
+
# Static assets
|
|
772
|
+
if path == "/static/client.js":
|
|
773
|
+
try:
|
|
774
|
+
server.introspector.load()
|
|
775
|
+
server.introspector.ensure_bundle()
|
|
776
|
+
ResponseBuilder.send_javascript(
|
|
777
|
+
self, server.introspector._bundle.code
|
|
778
|
+
)
|
|
779
|
+
except RuntimeError as exc:
|
|
780
|
+
ResponseBuilder.send_json(self, 503, {"error": str(exc)})
|
|
781
|
+
return
|
|
782
|
+
|
|
783
|
+
# Root endpoint
|
|
784
|
+
if path == "/":
|
|
785
|
+
ResponseBuilder.send_json(
|
|
786
|
+
self,
|
|
787
|
+
200,
|
|
788
|
+
{
|
|
789
|
+
"message": "Jac API Server",
|
|
790
|
+
"endpoints": {
|
|
791
|
+
"POST /user/create": "Create a new user",
|
|
792
|
+
"POST /user/login": "Authenticate and get token",
|
|
793
|
+
"GET /functions": "List all available functions",
|
|
794
|
+
"GET /walkers": "List all available walkers",
|
|
795
|
+
"GET /function/<name>": "Get function signature",
|
|
796
|
+
"GET /walker/<name>": "Get walker fields",
|
|
797
|
+
"POST /function/<name>": "Call a function",
|
|
798
|
+
"POST /walker/<name>": "Spawn a walker",
|
|
799
|
+
"GET /page/<name>": "Render HTML page for client function",
|
|
800
|
+
},
|
|
801
|
+
},
|
|
802
|
+
)
|
|
803
|
+
return
|
|
804
|
+
|
|
805
|
+
# Client page rendering (public or authenticated)
|
|
806
|
+
if path.startswith("/page/"):
|
|
807
|
+
func_name = path.split("/")[-1]
|
|
808
|
+
query_params = parse_qs(parsed_path.query, keep_blank_values=True)
|
|
809
|
+
args = {
|
|
810
|
+
key: values[0] if len(values) == 1 else values
|
|
811
|
+
for key, values in query_params.items()
|
|
812
|
+
if key != "mode"
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
username = self._authenticate()
|
|
816
|
+
if not username:
|
|
817
|
+
username = "__guest__"
|
|
818
|
+
if username not in server.user_manager._users:
|
|
819
|
+
server.user_manager.create_user(username, "__no_password__")
|
|
820
|
+
|
|
821
|
+
try:
|
|
822
|
+
render_payload = server.introspector.render_page(
|
|
823
|
+
func_name, args, username
|
|
824
|
+
)
|
|
825
|
+
ResponseBuilder.send_html(self, 200, render_payload["html"])
|
|
826
|
+
except ValueError as exc:
|
|
827
|
+
ResponseBuilder.send_json(self, 404, {"error": str(exc)})
|
|
828
|
+
except RuntimeError as exc:
|
|
829
|
+
ResponseBuilder.send_json(self, 503, {"error": str(exc)})
|
|
830
|
+
return
|
|
831
|
+
|
|
832
|
+
# Protected endpoints
|
|
833
|
+
username = self._authenticate()
|
|
834
|
+
if not username:
|
|
835
|
+
ResponseBuilder.send_json(self, 401, {"error": "Unauthorized"})
|
|
836
|
+
return
|
|
837
|
+
|
|
838
|
+
# Route to introspection handlers
|
|
839
|
+
if path == "/functions":
|
|
840
|
+
self._send_response(server.introspection_handler.list_functions())
|
|
841
|
+
elif path == "/walkers":
|
|
842
|
+
self._send_response(server.introspection_handler.list_walkers())
|
|
843
|
+
elif path.startswith("/function/"):
|
|
844
|
+
name = path.split("/")[-1]
|
|
845
|
+
self._send_response(
|
|
846
|
+
server.introspection_handler.get_function_info(name)
|
|
847
|
+
)
|
|
848
|
+
elif path.startswith("/walker/"):
|
|
849
|
+
name = path.split("/")[-1]
|
|
850
|
+
self._send_response(
|
|
851
|
+
server.introspection_handler.get_walker_info(name)
|
|
852
|
+
)
|
|
853
|
+
else:
|
|
854
|
+
ResponseBuilder.send_json(self, 404, {"error": "Not found"})
|
|
855
|
+
|
|
856
|
+
def do_POST(self) -> None: # noqa: N802
|
|
857
|
+
"""Handle POST requests."""
|
|
858
|
+
parsed_path = urlparse(self.path)
|
|
859
|
+
path = parsed_path.path
|
|
860
|
+
|
|
861
|
+
try:
|
|
862
|
+
data = self._read_json()
|
|
863
|
+
except json.JSONDecodeError:
|
|
864
|
+
ResponseBuilder.send_json(self, 400, {"error": "Invalid JSON"})
|
|
865
|
+
return
|
|
866
|
+
|
|
867
|
+
# Public auth endpoints
|
|
868
|
+
if path == "/user/create":
|
|
869
|
+
response = server.auth_handler.create_user(
|
|
870
|
+
data.get("username", ""), data.get("password", "")
|
|
871
|
+
)
|
|
872
|
+
self._send_response(response)
|
|
873
|
+
return
|
|
874
|
+
|
|
875
|
+
if path == "/user/login":
|
|
876
|
+
response = server.auth_handler.login(
|
|
877
|
+
data.get("username", ""), data.get("password", "")
|
|
878
|
+
)
|
|
879
|
+
self._send_response(response)
|
|
880
|
+
return
|
|
881
|
+
|
|
882
|
+
# Protected endpoints
|
|
883
|
+
username = self._authenticate()
|
|
884
|
+
if not username:
|
|
885
|
+
ResponseBuilder.send_json(self, 401, {"error": "Unauthorized"})
|
|
886
|
+
return
|
|
887
|
+
|
|
888
|
+
# Route to execution handlers
|
|
889
|
+
if path.startswith("/function/"):
|
|
890
|
+
name = path.split("/")[-1]
|
|
891
|
+
response = server.execution_handler.call_function(
|
|
892
|
+
name, data.get("args", {}), username
|
|
893
|
+
)
|
|
894
|
+
self._send_response(response)
|
|
895
|
+
elif path.startswith("/walker/"):
|
|
896
|
+
name = path.split("/")[-1]
|
|
897
|
+
response = server.execution_handler.spawn_walker(
|
|
898
|
+
name, data.get("fields", {}), username
|
|
899
|
+
)
|
|
900
|
+
self._send_response(response)
|
|
901
|
+
else:
|
|
902
|
+
ResponseBuilder.send_json(self, 404, {"error": "Not found"})
|
|
903
|
+
|
|
904
|
+
def log_message(self, format: str, *args: object) -> None:
|
|
905
|
+
"""Log HTTP requests."""
|
|
906
|
+
print(f"{self.address_string()} - {format % args}")
|
|
907
|
+
|
|
908
|
+
return JacRequestHandler
|
|
909
|
+
|
|
910
|
+
def load_module(self, force_reload: bool = False) -> None:
|
|
911
|
+
"""Load the target module (backward compatibility)."""
|
|
912
|
+
self.introspector.load(force_reload)
|
|
913
|
+
|
|
914
|
+
@property
|
|
915
|
+
def module(self) -> object:
|
|
916
|
+
"""Get loaded module (backward compatibility)."""
|
|
917
|
+
return self.introspector._module
|
|
918
|
+
|
|
919
|
+
def get_functions(self) -> dict[str, Callable[..., Any]]:
|
|
920
|
+
"""Get all functions (backward compatibility)."""
|
|
921
|
+
return self.introspector.get_functions()
|
|
922
|
+
|
|
923
|
+
def get_walkers(self) -> dict[str, type[WalkerArchetype]]:
|
|
924
|
+
"""Get all walkers (backward compatibility)."""
|
|
925
|
+
return self.introspector.get_walkers()
|
|
926
|
+
|
|
927
|
+
def introspect_callable(self, func: Callable[..., Any]) -> dict[str, Any]:
|
|
928
|
+
"""Introspect callable (backward compatibility)."""
|
|
929
|
+
return self.introspector.introspect_callable(func)
|
|
930
|
+
|
|
931
|
+
def introspect_walker(self, walker_cls: type[WalkerArchetype]) -> dict[str, Any]:
|
|
932
|
+
"""Introspect walker (backward compatibility)."""
|
|
933
|
+
return self.introspector.introspect_walker(walker_cls)
|
|
934
|
+
|
|
935
|
+
def render_client_page(
|
|
936
|
+
self, function_name: str, args: dict[str, Any], username: str
|
|
937
|
+
) -> dict[str, Any]:
|
|
938
|
+
"""Render client page (backward compatibility)."""
|
|
939
|
+
return self.introspector.render_page(function_name, args, username)
|
|
940
|
+
|
|
941
|
+
def get_client_bundle_code(self) -> str:
|
|
942
|
+
"""Get client bundle code (backward compatibility)."""
|
|
943
|
+
self.introspector.load()
|
|
944
|
+
self.introspector.ensure_bundle()
|
|
945
|
+
return self.introspector._bundle.code
|
|
946
|
+
|
|
947
|
+
def print_endpoint_docs(self) -> None:
|
|
948
|
+
"""Print comprehensive documentation for all endpoints that would be generated."""
|
|
949
|
+
print_endpoint_docs(self)
|
|
950
|
+
|
|
951
|
+
def start(self) -> None:
|
|
952
|
+
"""Start the HTTP server."""
|
|
953
|
+
self.introspector.load()
|
|
954
|
+
handler_class = self.create_handler()
|
|
955
|
+
|
|
956
|
+
with HTTPServer(("0.0.0.0", self.port), handler_class) as httpd:
|
|
957
|
+
print(f"Jac API Server running on http://0.0.0.0:{self.port}")
|
|
958
|
+
print(f"Module: {self.module_name}")
|
|
959
|
+
print(f"Session: {self.session_path}")
|
|
960
|
+
print("\nAvailable endpoints:")
|
|
961
|
+
print(" POST /user/create - Create a new user")
|
|
962
|
+
print(" POST /user/login - Login and get auth token")
|
|
963
|
+
print(" GET /functions - List all functions")
|
|
964
|
+
print(" GET /walkers - List all walkers")
|
|
965
|
+
print(" GET /function/<name> - Get function signature")
|
|
966
|
+
print(" GET /walker/<name> - Get walker info")
|
|
967
|
+
print(" POST /function/<name> - Call a function")
|
|
968
|
+
print(" POST /walker/<name> - Spawn a walker")
|
|
969
|
+
print("\nPress Ctrl+C to stop the server")
|
|
970
|
+
|
|
971
|
+
try:
|
|
972
|
+
httpd.serve_forever()
|
|
973
|
+
except KeyboardInterrupt:
|
|
974
|
+
print("\nShutting down server...")
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
def print_endpoint_docs(server: JacAPIServer) -> None:
|
|
978
|
+
"""Print comprehensive documentation for all endpoints that would be generated."""
|
|
979
|
+
server.introspector.load()
|
|
980
|
+
functions = server.introspector.get_functions()
|
|
981
|
+
walkers = server.introspector.get_walkers()
|
|
982
|
+
client_exports = server.introspector._client_manifest.get("exports", [])
|
|
983
|
+
|
|
984
|
+
def section(title: str, auth: str = "") -> None:
|
|
985
|
+
"""Print section header."""
|
|
986
|
+
print(f"\n{title}{f' ({auth})' if auth else ''}\n{'-' * 80}")
|
|
987
|
+
|
|
988
|
+
def endpoint(method: str, path: str, desc: str, details: str = "") -> None:
|
|
989
|
+
"""Print endpoint with description."""
|
|
990
|
+
print(f"\n{method:6} {path}")
|
|
991
|
+
print(f" {desc}")
|
|
992
|
+
if details:
|
|
993
|
+
print(f" {details}")
|
|
994
|
+
|
|
995
|
+
def format_param(name: str, info: dict[str, Any]) -> str:
|
|
996
|
+
"""Format parameter info."""
|
|
997
|
+
req = "required" if info["required"] else "optional"
|
|
998
|
+
default = f", default: {info['default']}" if info.get("default") else ""
|
|
999
|
+
return f'{name}: {info["type"]} ({req}{default})'
|
|
1000
|
+
|
|
1001
|
+
# Header
|
|
1002
|
+
print("\n" + "=" * 80)
|
|
1003
|
+
print(f"JAC API SERVER - {server.module_name}")
|
|
1004
|
+
print("=" * 80)
|
|
1005
|
+
|
|
1006
|
+
# Auth endpoints
|
|
1007
|
+
section("AUTHENTICATION")
|
|
1008
|
+
endpoint(
|
|
1009
|
+
"POST",
|
|
1010
|
+
"/user/create",
|
|
1011
|
+
"Create new user account",
|
|
1012
|
+
'Body: { "username": str, "password": str }',
|
|
1013
|
+
)
|
|
1014
|
+
endpoint(
|
|
1015
|
+
"POST",
|
|
1016
|
+
"/user/login",
|
|
1017
|
+
"Authenticate and get token",
|
|
1018
|
+
'Body: { "username": str, "password": str }',
|
|
1019
|
+
)
|
|
1020
|
+
|
|
1021
|
+
# Introspection
|
|
1022
|
+
section("INTROSPECTION", "Authenticated")
|
|
1023
|
+
endpoint("GET", "/functions", "List all functions → string[]")
|
|
1024
|
+
endpoint("GET", "/walkers", "List all walkers → string[]")
|
|
1025
|
+
|
|
1026
|
+
# Functions
|
|
1027
|
+
if functions:
|
|
1028
|
+
section("FUNCTIONS", "Authenticated")
|
|
1029
|
+
for name, func in functions.items():
|
|
1030
|
+
sig = server.introspector.introspect_callable(func)
|
|
1031
|
+
params = [format_param(n, i) for n, i in sig["parameters"].items()]
|
|
1032
|
+
params_str = ", ".join(params) if params else "none"
|
|
1033
|
+
|
|
1034
|
+
endpoint("GET", f"/function/{name}", "Get signature")
|
|
1035
|
+
endpoint(
|
|
1036
|
+
"POST",
|
|
1037
|
+
f"/function/{name}",
|
|
1038
|
+
f"Call function → {sig['return_type']}",
|
|
1039
|
+
f"Args: {{ {params_str} }}" if params else "No arguments",
|
|
1040
|
+
)
|
|
1041
|
+
|
|
1042
|
+
# Walkers
|
|
1043
|
+
if walkers:
|
|
1044
|
+
section("WALKERS", "Authenticated")
|
|
1045
|
+
for name, walker_cls in walkers.items():
|
|
1046
|
+
info = server.introspector.introspect_walker(walker_cls)
|
|
1047
|
+
fields = [format_param(n, i) for n, i in info["fields"].items()]
|
|
1048
|
+
fields_str = ", ".join(fields[:3])
|
|
1049
|
+
if len(fields) > 3:
|
|
1050
|
+
fields_str += f", ... (+{len(fields) - 3} more)"
|
|
1051
|
+
|
|
1052
|
+
endpoint("GET", f"/walker/{name}", "Get walker fields")
|
|
1053
|
+
endpoint(
|
|
1054
|
+
"POST",
|
|
1055
|
+
f"/walker/{name}",
|
|
1056
|
+
"Spawn walker",
|
|
1057
|
+
f"Fields: {{ {fields_str} }}" if fields else "No fields",
|
|
1058
|
+
)
|
|
1059
|
+
|
|
1060
|
+
# Client pages
|
|
1061
|
+
section("CLIENT PAGES", "Public")
|
|
1062
|
+
if client_exports:
|
|
1063
|
+
funcs_list = ", ".join(sorted(client_exports)[:8])
|
|
1064
|
+
if len(client_exports) > 8:
|
|
1065
|
+
funcs_list += f" (+{len(client_exports) - 8} more)"
|
|
1066
|
+
print(f"\nAvailable ({len(client_exports)}): {funcs_list}")
|
|
1067
|
+
endpoint(
|
|
1068
|
+
"GET",
|
|
1069
|
+
"/page/<name>",
|
|
1070
|
+
"Render HTML for client function",
|
|
1071
|
+
"Example: /page/App?arg1=value1",
|
|
1072
|
+
)
|
|
1073
|
+
else:
|
|
1074
|
+
print("\nNo client functions. Define with 'cl def' for browser-side rendering.")
|
|
1075
|
+
|
|
1076
|
+
# Static
|
|
1077
|
+
section("STATIC")
|
|
1078
|
+
endpoint("GET", "/", "API information and endpoint list")
|
|
1079
|
+
endpoint("GET", "/static/client.js", "Client JavaScript bundle")
|
|
1080
|
+
|
|
1081
|
+
# Summary
|
|
1082
|
+
total = 2 + 2 + len(functions) * 2 + len(walkers) * 2 + 2
|
|
1083
|
+
print("\n" + "=" * 80)
|
|
1084
|
+
print(
|
|
1085
|
+
f"TOTAL: {len(functions)} functions · {len(walkers)} walkers · "
|
|
1086
|
+
f"{len(client_exports)} client functions · {total} endpoints"
|
|
1087
|
+
)
|
|
1088
|
+
print("=" * 80)
|
|
1089
|
+
print("\nAuth: Bearer token (Authorization: Bearer <token>)\n")
|