hyperpocket 0.1.9__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hyperpocket/__init__.py +4 -4
- hyperpocket/auth/__init__.py +12 -7
- hyperpocket/auth/calendly/oauth2_handler.py +24 -17
- hyperpocket/auth/calendly/oauth2_schema.py +3 -1
- hyperpocket/auth/context.py +2 -1
- hyperpocket/auth/github/oauth2_handler.py +13 -8
- hyperpocket/auth/github/token_handler.py +27 -21
- hyperpocket/auth/google/context.py +1 -3
- hyperpocket/auth/google/oauth2_context.py +1 -1
- hyperpocket/auth/google/oauth2_handler.py +22 -17
- hyperpocket/auth/gumloop/token_context.py +1 -4
- hyperpocket/auth/gumloop/token_handler.py +48 -20
- hyperpocket/auth/gumloop/token_schema.py +2 -1
- hyperpocket/auth/handler.py +21 -6
- hyperpocket/auth/linear/token_context.py +2 -5
- hyperpocket/auth/linear/token_handler.py +45 -21
- hyperpocket/auth/notion/context.py +2 -2
- hyperpocket/auth/notion/token_context.py +2 -4
- hyperpocket/auth/notion/token_handler.py +45 -21
- hyperpocket/auth/notion/token_schema.py +0 -1
- hyperpocket/auth/reddit/oauth2_handler.py +9 -10
- hyperpocket/auth/reddit/oauth2_schema.py +0 -2
- hyperpocket/auth/schema.py +4 -1
- hyperpocket/auth/slack/oauth2_context.py +3 -1
- hyperpocket/auth/slack/oauth2_handler.py +55 -35
- hyperpocket/auth/slack/token_context.py +2 -4
- hyperpocket/auth/slack/token_handler.py +42 -19
- hyperpocket/builtin.py +4 -2
- hyperpocket/cli/__main__.py +4 -2
- hyperpocket/cli/auth.py +59 -28
- hyperpocket/cli/codegen/auth/auth_context_template.py +3 -2
- hyperpocket/cli/codegen/auth/auth_token_context_template.py +3 -2
- hyperpocket/cli/codegen/auth/auth_token_handler_template.py +6 -5
- hyperpocket/cli/codegen/auth/auth_token_schema_template.py +3 -2
- hyperpocket/cli/codegen/auth/server_auth_template.py +3 -2
- hyperpocket/cli/pull.py +5 -5
- hyperpocket/config/__init__.py +3 -8
- hyperpocket/config/auth.py +3 -1
- hyperpocket/config/logger.py +20 -15
- hyperpocket/config/session.py +4 -2
- hyperpocket/config/settings.py +19 -2
- hyperpocket/futures/__init__.py +1 -1
- hyperpocket/futures/futurestore.py +3 -2
- hyperpocket/pocket_auth.py +171 -84
- hyperpocket/pocket_core.py +51 -33
- hyperpocket/pocket_main.py +122 -93
- hyperpocket/prompts.py +2 -2
- hyperpocket/repository/__init__.py +1 -1
- hyperpocket/repository/lock.py +47 -33
- hyperpocket/repository/lockfile.py +2 -2
- hyperpocket/repository/repository.py +1 -1
- hyperpocket/server/__init__.py +1 -1
- hyperpocket/server/auth/github.py +2 -1
- hyperpocket/server/auth/linear.py +1 -3
- hyperpocket/server/auth/notion.py +2 -5
- hyperpocket/server/auth/slack.py +1 -3
- hyperpocket/server/auth/token.py +17 -11
- hyperpocket/server/proxy.py +29 -13
- hyperpocket/server/server.py +75 -31
- hyperpocket/server/tool/dto/script.py +15 -10
- hyperpocket/server/tool/wasm.py +14 -11
- hyperpocket/session/__init__.py +6 -2
- hyperpocket/session/in_memory.py +44 -24
- hyperpocket/session/interface.py +42 -24
- hyperpocket/session/redis.py +48 -31
- hyperpocket/tool/__init__.py +10 -10
- hyperpocket/tool/function/__init__.py +1 -5
- hyperpocket/tool/function/annotation.py +11 -9
- hyperpocket/tool/function/tool.py +37 -27
- hyperpocket/tool/tool.py +59 -36
- hyperpocket/tool/wasm/__init__.py +1 -1
- hyperpocket/tool/wasm/browser.py +15 -10
- hyperpocket/tool/wasm/invoker.py +16 -16
- hyperpocket/tool/wasm/script.py +27 -14
- hyperpocket/tool/wasm/templates/__init__.py +22 -15
- hyperpocket/tool/wasm/templates/node.py +2 -2
- hyperpocket/tool/wasm/templates/python.py +2 -2
- hyperpocket/tool/wasm/tool.py +27 -14
- hyperpocket/tool_like.py +3 -3
- hyperpocket/util/__init__.py +1 -1
- hyperpocket/util/extract_func_param_desc_from_docstring.py +33 -14
- hyperpocket/util/find_all_leaf_class_in_package.py +4 -3
- hyperpocket/util/find_all_subclass_in_package.py +4 -2
- hyperpocket/util/flatten_json_schema.py +10 -6
- hyperpocket/util/function_to_model.py +33 -12
- hyperpocket/util/get_objects_from_subpackage.py +1 -1
- hyperpocket/util/json_schema_to_model.py +14 -5
- {hyperpocket-0.1.9.dist-info → hyperpocket-0.2.0.dist-info}/METADATA +29 -24
- hyperpocket-0.2.0.dist-info/RECORD +137 -0
- hyperpocket-0.1.9.dist-info/RECORD +0 -137
- {hyperpocket-0.1.9.dist-info → hyperpocket-0.2.0.dist-info}/WHEEL +0 -0
- {hyperpocket-0.1.9.dist-info → hyperpocket-0.2.0.dist-info}/entry_points.txt +0 -0
hyperpocket/tool/wasm/script.py
CHANGED
@@ -14,29 +14,38 @@ class ScriptRuntime(enum.Enum):
|
|
14
14
|
Python = "python"
|
15
15
|
Wasm = "wasm"
|
16
16
|
|
17
|
+
|
17
18
|
_RuntimePackageFiles = {
|
18
19
|
ScriptRuntime.Node: ["dist/index.js"],
|
19
20
|
ScriptRuntime.Python: ["main.py", "requirements.txt"],
|
20
21
|
ScriptRuntime.Wasm: ["dist/index.wasm"],
|
21
22
|
}
|
22
23
|
|
24
|
+
|
23
25
|
class ScriptFileNodeContent(BaseModel):
|
24
26
|
contents: str
|
25
27
|
|
28
|
+
|
26
29
|
class ScriptFileNode(BaseModel):
|
27
|
-
directory: Optional[dict[str,
|
30
|
+
directory: Optional[dict[str, "ScriptFileNode"]] = None
|
28
31
|
file: Optional[ScriptFileNodeContent] = None
|
29
|
-
|
32
|
+
|
30
33
|
@classmethod
|
31
|
-
def create_file_tree(cls, path: str, contents: str) -> dict[str,
|
34
|
+
def create_file_tree(cls, path: str, contents: str) -> dict[str, "ScriptFileNode"]:
|
32
35
|
path_split = path.split("/")
|
33
36
|
if len(path_split) == 1:
|
34
|
-
return {
|
35
|
-
|
37
|
+
return {
|
38
|
+
path_split[0]: ScriptFileNode(
|
39
|
+
file=ScriptFileNodeContent(contents=contents)
|
40
|
+
)
|
41
|
+
}
|
42
|
+
node = cls.create_file_tree("/".join(path_split[1:]), contents)
|
36
43
|
return {path_split[0]: ScriptFileNode(directory=node)}
|
37
|
-
|
44
|
+
|
38
45
|
@staticmethod
|
39
|
-
def merge(
|
46
|
+
def merge(
|
47
|
+
a: dict[str, "ScriptFileNode"], b: [str, "ScriptFileNode"]
|
48
|
+
) -> dict[str, "ScriptFileNode"]:
|
40
49
|
for k, v in b.items():
|
41
50
|
if k in a:
|
42
51
|
if a[k].directory and v.directory:
|
@@ -53,19 +62,21 @@ class Script(BaseModel):
|
|
53
62
|
tool_path: str
|
54
63
|
rendered_html: str
|
55
64
|
runtime: ScriptRuntime
|
56
|
-
|
65
|
+
|
57
66
|
def load_file_tree(self) -> dict[str, ScriptFileNode]:
|
58
67
|
relpaths = _RuntimePackageFiles[self.runtime]
|
59
68
|
file_tree = dict()
|
60
69
|
for p in relpaths:
|
61
70
|
filepath = pathlib.Path(self.tool_path) / p
|
62
71
|
with filepath.open("r") as f:
|
63
|
-
contents = f.read().encode(
|
72
|
+
contents = f.read().encode("utf-8")
|
64
73
|
encoded_bytes = base64.b64encode(contents)
|
65
74
|
encoded_str = encoded_bytes.decode()
|
66
|
-
file_tree = ScriptFileNode.merge(
|
75
|
+
file_tree = ScriptFileNode.merge(
|
76
|
+
file_tree, ScriptFileNode.create_file_tree(p, encoded_str)
|
77
|
+
)
|
67
78
|
return file_tree
|
68
|
-
|
79
|
+
|
69
80
|
@property
|
70
81
|
def package_name(self) -> Optional[str]:
|
71
82
|
if self.runtime != ScriptRuntime.Python:
|
@@ -78,7 +89,7 @@ class Script(BaseModel):
|
|
78
89
|
if not name:
|
79
90
|
raise ValueError("Could not find package name")
|
80
91
|
return name.replace("-", "_")
|
81
|
-
|
92
|
+
|
82
93
|
@property
|
83
94
|
def entrypoint(self) -> str:
|
84
95
|
pocket_logger.info(self.tool_path)
|
@@ -99,10 +110,11 @@ class Script(BaseModel):
|
|
99
110
|
if not wheel_path.exists():
|
100
111
|
raise ValueError(f"Wheel file {wheel_path} does not exist")
|
101
112
|
return wheel_name
|
102
|
-
|
113
|
+
|
103
114
|
def dist_file_path(self, file_name: str) -> str:
|
104
115
|
return str(pathlib.Path(self.tool_path) / "dist" / file_name)
|
105
116
|
|
117
|
+
|
106
118
|
class _ScriptStore(object):
|
107
119
|
scripts: dict[str, Script] = {}
|
108
120
|
|
@@ -113,9 +125,10 @@ class _ScriptStore(object):
|
|
113
125
|
if script.id in self.scripts:
|
114
126
|
raise ValueError("Script id already exists")
|
115
127
|
self.scripts[script.id] = script
|
116
|
-
|
128
|
+
|
117
129
|
def get_script(self, script_id: str) -> Script:
|
118
130
|
# ValueError exception is intentional
|
119
131
|
return self.scripts[script_id]
|
120
132
|
|
133
|
+
|
121
134
|
ScriptStore = _ScriptStore()
|
@@ -1,28 +1,35 @@
|
|
1
1
|
import base64
|
2
2
|
import json
|
3
3
|
|
4
|
-
from jinja2 import
|
4
|
+
from jinja2 import DictLoader, Environment
|
5
5
|
|
6
6
|
from hyperpocket.tool.wasm.templates.node import node_template
|
7
7
|
from hyperpocket.tool.wasm.templates.python import python_template
|
8
8
|
|
9
9
|
TemplateEnvironments = Environment(
|
10
|
-
loader=DictLoader(
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
10
|
+
loader=DictLoader(
|
11
|
+
{
|
12
|
+
"python.html": python_template,
|
13
|
+
"node.html": node_template,
|
14
|
+
}
|
15
|
+
),
|
16
|
+
autoescape=False,
|
15
17
|
)
|
16
18
|
|
17
19
|
|
18
|
-
def render(
|
20
|
+
def render(
|
21
|
+
language: str, script_id: str, env: dict[str, str], body: str, **kwargs
|
22
|
+
) -> str:
|
19
23
|
env_json = json.dumps(env)
|
20
|
-
template = TemplateEnvironments.get_template(f
|
21
|
-
body_bytes = body.encode(
|
24
|
+
template = TemplateEnvironments.get_template(f"{language.lower()}.html")
|
25
|
+
body_bytes = body.encode("utf-8")
|
22
26
|
body_b64_bytes = base64.b64encode(body_bytes)
|
23
|
-
body_b64 = body_b64_bytes.decode(
|
24
|
-
return template.render(
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
27
|
+
body_b64 = body_b64_bytes.decode("ascii")
|
28
|
+
return template.render(
|
29
|
+
**{
|
30
|
+
"SCRIPT_ID": script_id,
|
31
|
+
"ENV_JSON": env_json,
|
32
|
+
"BODY_JSON_B64": body_b64,
|
33
|
+
}
|
34
|
+
| kwargs
|
35
|
+
)
|
hyperpocket/tool/wasm/tool.py
CHANGED
@@ -1,11 +1,11 @@
|
|
1
|
-
import os
|
2
1
|
import json
|
3
2
|
import pathlib
|
4
3
|
from typing import Any, Optional
|
5
4
|
|
6
5
|
import toml
|
6
|
+
|
7
7
|
from hyperpocket.auth import AuthProvider
|
8
|
-
from hyperpocket.config import
|
8
|
+
from hyperpocket.config import pocket_logger
|
9
9
|
from hyperpocket.repository import Lock, Lockfile
|
10
10
|
from hyperpocket.repository.lock import GitLock, LocalLock
|
11
11
|
from hyperpocket.tool import Tool, ToolRequest
|
@@ -26,15 +26,24 @@ class WasmToolRequest(ToolRequest):
|
|
26
26
|
def __str__(self):
|
27
27
|
return f"ToolRequest(lock={self.lock}, rel_path={self.rel_path})"
|
28
28
|
|
29
|
-
|
29
|
+
|
30
|
+
def from_local(
|
31
|
+
path: str, tool_vars: Optional[dict[str, str]] = None
|
32
|
+
) -> WasmToolRequest:
|
30
33
|
if tool_vars is None:
|
31
34
|
tool_vars = dict()
|
32
35
|
return WasmToolRequest(LocalLock(path), "", tool_vars)
|
33
36
|
|
34
|
-
|
37
|
+
|
38
|
+
def from_git(
|
39
|
+
repository: str, ref: str, rel_path: str, tool_vars: Optional[dict[str, str]] = None
|
40
|
+
) -> WasmToolRequest:
|
35
41
|
if not tool_vars:
|
36
42
|
tool_vars = dict()
|
37
|
-
return WasmToolRequest(
|
43
|
+
return WasmToolRequest(
|
44
|
+
GitLock(repository_url=repository, git_ref=ref), rel_path, tool_vars
|
45
|
+
)
|
46
|
+
|
38
47
|
|
39
48
|
class WasmTool(Tool):
|
40
49
|
"""
|
@@ -55,7 +64,9 @@ class WasmTool(Tool):
|
|
55
64
|
return self._invoker
|
56
65
|
|
57
66
|
@classmethod
|
58
|
-
def from_tool_request(
|
67
|
+
def from_tool_request(
|
68
|
+
cls, tool_req: WasmToolRequest, lockfile: Lockfile = None, **kwargs
|
69
|
+
) -> "WasmTool":
|
59
70
|
if not lockfile:
|
60
71
|
raise ValueError("lockfile is required")
|
61
72
|
tool_req.lock = lockfile.get_lock(tool_req.lock.key())
|
@@ -70,20 +81,22 @@ class WasmTool(Tool):
|
|
70
81
|
with schema_path.open("r") as f:
|
71
82
|
json_schema = json.load(f)
|
72
83
|
except Exception as e:
|
73
|
-
pocket_logger.warning(
|
84
|
+
pocket_logger.warning(
|
85
|
+
f"{toolpkg_path} failed to load json schema. error : {e}"
|
86
|
+
)
|
74
87
|
json_schema = None
|
75
88
|
|
76
89
|
default_tool_vars = dict()
|
77
90
|
try:
|
78
91
|
with config_path.open("r") as f:
|
79
92
|
config = toml.load(f)
|
80
|
-
name = config.get(
|
81
|
-
description = config.get(
|
82
|
-
if language := config.get(
|
93
|
+
name = config.get("name")
|
94
|
+
description = config.get("description")
|
95
|
+
if language := config.get("language"):
|
83
96
|
lang = language.lower()
|
84
|
-
if lang ==
|
97
|
+
if lang == "python":
|
85
98
|
runtime = ScriptRuntime.Python
|
86
|
-
elif lang ==
|
99
|
+
elif lang == "node":
|
87
100
|
runtime = ScriptRuntime.Node
|
88
101
|
else:
|
89
102
|
raise ValueError(f"The language `{lang}` is not supported.")
|
@@ -99,7 +112,7 @@ class WasmTool(Tool):
|
|
99
112
|
readme = f.read()
|
100
113
|
else:
|
101
114
|
readme = None
|
102
|
-
|
115
|
+
|
103
116
|
return cls(
|
104
117
|
name=name,
|
105
118
|
description=description,
|
@@ -127,7 +140,7 @@ class WasmTool(Tool):
|
|
127
140
|
auth_handler=auth_handler,
|
128
141
|
scopes=scopes,
|
129
142
|
)
|
130
|
-
|
143
|
+
|
131
144
|
def template_arguments(self) -> dict[str, str]:
|
132
145
|
return {}
|
133
146
|
|
hyperpocket/tool_like.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
from typing import
|
1
|
+
from typing import Callable, Union
|
2
2
|
|
3
|
-
from hyperpocket.tool import
|
3
|
+
from hyperpocket.tool import Tool, ToolRequest
|
4
4
|
|
5
|
-
ToolLike = Union[Tool, str, Callable, ToolRequest]
|
5
|
+
ToolLike = Union[Tool, str, Callable, ToolRequest]
|
hyperpocket/util/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__all__ = [
|
1
|
+
__all__ = ["json_schema_to_model"]
|
@@ -23,22 +23,23 @@ def extract_param_docstring_mapping(func) -> dict[str, str]:
|
|
23
23
|
if not docstring:
|
24
24
|
return {}
|
25
25
|
|
26
|
-
pocket_logger.debug(f"try to extract docstring of {func.__name__} by google style..")
|
27
26
|
param_mapping = extract_param_desc_by_google_stype_docstring(docstring, func_params)
|
28
27
|
if param_mapping:
|
29
|
-
pocket_logger.debug(
|
28
|
+
pocket_logger.debug(
|
29
|
+
f"success extract docstring of {func.__name__} by google style!"
|
30
|
+
)
|
30
31
|
return param_mapping
|
31
32
|
pocket_logger.debug(f"not found param desc of {func.__name__} by google style..")
|
32
33
|
|
33
|
-
pocket_logger.debug(f"try to extract docstring of {func.__name__} by other style..")
|
34
34
|
param_mapping = extract_param_desc_by_other_styles(docstring, func_params)
|
35
35
|
if param_mapping:
|
36
|
-
pocket_logger.debug(
|
36
|
+
pocket_logger.debug(
|
37
|
+
f"success extract docstring of {func.__name__} by other style!"
|
38
|
+
)
|
37
39
|
return param_mapping
|
38
40
|
pocket_logger.debug(f"not found param desc of {func.__name__} by other styles..")
|
39
41
|
|
40
42
|
# Plain Text Style matching
|
41
|
-
pocket_logger.debug(f"try to extract docstring of {func.__name__} by plain text style..")
|
42
43
|
param_descriptions = []
|
43
44
|
for line in docstring.split("\n"):
|
44
45
|
split_line = line.strip().split(":")
|
@@ -46,18 +47,26 @@ def extract_param_docstring_mapping(func) -> dict[str, str]:
|
|
46
47
|
continue
|
47
48
|
|
48
49
|
param_name = split_line[0]
|
49
|
-
cleaned_param_name =
|
50
|
+
cleaned_param_name = clean_string(param_name)
|
51
|
+
cleaned_param_name = clean_bracket_content(cleaned_param_name)
|
50
52
|
description = ":".join(split_line[1:]).strip()
|
51
53
|
if cleaned_param_name in func_params:
|
52
54
|
param_descriptions.append((cleaned_param_name, description))
|
53
55
|
|
54
56
|
# Ensure no duplicates and match with function parameters
|
55
|
-
param_mapping = {
|
57
|
+
param_mapping = {
|
58
|
+
param: desc for param, desc in param_descriptions if param in func_params
|
59
|
+
}
|
56
60
|
pocket_logger.debug(f"final param_mapping of {func.__name__} : {param_mapping}")
|
57
61
|
|
58
62
|
return param_mapping
|
59
63
|
|
60
64
|
|
65
|
+
def clean_string(input_string):
|
66
|
+
cleaned = re.sub(r"^[^a-zA-Z_]*|[^a-zA-Z0-9_()\s]*$", "", input_string)
|
67
|
+
return cleaned.strip()
|
68
|
+
|
69
|
+
|
61
70
|
def clean_bracket_content(content):
|
62
71
|
return re.sub(r"[(\[{<].*?[)\]}>]", "", content)
|
63
72
|
|
@@ -65,17 +74,21 @@ def clean_bracket_content(content):
|
|
65
74
|
def extract_param_desc_by_other_styles(docstring, func_params) -> dict[str, str]:
|
66
75
|
param_descriptions = []
|
67
76
|
# Pattern for Sphinx-style or Javadoc-style `:param`, `@param`, `:arg`, `@arg`
|
68
|
-
param_pattern = r"(?:@param|:param|:arg|@arg)
|
69
|
-
matches = re.findall(param_pattern, docstring)
|
70
|
-
for param, desc in matches:
|
77
|
+
param_pattern = r"^\s*(?:@param|:param|:arg|@arg):?\s+(\w+)(?:\((.*?)\))?:?\s*(.*)"
|
78
|
+
matches = re.findall(param_pattern, docstring, re.MULTILINE)
|
79
|
+
for param, _, desc in matches:
|
71
80
|
cleaned_param = clean_bracket_content(param)
|
72
81
|
param_descriptions.append((cleaned_param, desc.strip()))
|
73
82
|
# Ensure no duplicates and match with function parameters
|
74
|
-
param_mapping = {
|
83
|
+
param_mapping = {
|
84
|
+
param: desc for param, desc in param_descriptions if param in func_params
|
85
|
+
}
|
75
86
|
return param_mapping
|
76
87
|
|
77
88
|
|
78
|
-
def extract_param_desc_by_google_stype_docstring(
|
89
|
+
def extract_param_desc_by_google_stype_docstring(
|
90
|
+
docstring, func_params
|
91
|
+
) -> dict[str, str]:
|
79
92
|
# Regex pattern to extract parameter descriptions in Google style
|
80
93
|
param_pattern = r"Args:\n(.*?)(?=\n\S|$)" # Matches the Args: section
|
81
94
|
match = re.search(param_pattern, docstring, re.DOTALL)
|
@@ -87,11 +100,17 @@ def extract_param_desc_by_google_stype_docstring(docstring, func_params) -> dict
|
|
87
100
|
param_descriptions = {}
|
88
101
|
for line in param_lines:
|
89
102
|
# Match parameter line with "name (type): description"
|
90
|
-
param_match = re.match(
|
103
|
+
param_match = re.match(
|
104
|
+
r"^[^a-zA-Z_]*([a-zA-Z_]\w*)\s*[\(\[]\s*(.*?)\s*[\)\]]\s*:\s*(.*)", line
|
105
|
+
)
|
91
106
|
if param_match:
|
92
107
|
param, _, desc = param_match.groups()
|
93
108
|
cleaned_param = clean_bracket_content(param)
|
94
109
|
param_descriptions[cleaned_param] = desc.strip()
|
95
110
|
# Match parameters to descriptions
|
96
|
-
param_mapping = {
|
111
|
+
param_mapping = {
|
112
|
+
param: desc
|
113
|
+
for param, desc in param_descriptions.items()
|
114
|
+
if param in func_params
|
115
|
+
}
|
97
116
|
return param_mapping
|
@@ -1,12 +1,13 @@
|
|
1
|
-
from typing import
|
2
|
-
|
1
|
+
from typing import List, Type, TypeVar
|
3
2
|
|
4
3
|
from hyperpocket.util.find_all_subclass_in_package import find_all_subclass_in_package
|
5
4
|
|
6
5
|
T = TypeVar("T")
|
7
6
|
|
8
7
|
|
9
|
-
def find_all_leaf_class_in_package(
|
8
|
+
def find_all_leaf_class_in_package(
|
9
|
+
package_name: str, interface_type: Type[T]
|
10
|
+
) -> List[T]:
|
10
11
|
parent_class_set = set()
|
11
12
|
subclasses = find_all_subclass_in_package(package_name, interface_type)
|
12
13
|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import importlib
|
2
2
|
import inspect
|
3
3
|
import pkgutil
|
4
|
-
from typing import
|
4
|
+
from typing import List, Type, TypeVar
|
5
5
|
|
6
6
|
from hyperpocket.config import pocket_logger
|
7
7
|
|
@@ -12,7 +12,9 @@ def find_all_subclass_in_package(package_name: str, interface_type: Type[T]) ->
|
|
12
12
|
subclasses = set()
|
13
13
|
package = importlib.import_module(package_name)
|
14
14
|
|
15
|
-
for _, module_name, is_pkg in pkgutil.walk_packages(
|
15
|
+
for _, module_name, is_pkg in pkgutil.walk_packages(
|
16
|
+
package.__path__, package.__name__ + "."
|
17
|
+
):
|
16
18
|
try:
|
17
19
|
if "tests" in module_name or is_pkg:
|
18
20
|
continue
|
@@ -6,7 +6,7 @@ def flatten_json_schema(schema: dict):
|
|
6
6
|
Flatten JSON Schema by resolving all $refs using definitions in $defs
|
7
7
|
and convert to a fully nested schema.
|
8
8
|
"""
|
9
|
-
definitions = schema.get(
|
9
|
+
definitions = schema.get("$defs", {})
|
10
10
|
schema_copy = copy.deepcopy(schema)
|
11
11
|
|
12
12
|
# Resolve references within $defs first
|
@@ -15,7 +15,7 @@ def flatten_json_schema(schema: dict):
|
|
15
15
|
resolved_definitions[key] = resolve_refs(value, definitions)
|
16
16
|
|
17
17
|
# Resolve references in the main schema
|
18
|
-
schema_copy.pop(
|
18
|
+
schema_copy.pop("$defs", None) # Remove $defs
|
19
19
|
return resolve_refs(schema_copy, resolved_definitions)
|
20
20
|
|
21
21
|
|
@@ -28,12 +28,16 @@ def resolve_refs(schema, definitions):
|
|
28
28
|
resolved_schema = {}
|
29
29
|
for key, value in schema.items():
|
30
30
|
# If $ref exists, resolve the reference
|
31
|
-
if key ==
|
32
|
-
ref_path = schema[
|
33
|
-
ref_name = ref_path.split(
|
31
|
+
if key == "$ref":
|
32
|
+
ref_path = schema["$ref"]
|
33
|
+
ref_name = ref_path.split("/")[
|
34
|
+
-1
|
35
|
+
] # Extract the reference name from $defs/Req -> Req
|
34
36
|
resolved = definitions.get(ref_name)
|
35
37
|
if resolved:
|
36
|
-
resolved_schema |= resolve_refs(
|
38
|
+
resolved_schema |= resolve_refs(
|
39
|
+
copy.deepcopy(resolved), definitions
|
40
|
+
)
|
37
41
|
else:
|
38
42
|
resolved_schema[key] = resolve_refs(value, definitions)
|
39
43
|
return resolved_schema
|
@@ -1,25 +1,29 @@
|
|
1
1
|
import inspect
|
2
|
-
from inspect import
|
3
|
-
from typing import Any, Dict,
|
2
|
+
from inspect import Parameter, signature
|
3
|
+
from typing import Any, Dict, Tuple, Type
|
4
4
|
|
5
5
|
from pydantic import BaseModel, create_model
|
6
6
|
from pydantic.fields import FieldInfo
|
7
7
|
|
8
8
|
from hyperpocket.config import pocket_logger
|
9
|
-
from hyperpocket.util.extract_func_param_desc_from_docstring import
|
9
|
+
from hyperpocket.util.extract_func_param_desc_from_docstring import (
|
10
|
+
extract_param_docstring_mapping,
|
11
|
+
)
|
10
12
|
|
11
13
|
|
12
14
|
def function_to_model(func: callable) -> Type[BaseModel]:
|
13
15
|
docstring = inspect.getdoc(func)
|
14
16
|
if docstring is None:
|
15
|
-
pocket_logger.info(
|
17
|
+
pocket_logger.info(
|
18
|
+
"not found docstring. use function name as description instead."
|
19
|
+
)
|
16
20
|
docstring = func.__name__
|
17
21
|
fields: Dict[str, Tuple[Type, Any]] = {}
|
18
22
|
sig = signature(func)
|
19
23
|
param_desc_map = extract_param_docstring_mapping(func)
|
20
24
|
|
21
25
|
for param_name, param in sig.parameters.items():
|
22
|
-
if param_name in (
|
26
|
+
if param_name in ("self", "cls"):
|
23
27
|
continue
|
24
28
|
|
25
29
|
if param.kind == Parameter.VAR_POSITIONAL:
|
@@ -31,19 +35,36 @@ def function_to_model(func: callable) -> Type[BaseModel]:
|
|
31
35
|
continue
|
32
36
|
|
33
37
|
if param.annotation is Parameter.empty:
|
34
|
-
raise Exception(
|
38
|
+
raise Exception(
|
39
|
+
f"Should all arguments be annotated but {param_name} is not annotated"
|
40
|
+
)
|
35
41
|
|
36
|
-
if
|
42
|
+
if (
|
43
|
+
param.annotation.__module__ == "typing"
|
44
|
+
and param.annotation.__name__ == "Optional"
|
45
|
+
):
|
37
46
|
fields[param_name] = (
|
38
|
-
param.annotation.__args__[0],
|
39
|
-
|
47
|
+
param.annotation.__args__[0],
|
48
|
+
FieldInfo(
|
49
|
+
default=param.default,
|
50
|
+
description=param_desc_map.get(param_name, ""),
|
51
|
+
),
|
52
|
+
)
|
53
|
+
elif param.annotation.__module__ != "builtins" and not issubclass(
|
54
|
+
param.annotation, BaseModel
|
55
|
+
):
|
40
56
|
raise Exception(
|
41
|
-
f"currently only support builtin types and pydantic BaseModel but {param_name} is not builtin type"
|
57
|
+
f"currently only support builtin types and pydantic BaseModel but {param_name} is not builtin type"
|
58
|
+
)
|
42
59
|
|
43
60
|
default = param.default if param.default is not Parameter.empty else ...
|
44
61
|
|
45
62
|
fields[param_name] = (
|
46
|
-
param.annotation,
|
63
|
+
param.annotation,
|
64
|
+
FieldInfo(default=default, description=param_desc_map.get(param_name, "")),
|
65
|
+
)
|
47
66
|
|
48
|
-
model = create_model(
|
67
|
+
model = create_model(
|
68
|
+
f"{func.__name__.capitalize()}Model", **fields, __doc__=docstring
|
69
|
+
)
|
49
70
|
return model
|
@@ -1,10 +1,12 @@
|
|
1
1
|
from typing import Type, Union
|
2
2
|
|
3
|
-
from pydantic import BaseModel,
|
3
|
+
from pydantic import BaseModel, Field, create_model
|
4
4
|
|
5
5
|
|
6
6
|
# Convert JSON Schema to a Pydantic model
|
7
|
-
def json_schema_to_model(
|
7
|
+
def json_schema_to_model(
|
8
|
+
schema: dict, model_name: str = "DynamicModel"
|
9
|
+
) -> Type[BaseModel]:
|
8
10
|
"""Recursively create a Pydantic model from a JSON Schema."""
|
9
11
|
fields = {}
|
10
12
|
config_extra = "forbid"
|
@@ -16,12 +18,16 @@ def json_schema_to_model(schema: dict, model_name: str = "DynamicModel") -> Type
|
|
16
18
|
if "anyOf" in property_schema:
|
17
19
|
types = []
|
18
20
|
for item in property_schema["anyOf"]:
|
19
|
-
sub_type = _convert_to_python_type(
|
21
|
+
sub_type = _convert_to_python_type(
|
22
|
+
item["type"], model_name, property_schema
|
23
|
+
)
|
20
24
|
types.append(sub_type)
|
21
25
|
|
22
26
|
field_type = Union[tuple(types)]
|
23
27
|
elif "type" in property_schema:
|
24
|
-
field_type = _convert_to_python_type(
|
28
|
+
field_type = _convert_to_python_type(
|
29
|
+
property_schema["type"], model_name, property_schema
|
30
|
+
)
|
25
31
|
else:
|
26
32
|
raise RuntimeError("have no type in json schema.")
|
27
33
|
|
@@ -35,7 +41,10 @@ def json_schema_to_model(schema: dict, model_name: str = "DynamicModel") -> Type
|
|
35
41
|
if required:
|
36
42
|
fields[property_name] = (field_type, Field(description=field_description))
|
37
43
|
else:
|
38
|
-
fields[property_name] = (
|
44
|
+
fields[property_name] = (
|
45
|
+
field_type,
|
46
|
+
Field(default=field_default, description=field_description),
|
47
|
+
)
|
39
48
|
|
40
49
|
# Handle additionalProperties
|
41
50
|
if "additionalProperties" in schema:
|