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.
Files changed (97) hide show
  1. hyperpocket/auth/README.md +3 -3
  2. hyperpocket/auth/__init__.py +0 -8
  3. hyperpocket/auth/gumloop/context.py +13 -0
  4. hyperpocket/auth/gumloop/token_context.py +15 -0
  5. hyperpocket/auth/gumloop/token_handler.py +66 -0
  6. hyperpocket/auth/gumloop/token_schema.py +8 -0
  7. hyperpocket/auth/linear/token_context.py +1 -1
  8. hyperpocket/auth/notion/README.md +28 -0
  9. hyperpocket/auth/notion/context.py +15 -0
  10. hyperpocket/auth/notion/token_context.py +14 -0
  11. hyperpocket/auth/notion/token_handler.py +65 -0
  12. hyperpocket/auth/notion/token_schema.py +10 -0
  13. hyperpocket/auth/provider.py +8 -5
  14. hyperpocket/auth/reddit/context.py +15 -0
  15. hyperpocket/auth/reddit/oauth2_context.py +32 -0
  16. hyperpocket/auth/reddit/oauth2_handler.py +151 -0
  17. hyperpocket/auth/reddit/oauth2_schema.py +18 -0
  18. hyperpocket/auth/slack/token_context.py +1 -1
  19. hyperpocket/builtin.py +63 -0
  20. hyperpocket/cli/__main__.py +12 -0
  21. hyperpocket/cli/auth.py +83 -0
  22. hyperpocket/cli/codegen/auth/__init__.py +13 -0
  23. hyperpocket/cli/codegen/auth/auth_context_template.py +16 -0
  24. hyperpocket/cli/codegen/auth/auth_token_context_template.py +16 -0
  25. hyperpocket/cli/codegen/auth/auth_token_handler_template.py +69 -0
  26. hyperpocket/cli/codegen/auth/auth_token_schema_template.py +12 -0
  27. hyperpocket/cli/codegen/auth/server_auth_template.py +18 -0
  28. hyperpocket/cli/eject.py +19 -0
  29. hyperpocket/cli/sync.py +5 -5
  30. hyperpocket/config/settings.py +14 -12
  31. hyperpocket/futures/futurestore.py +0 -1
  32. hyperpocket/pocket_auth.py +25 -5
  33. hyperpocket/pocket_core.py +264 -0
  34. hyperpocket/pocket_main.py +127 -174
  35. hyperpocket/prompts.py +6 -8
  36. hyperpocket/repository/__init__.py +2 -2
  37. hyperpocket/repository/lock.py +71 -1
  38. hyperpocket/repository/lockfile.py +19 -13
  39. hyperpocket/repository/repository.py +26 -1
  40. hyperpocket/server/auth/__init__.py +0 -6
  41. hyperpocket/server/auth/gumloop.py +16 -0
  42. hyperpocket/server/auth/notion.py +19 -0
  43. hyperpocket/server/auth/reddit.py +16 -0
  44. hyperpocket/server/server.py +56 -20
  45. hyperpocket/server/tool/dto/script.py +15 -2
  46. hyperpocket/server/tool/wasm.py +20 -8
  47. hyperpocket/session/README.md +2 -2
  48. hyperpocket/session/in_memory.py +18 -5
  49. hyperpocket/session/interface.py +14 -0
  50. hyperpocket/session/redis.py +29 -5
  51. hyperpocket/tool/README.md +16 -12
  52. hyperpocket/tool/__init__.py +4 -3
  53. hyperpocket/tool/function/README.md +39 -10
  54. hyperpocket/tool/function/__init__.py +2 -0
  55. hyperpocket/tool/function/annotation.py +2 -1
  56. hyperpocket/tool/function/tool.py +108 -29
  57. hyperpocket/tool/tool.py +100 -28
  58. hyperpocket/tool/wasm/README.md +27 -5
  59. hyperpocket/tool/wasm/browser.py +2 -7
  60. hyperpocket/tool/wasm/script.py +40 -1
  61. hyperpocket/tool/wasm/templates/python.py +32 -14
  62. hyperpocket/tool/wasm/tool.py +21 -18
  63. hyperpocket/tool_like.py +5 -0
  64. hyperpocket/util/__init__.py +1 -1
  65. hyperpocket/util/extract_func_param_desc_from_docstring.py +4 -4
  66. hyperpocket/util/function_to_model.py +5 -2
  67. hyperpocket/util/json_schema_to_model.py +47 -26
  68. {hyperpocket-0.0.3.dist-info → hyperpocket-0.1.9.dist-info}/METADATA +107 -88
  69. hyperpocket-0.1.9.dist-info/RECORD +137 -0
  70. {hyperpocket-0.0.3.dist-info → hyperpocket-0.1.9.dist-info}/WHEEL +1 -1
  71. hyperpocket-0.1.9.dist-info/entry_points.txt +2 -0
  72. hyperpocket/auth/README.KR.md +0 -309
  73. hyperpocket/auth/slack/tests/test_oauth2_handler.py +0 -32
  74. hyperpocket/auth/slack/tests/test_token_handler.py +0 -23
  75. hyperpocket/auth/tests/test_google_oauth2_handler.py +0 -147
  76. hyperpocket/auth/tests/test_slack_oauth2_handler.py +0 -147
  77. hyperpocket/auth/tests/test_slack_token_handler.py +0 -66
  78. hyperpocket/external/__init__.py +0 -7
  79. hyperpocket/external/github_client.py +0 -19
  80. hyperpocket/session/README.KR.md +0 -62
  81. hyperpocket/session/tests/test_in_memory.py +0 -145
  82. hyperpocket/session/tests/test_redis.py +0 -151
  83. hyperpocket/tests/test_pocket.py +0 -116
  84. hyperpocket/tests/test_pocket_auth.py +0 -982
  85. hyperpocket/tool/README.KR.md +0 -68
  86. hyperpocket/tool/builtins/__init__.py +0 -0
  87. hyperpocket/tool/builtins/example/__init__.py +0 -0
  88. hyperpocket/tool/builtins/example/add_tool.py +0 -18
  89. hyperpocket/tool/function/README.KR.md +0 -159
  90. hyperpocket/tool/tests/test_function_tool.py +0 -266
  91. hyperpocket/tool/wasm/README.KR.md +0 -144
  92. hyperpocket-0.0.3.dist-info/RECORD +0 -130
  93. hyperpocket-0.0.3.dist-info/entry_points.txt +0 -3
  94. /hyperpocket/auth/{slack/tests → gumloop}/__init__.py +0 -0
  95. /hyperpocket/auth/{tests → notion}/__init__.py +0 -0
  96. /hyperpocket/{session/tests → auth/reddit}/__init__.py +0 -0
  97. /hyperpocket/{tests → cli/codegen}/__init__.py +0 -0
@@ -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
+ ''')
@@ -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='PATHS', type=click.Path(exists=True))
11
- @click.option("--force-update", type=str, default='HEAD')
12
- def sync(url: str, lockfile: Optional[pathlib.Path], force_update: bool):
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() / 'pocket.lock'
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)
@@ -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
- pocket_root = Path.home() / ".pocket"
13
- if not pocket_root.exists():
14
- os.makedirs(pocket_root)
15
- settings_path = pocket_root / "settings.toml"
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
- secret_path = pocket_root / ".secrets.toml"
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
- toolpkg_path = pocket_root / "toolpkg"
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 == 'https' and self.public_server_port == 443:
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 == 'http' and self.public_server_port == 80:
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
 
@@ -1,5 +1,4 @@
1
1
  import asyncio
2
- import enum
3
2
  from typing import Any
4
3
 
5
4
  from hyperpocket.config import pocket_logger
@@ -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=handler.provider().name, auth_req=auth_req):
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