hyperpocket 0.0.1__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- hyperpocket/__init__.py +7 -0
- hyperpocket/auth/README.KR.md +309 -0
- hyperpocket/auth/README.md +323 -0
- hyperpocket/auth/__init__.py +24 -0
- hyperpocket/auth/calendly/__init__.py +0 -0
- hyperpocket/auth/calendly/context.py +13 -0
- hyperpocket/auth/calendly/oauth2_context.py +25 -0
- hyperpocket/auth/calendly/oauth2_handler.py +146 -0
- hyperpocket/auth/calendly/oauth2_schema.py +16 -0
- hyperpocket/auth/context.py +38 -0
- hyperpocket/auth/github/__init__.py +0 -0
- hyperpocket/auth/github/context.py +13 -0
- hyperpocket/auth/github/oauth2_context.py +25 -0
- hyperpocket/auth/github/oauth2_handler.py +143 -0
- hyperpocket/auth/github/oauth2_schema.py +16 -0
- hyperpocket/auth/github/token_context.py +12 -0
- hyperpocket/auth/github/token_handler.py +79 -0
- hyperpocket/auth/github/token_schema.py +9 -0
- hyperpocket/auth/google/__init__.py +0 -0
- hyperpocket/auth/google/context.py +15 -0
- hyperpocket/auth/google/oauth2_context.py +31 -0
- hyperpocket/auth/google/oauth2_handler.py +137 -0
- hyperpocket/auth/google/oauth2_schema.py +18 -0
- hyperpocket/auth/handler.py +171 -0
- hyperpocket/auth/linear/__init__.py +0 -0
- hyperpocket/auth/linear/context.py +15 -0
- hyperpocket/auth/linear/token_context.py +15 -0
- hyperpocket/auth/linear/token_handler.py +68 -0
- hyperpocket/auth/linear/token_schema.py +9 -0
- hyperpocket/auth/provider.py +16 -0
- hyperpocket/auth/schema.py +19 -0
- hyperpocket/auth/slack/__init__.py +0 -0
- hyperpocket/auth/slack/context.py +15 -0
- hyperpocket/auth/slack/oauth2_context.py +40 -0
- hyperpocket/auth/slack/oauth2_handler.py +151 -0
- hyperpocket/auth/slack/oauth2_schema.py +40 -0
- hyperpocket/auth/slack/tests/__init__.py +0 -0
- hyperpocket/auth/slack/tests/test_oauth2_handler.py +32 -0
- hyperpocket/auth/slack/tests/test_token_handler.py +23 -0
- hyperpocket/auth/slack/token_context.py +14 -0
- hyperpocket/auth/slack/token_handler.py +64 -0
- hyperpocket/auth/slack/token_schema.py +9 -0
- hyperpocket/auth/tests/__init__.py +0 -0
- hyperpocket/auth/tests/test_google_oauth2_handler.py +147 -0
- hyperpocket/auth/tests/test_slack_oauth2_handler.py +147 -0
- hyperpocket/auth/tests/test_slack_token_handler.py +66 -0
- hyperpocket/cli/__init__.py +0 -0
- hyperpocket/cli/__main__.py +12 -0
- hyperpocket/cli/pull.py +18 -0
- hyperpocket/cli/sync.py +17 -0
- hyperpocket/config/__init__.py +9 -0
- hyperpocket/config/auth.py +36 -0
- hyperpocket/config/git.py +17 -0
- hyperpocket/config/logger.py +81 -0
- hyperpocket/config/session.py +35 -0
- hyperpocket/config/settings.py +62 -0
- hyperpocket/constants.py +0 -0
- hyperpocket/curated_tools.py +10 -0
- hyperpocket/external/__init__.py +7 -0
- hyperpocket/external/github_client.py +19 -0
- hyperpocket/futures/__init__.py +7 -0
- hyperpocket/futures/futurestore.py +48 -0
- hyperpocket/pocket_auth.py +344 -0
- hyperpocket/pocket_main.py +351 -0
- hyperpocket/prompts.py +15 -0
- hyperpocket/repository/__init__.py +5 -0
- hyperpocket/repository/lock.py +156 -0
- hyperpocket/repository/lockfile.py +56 -0
- hyperpocket/repository/repository.py +18 -0
- hyperpocket/server/__init__.py +3 -0
- hyperpocket/server/auth/__init__.py +15 -0
- hyperpocket/server/auth/calendly.py +16 -0
- hyperpocket/server/auth/github.py +25 -0
- hyperpocket/server/auth/google.py +16 -0
- hyperpocket/server/auth/linear.py +18 -0
- hyperpocket/server/auth/slack.py +28 -0
- hyperpocket/server/auth/token.py +51 -0
- hyperpocket/server/proxy.py +63 -0
- hyperpocket/server/server.py +178 -0
- hyperpocket/server/tool/__init__.py +10 -0
- hyperpocket/server/tool/dto/__init__.py +0 -0
- hyperpocket/server/tool/dto/script.py +15 -0
- hyperpocket/server/tool/wasm.py +31 -0
- hyperpocket/session/README.KR.md +62 -0
- hyperpocket/session/README.md +61 -0
- hyperpocket/session/__init__.py +4 -0
- hyperpocket/session/in_memory.py +76 -0
- hyperpocket/session/interface.py +118 -0
- hyperpocket/session/redis.py +126 -0
- hyperpocket/session/tests/__init__.py +0 -0
- hyperpocket/session/tests/test_in_memory.py +145 -0
- hyperpocket/session/tests/test_redis.py +151 -0
- hyperpocket/tests/__init__.py +0 -0
- hyperpocket/tests/test_pocket.py +118 -0
- hyperpocket/tests/test_pocket_auth.py +982 -0
- hyperpocket/tool/README.KR.md +68 -0
- hyperpocket/tool/README.md +75 -0
- hyperpocket/tool/__init__.py +13 -0
- hyperpocket/tool/builtins/__init__.py +0 -0
- hyperpocket/tool/builtins/example/__init__.py +0 -0
- hyperpocket/tool/builtins/example/add_tool.py +18 -0
- hyperpocket/tool/function/README.KR.md +159 -0
- hyperpocket/tool/function/README.md +169 -0
- hyperpocket/tool/function/__init__.py +9 -0
- hyperpocket/tool/function/annotation.py +30 -0
- hyperpocket/tool/function/tool.py +87 -0
- hyperpocket/tool/tests/__init__.py +0 -0
- hyperpocket/tool/tests/test_function_tool.py +266 -0
- hyperpocket/tool/tool.py +106 -0
- hyperpocket/tool/wasm/README.KR.md +144 -0
- hyperpocket/tool/wasm/README.md +144 -0
- hyperpocket/tool/wasm/__init__.py +3 -0
- hyperpocket/tool/wasm/browser.py +63 -0
- hyperpocket/tool/wasm/invoker.py +41 -0
- hyperpocket/tool/wasm/script.py +82 -0
- hyperpocket/tool/wasm/templates/__init__.py +28 -0
- hyperpocket/tool/wasm/templates/node.py +87 -0
- hyperpocket/tool/wasm/templates/python.py +75 -0
- hyperpocket/tool/wasm/tool.py +147 -0
- hyperpocket/util/__init__.py +1 -0
- hyperpocket/util/extract_func_param_desc_from_docstring.py +97 -0
- hyperpocket/util/find_all_leaf_class_in_package.py +17 -0
- hyperpocket/util/find_all_subclass_in_package.py +29 -0
- hyperpocket/util/flatten_json_schema.py +45 -0
- hyperpocket/util/function_to_model.py +46 -0
- hyperpocket/util/get_objects_from_subpackage.py +28 -0
- hyperpocket/util/json_schema_to_model.py +69 -0
- hyperpocket-0.0.1.dist-info/METADATA +304 -0
- hyperpocket-0.0.1.dist-info/RECORD +131 -0
- hyperpocket-0.0.1.dist-info/WHEEL +4 -0
- hyperpocket-0.0.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,63 @@
|
|
1
|
+
import asyncio
|
2
|
+
|
3
|
+
from playwright.async_api import async_playwright, Page, Playwright, BrowserContext, Route
|
4
|
+
|
5
|
+
|
6
|
+
class InvokerBrowser(object):
|
7
|
+
_instance: 'InvokerBrowser' = None
|
8
|
+
_lock = asyncio.Lock()
|
9
|
+
playwright: Playwright
|
10
|
+
browser_context: BrowserContext
|
11
|
+
|
12
|
+
def __init__(self):
|
13
|
+
raise RuntimeError("Use InvokerBrowser.get_instance() instead")
|
14
|
+
|
15
|
+
async def _async_init(self):
|
16
|
+
# false only in dev
|
17
|
+
# TODO(moon.dev) : load from config by environment
|
18
|
+
import os
|
19
|
+
pocket_env = os.getenv("POCKET_ENV", "DEVELOPMENT")
|
20
|
+
is_headless = False if pocket_env == "DEVELOPMENT" else True
|
21
|
+
|
22
|
+
self.playwright = await async_playwright().start()
|
23
|
+
self.browser_context = await self.playwright.chromium.launch_persistent_context(
|
24
|
+
headless=is_headless,
|
25
|
+
args=[
|
26
|
+
'--disable-web-security=True',
|
27
|
+
],
|
28
|
+
user_data_dir='/tmp/chrome_dev_user',
|
29
|
+
)
|
30
|
+
|
31
|
+
@classmethod
|
32
|
+
async def get_instance(cls):
|
33
|
+
if not cls._instance:
|
34
|
+
async with cls._lock:
|
35
|
+
if cls._instance is None:
|
36
|
+
instance = cls.__new__(cls)
|
37
|
+
await instance._async_init()
|
38
|
+
cls._instance = instance
|
39
|
+
return cls._instance
|
40
|
+
|
41
|
+
async def new_page(self) -> Page:
|
42
|
+
page = await self.browser_context.new_page()
|
43
|
+
|
44
|
+
async def _hijack_route(route: Route):
|
45
|
+
response = await route.fetch()
|
46
|
+
body = await response.body()
|
47
|
+
await route.fulfill(
|
48
|
+
response=response,
|
49
|
+
body=body,
|
50
|
+
headers={
|
51
|
+
**response.headers,
|
52
|
+
'Cross-Origin-Opener-Policy': 'same-origin',
|
53
|
+
'Cross-Origin-Embedder-Policy': 'require-corp',
|
54
|
+
'Cross-Origin-Resource-Policy': 'cross-origin',
|
55
|
+
}
|
56
|
+
)
|
57
|
+
|
58
|
+
await page.route('**/*', _hijack_route)
|
59
|
+
return page
|
60
|
+
|
61
|
+
async def teardown(self):
|
62
|
+
await self.browser_context.close()
|
63
|
+
await self.playwright.stop()
|
@@ -0,0 +1,41 @@
|
|
1
|
+
import asyncio
|
2
|
+
import json
|
3
|
+
import uuid
|
4
|
+
from typing import Any
|
5
|
+
from urllib.parse import urljoin
|
6
|
+
|
7
|
+
from hyperpocket.config import config
|
8
|
+
from hyperpocket.futures import FutureStore
|
9
|
+
from hyperpocket.tool.wasm.browser import InvokerBrowser
|
10
|
+
from hyperpocket.tool.wasm.script import ScriptRuntime, ScriptStore, Script
|
11
|
+
from hyperpocket.tool.wasm.templates import render
|
12
|
+
|
13
|
+
|
14
|
+
class WasmInvoker(object):
|
15
|
+
def invoke(self,
|
16
|
+
tool_path: str,
|
17
|
+
runtime: ScriptRuntime,
|
18
|
+
body: Any,
|
19
|
+
envs: dict,
|
20
|
+
**kwargs) -> str:
|
21
|
+
loop = asyncio.get_running_loop()
|
22
|
+
return loop.run_until_complete(self.ainvoke(tool_path, runtime, body, envs, **kwargs))
|
23
|
+
|
24
|
+
async def ainvoke(self,
|
25
|
+
tool_path: str,
|
26
|
+
runtime: ScriptRuntime,
|
27
|
+
body: Any,
|
28
|
+
envs: dict,
|
29
|
+
**kwargs) -> str:
|
30
|
+
uid = str(uuid.uuid4())
|
31
|
+
html = render(runtime.value, uid, envs, json.dumps(body))
|
32
|
+
script = Script(id=uid, tool_path=tool_path, rendered_html=html, runtime=runtime)
|
33
|
+
ScriptStore.add_script(script=script)
|
34
|
+
future_data = FutureStore.create_future(uid=uid)
|
35
|
+
browser = await InvokerBrowser.get_instance()
|
36
|
+
page = await browser.new_page()
|
37
|
+
url = urljoin(config.internal_base_url + '/', f'tools/wasm/scripts/{uid}/browse')
|
38
|
+
await page.goto(url)
|
39
|
+
stdout = await future_data.future
|
40
|
+
await page.close()
|
41
|
+
return stdout
|
@@ -0,0 +1,82 @@
|
|
1
|
+
import base64
|
2
|
+
import enum
|
3
|
+
import pathlib
|
4
|
+
from typing import Optional
|
5
|
+
|
6
|
+
from pydantic import BaseModel
|
7
|
+
|
8
|
+
|
9
|
+
class ScriptRuntime(enum.Enum):
|
10
|
+
Node = "node"
|
11
|
+
Python = "python"
|
12
|
+
Wasm = "wasm"
|
13
|
+
|
14
|
+
_RuntimePackageFiles = {
|
15
|
+
ScriptRuntime.Node: ["dist/index.js"],
|
16
|
+
ScriptRuntime.Python: ["main.py", "requirements.txt"],
|
17
|
+
ScriptRuntime.Wasm: ["dist/index.wasm"],
|
18
|
+
}
|
19
|
+
|
20
|
+
class ScriptFileNodeContent(BaseModel):
|
21
|
+
contents: str
|
22
|
+
|
23
|
+
class ScriptFileNode(BaseModel):
|
24
|
+
directory: Optional[dict[str, 'ScriptFileNode']] = None
|
25
|
+
file: Optional[ScriptFileNodeContent] = None
|
26
|
+
|
27
|
+
@classmethod
|
28
|
+
def create_file_tree(cls, path: str, contents: str) -> dict[str, 'ScriptFileNode']:
|
29
|
+
path_split = path.split("/")
|
30
|
+
if len(path_split) == 1:
|
31
|
+
return {path_split[0]: ScriptFileNode(file=ScriptFileNodeContent(contents=contents))}
|
32
|
+
node = cls.create_file_tree('/'.join(path_split[1:]), contents)
|
33
|
+
return {path_split[0]: ScriptFileNode(directory=node)}
|
34
|
+
|
35
|
+
@staticmethod
|
36
|
+
def merge(a: dict[str, 'ScriptFileNode'], b: [str, 'ScriptFileNode']) -> dict[str, 'ScriptFileNode']:
|
37
|
+
for k, v in b.items():
|
38
|
+
if k in a:
|
39
|
+
if a[k].directory and v.directory:
|
40
|
+
a[k].directory = ScriptFileNode.merge(a[k].directory, v.directory)
|
41
|
+
elif a[k].file and v.file:
|
42
|
+
a[k].file = v.file
|
43
|
+
else:
|
44
|
+
a[k] = v
|
45
|
+
return a
|
46
|
+
|
47
|
+
|
48
|
+
class Script(BaseModel):
|
49
|
+
id: str
|
50
|
+
tool_path: str
|
51
|
+
rendered_html: str
|
52
|
+
runtime: ScriptRuntime
|
53
|
+
|
54
|
+
def load_file_tree(self) -> dict[str, ScriptFileNode]:
|
55
|
+
relpaths = _RuntimePackageFiles[self.runtime]
|
56
|
+
file_tree = dict()
|
57
|
+
for p in relpaths:
|
58
|
+
filepath = pathlib.Path(self.tool_path) / p
|
59
|
+
with filepath.open("r") as f:
|
60
|
+
contents = f.read().encode('utf-8')
|
61
|
+
encoded_bytes = base64.b64encode(contents)
|
62
|
+
encoded_str = encoded_bytes.decode()
|
63
|
+
file_tree = ScriptFileNode.merge(file_tree, ScriptFileNode.create_file_tree(p, encoded_str))
|
64
|
+
return file_tree
|
65
|
+
|
66
|
+
|
67
|
+
class _ScriptStore(object):
|
68
|
+
scripts: dict[str, Script] = {}
|
69
|
+
|
70
|
+
def __init__(self):
|
71
|
+
self.rendered_html = {}
|
72
|
+
|
73
|
+
def add_script(self, script: Script):
|
74
|
+
if script.id in self.scripts:
|
75
|
+
raise ValueError("Script id already exists")
|
76
|
+
self.scripts[script.id] = script
|
77
|
+
|
78
|
+
def get_script(self, script_id: str) -> Script:
|
79
|
+
# ValueError exception is intentional
|
80
|
+
return self.scripts[script_id]
|
81
|
+
|
82
|
+
ScriptStore = _ScriptStore()
|
@@ -0,0 +1,28 @@
|
|
1
|
+
import base64
|
2
|
+
import json
|
3
|
+
|
4
|
+
from jinja2 import Environment, DictLoader
|
5
|
+
|
6
|
+
from hyperpocket.tool.wasm.templates.node import node_template
|
7
|
+
from hyperpocket.tool.wasm.templates.python import python_template
|
8
|
+
|
9
|
+
TemplateEnvironments = Environment(
|
10
|
+
loader=DictLoader({
|
11
|
+
'python.html': python_template,
|
12
|
+
'node.html': node_template,
|
13
|
+
}),
|
14
|
+
autoescape=False
|
15
|
+
)
|
16
|
+
|
17
|
+
|
18
|
+
def render(language: str, script_id: str, env: dict[str, str], body: str, **kwargs) -> str:
|
19
|
+
env_json = json.dumps(env)
|
20
|
+
template = TemplateEnvironments.get_template(f'{language.lower()}.html')
|
21
|
+
body_bytes = body.encode('utf-8')
|
22
|
+
body_b64_bytes = base64.b64encode(body_bytes)
|
23
|
+
body_b64 = body_b64_bytes.decode('ascii')
|
24
|
+
return template.render(**{
|
25
|
+
'SCRIPT_ID': script_id,
|
26
|
+
'ENV_JSON': env_json,
|
27
|
+
'BODY_JSON_B64': body_b64,
|
28
|
+
} | kwargs)
|
@@ -0,0 +1,87 @@
|
|
1
|
+
node_template = '''
|
2
|
+
<!DOCTYPE html>
|
3
|
+
<html lang="en">
|
4
|
+
<head>
|
5
|
+
<meta charset="UTF-8">
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
7
|
+
<title>PyScript Offline</title>
|
8
|
+
</head>
|
9
|
+
<body>
|
10
|
+
<script type="module">
|
11
|
+
function loadConfig() {
|
12
|
+
globalThis.toolConfigs = {
|
13
|
+
envs: `{{ ENV_JSON }}`,
|
14
|
+
body: `{{ BODY_JSON_B64 }}`,
|
15
|
+
scriptID: `{{ SCRIPT_ID }}`
|
16
|
+
}
|
17
|
+
}
|
18
|
+
import { WebContainer } from 'https://esm.run/@webcontainer/api@1.5.1';
|
19
|
+
function decodeContent(content) {
|
20
|
+
return Uint8Array.from(atob(content), c => c.charCodeAt(0));
|
21
|
+
}
|
22
|
+
function decodeFileTree(filetree) {
|
23
|
+
const decoded = {};
|
24
|
+
for (const [key, value] of Object.entries(filetree)) {
|
25
|
+
if (value.file) {
|
26
|
+
decoded[key] = {
|
27
|
+
file: {
|
28
|
+
contents: decodeContent(value.file.contents)
|
29
|
+
}
|
30
|
+
}
|
31
|
+
} else if (value.directory) {
|
32
|
+
decoded[key] = {
|
33
|
+
directory: decodeFileTree(value.directory)
|
34
|
+
}
|
35
|
+
} else if (value.symlink) {
|
36
|
+
decoded[key] = {
|
37
|
+
symlink: value.symlink
|
38
|
+
}
|
39
|
+
}
|
40
|
+
}
|
41
|
+
return decoded;
|
42
|
+
}
|
43
|
+
async function main() {
|
44
|
+
loadConfig();
|
45
|
+
const b64FilesResp = await fetch(`/tools/wasm/scripts/${globalThis.toolConfigs.scriptID}/file_tree`);
|
46
|
+
const b64Files = await b64FilesResp.json();
|
47
|
+
const files = decodeFileTree(b64Files.tree);
|
48
|
+
const webcontainer = await WebContainer.boot();
|
49
|
+
|
50
|
+
await webcontainer.mount(files)
|
51
|
+
const envs = JSON.parse(globalThis.toolConfigs.envs)
|
52
|
+
envs['DEPLOYED'] = 'true'
|
53
|
+
const runProcess = await webcontainer.spawn('node', ['dist/index.js'], {
|
54
|
+
output: true,
|
55
|
+
env: envs,
|
56
|
+
});
|
57
|
+
const stdin = runProcess.input.getWriter();
|
58
|
+
const decodedBytes = atob(globalThis.toolConfigs.body);
|
59
|
+
await (async () => {
|
60
|
+
await stdin.ready
|
61
|
+
await stdin.write(decodedBytes);
|
62
|
+
})()
|
63
|
+
let stdout = '';
|
64
|
+
runProcess.output.pipeTo(
|
65
|
+
new WritableStream({
|
66
|
+
write(chunk) {
|
67
|
+
stdout += chunk;
|
68
|
+
}
|
69
|
+
})
|
70
|
+
)
|
71
|
+
await runProcess.exit;
|
72
|
+
if (stdout.startsWith(decodedBytes)) {
|
73
|
+
stdout = stdout.slice(decodedBytes);
|
74
|
+
}
|
75
|
+
await fetch(`/tools/wasm/scripts/${globalThis.toolConfigs.scriptID}/done`, {
|
76
|
+
method: 'POST',
|
77
|
+
headers: {
|
78
|
+
'Content-Type': 'application/json'
|
79
|
+
},
|
80
|
+
body: JSON.stringify({ stdout })
|
81
|
+
});
|
82
|
+
}
|
83
|
+
main();
|
84
|
+
</script>
|
85
|
+
</body>
|
86
|
+
</html>
|
87
|
+
'''
|
@@ -0,0 +1,75 @@
|
|
1
|
+
python_template = '''
|
2
|
+
<!DOCTYPE html>
|
3
|
+
<html lang="en">
|
4
|
+
<head>
|
5
|
+
<meta charset="UTF-8">
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
7
|
+
<title>PyScript Offline</title>
|
8
|
+
<script src="https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js"></script>
|
9
|
+
</head>
|
10
|
+
<body>
|
11
|
+
<script type="module">
|
12
|
+
function loadConfig() {
|
13
|
+
globalThis.toolConfigs = {
|
14
|
+
envs: `{{ ENV_JSON }}`,
|
15
|
+
body: `{{ BODY_JSON_B64 }}`,
|
16
|
+
scriptID: `{{ SCRIPT_ID }}`
|
17
|
+
}
|
18
|
+
}
|
19
|
+
async function main() {
|
20
|
+
loadConfig();
|
21
|
+
const b64FilesResp = await fetch(`/tools/wasm/scripts/${globalThis.toolConfigs.scriptID}/file_tree`);
|
22
|
+
const b64Files = await b64FilesResp.json();
|
23
|
+
const code = atob(b64Files.tree["main.py"].file.contents);
|
24
|
+
const requirements = atob(b64Files.tree["requirements.txt"].file.contents);
|
25
|
+
|
26
|
+
const pyodide = await loadPyodide({
|
27
|
+
env: JSON.parse(globalThis.toolConfigs.envs),
|
28
|
+
});
|
29
|
+
await pyodide.loadPackage("micropip");
|
30
|
+
await pyodide.loadPackage("ssl");
|
31
|
+
const micropip = pyodide.pyimport("micropip");
|
32
|
+
await micropip.install("pyodide-http")
|
33
|
+
const installation = requirements.split("\\n").map(async (req) => {
|
34
|
+
if (req) {
|
35
|
+
const pkg = req.split("==")[0];
|
36
|
+
await micropip.install(pkg);
|
37
|
+
}
|
38
|
+
});
|
39
|
+
await Promise.all(installation);
|
40
|
+
let emitted = false;
|
41
|
+
const decodedBytes = atob(globalThis.toolConfigs.body);
|
42
|
+
pyodide.setStdin({
|
43
|
+
stdin: () => {
|
44
|
+
if (emitted) {
|
45
|
+
return null;
|
46
|
+
}
|
47
|
+
emitted = true;
|
48
|
+
return decodedBytes;
|
49
|
+
},
|
50
|
+
autoEOF: true,
|
51
|
+
})
|
52
|
+
let stdout = "";
|
53
|
+
pyodide.setStdout({
|
54
|
+
batched: (x) => { stdout += x; },
|
55
|
+
})
|
56
|
+
await pyodide.runPythonAsync(`
|
57
|
+
import pyodide_http
|
58
|
+
pyodide_http.patch_all()
|
59
|
+
|
60
|
+
${code}
|
61
|
+
`);
|
62
|
+
console.log(stdout)
|
63
|
+
await fetch(`/tools/wasm/scripts/${globalThis.toolConfigs.scriptID}/done`, {
|
64
|
+
method: 'POST',
|
65
|
+
headers: {
|
66
|
+
'Content-Type': 'application/json'
|
67
|
+
},
|
68
|
+
body: JSON.stringify({ stdout })
|
69
|
+
});
|
70
|
+
}
|
71
|
+
main();
|
72
|
+
</script>
|
73
|
+
</body>
|
74
|
+
</html>
|
75
|
+
'''
|
@@ -0,0 +1,147 @@
|
|
1
|
+
import json
|
2
|
+
import pathlib
|
3
|
+
from typing import Any, Optional
|
4
|
+
|
5
|
+
import toml
|
6
|
+
|
7
|
+
from hyperpocket.auth import AuthProvider
|
8
|
+
from hyperpocket.config import pocket_logger
|
9
|
+
from hyperpocket.repository import Lock, Lockfile
|
10
|
+
from hyperpocket.repository.lock import GitLock, LocalLock
|
11
|
+
from hyperpocket.tool import Tool, ToolRequest
|
12
|
+
from hyperpocket.tool.tool import ToolAuth
|
13
|
+
from hyperpocket.tool.wasm.invoker import WasmInvoker
|
14
|
+
from hyperpocket.tool.wasm.script import ScriptRuntime
|
15
|
+
|
16
|
+
|
17
|
+
class WasmToolRequest(ToolRequest):
|
18
|
+
lock: Lock
|
19
|
+
rel_path: str
|
20
|
+
|
21
|
+
def __init__(self, lock: Lock, rel_path: str):
|
22
|
+
self.lock = lock
|
23
|
+
self.rel_path = rel_path
|
24
|
+
|
25
|
+
def __str__(self):
|
26
|
+
return f"ToolRequest(lock={self.lock}, rel_path={self.rel_path})"
|
27
|
+
|
28
|
+
|
29
|
+
def from_local(path: str) -> WasmToolRequest:
|
30
|
+
return WasmToolRequest(LocalLock(path), "")
|
31
|
+
|
32
|
+
|
33
|
+
def from_git(repository: str, ref: str, rel_path: str) -> WasmToolRequest:
|
34
|
+
return WasmToolRequest(GitLock(repository_url=repository, git_ref=ref), rel_path)
|
35
|
+
|
36
|
+
|
37
|
+
def from_github(owner: str, repo: str, ref: str, rel_path: str) -> WasmToolRequest:
|
38
|
+
repository = f"https://github.com/{owner}/{repo}"
|
39
|
+
return from_git(repository, ref, rel_path)
|
40
|
+
|
41
|
+
|
42
|
+
class WasmTool(Tool):
|
43
|
+
"""
|
44
|
+
WasmTool is Tool executing local python method.
|
45
|
+
"""
|
46
|
+
|
47
|
+
_invoker: WasmInvoker = None
|
48
|
+
pkg_lock: Lock = None
|
49
|
+
rel_path: str
|
50
|
+
runtime: ScriptRuntime = None
|
51
|
+
json_schema: Optional[dict] = None
|
52
|
+
readme: Optional[str] = None
|
53
|
+
|
54
|
+
@property
|
55
|
+
def invoker(self) -> WasmInvoker:
|
56
|
+
if not self._invoker:
|
57
|
+
self._invoker = WasmInvoker()
|
58
|
+
return self._invoker
|
59
|
+
|
60
|
+
@classmethod
|
61
|
+
def from_tool_request(cls, tool_req: WasmToolRequest, lockfile: Lockfile = None, **kwargs) -> 'WasmTool':
|
62
|
+
if not lockfile:
|
63
|
+
raise ValueError("lockfile is required")
|
64
|
+
tool_req.lock = lockfile.get_lock(tool_req.lock.key())
|
65
|
+
toolpkg_path = tool_req.lock.toolpkg_path()
|
66
|
+
rel_path = tool_req.rel_path
|
67
|
+
rootpath = pathlib.Path(toolpkg_path) / rel_path
|
68
|
+
schema_path = rootpath / "schema.json"
|
69
|
+
config_path = rootpath / "config.toml"
|
70
|
+
readme_path = rootpath / "README.md"
|
71
|
+
|
72
|
+
try:
|
73
|
+
with schema_path.open("r") as f:
|
74
|
+
json_schema = json.load(f)
|
75
|
+
except Exception as e:
|
76
|
+
pocket_logger.warning(f"{toolpkg_path} failed to load json schema. error : {e}")
|
77
|
+
json_schema = None
|
78
|
+
|
79
|
+
try:
|
80
|
+
with config_path.open("r") as f:
|
81
|
+
config = toml.load(f)
|
82
|
+
name = config.get('name')
|
83
|
+
description = config.get('description')
|
84
|
+
if language := config.get('language'):
|
85
|
+
lang = language.lower()
|
86
|
+
if lang == 'python':
|
87
|
+
runtime = ScriptRuntime.Python
|
88
|
+
elif lang == 'node':
|
89
|
+
runtime = ScriptRuntime.Node
|
90
|
+
else:
|
91
|
+
raise ValueError(f"The language `{lang}` is not supported.")
|
92
|
+
else:
|
93
|
+
raise ValueError("`language` field is required in config.toml")
|
94
|
+
auth = cls._get_auth(config)
|
95
|
+
except Exception as e:
|
96
|
+
raise ValueError(f"Failed to load config.toml: {e}")
|
97
|
+
|
98
|
+
if readme_path.exists():
|
99
|
+
with readme_path.open("r") as f:
|
100
|
+
readme = f.read()
|
101
|
+
else:
|
102
|
+
readme = None
|
103
|
+
return cls(
|
104
|
+
name=name,
|
105
|
+
description=description,
|
106
|
+
argument_json_schema=json_schema,
|
107
|
+
auth=auth,
|
108
|
+
runtime=runtime,
|
109
|
+
readme=readme,
|
110
|
+
pkg_lock=tool_req.lock,
|
111
|
+
rel_path=tool_req.rel_path,
|
112
|
+
)
|
113
|
+
|
114
|
+
@classmethod
|
115
|
+
def _get_auth(cls, config: dict) -> Optional[ToolAuth]:
|
116
|
+
auth = config.get("auth")
|
117
|
+
if not auth:
|
118
|
+
return
|
119
|
+
auth_provider = auth.get("auth_provider")
|
120
|
+
auth_handler = auth.get("auth_handler")
|
121
|
+
scopes = auth.get("scopes", [])
|
122
|
+
return ToolAuth(
|
123
|
+
auth_provider=AuthProvider.get_auth_provider(auth_provider),
|
124
|
+
auth_handler=auth_handler,
|
125
|
+
scopes=scopes,
|
126
|
+
)
|
127
|
+
|
128
|
+
def template_arguments(self) -> dict[str, str]:
|
129
|
+
return {}
|
130
|
+
|
131
|
+
def invoke(self, body: Any, envs: dict, **kwargs) -> str:
|
132
|
+
return self.invoker.invoke(
|
133
|
+
str(self.pkg_lock.toolpkg_path() / self.rel_path),
|
134
|
+
self.runtime,
|
135
|
+
body,
|
136
|
+
envs,
|
137
|
+
**kwargs,
|
138
|
+
)
|
139
|
+
|
140
|
+
async def ainvoke(self, body: Any, envs: dict, **kwargs) -> str:
|
141
|
+
return await self.invoker.ainvoke(
|
142
|
+
str(self.pkg_lock.toolpkg_path() / self.rel_path),
|
143
|
+
self.runtime,
|
144
|
+
body,
|
145
|
+
envs,
|
146
|
+
**kwargs,
|
147
|
+
)
|
@@ -0,0 +1 @@
|
|
1
|
+
__all__ = ['json_schema_to_model', "tool_to_open_ai_spec"]
|
@@ -0,0 +1,97 @@
|
|
1
|
+
import inspect
|
2
|
+
import re
|
3
|
+
|
4
|
+
from hyperpocket.config import pocket_logger
|
5
|
+
|
6
|
+
|
7
|
+
def extract_param_docstring_mapping(func) -> dict[str, str]:
|
8
|
+
"""
|
9
|
+
Extracts a mapping between function parameters and their descriptions
|
10
|
+
from the Google-style docstring.
|
11
|
+
|
12
|
+
Args:
|
13
|
+
func (function): The function whose docstring needs to be parsed.
|
14
|
+
|
15
|
+
Returns:
|
16
|
+
list: A list of tuples where each tuple contains a parameter name
|
17
|
+
and its description.
|
18
|
+
"""
|
19
|
+
# Get the docstring of the function
|
20
|
+
docstring = inspect.getdoc(func)
|
21
|
+
func_params = inspect.signature(func).parameters.keys()
|
22
|
+
|
23
|
+
if not docstring:
|
24
|
+
return {}
|
25
|
+
|
26
|
+
pocket_logger.debug(f"try to extract docstring of {func.__name__} by google style..")
|
27
|
+
param_mapping = extract_param_desc_by_google_stype_docstring(docstring, func_params)
|
28
|
+
if param_mapping:
|
29
|
+
pocket_logger.debug(f"success extract docstring of {func.__name__} by google style!")
|
30
|
+
return param_mapping
|
31
|
+
pocket_logger.debug(f"not found param desc of {func.__name__} by google style..")
|
32
|
+
|
33
|
+
pocket_logger.debug(f"try to extract docstring of {func.__name__} by other style..")
|
34
|
+
param_mapping = extract_param_desc_by_other_styles(docstring, func_params)
|
35
|
+
if param_mapping:
|
36
|
+
pocket_logger.debug(f"success extract docstring of {func.__name__} by other style!")
|
37
|
+
return param_mapping
|
38
|
+
pocket_logger.debug(f"not found param desc of {func.__name__} by other styles..")
|
39
|
+
|
40
|
+
# Plain Text Style matching
|
41
|
+
pocket_logger.debug(f"try to extract docstring of {func.__name__} by plain text style..")
|
42
|
+
param_descriptions = []
|
43
|
+
for line in docstring.split("\n"):
|
44
|
+
l = line.strip().split(":")
|
45
|
+
if len(l) < 2:
|
46
|
+
continue
|
47
|
+
|
48
|
+
param_name = l[0]
|
49
|
+
cleaned_param_name = clean_bracket_content(param_name)
|
50
|
+
description = ":".join(l[1:]).strip()
|
51
|
+
if cleaned_param_name in func_params:
|
52
|
+
param_descriptions.append((cleaned_param_name, description))
|
53
|
+
|
54
|
+
# Ensure no duplicates and match with function parameters
|
55
|
+
param_mapping = {param: desc for param, desc in param_descriptions if param in func_params}
|
56
|
+
pocket_logger.debug(f"final param_mapping of {func.__name__} : {param_mapping}")
|
57
|
+
|
58
|
+
return param_mapping
|
59
|
+
|
60
|
+
|
61
|
+
def clean_bracket_content(content):
|
62
|
+
return re.sub(r"[(\[{<].*?[)\]}>]", "", content)
|
63
|
+
|
64
|
+
|
65
|
+
def extract_param_desc_by_other_styles(docstring, func_params) -> dict[str, str]:
|
66
|
+
param_descriptions = []
|
67
|
+
# Pattern for Sphinx-style or Javadoc-style `:param`, `@param`, `:arg`, `@arg`
|
68
|
+
param_pattern = r"(?:@param|:param|:arg|@arg)\s+(\w+)(?:\s*:\s*|\s+|:\s+)(.*)"
|
69
|
+
matches = re.findall(param_pattern, docstring)
|
70
|
+
for param, desc in matches:
|
71
|
+
cleaned_param = clean_bracket_content(param)
|
72
|
+
param_descriptions.append((cleaned_param, desc.strip()))
|
73
|
+
# Ensure no duplicates and match with function parameters
|
74
|
+
param_mapping = {param: desc for param, desc in param_descriptions if param in func_params}
|
75
|
+
return param_mapping
|
76
|
+
|
77
|
+
|
78
|
+
def extract_param_desc_by_google_stype_docstring(docstring, func_params) -> dict[str, str]:
|
79
|
+
# Regex pattern to extract parameter descriptions in Google style
|
80
|
+
param_pattern = r"Args:\n(.*?)(?=\n\S|$)" # Matches the Args: section
|
81
|
+
match = re.search(param_pattern, docstring, re.DOTALL)
|
82
|
+
if not match:
|
83
|
+
return {}
|
84
|
+
param_section = match.group(1)
|
85
|
+
# Parse the parameter names and descriptions
|
86
|
+
param_lines = param_section.split("\n")
|
87
|
+
param_descriptions = {}
|
88
|
+
for line in param_lines:
|
89
|
+
# Match parameter line with "name (type): description"
|
90
|
+
param_match = re.match(r"^\s*(\w+)\s*\(\s*(.*?)\s*\)\s*:\s*(.*)", line)
|
91
|
+
if param_match:
|
92
|
+
param, _, desc = param_match.groups()
|
93
|
+
cleaned_param = clean_bracket_content(param)
|
94
|
+
param_descriptions[cleaned_param] = desc.strip()
|
95
|
+
# Match parameters to descriptions
|
96
|
+
param_mapping = {param: desc for param, desc in param_descriptions.items() if param in func_params}
|
97
|
+
return param_mapping
|
@@ -0,0 +1,17 @@
|
|
1
|
+
from typing import TypeVar, Type, List
|
2
|
+
|
3
|
+
|
4
|
+
from hyperpocket.util.find_all_subclass_in_package import find_all_subclass_in_package
|
5
|
+
|
6
|
+
T = TypeVar("T")
|
7
|
+
|
8
|
+
|
9
|
+
def find_all_leaf_class_in_package(package_name: str, interface_type: Type[T]) -> List[T]:
|
10
|
+
parent_class_set = set()
|
11
|
+
subclasses = find_all_subclass_in_package(package_name, interface_type)
|
12
|
+
|
13
|
+
for sub in subclasses:
|
14
|
+
parent_class_set.add(*sub.__bases__)
|
15
|
+
|
16
|
+
leaf_sub_classes = [sub for sub in subclasses if sub not in parent_class_set]
|
17
|
+
return leaf_sub_classes
|
@@ -0,0 +1,29 @@
|
|
1
|
+
import importlib
|
2
|
+
import inspect
|
3
|
+
import pkgutil
|
4
|
+
from typing import TypeVar, Type, List
|
5
|
+
|
6
|
+
from hyperpocket.config import pocket_logger
|
7
|
+
|
8
|
+
T = TypeVar("T")
|
9
|
+
|
10
|
+
|
11
|
+
def find_all_subclass_in_package(package_name: str, interface_type: Type[T]) -> List[T]:
|
12
|
+
subclasses = set()
|
13
|
+
package = importlib.import_module(package_name)
|
14
|
+
|
15
|
+
for _, module_name, is_pkg in pkgutil.walk_packages(package.__path__, package.__name__ + "."):
|
16
|
+
try:
|
17
|
+
if "tests" in module_name or is_pkg:
|
18
|
+
continue
|
19
|
+
|
20
|
+
module = importlib.import_module(module_name)
|
21
|
+
module_classes = inspect.getmembers(module, inspect.isclass)
|
22
|
+
for _, obj in module_classes:
|
23
|
+
if issubclass(obj, interface_type) and obj is not interface_type:
|
24
|
+
subclasses.add(obj)
|
25
|
+
except ImportError as e:
|
26
|
+
pocket_logger.warning(f"failed to import {module_name}. error : {e}")
|
27
|
+
continue
|
28
|
+
|
29
|
+
return list(subclasses)
|