hyperpocket 0.0.3__py3-none-any.whl → 0.1.9__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- hyperpocket/auth/README.md +3 -3
- hyperpocket/auth/__init__.py +0 -8
- hyperpocket/auth/gumloop/context.py +13 -0
- hyperpocket/auth/gumloop/token_context.py +15 -0
- hyperpocket/auth/gumloop/token_handler.py +66 -0
- hyperpocket/auth/gumloop/token_schema.py +8 -0
- hyperpocket/auth/linear/token_context.py +1 -1
- hyperpocket/auth/notion/README.md +28 -0
- hyperpocket/auth/notion/context.py +15 -0
- hyperpocket/auth/notion/token_context.py +14 -0
- hyperpocket/auth/notion/token_handler.py +65 -0
- hyperpocket/auth/notion/token_schema.py +10 -0
- hyperpocket/auth/provider.py +8 -5
- hyperpocket/auth/reddit/context.py +15 -0
- hyperpocket/auth/reddit/oauth2_context.py +32 -0
- hyperpocket/auth/reddit/oauth2_handler.py +151 -0
- hyperpocket/auth/reddit/oauth2_schema.py +18 -0
- hyperpocket/auth/slack/token_context.py +1 -1
- hyperpocket/builtin.py +63 -0
- hyperpocket/cli/__main__.py +12 -0
- hyperpocket/cli/auth.py +83 -0
- hyperpocket/cli/codegen/auth/__init__.py +13 -0
- hyperpocket/cli/codegen/auth/auth_context_template.py +16 -0
- hyperpocket/cli/codegen/auth/auth_token_context_template.py +16 -0
- hyperpocket/cli/codegen/auth/auth_token_handler_template.py +69 -0
- hyperpocket/cli/codegen/auth/auth_token_schema_template.py +12 -0
- hyperpocket/cli/codegen/auth/server_auth_template.py +18 -0
- hyperpocket/cli/eject.py +19 -0
- hyperpocket/cli/sync.py +5 -5
- hyperpocket/config/settings.py +14 -12
- hyperpocket/futures/futurestore.py +0 -1
- hyperpocket/pocket_auth.py +25 -5
- hyperpocket/pocket_core.py +264 -0
- hyperpocket/pocket_main.py +127 -174
- hyperpocket/prompts.py +6 -8
- hyperpocket/repository/__init__.py +2 -2
- hyperpocket/repository/lock.py +71 -1
- hyperpocket/repository/lockfile.py +19 -13
- hyperpocket/repository/repository.py +26 -1
- hyperpocket/server/auth/__init__.py +0 -6
- hyperpocket/server/auth/gumloop.py +16 -0
- hyperpocket/server/auth/notion.py +19 -0
- hyperpocket/server/auth/reddit.py +16 -0
- hyperpocket/server/server.py +56 -20
- hyperpocket/server/tool/dto/script.py +15 -2
- hyperpocket/server/tool/wasm.py +20 -8
- hyperpocket/session/README.md +2 -2
- hyperpocket/session/in_memory.py +18 -5
- hyperpocket/session/interface.py +14 -0
- hyperpocket/session/redis.py +29 -5
- hyperpocket/tool/README.md +16 -12
- hyperpocket/tool/__init__.py +4 -3
- hyperpocket/tool/function/README.md +39 -10
- hyperpocket/tool/function/__init__.py +2 -0
- hyperpocket/tool/function/annotation.py +2 -1
- hyperpocket/tool/function/tool.py +108 -29
- hyperpocket/tool/tool.py +100 -28
- hyperpocket/tool/wasm/README.md +27 -5
- hyperpocket/tool/wasm/browser.py +2 -7
- hyperpocket/tool/wasm/script.py +40 -1
- hyperpocket/tool/wasm/templates/python.py +32 -14
- hyperpocket/tool/wasm/tool.py +21 -18
- hyperpocket/tool_like.py +5 -0
- hyperpocket/util/__init__.py +1 -1
- hyperpocket/util/extract_func_param_desc_from_docstring.py +4 -4
- hyperpocket/util/function_to_model.py +5 -2
- hyperpocket/util/json_schema_to_model.py +47 -26
- {hyperpocket-0.0.3.dist-info → hyperpocket-0.1.9.dist-info}/METADATA +107 -88
- hyperpocket-0.1.9.dist-info/RECORD +137 -0
- {hyperpocket-0.0.3.dist-info → hyperpocket-0.1.9.dist-info}/WHEEL +1 -1
- hyperpocket-0.1.9.dist-info/entry_points.txt +2 -0
- hyperpocket/auth/README.KR.md +0 -309
- hyperpocket/auth/slack/tests/test_oauth2_handler.py +0 -32
- hyperpocket/auth/slack/tests/test_token_handler.py +0 -23
- hyperpocket/auth/tests/test_google_oauth2_handler.py +0 -147
- hyperpocket/auth/tests/test_slack_oauth2_handler.py +0 -147
- hyperpocket/auth/tests/test_slack_token_handler.py +0 -66
- hyperpocket/external/__init__.py +0 -7
- hyperpocket/external/github_client.py +0 -19
- hyperpocket/session/README.KR.md +0 -62
- hyperpocket/session/tests/test_in_memory.py +0 -145
- hyperpocket/session/tests/test_redis.py +0 -151
- hyperpocket/tests/test_pocket.py +0 -116
- hyperpocket/tests/test_pocket_auth.py +0 -982
- hyperpocket/tool/README.KR.md +0 -68
- hyperpocket/tool/builtins/__init__.py +0 -0
- hyperpocket/tool/builtins/example/__init__.py +0 -0
- hyperpocket/tool/builtins/example/add_tool.py +0 -18
- hyperpocket/tool/function/README.KR.md +0 -159
- hyperpocket/tool/tests/test_function_tool.py +0 -266
- hyperpocket/tool/wasm/README.KR.md +0 -144
- hyperpocket-0.0.3.dist-info/RECORD +0 -130
- hyperpocket-0.0.3.dist-info/entry_points.txt +0 -3
- /hyperpocket/auth/{slack/tests → gumloop}/__init__.py +0 -0
- /hyperpocket/auth/{tests → notion}/__init__.py +0 -0
- /hyperpocket/{session/tests → auth/reddit}/__init__.py +0 -0
- /hyperpocket/{tests → cli/codegen}/__init__.py +0 -0
hyperpocket/cli/auth.py
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
import os
|
2
|
+
import click
|
3
|
+
from pathlib import Path
|
4
|
+
from hyperpocket.cli.codegen.auth import get_server_auth_token_template, get_auth_context_template, get_auth_token_context_template, get_auth_token_handler_template, get_auth_token_schema_template
|
5
|
+
|
6
|
+
@click.command()
|
7
|
+
@click.argument('service_name', type=str)
|
8
|
+
def create_token_auth_template(service_name):
|
9
|
+
## Validate service_name
|
10
|
+
if not service_name.islower() or not service_name.replace('_', '').isalpha():
|
11
|
+
raise ValueError("service_name must be lowercase and contain only letters and underscores")
|
12
|
+
|
13
|
+
capitliazed_service_name = service_name.capitalize()
|
14
|
+
if '_' in service_name:
|
15
|
+
capitliazed_service_name = ''.join([word.capitalize() for word in service_name.split('_')])
|
16
|
+
|
17
|
+
## Get the current working directory
|
18
|
+
cwd = Path.cwd()
|
19
|
+
parent_path = cwd.parent
|
20
|
+
|
21
|
+
generate_server_auth(service_name, parent_path)
|
22
|
+
generate_hyperpocket_auth_dir(service_name, parent_path)
|
23
|
+
generate_auth_context(service_name, capitliazed_service_name, parent_path)
|
24
|
+
generate_auth_token_context(service_name, capitliazed_service_name, parent_path)
|
25
|
+
generate_auth_token_handler(service_name, capitliazed_service_name, parent_path)
|
26
|
+
generate_auth_token_schema(service_name, capitliazed_service_name, parent_path)
|
27
|
+
##TODO: Add service to hyperpocket/auth/provider
|
28
|
+
|
29
|
+
def generate_server_auth(service_name, parent_path):
|
30
|
+
print(f"Generating server/auth for '{service_name}'.")
|
31
|
+
output_from_parsed_template = get_server_auth_token_template().render(service_name=service_name)
|
32
|
+
output_path = parent_path / f'hyperpocket/hyperpocket/server/auth/{service_name}.py'
|
33
|
+
with open(output_path, "w") as f:
|
34
|
+
f.write(output_from_parsed_template)
|
35
|
+
|
36
|
+
def generate_hyperpocket_auth_dir(service_name, parent_path):
|
37
|
+
if not os.path.exists(parent_path / f'hyperpocket/hyperpocket/auth/{service_name}'):
|
38
|
+
os.makedirs(parent_path / f'hyperpocket/hyperpocket/auth/{service_name}')
|
39
|
+
|
40
|
+
output_path = parent_path / f'hyperpocket/hyperpocket/auth/{service_name}/__init__.py'
|
41
|
+
with open(output_path, "w") as f:
|
42
|
+
pass
|
43
|
+
|
44
|
+
def generate_auth_context(service_name, capitliazed_service_name, parent_path):
|
45
|
+
print(f"Generating auth/context for '{service_name}'.")
|
46
|
+
output_from_parsed_template = get_auth_context_template().render(
|
47
|
+
caplitalized_service_name=capitliazed_service_name,
|
48
|
+
upper_service_name=service_name.upper()
|
49
|
+
)
|
50
|
+
output_path = parent_path / f'hyperpocket/hyperpocket/auth/{service_name}/context.py'
|
51
|
+
with open(output_path, "w") as f:
|
52
|
+
f.write(output_from_parsed_template)
|
53
|
+
|
54
|
+
def generate_auth_token_context(service_name, capitliazed_service_name, parent_path):
|
55
|
+
print(f"Generating auth/token context for '{service_name}'.")
|
56
|
+
output_from_parsed_template = get_auth_token_context_template().render(
|
57
|
+
service_name = service_name,
|
58
|
+
caplitalized_service_name=capitliazed_service_name,
|
59
|
+
)
|
60
|
+
output_path = parent_path / f'hyperpocket/hyperpocket/auth/{service_name}/token_context.py'
|
61
|
+
with open(output_path, "w") as f:
|
62
|
+
f.write(output_from_parsed_template)
|
63
|
+
|
64
|
+
def generate_auth_token_handler(service_name, capitliazed_service_name, parent_path):
|
65
|
+
print(f"Generating auth/token handler for '{service_name}'.")
|
66
|
+
output_from_parsed_template = get_auth_token_handler_template().render(
|
67
|
+
service_name = service_name,
|
68
|
+
auth_handler_name = service_name.replace('_','-'),
|
69
|
+
caplitalized_service_name=capitliazed_service_name,
|
70
|
+
upper_service_name=service_name.upper()
|
71
|
+
)
|
72
|
+
output_path = parent_path / f'hyperpocket/hyperpocket/auth/{service_name}/token_handler.py'
|
73
|
+
with open(output_path, "w") as f:
|
74
|
+
f.write(output_from_parsed_template)
|
75
|
+
|
76
|
+
def generate_auth_token_schema(service_name, capitliazed_service_name, parent_path):
|
77
|
+
print(f"Generating auth/token schema for '{service_name}'.")
|
78
|
+
output_from_parsed_template = get_auth_token_schema_template().render(
|
79
|
+
caplitalized_service_name=capitliazed_service_name,
|
80
|
+
)
|
81
|
+
output_path = parent_path / f'hyperpocket/hyperpocket/auth/{service_name}/token_schema.py'
|
82
|
+
with open(output_path, "w") as f:
|
83
|
+
f.write(output_from_parsed_template)
|
@@ -0,0 +1,13 @@
|
|
1
|
+
from .auth_context_template import get_auth_context_template
|
2
|
+
from .auth_token_context_template import get_auth_token_context_template
|
3
|
+
from .auth_token_handler_template import get_auth_token_handler_template
|
4
|
+
from .auth_token_schema_template import get_auth_token_schema_template
|
5
|
+
from .server_auth_template import get_server_auth_token_template
|
6
|
+
|
7
|
+
__all__ = [
|
8
|
+
"get_auth_context_template",
|
9
|
+
"get_auth_token_context_template",
|
10
|
+
"get_auth_token_handler_template",
|
11
|
+
"get_auth_token_schema_template",
|
12
|
+
"get_server_auth_token_template",
|
13
|
+
]
|
@@ -0,0 +1,16 @@
|
|
1
|
+
from jinja2 import Template
|
2
|
+
|
3
|
+
def get_auth_context_template() -> Template:
|
4
|
+
return Template('''
|
5
|
+
from hyperpocket.auth.context import AuthContext
|
6
|
+
class {{ caplitalized_service_name }}AuthContext(AuthContext):
|
7
|
+
_ACCESS_TOKEN_KEY: str = "{{ upper_service_name }}_TOKEN"
|
8
|
+
def to_dict(self) -> dict[str, str]:
|
9
|
+
return {
|
10
|
+
self._ACCESS_TOKEN_KEY: self.access_token,
|
11
|
+
}
|
12
|
+
def to_profiled_dict(self, profile: str) -> dict[str, str]:
|
13
|
+
return {
|
14
|
+
f"{profile.upper()}_{self._ACCESS_TOKEN_KEY}": self.access_token,
|
15
|
+
}
|
16
|
+
''')
|
@@ -0,0 +1,16 @@
|
|
1
|
+
from jinja2 import Template
|
2
|
+
|
3
|
+
def get_auth_token_context_template() -> Template:
|
4
|
+
return Template('''
|
5
|
+
from hyperpocket.auth.{{ service_name }}.context import {{ caplitalized_service_name }}AuthContext
|
6
|
+
from hyperpocket.auth.{{ service_name }}.token_schema import {{ caplitalized_service_name }}TokenResponse
|
7
|
+
class {{ caplitalized_service_name }}TokenAuthContext({{ caplitalized_service_name }}AuthContext):
|
8
|
+
@classmethod
|
9
|
+
def from_{{ service_name }}_token_response(cls, response: {{ caplitalized_service_name }}TokenResponse):
|
10
|
+
description = f'{{ caplitalized_service_name }} Token Context logged in'
|
11
|
+
return cls(
|
12
|
+
access_token=response.access_token,
|
13
|
+
description=description,
|
14
|
+
expires_at=None
|
15
|
+
)
|
16
|
+
''')
|
@@ -0,0 +1,69 @@
|
|
1
|
+
from jinja2 import Template
|
2
|
+
|
3
|
+
def get_auth_token_handler_template() -> Template:
|
4
|
+
return Template('''
|
5
|
+
from typing import Optional
|
6
|
+
from urllib.parse import urljoin, urlencode
|
7
|
+
|
8
|
+
from hyperpocket.auth import AuthProvider
|
9
|
+
from hyperpocket.auth.context import AuthContext
|
10
|
+
from hyperpocket.auth.handler import AuthHandlerInterface, AuthenticateRequest
|
11
|
+
from hyperpocket.auth.{{ service_name }}.token_context import {{ caplitalized_service_name }}TokenAuthContext
|
12
|
+
from hyperpocket.auth.{{ service_name }}.token_schema import {{ caplitalized_service_name }}TokenResponse, {{ caplitalized_service_name }}TokenRequest
|
13
|
+
from hyperpocket.config import config
|
14
|
+
from hyperpocket.futures import FutureStore
|
15
|
+
|
16
|
+
|
17
|
+
class {{ caplitalized_service_name }}TokenAuthHandler(AuthHandlerInterface):
|
18
|
+
name: str = "{{ auth_handler_name }}-token"
|
19
|
+
description: str = "This handler is used to authenticate users using the {{ caplitalized_service_name }} token."
|
20
|
+
scoped: bool = False
|
21
|
+
|
22
|
+
_TOKEN_URL: str = urljoin(config.public_base_url + "/", f"{config.callback_url_rewrite_prefix}/auth/token")
|
23
|
+
|
24
|
+
@staticmethod
|
25
|
+
def provider() -> AuthProvider:
|
26
|
+
return AuthProvider.{{ upper_service_name }}
|
27
|
+
|
28
|
+
@staticmethod
|
29
|
+
def recommended_scopes() -> set[str]:
|
30
|
+
return set()
|
31
|
+
|
32
|
+
def prepare(self, auth_req: {{ caplitalized_service_name }}TokenRequest, thread_id: str, profile: str,
|
33
|
+
future_uid: str, *args, **kwargs) -> str:
|
34
|
+
redirect_uri = urljoin(
|
35
|
+
config.public_base_url + "/",
|
36
|
+
f"{config.callback_url_rewrite_prefix}/auth/{{ service_name }}/token/callback",
|
37
|
+
)
|
38
|
+
url = self._make_auth_url(auth_req=auth_req, redirect_uri=redirect_uri, state=future_uid)
|
39
|
+
FutureStore.create_future(future_uid, data={
|
40
|
+
"redirect_uri": redirect_uri,
|
41
|
+
"thread_id": thread_id,
|
42
|
+
"profile": profile,
|
43
|
+
})
|
44
|
+
|
45
|
+
return f'User needs to authenticate using the following URL: {url}'
|
46
|
+
|
47
|
+
async def authenticate(self, auth_req: {{ caplitalized_service_name }}TokenRequest, future_uid: str, *args, **kwargs) -> AuthContext:
|
48
|
+
future_data = FutureStore.get_future(future_uid)
|
49
|
+
access_token = await future_data.future
|
50
|
+
|
51
|
+
response = {{ caplitalized_service_name }}TokenResponse(access_token=access_token)
|
52
|
+
context = {{ caplitalized_service_name }}TokenAuthContext.from_{{ service_name }}_token_response(response)
|
53
|
+
|
54
|
+
return context
|
55
|
+
|
56
|
+
async def refresh(self, auth_req: {{ caplitalized_service_name }}TokenRequest, context: AuthContext, *args, **kwargs) -> AuthContext:
|
57
|
+
raise Exception("{{ caplitalized_service_name }} token doesn't support refresh")
|
58
|
+
|
59
|
+
def _make_auth_url(self, auth_req: {{ caplitalized_service_name }}TokenRequest, redirect_uri: str, state: str):
|
60
|
+
params = {
|
61
|
+
"redirect_uri": redirect_uri,
|
62
|
+
"state": state,
|
63
|
+
}
|
64
|
+
auth_url = f"{self._TOKEN_URL}?{urlencode(params)}"
|
65
|
+
return auth_url
|
66
|
+
|
67
|
+
def make_request(self, auth_scopes: Optional[list[str]] = None, **kwargs) -> {{ caplitalized_service_name }}TokenRequest:
|
68
|
+
return {{ caplitalized_service_name }}TokenRequest()
|
69
|
+
''')
|
@@ -0,0 +1,12 @@
|
|
1
|
+
from jinja2 import Template
|
2
|
+
|
3
|
+
def get_auth_token_schema_template() -> Template:
|
4
|
+
return Template('''
|
5
|
+
from typing import List, Optional
|
6
|
+
from pydantic import BaseModel
|
7
|
+
from hyperpocket.auth.schema import AuthenticateRequest, AuthenticateResponse
|
8
|
+
class {{ caplitalized_service_name }}TokenRequest(AuthenticateRequest):
|
9
|
+
pass
|
10
|
+
class {{ caplitalized_service_name }}TokenResponse(AuthenticateResponse):
|
11
|
+
access_token: str
|
12
|
+
''')
|
@@ -0,0 +1,18 @@
|
|
1
|
+
from jinja2 import Template
|
2
|
+
|
3
|
+
def get_server_auth_token_template() -> Template:
|
4
|
+
return Template('''
|
5
|
+
from fastapi import APIRouter
|
6
|
+
from starlette.responses import HTMLResponse
|
7
|
+
from hyperpocket.futures import FutureStore
|
8
|
+
{{ service_name }}_auth_router = APIRouter(
|
9
|
+
prefix="/{{ service_name }}"
|
10
|
+
)
|
11
|
+
@{{ service_name }}_auth_router.get("/token/callback")
|
12
|
+
async def {{ service_name }}_token_callback(state: str, token: str):
|
13
|
+
try:
|
14
|
+
FutureStore.resolve_future(state, token)
|
15
|
+
except ValueError:
|
16
|
+
return HTMLResponse(content="failed")
|
17
|
+
return HTMLResponse(content="success")
|
18
|
+
''')
|
hyperpocket/cli/eject.py
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
import pathlib
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
import click
|
5
|
+
|
6
|
+
import hyperpocket.repository as repository
|
7
|
+
|
8
|
+
|
9
|
+
@click.command()
|
10
|
+
@click.argument("url", type=str)
|
11
|
+
@click.argument("ref", type=str)
|
12
|
+
@click.argument("remote_path", type=str)
|
13
|
+
@click.option("--lockfile", envvar="PATHS", type=click.Path(exists=True))
|
14
|
+
def eject(url: str, ref: str, remote_path: str, lockfile: Optional[pathlib.Path]):
|
15
|
+
if not lockfile:
|
16
|
+
lockfile = pathlib.Path.cwd() / "pocket.lock"
|
17
|
+
if not lockfile.exists():
|
18
|
+
raise ValueError("To eject a tool, you first need to pull it")
|
19
|
+
repository.eject(url, ref, remote_path, repository.Lockfile(lockfile))
|
hyperpocket/cli/sync.py
CHANGED
@@ -7,11 +7,11 @@ import hyperpocket.repository as repository
|
|
7
7
|
|
8
8
|
|
9
9
|
@click.command()
|
10
|
-
@click.option("--lockfile", envvar=
|
11
|
-
@click.option("--force-update", type=str, default=
|
12
|
-
def sync(
|
10
|
+
@click.option("--lockfile", envvar="PATHS", type=click.Path(exists=True))
|
11
|
+
@click.option("--force-update", type=str, default="HEAD")
|
12
|
+
def sync(lockfile: Optional[pathlib.Path], force_update: bool):
|
13
13
|
if not lockfile:
|
14
|
-
lockfile = pathlib.Path.cwd() /
|
14
|
+
lockfile = pathlib.Path.cwd() / "pocket.lock"
|
15
15
|
if not lockfile.exists():
|
16
16
|
lockfile.touch()
|
17
|
-
repository.sync(repository.Lockfile(path=lockfile), force_update)
|
17
|
+
repository.sync(repository.Lockfile(path=lockfile), force_update)
|
hyperpocket/config/settings.py
CHANGED
@@ -1,26 +1,28 @@
|
|
1
1
|
import os
|
2
2
|
from pathlib import Path
|
3
|
-
from typing import Literal
|
4
3
|
|
5
4
|
from dynaconf import Dynaconf
|
6
|
-
from pydantic import BaseModel
|
5
|
+
from pydantic import BaseModel, Field
|
7
6
|
|
8
7
|
from hyperpocket.config.auth import AuthConfig, DefaultAuthConfig
|
9
|
-
from hyperpocket.config.git import DefaultGitConfig, GitConfig
|
10
8
|
from hyperpocket.config.session import DefaultSessionConfig, SessionConfig
|
11
9
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
10
|
+
POCKET_ROOT = Path.home() / ".pocket"
|
11
|
+
SETTING_ROOT = Path.cwd()
|
12
|
+
|
13
|
+
|
14
|
+
settings_path = SETTING_ROOT / "settings.toml"
|
16
15
|
if not settings_path.exists():
|
17
16
|
with open(settings_path, "w"):
|
18
17
|
pass
|
19
|
-
|
18
|
+
|
19
|
+
secret_path = SETTING_ROOT / ".secrets.toml"
|
20
20
|
if not secret_path.exists():
|
21
21
|
with open(secret_path, "w"):
|
22
22
|
pass
|
23
|
-
|
23
|
+
|
24
|
+
|
25
|
+
toolpkg_path = POCKET_ROOT / "toolpkg"
|
24
26
|
if not toolpkg_path.exists():
|
25
27
|
os.makedirs(toolpkg_path)
|
26
28
|
|
@@ -43,8 +45,8 @@ class Config(BaseModel):
|
|
43
45
|
callback_url_rewrite_prefix: str = "proxy" # should not start with a slash
|
44
46
|
log_level: str = "INFO"
|
45
47
|
auth: AuthConfig = DefaultAuthConfig
|
46
|
-
git: GitConfig = DefaultGitConfig
|
47
48
|
session: SessionConfig = DefaultSessionConfig
|
49
|
+
tool_vars: dict[str, str] = Field(default_factory=dict)
|
48
50
|
|
49
51
|
@property
|
50
52
|
def internal_base_url(self):
|
@@ -52,9 +54,9 @@ class Config(BaseModel):
|
|
52
54
|
|
53
55
|
@property
|
54
56
|
def public_base_url(self):
|
55
|
-
if self.public_server_protocol ==
|
57
|
+
if self.public_server_protocol == "https" and self.public_server_port == 443:
|
56
58
|
return f"{self.public_server_protocol}://{self.public_hostname}"
|
57
|
-
elif self.public_server_protocol ==
|
59
|
+
elif self.public_server_protocol == "http" and self.public_server_port == 80:
|
58
60
|
return f"{self.public_server_protocol}://{self.public_hostname}"
|
59
61
|
return f"{self.public_server_protocol}://{self.public_hostname}:{self.public_server_port}"
|
60
62
|
|
hyperpocket/pocket_auth.py
CHANGED
@@ -9,7 +9,7 @@ from hyperpocket.auth.handler import AuthHandlerInterface, AuthenticateRequest
|
|
9
9
|
from hyperpocket.config import config, pocket_logger
|
10
10
|
from hyperpocket.futures import FutureStore
|
11
11
|
from hyperpocket.session import SESSION_STORAGE_LIST
|
12
|
-
from hyperpocket.session.interface import SessionStorageInterface
|
12
|
+
from hyperpocket.session.interface import SessionStorageInterface, BaseSessionValue
|
13
13
|
|
14
14
|
|
15
15
|
class AuthState(enum.Enum):
|
@@ -99,7 +99,12 @@ class PocketAuth(object):
|
|
99
99
|
"""
|
100
100
|
handler = self.find_handler_instance(auth_handler_name, auth_provider)
|
101
101
|
session = self.session_storage.get(handler.provider(), thread_id, profile)
|
102
|
+
auth_state = self.get_session_state(session=session, auth_req=auth_req)
|
102
103
|
|
104
|
+
return auth_state
|
105
|
+
|
106
|
+
@staticmethod
|
107
|
+
def get_session_state(session: Optional[BaseSessionValue], auth_req: Optional[AuthenticateRequest]) -> AuthState:
|
103
108
|
if not session:
|
104
109
|
return AuthState.NO_SESSION
|
105
110
|
|
@@ -110,7 +115,8 @@ class PocketAuth(object):
|
|
110
115
|
|
111
116
|
return AuthState.PENDING_RESOLVE
|
112
117
|
|
113
|
-
if not session.is_auth_applicable(auth_provider_name=
|
118
|
+
if auth_req is not None and not session.is_auth_applicable(auth_provider_name=session.auth_provider_name,
|
119
|
+
auth_req=auth_req):
|
114
120
|
return AuthState.DO_AUTH
|
115
121
|
|
116
122
|
if session.is_near_expires():
|
@@ -282,9 +288,6 @@ class PocketAuth(object):
|
|
282
288
|
FutureStore.delete_future(session.auth_resolve_uid)
|
283
289
|
raise e
|
284
290
|
|
285
|
-
def delete_session(self, auth_provider: AuthProvider, thread_id: str = "default", profile: str = "default") -> bool:
|
286
|
-
return self.session_storage.delete(auth_provider, thread_id, profile)
|
287
|
-
|
288
291
|
def get_auth_context(self, auth_provider: AuthProvider, thread_id: str = "default", profile: str = "default",
|
289
292
|
**kwargs) -> Optional[AuthContext]:
|
290
293
|
session = self.session_storage.get(auth_provider, thread_id, profile, **kwargs)
|
@@ -293,6 +296,23 @@ class PocketAuth(object):
|
|
293
296
|
|
294
297
|
return session.auth_context
|
295
298
|
|
299
|
+
def list_session_state(self, thread_id: str, auth_provider: Optional[AuthProvider] = None):
|
300
|
+
session_list = self.session_storage.get_by_thread_id(thread_id=thread_id, auth_provider=auth_provider)
|
301
|
+
session_state_list = []
|
302
|
+
for session in session_list:
|
303
|
+
state = self.get_session_state(session=session, auth_req=None)
|
304
|
+
|
305
|
+
session_state_list.append({
|
306
|
+
"provider": session.auth_provider_name,
|
307
|
+
"scope": session.auth_scopes,
|
308
|
+
"state": state,
|
309
|
+
})
|
310
|
+
|
311
|
+
return session_state_list
|
312
|
+
|
313
|
+
def delete_session(self, auth_provider: AuthProvider, thread_id: str = "default", profile: str = "default") -> bool:
|
314
|
+
return self.session_storage.delete(auth_provider, thread_id, profile)
|
315
|
+
|
296
316
|
def find_handler_instance(self, name: Optional[str] = None,
|
297
317
|
auth_provider: Optional[AuthProvider] = None) -> AuthHandlerInterface:
|
298
318
|
if name:
|
@@ -0,0 +1,264 @@
|
|
1
|
+
import asyncio
|
2
|
+
import pathlib
|
3
|
+
from typing import Any, Optional, Callable, List, Union
|
4
|
+
|
5
|
+
from hyperpocket.builtin import get_builtin_tools
|
6
|
+
from hyperpocket.config import pocket_logger
|
7
|
+
from hyperpocket.pocket_auth import PocketAuth
|
8
|
+
from hyperpocket.repository import Lockfile
|
9
|
+
from hyperpocket.repository.lock import LocalLock, GitLock
|
10
|
+
from hyperpocket.tool import Tool, ToolRequest
|
11
|
+
from hyperpocket.tool.function import from_func
|
12
|
+
from hyperpocket.tool.wasm import WasmTool
|
13
|
+
from hyperpocket.tool.wasm.tool import WasmToolRequest
|
14
|
+
from hyperpocket.tool_like import ToolLike
|
15
|
+
|
16
|
+
|
17
|
+
class PocketCore:
|
18
|
+
auth: PocketAuth
|
19
|
+
tools: dict[str, Tool]
|
20
|
+
|
21
|
+
def __init__(self,
|
22
|
+
tools: list[ToolLike],
|
23
|
+
auth: PocketAuth = None,
|
24
|
+
lockfile_path: Optional[str] = None,
|
25
|
+
force_update: bool = False):
|
26
|
+
if auth is None:
|
27
|
+
auth = PocketAuth()
|
28
|
+
self.auth = auth
|
29
|
+
|
30
|
+
if lockfile_path is None:
|
31
|
+
lockfile_path = "./pocket.lock"
|
32
|
+
lockfile_pathlib_path = pathlib.Path(lockfile_path)
|
33
|
+
lockfile = Lockfile(lockfile_pathlib_path)
|
34
|
+
tool_likes = []
|
35
|
+
for tool_like in tools:
|
36
|
+
if isinstance(tool_like, str):
|
37
|
+
if pathlib.Path(tool_like).exists():
|
38
|
+
lock = LocalLock(tool_like)
|
39
|
+
req = WasmToolRequest(lock, "")
|
40
|
+
else:
|
41
|
+
base_repo_url, git_ref, rel_path = GitLock.parsing_repo_url(repo_url=tool_like)
|
42
|
+
lock = GitLock(repository_url=base_repo_url, git_ref=git_ref)
|
43
|
+
req = WasmToolRequest(lock=lock, rel_path=rel_path, tool_vars={})
|
44
|
+
|
45
|
+
lockfile.add_lock(lock)
|
46
|
+
tool_likes.append(req)
|
47
|
+
elif isinstance(tool_like, WasmToolRequest):
|
48
|
+
lockfile.add_lock(tool_like.lock)
|
49
|
+
tool_likes.append(tool_like)
|
50
|
+
elif isinstance(tool_like, ToolRequest):
|
51
|
+
raise ValueError(f"unreachable. tool_like:{tool_like}")
|
52
|
+
elif isinstance(tool_like, WasmTool):
|
53
|
+
raise ValueError("WasmTool should pass ToolRequest instance instead.")
|
54
|
+
else:
|
55
|
+
tool_likes.append(tool_like)
|
56
|
+
lockfile.sync(force_update=force_update, referenced_only=True)
|
57
|
+
|
58
|
+
self.tools = dict()
|
59
|
+
for tool_like in tool_likes:
|
60
|
+
tool = self._load_tool(tool_like, lockfile)
|
61
|
+
if tool.name in self.tools:
|
62
|
+
pocket_logger.error(f"Duplicate tool name: {tool.name}.")
|
63
|
+
raise ValueError(f"Duplicate tool name: {tool.name}")
|
64
|
+
self.tools[tool.name] = tool
|
65
|
+
|
66
|
+
pocket_logger.info(f"All Registered Tools Loaded successfully. total registered tools : {len(self.tools)}")
|
67
|
+
|
68
|
+
builtin_tools = get_builtin_tools(self.auth)
|
69
|
+
for tool in builtin_tools:
|
70
|
+
self.tools[tool.name] = tool
|
71
|
+
pocket_logger.info(f"All BuiltIn Tools Loaded successfully. total tools : {len(self.tools)}")
|
72
|
+
|
73
|
+
async def acall(self,
|
74
|
+
tool_name: str,
|
75
|
+
body: Any,
|
76
|
+
thread_id: str = 'default',
|
77
|
+
profile: str = 'default',
|
78
|
+
*args, **kwargs) -> tuple[str, bool]:
|
79
|
+
"""
|
80
|
+
Invoke tool asynchronously, not that different from `Pocket.invoke`
|
81
|
+
But this method is called only in subprocess.
|
82
|
+
|
83
|
+
This function performs the following steps:
|
84
|
+
1. `prepare_auth` : preparing the authentication process for the tool if necessary.
|
85
|
+
2. `authenticate` : performing authentication that needs to invoke tool.
|
86
|
+
3. `tool_call` : Executing tool actually with authentication information.
|
87
|
+
|
88
|
+
Args:
|
89
|
+
tool_name(str): tool name to invoke
|
90
|
+
body(Any): tool arguments. should be json format
|
91
|
+
thread_id(str): thread id
|
92
|
+
profile(str): profile name
|
93
|
+
|
94
|
+
Returns:
|
95
|
+
tuple[str, bool]: tool result and state.
|
96
|
+
"""
|
97
|
+
tool = self._tool_instance(tool_name)
|
98
|
+
if tool.auth is not None:
|
99
|
+
callback_info = self.prepare_auth(tool_name, thread_id, profile, **kwargs)
|
100
|
+
if callback_info:
|
101
|
+
return callback_info, True
|
102
|
+
# 02. authenticate
|
103
|
+
credentials = await self.authenticate(tool_name, thread_id, profile, **kwargs)
|
104
|
+
# 03. call tool
|
105
|
+
result = await self.tool_call(tool_name, body=body, envs=credentials, **kwargs)
|
106
|
+
return result, False
|
107
|
+
|
108
|
+
def prepare_auth(self,
|
109
|
+
tool_name: Union[str, List[str]],
|
110
|
+
thread_id: str = 'default',
|
111
|
+
profile: str = 'default',
|
112
|
+
**kwargs) -> Optional[str]:
|
113
|
+
"""
|
114
|
+
Prepares the authentication process for the tool if necessary.
|
115
|
+
Returns callback URL and whether the tool requires authentication.
|
116
|
+
|
117
|
+
Args:
|
118
|
+
tool_name(Union[str,List[str]]): tool name to invoke
|
119
|
+
thread_id(str): thread id
|
120
|
+
profile(str): profile name
|
121
|
+
|
122
|
+
Returns:
|
123
|
+
Optional[str]: callback URI if necessary
|
124
|
+
"""
|
125
|
+
|
126
|
+
if isinstance(tool_name, str):
|
127
|
+
tool_name = [tool_name]
|
128
|
+
|
129
|
+
tools: List[Tool] = []
|
130
|
+
for name in tool_name:
|
131
|
+
tool = self._tool_instance(name)
|
132
|
+
if tool.auth is not None:
|
133
|
+
tools.append(tool)
|
134
|
+
|
135
|
+
if len(tools) == 0:
|
136
|
+
return None
|
137
|
+
|
138
|
+
auth_handler_name = tools[0].auth.auth_handler
|
139
|
+
auth_provider = tools[0].auth.auth_provider
|
140
|
+
auth_scopes = set()
|
141
|
+
|
142
|
+
for tool in tools:
|
143
|
+
if tool.auth.auth_handler != auth_handler_name:
|
144
|
+
pocket_logger.error(
|
145
|
+
f"All Tools should have same auth handler. but it's different {tool.auth.auth_handler}, {auth_handler_name}")
|
146
|
+
|
147
|
+
return f"All Tools should have same auth handler. but it's different {tool.auth.auth_handler}, {auth_handler_name}"
|
148
|
+
if tool.auth.auth_provider != auth_provider:
|
149
|
+
pocket_logger.error(
|
150
|
+
f"All Tools should have same auth provider. but it's different {tool.auth.auth_provider}, {auth_provider}")
|
151
|
+
return f"All Tools should have same auth provider. but it's different {tool.auth.auth_provider}, {auth_provider}"
|
152
|
+
|
153
|
+
if tool.auth.scopes is not None:
|
154
|
+
auth_scopes |= set(tool.auth.scopes)
|
155
|
+
|
156
|
+
auth_req = self.auth.make_request(
|
157
|
+
auth_handler_name=auth_handler_name,
|
158
|
+
auth_provider=auth_provider,
|
159
|
+
auth_scopes=list(auth_scopes))
|
160
|
+
|
161
|
+
return self.auth.prepare(
|
162
|
+
auth_req=auth_req,
|
163
|
+
auth_handler_name=auth_handler_name,
|
164
|
+
auth_provider=auth_provider,
|
165
|
+
thread_id=thread_id,
|
166
|
+
profile=profile,
|
167
|
+
**kwargs
|
168
|
+
)
|
169
|
+
|
170
|
+
async def authenticate(
|
171
|
+
self,
|
172
|
+
tool_name: str,
|
173
|
+
thread_id: str = 'default',
|
174
|
+
profile: str = 'default',
|
175
|
+
**kwargs) -> dict[str, str]:
|
176
|
+
"""
|
177
|
+
Authenticates the handler included in the tool and returns credentials.
|
178
|
+
|
179
|
+
Args:
|
180
|
+
tool_name(str): tool name to invoke
|
181
|
+
thread_id(str): thread id
|
182
|
+
profile(str): profile name
|
183
|
+
|
184
|
+
Returns:
|
185
|
+
dict[str, str]: credentials
|
186
|
+
"""
|
187
|
+
tool = self._tool_instance(tool_name)
|
188
|
+
if tool.auth is None:
|
189
|
+
return {}
|
190
|
+
auth_req = self.auth.make_request(
|
191
|
+
auth_handler_name=tool.auth.auth_handler,
|
192
|
+
auth_provider=tool.auth.auth_provider,
|
193
|
+
auth_scopes=tool.auth.scopes)
|
194
|
+
auth_ctx = await self.auth.authenticate_async(
|
195
|
+
auth_req=auth_req,
|
196
|
+
auth_handler_name=tool.auth.auth_handler,
|
197
|
+
auth_provider=tool.auth.auth_provider,
|
198
|
+
thread_id=thread_id,
|
199
|
+
profile=profile,
|
200
|
+
**kwargs,
|
201
|
+
)
|
202
|
+
return auth_ctx.to_dict()
|
203
|
+
|
204
|
+
async def tool_call(self, tool_name: str, **kwargs) -> str:
|
205
|
+
"""
|
206
|
+
Executing tool actually
|
207
|
+
|
208
|
+
Args:
|
209
|
+
tool_name(str): tool name to invoke
|
210
|
+
kwargs(dict): keyword arguments. authentication information is passed through this.
|
211
|
+
|
212
|
+
Returns:
|
213
|
+
str: tool result
|
214
|
+
"""
|
215
|
+
tool = self._tool_instance(tool_name)
|
216
|
+
try:
|
217
|
+
result = await asyncio.wait_for(tool.ainvoke(**kwargs), timeout=180)
|
218
|
+
except asyncio.TimeoutError:
|
219
|
+
pocket_logger.warning("Timeout tool call.")
|
220
|
+
return "timeout tool call"
|
221
|
+
|
222
|
+
if tool.postprocessings is not None:
|
223
|
+
for postprocessing in tool.postprocessings:
|
224
|
+
try:
|
225
|
+
result = postprocessing(result)
|
226
|
+
except Exception as e:
|
227
|
+
exception_str = (
|
228
|
+
f"Error in postprocessing `{postprocessing.__name__}`: {e}"
|
229
|
+
)
|
230
|
+
pocket_logger.error(exception_str)
|
231
|
+
return exception_str
|
232
|
+
|
233
|
+
return result
|
234
|
+
|
235
|
+
def grouping_tool_by_auth_provider(self) -> dict[str, List[Tool]]:
|
236
|
+
tool_by_provider = {}
|
237
|
+
for tool_name, tool in self.tools.items():
|
238
|
+
if tool.auth is None:
|
239
|
+
continue
|
240
|
+
|
241
|
+
auth_provider_name = tool.auth.auth_provider.name
|
242
|
+
if tool_by_provider.get(auth_provider_name):
|
243
|
+
tool_by_provider[auth_provider_name].append(tool)
|
244
|
+
else:
|
245
|
+
tool_by_provider[auth_provider_name] = [tool]
|
246
|
+
return tool_by_provider
|
247
|
+
|
248
|
+
def _tool_instance(self, tool_name: str) -> Tool:
|
249
|
+
return self.tools[tool_name]
|
250
|
+
|
251
|
+
@staticmethod
|
252
|
+
def _load_tool(tool_like: ToolLike, lockfile: Lockfile) -> Tool:
|
253
|
+
pocket_logger.info(f"Loading Tool {tool_like}")
|
254
|
+
if isinstance(tool_like, Tool):
|
255
|
+
tool = tool_like
|
256
|
+
elif isinstance(tool_like, ToolRequest):
|
257
|
+
tool = Tool.from_tool_request(tool_like, lockfile=lockfile)
|
258
|
+
elif isinstance(tool_like, Callable):
|
259
|
+
tool = from_func(tool_like)
|
260
|
+
else:
|
261
|
+
raise ValueError(f"Invalid tool type: {type(tool_like)}")
|
262
|
+
|
263
|
+
pocket_logger.info(f"Complete Loading Tool {tool.name}")
|
264
|
+
return tool
|