hyperpocket 0.1.9__py3-none-any.whl → 0.2.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|