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.
Files changed (131) hide show
  1. hyperpocket/__init__.py +7 -0
  2. hyperpocket/auth/README.KR.md +309 -0
  3. hyperpocket/auth/README.md +323 -0
  4. hyperpocket/auth/__init__.py +24 -0
  5. hyperpocket/auth/calendly/__init__.py +0 -0
  6. hyperpocket/auth/calendly/context.py +13 -0
  7. hyperpocket/auth/calendly/oauth2_context.py +25 -0
  8. hyperpocket/auth/calendly/oauth2_handler.py +146 -0
  9. hyperpocket/auth/calendly/oauth2_schema.py +16 -0
  10. hyperpocket/auth/context.py +38 -0
  11. hyperpocket/auth/github/__init__.py +0 -0
  12. hyperpocket/auth/github/context.py +13 -0
  13. hyperpocket/auth/github/oauth2_context.py +25 -0
  14. hyperpocket/auth/github/oauth2_handler.py +143 -0
  15. hyperpocket/auth/github/oauth2_schema.py +16 -0
  16. hyperpocket/auth/github/token_context.py +12 -0
  17. hyperpocket/auth/github/token_handler.py +79 -0
  18. hyperpocket/auth/github/token_schema.py +9 -0
  19. hyperpocket/auth/google/__init__.py +0 -0
  20. hyperpocket/auth/google/context.py +15 -0
  21. hyperpocket/auth/google/oauth2_context.py +31 -0
  22. hyperpocket/auth/google/oauth2_handler.py +137 -0
  23. hyperpocket/auth/google/oauth2_schema.py +18 -0
  24. hyperpocket/auth/handler.py +171 -0
  25. hyperpocket/auth/linear/__init__.py +0 -0
  26. hyperpocket/auth/linear/context.py +15 -0
  27. hyperpocket/auth/linear/token_context.py +15 -0
  28. hyperpocket/auth/linear/token_handler.py +68 -0
  29. hyperpocket/auth/linear/token_schema.py +9 -0
  30. hyperpocket/auth/provider.py +16 -0
  31. hyperpocket/auth/schema.py +19 -0
  32. hyperpocket/auth/slack/__init__.py +0 -0
  33. hyperpocket/auth/slack/context.py +15 -0
  34. hyperpocket/auth/slack/oauth2_context.py +40 -0
  35. hyperpocket/auth/slack/oauth2_handler.py +151 -0
  36. hyperpocket/auth/slack/oauth2_schema.py +40 -0
  37. hyperpocket/auth/slack/tests/__init__.py +0 -0
  38. hyperpocket/auth/slack/tests/test_oauth2_handler.py +32 -0
  39. hyperpocket/auth/slack/tests/test_token_handler.py +23 -0
  40. hyperpocket/auth/slack/token_context.py +14 -0
  41. hyperpocket/auth/slack/token_handler.py +64 -0
  42. hyperpocket/auth/slack/token_schema.py +9 -0
  43. hyperpocket/auth/tests/__init__.py +0 -0
  44. hyperpocket/auth/tests/test_google_oauth2_handler.py +147 -0
  45. hyperpocket/auth/tests/test_slack_oauth2_handler.py +147 -0
  46. hyperpocket/auth/tests/test_slack_token_handler.py +66 -0
  47. hyperpocket/cli/__init__.py +0 -0
  48. hyperpocket/cli/__main__.py +12 -0
  49. hyperpocket/cli/pull.py +18 -0
  50. hyperpocket/cli/sync.py +17 -0
  51. hyperpocket/config/__init__.py +9 -0
  52. hyperpocket/config/auth.py +36 -0
  53. hyperpocket/config/git.py +17 -0
  54. hyperpocket/config/logger.py +81 -0
  55. hyperpocket/config/session.py +35 -0
  56. hyperpocket/config/settings.py +62 -0
  57. hyperpocket/constants.py +0 -0
  58. hyperpocket/curated_tools.py +10 -0
  59. hyperpocket/external/__init__.py +7 -0
  60. hyperpocket/external/github_client.py +19 -0
  61. hyperpocket/futures/__init__.py +7 -0
  62. hyperpocket/futures/futurestore.py +48 -0
  63. hyperpocket/pocket_auth.py +344 -0
  64. hyperpocket/pocket_main.py +351 -0
  65. hyperpocket/prompts.py +15 -0
  66. hyperpocket/repository/__init__.py +5 -0
  67. hyperpocket/repository/lock.py +156 -0
  68. hyperpocket/repository/lockfile.py +56 -0
  69. hyperpocket/repository/repository.py +18 -0
  70. hyperpocket/server/__init__.py +3 -0
  71. hyperpocket/server/auth/__init__.py +15 -0
  72. hyperpocket/server/auth/calendly.py +16 -0
  73. hyperpocket/server/auth/github.py +25 -0
  74. hyperpocket/server/auth/google.py +16 -0
  75. hyperpocket/server/auth/linear.py +18 -0
  76. hyperpocket/server/auth/slack.py +28 -0
  77. hyperpocket/server/auth/token.py +51 -0
  78. hyperpocket/server/proxy.py +63 -0
  79. hyperpocket/server/server.py +178 -0
  80. hyperpocket/server/tool/__init__.py +10 -0
  81. hyperpocket/server/tool/dto/__init__.py +0 -0
  82. hyperpocket/server/tool/dto/script.py +15 -0
  83. hyperpocket/server/tool/wasm.py +31 -0
  84. hyperpocket/session/README.KR.md +62 -0
  85. hyperpocket/session/README.md +61 -0
  86. hyperpocket/session/__init__.py +4 -0
  87. hyperpocket/session/in_memory.py +76 -0
  88. hyperpocket/session/interface.py +118 -0
  89. hyperpocket/session/redis.py +126 -0
  90. hyperpocket/session/tests/__init__.py +0 -0
  91. hyperpocket/session/tests/test_in_memory.py +145 -0
  92. hyperpocket/session/tests/test_redis.py +151 -0
  93. hyperpocket/tests/__init__.py +0 -0
  94. hyperpocket/tests/test_pocket.py +118 -0
  95. hyperpocket/tests/test_pocket_auth.py +982 -0
  96. hyperpocket/tool/README.KR.md +68 -0
  97. hyperpocket/tool/README.md +75 -0
  98. hyperpocket/tool/__init__.py +13 -0
  99. hyperpocket/tool/builtins/__init__.py +0 -0
  100. hyperpocket/tool/builtins/example/__init__.py +0 -0
  101. hyperpocket/tool/builtins/example/add_tool.py +18 -0
  102. hyperpocket/tool/function/README.KR.md +159 -0
  103. hyperpocket/tool/function/README.md +169 -0
  104. hyperpocket/tool/function/__init__.py +9 -0
  105. hyperpocket/tool/function/annotation.py +30 -0
  106. hyperpocket/tool/function/tool.py +87 -0
  107. hyperpocket/tool/tests/__init__.py +0 -0
  108. hyperpocket/tool/tests/test_function_tool.py +266 -0
  109. hyperpocket/tool/tool.py +106 -0
  110. hyperpocket/tool/wasm/README.KR.md +144 -0
  111. hyperpocket/tool/wasm/README.md +144 -0
  112. hyperpocket/tool/wasm/__init__.py +3 -0
  113. hyperpocket/tool/wasm/browser.py +63 -0
  114. hyperpocket/tool/wasm/invoker.py +41 -0
  115. hyperpocket/tool/wasm/script.py +82 -0
  116. hyperpocket/tool/wasm/templates/__init__.py +28 -0
  117. hyperpocket/tool/wasm/templates/node.py +87 -0
  118. hyperpocket/tool/wasm/templates/python.py +75 -0
  119. hyperpocket/tool/wasm/tool.py +147 -0
  120. hyperpocket/util/__init__.py +1 -0
  121. hyperpocket/util/extract_func_param_desc_from_docstring.py +97 -0
  122. hyperpocket/util/find_all_leaf_class_in_package.py +17 -0
  123. hyperpocket/util/find_all_subclass_in_package.py +29 -0
  124. hyperpocket/util/flatten_json_schema.py +45 -0
  125. hyperpocket/util/function_to_model.py +46 -0
  126. hyperpocket/util/get_objects_from_subpackage.py +28 -0
  127. hyperpocket/util/json_schema_to_model.py +69 -0
  128. hyperpocket-0.0.1.dist-info/METADATA +304 -0
  129. hyperpocket-0.0.1.dist-info/RECORD +131 -0
  130. hyperpocket-0.0.1.dist-info/WHEEL +4 -0
  131. 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)