hyperpocket 0.0.1__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- hyperpocket/__init__.py +7 -0
- hyperpocket/auth/README.KR.md +309 -0
- hyperpocket/auth/README.md +323 -0
- hyperpocket/auth/__init__.py +24 -0
- hyperpocket/auth/calendly/__init__.py +0 -0
- hyperpocket/auth/calendly/context.py +13 -0
- hyperpocket/auth/calendly/oauth2_context.py +25 -0
- hyperpocket/auth/calendly/oauth2_handler.py +146 -0
- hyperpocket/auth/calendly/oauth2_schema.py +16 -0
- hyperpocket/auth/context.py +38 -0
- hyperpocket/auth/github/__init__.py +0 -0
- hyperpocket/auth/github/context.py +13 -0
- hyperpocket/auth/github/oauth2_context.py +25 -0
- hyperpocket/auth/github/oauth2_handler.py +143 -0
- hyperpocket/auth/github/oauth2_schema.py +16 -0
- hyperpocket/auth/github/token_context.py +12 -0
- hyperpocket/auth/github/token_handler.py +79 -0
- hyperpocket/auth/github/token_schema.py +9 -0
- hyperpocket/auth/google/__init__.py +0 -0
- hyperpocket/auth/google/context.py +15 -0
- hyperpocket/auth/google/oauth2_context.py +31 -0
- hyperpocket/auth/google/oauth2_handler.py +137 -0
- hyperpocket/auth/google/oauth2_schema.py +18 -0
- hyperpocket/auth/handler.py +171 -0
- hyperpocket/auth/linear/__init__.py +0 -0
- hyperpocket/auth/linear/context.py +15 -0
- hyperpocket/auth/linear/token_context.py +15 -0
- hyperpocket/auth/linear/token_handler.py +68 -0
- hyperpocket/auth/linear/token_schema.py +9 -0
- hyperpocket/auth/provider.py +16 -0
- hyperpocket/auth/schema.py +19 -0
- hyperpocket/auth/slack/__init__.py +0 -0
- hyperpocket/auth/slack/context.py +15 -0
- hyperpocket/auth/slack/oauth2_context.py +40 -0
- hyperpocket/auth/slack/oauth2_handler.py +151 -0
- hyperpocket/auth/slack/oauth2_schema.py +40 -0
- hyperpocket/auth/slack/tests/__init__.py +0 -0
- hyperpocket/auth/slack/tests/test_oauth2_handler.py +32 -0
- hyperpocket/auth/slack/tests/test_token_handler.py +23 -0
- hyperpocket/auth/slack/token_context.py +14 -0
- hyperpocket/auth/slack/token_handler.py +64 -0
- hyperpocket/auth/slack/token_schema.py +9 -0
- hyperpocket/auth/tests/__init__.py +0 -0
- hyperpocket/auth/tests/test_google_oauth2_handler.py +147 -0
- hyperpocket/auth/tests/test_slack_oauth2_handler.py +147 -0
- hyperpocket/auth/tests/test_slack_token_handler.py +66 -0
- hyperpocket/cli/__init__.py +0 -0
- hyperpocket/cli/__main__.py +12 -0
- hyperpocket/cli/pull.py +18 -0
- hyperpocket/cli/sync.py +17 -0
- hyperpocket/config/__init__.py +9 -0
- hyperpocket/config/auth.py +36 -0
- hyperpocket/config/git.py +17 -0
- hyperpocket/config/logger.py +81 -0
- hyperpocket/config/session.py +35 -0
- hyperpocket/config/settings.py +62 -0
- hyperpocket/constants.py +0 -0
- hyperpocket/curated_tools.py +10 -0
- hyperpocket/external/__init__.py +7 -0
- hyperpocket/external/github_client.py +19 -0
- hyperpocket/futures/__init__.py +7 -0
- hyperpocket/futures/futurestore.py +48 -0
- hyperpocket/pocket_auth.py +344 -0
- hyperpocket/pocket_main.py +351 -0
- hyperpocket/prompts.py +15 -0
- hyperpocket/repository/__init__.py +5 -0
- hyperpocket/repository/lock.py +156 -0
- hyperpocket/repository/lockfile.py +56 -0
- hyperpocket/repository/repository.py +18 -0
- hyperpocket/server/__init__.py +3 -0
- hyperpocket/server/auth/__init__.py +15 -0
- hyperpocket/server/auth/calendly.py +16 -0
- hyperpocket/server/auth/github.py +25 -0
- hyperpocket/server/auth/google.py +16 -0
- hyperpocket/server/auth/linear.py +18 -0
- hyperpocket/server/auth/slack.py +28 -0
- hyperpocket/server/auth/token.py +51 -0
- hyperpocket/server/proxy.py +63 -0
- hyperpocket/server/server.py +178 -0
- hyperpocket/server/tool/__init__.py +10 -0
- hyperpocket/server/tool/dto/__init__.py +0 -0
- hyperpocket/server/tool/dto/script.py +15 -0
- hyperpocket/server/tool/wasm.py +31 -0
- hyperpocket/session/README.KR.md +62 -0
- hyperpocket/session/README.md +61 -0
- hyperpocket/session/__init__.py +4 -0
- hyperpocket/session/in_memory.py +76 -0
- hyperpocket/session/interface.py +118 -0
- hyperpocket/session/redis.py +126 -0
- hyperpocket/session/tests/__init__.py +0 -0
- hyperpocket/session/tests/test_in_memory.py +145 -0
- hyperpocket/session/tests/test_redis.py +151 -0
- hyperpocket/tests/__init__.py +0 -0
- hyperpocket/tests/test_pocket.py +118 -0
- hyperpocket/tests/test_pocket_auth.py +982 -0
- hyperpocket/tool/README.KR.md +68 -0
- hyperpocket/tool/README.md +75 -0
- hyperpocket/tool/__init__.py +13 -0
- hyperpocket/tool/builtins/__init__.py +0 -0
- hyperpocket/tool/builtins/example/__init__.py +0 -0
- hyperpocket/tool/builtins/example/add_tool.py +18 -0
- hyperpocket/tool/function/README.KR.md +159 -0
- hyperpocket/tool/function/README.md +169 -0
- hyperpocket/tool/function/__init__.py +9 -0
- hyperpocket/tool/function/annotation.py +30 -0
- hyperpocket/tool/function/tool.py +87 -0
- hyperpocket/tool/tests/__init__.py +0 -0
- hyperpocket/tool/tests/test_function_tool.py +266 -0
- hyperpocket/tool/tool.py +106 -0
- hyperpocket/tool/wasm/README.KR.md +144 -0
- hyperpocket/tool/wasm/README.md +144 -0
- hyperpocket/tool/wasm/__init__.py +3 -0
- hyperpocket/tool/wasm/browser.py +63 -0
- hyperpocket/tool/wasm/invoker.py +41 -0
- hyperpocket/tool/wasm/script.py +82 -0
- hyperpocket/tool/wasm/templates/__init__.py +28 -0
- hyperpocket/tool/wasm/templates/node.py +87 -0
- hyperpocket/tool/wasm/templates/python.py +75 -0
- hyperpocket/tool/wasm/tool.py +147 -0
- hyperpocket/util/__init__.py +1 -0
- hyperpocket/util/extract_func_param_desc_from_docstring.py +97 -0
- hyperpocket/util/find_all_leaf_class_in_package.py +17 -0
- hyperpocket/util/find_all_subclass_in_package.py +29 -0
- hyperpocket/util/flatten_json_schema.py +45 -0
- hyperpocket/util/function_to_model.py +46 -0
- hyperpocket/util/get_objects_from_subpackage.py +28 -0
- hyperpocket/util/json_schema_to_model.py +69 -0
- hyperpocket-0.0.1.dist-info/METADATA +304 -0
- hyperpocket-0.0.1.dist-info/RECORD +131 -0
- hyperpocket-0.0.1.dist-info/WHEEL +4 -0
- hyperpocket-0.0.1.dist-info/entry_points.txt +3 -0
File without changes
|
@@ -0,0 +1,15 @@
|
|
1
|
+
from pydantic import BaseModel, Field
|
2
|
+
|
3
|
+
from hyperpocket.tool.wasm.script import ScriptFileNode
|
4
|
+
|
5
|
+
|
6
|
+
class Script(BaseModel):
|
7
|
+
id: str = Field(alias='id')
|
8
|
+
tool_id: str = Field(alias='tool_id')
|
9
|
+
|
10
|
+
|
11
|
+
class ScriptStdout(BaseModel):
|
12
|
+
stdout: str = Field(alias='stdout')
|
13
|
+
|
14
|
+
class ScriptFileTree(BaseModel):
|
15
|
+
tree: dict[str, ScriptFileNode] = Field(alias='tree')
|
@@ -0,0 +1,31 @@
|
|
1
|
+
from fastapi import APIRouter
|
2
|
+
from fastapi.responses import HTMLResponse
|
3
|
+
|
4
|
+
from hyperpocket.futures import FutureStore
|
5
|
+
from hyperpocket.server.tool.dto import script as scriptdto
|
6
|
+
from hyperpocket.tool.wasm.script import ScriptStore
|
7
|
+
|
8
|
+
wasm_tool_router = APIRouter(
|
9
|
+
prefix="/wasm"
|
10
|
+
)
|
11
|
+
|
12
|
+
|
13
|
+
@wasm_tool_router.get("/scripts/{script_id}/browse", response_class=HTMLResponse)
|
14
|
+
async def browse_script_page(script_id: str):
|
15
|
+
html = ScriptStore.get_script(script_id).rendered_html
|
16
|
+
return HTMLResponse(content=html)
|
17
|
+
|
18
|
+
|
19
|
+
@wasm_tool_router.post("/scripts/{script_id}/done")
|
20
|
+
async def done_script_page(script_id: str, req: scriptdto.ScriptStdout) -> scriptdto.ScriptStdout:
|
21
|
+
FutureStore.resolve_future(script_id, req.stdout)
|
22
|
+
return scriptdto.ScriptStdout(stdout=req.stdout)
|
23
|
+
|
24
|
+
@wasm_tool_router.post("/scripts/{script_id}/fail")
|
25
|
+
async def fail_script_page(script_id: str, req: scriptdto.ScriptStdout) -> scriptdto.ScriptStdout:
|
26
|
+
pass
|
27
|
+
|
28
|
+
@wasm_tool_router.get("/scripts/{script_id}/file_tree")
|
29
|
+
async def get_file_tree(script_id: str) -> scriptdto.ScriptFileTree:
|
30
|
+
script = ScriptStore.get_script(script_id)
|
31
|
+
return scriptdto.ScriptFileTree(tree=script.load_file_tree())
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# Session Storage
|
2
|
+
|
3
|
+
유저의 authentication(auth) session을 저장하기 위한 storage
|
4
|
+
|
5
|
+
## Current supported session storage
|
6
|
+
|
7
|
+
- [x] InMemory
|
8
|
+
- [x] Redis
|
9
|
+
- [ ] Postgres
|
10
|
+
- [ ] Mysql
|
11
|
+
- [ ] Mongodb
|
12
|
+
|
13
|
+
## SessionKey
|
14
|
+
|
15
|
+
세션 키는 유저의 인증 정보를 식별하기 위한 유니크 키이다.
|
16
|
+
|
17
|
+
일반적으로 세션 키는 다음 세 가지 요소로 식별될 수 있다.
|
18
|
+
|
19
|
+
- auth provider
|
20
|
+
- profile
|
21
|
+
- thread_id
|
22
|
+
|
23
|
+
하나의 auth provider에서는 하나의 session만 가질 수 있다.
|
24
|
+
|
25
|
+
- 예를 들어 slack token으로 이미 인증한 사용자가 oauth로 인증하려 하면 이전 세션은 사라지게 된다.
|
26
|
+
|
27
|
+
- profile은 pocket에서 지원하는 개념으로 하나의 thread_id에 여러 profile이 존재할 수 있다.
|
28
|
+
|
29
|
+
- profile을 이용해 한 사용자는 여러 persona를 갖고 작업을 수행하도록 할 수 있다.
|
30
|
+
- e.g., A group의 슬랙 메세지를 읽어 B group으로 요약해서 전달
|
31
|
+
|
32
|
+
## BaseSessionValue
|
33
|
+
|
34
|
+
```python
|
35
|
+
class BaseSessionValue(BaseModel):
|
36
|
+
auth_provider_name: str
|
37
|
+
auth_context: Optional[AuthContext] = None
|
38
|
+
scoped: bool
|
39
|
+
auth_scopes: Optional[Set[str]] = None
|
40
|
+
auth_resolve_uid: Optional[str] = None
|
41
|
+
```
|
42
|
+
|
43
|
+
기본적으로 Session에는 다음 정보들을 갖고 있다.
|
44
|
+
|
45
|
+
- auth_provider_name : 현재 세션을 인증한 auth provider name
|
46
|
+
- auth_context : 실제 세션 내용이 들어있는 auth context
|
47
|
+
- scoped : 현재 세션이 scoped session인지 여부
|
48
|
+
- auth_scopes : 현재 세션의 auth scopes. scoped session의 경우에만 존재
|
49
|
+
- auth_resolve_uid : 유저가 auth 인증을 완료했는지를 비동기적으로 확인하기 위한 uid
|
50
|
+
|
51
|
+
## SessionStorageInterface
|
52
|
+
|
53
|
+
TBU
|
54
|
+
|
55
|
+
## How To Implement
|
56
|
+
|
57
|
+
1. `pocket/config/session.py` 내에 새로운 SessionType Enum 추가
|
58
|
+
2. `pocket/config/session.py` 내에 새로운 SessionConfig 추가
|
59
|
+
3. SessionStorageInterface 구현
|
60
|
+
- session storage가 초기화될 때 위에서 정의한 SessionConfig를 입력으로 받아야 한다.
|
61
|
+
|
62
|
+
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# Session Storage
|
2
|
+
|
3
|
+
Storage for saving user authentication(auth) sessions.
|
4
|
+
|
5
|
+
## Current Supported Session Storages
|
6
|
+
|
7
|
+
- [x] InMemory
|
8
|
+
- [x] Redis
|
9
|
+
- [ ] Postgres
|
10
|
+
- [ ] Mysql
|
11
|
+
- [ ] Mongodb
|
12
|
+
|
13
|
+
## SessionKey
|
14
|
+
|
15
|
+
Session key is a unique key to identify each authentication information.
|
16
|
+
|
17
|
+
Generally this session key can be composed of the following three components:
|
18
|
+
|
19
|
+
- Auth Provider : authentication provider
|
20
|
+
- Thread ID : thread id
|
21
|
+
- Profile : profile
|
22
|
+
|
23
|
+
Each auth provider can only have one active session at a time.
|
24
|
+
|
25
|
+
- For example, if a user already authenticated with a Slack token attempts to authenticate with OAuth, the previous
|
26
|
+
session will be invalidated.
|
27
|
+
|
28
|
+
- Profile is a concept supported by Pocket that allows multiple profiles to exist within a single thread ID.
|
29
|
+
|
30
|
+
- Using profiles, a single user can operate with multiple personas for various tasks.
|
31
|
+
- e.g., Reading messages from Group A’s Slack and summarizing them for Group B.
|
32
|
+
|
33
|
+
## BaseSessionValue
|
34
|
+
|
35
|
+
```python
|
36
|
+
class BaseSessionValue(BaseModel):
|
37
|
+
auth_provider_name: str
|
38
|
+
auth_context: Optional[AuthContext] = None
|
39
|
+
scoped: bool
|
40
|
+
auth_scopes: Optional[Set[str]] = None
|
41
|
+
auth_resolve_uid: Optional[str] = None
|
42
|
+
```
|
43
|
+
|
44
|
+
A session contains the following basic information:
|
45
|
+
|
46
|
+
- auth_provider_name: The name of the auth provider used for the current session.
|
47
|
+
- auth_context: The actual session data contained in the auth context.
|
48
|
+
- scoped: Indicates whether the current session is a scoped session.
|
49
|
+
- auth_scopes: Auth scopes for the current session. This exists only for scoped sessions.
|
50
|
+
- auth_resolve_uid: A UID for asynchronously checking whether the user has completed authentication.
|
51
|
+
|
52
|
+
## SessionStorageInterface
|
53
|
+
|
54
|
+
To Be Updated (TBU)
|
55
|
+
|
56
|
+
## How to Implement
|
57
|
+
|
58
|
+
1. Add the SessionType enum in `pocket/config/session.py`.
|
59
|
+
2. Add the SessionConfig in `pocket/config/session.py`.
|
60
|
+
3. Implement the SessionStorageInterface
|
61
|
+
- The session storage must be initialized with the SessionConfig defined above.
|
@@ -0,0 +1,76 @@
|
|
1
|
+
from typing import Dict, List, Optional
|
2
|
+
|
3
|
+
from hyperpocket.auth import AuthProvider
|
4
|
+
from hyperpocket.auth.context import AuthContext
|
5
|
+
from hyperpocket.config.session import SessionConfigInMemory
|
6
|
+
from hyperpocket.config.session import SessionType
|
7
|
+
from hyperpocket.session.interface import SessionStorageInterface, SESSION_KEY_DELIMITER, \
|
8
|
+
BaseSessionValue, V, K
|
9
|
+
|
10
|
+
InMemorySessionKey = str
|
11
|
+
InMemorySessionValue = BaseSessionValue
|
12
|
+
|
13
|
+
|
14
|
+
class InMemorySessionStorage(SessionStorageInterface[InMemorySessionKey, InMemorySessionValue]):
|
15
|
+
# TODO(moon) : Force it to always take SessionConfig as an input
|
16
|
+
def __init__(self, session_config: SessionConfigInMemory):
|
17
|
+
super().__init__()
|
18
|
+
self.storage: Dict[InMemorySessionKey, InMemorySessionValue] = {}
|
19
|
+
|
20
|
+
@classmethod
|
21
|
+
def session_storage_type(cls) -> SessionType:
|
22
|
+
return SessionType.IN_MEMORY
|
23
|
+
|
24
|
+
def get(self, auth_provider: AuthProvider, thread_id: str, profile: str, **kwargs) -> Optional[V]:
|
25
|
+
key = self._make_session_key(auth_provider, thread_id, profile)
|
26
|
+
return self.storage.get(key, None)
|
27
|
+
|
28
|
+
def set(self, auth_provider: AuthProvider,
|
29
|
+
thread_id: str,
|
30
|
+
profile: str,
|
31
|
+
auth_scopes: List[str],
|
32
|
+
auth_resolve_uid: Optional[str],
|
33
|
+
auth_context: Optional[AuthContext],
|
34
|
+
is_auth_scope_universal: bool, **kwargs) -> V:
|
35
|
+
key = self._make_session_key(auth_provider, thread_id, profile)
|
36
|
+
session = self._make_session(
|
37
|
+
auth_provider_name=auth_provider.name,
|
38
|
+
auth_scopes=auth_scopes,
|
39
|
+
auth_resolve_uid=auth_resolve_uid,
|
40
|
+
auth_context=auth_context,
|
41
|
+
is_auth_scope_universal=is_auth_scope_universal)
|
42
|
+
|
43
|
+
self.storage[key] = session
|
44
|
+
return session
|
45
|
+
|
46
|
+
def delete(self, auth_provider: AuthProvider, thread_id: str, profile: str, **kwargs) -> bool:
|
47
|
+
key = self._make_session_key(auth_provider, thread_id, profile)
|
48
|
+
if key in self.storage:
|
49
|
+
self.storage.pop(key)
|
50
|
+
return True
|
51
|
+
|
52
|
+
return False
|
53
|
+
|
54
|
+
@staticmethod
|
55
|
+
def _make_session_key(auth_provider: AuthProvider, thread_id: str, profile: str) -> K:
|
56
|
+
return "{auth_provider}{delimiter}{thread_id}{delimiter}{profile}".format(
|
57
|
+
auth_provider=auth_provider.name,
|
58
|
+
thread_id=thread_id,
|
59
|
+
profile=profile,
|
60
|
+
delimiter=SESSION_KEY_DELIMITER,
|
61
|
+
)
|
62
|
+
|
63
|
+
@staticmethod
|
64
|
+
def _make_session(
|
65
|
+
auth_provider_name: str,
|
66
|
+
auth_scopes: List[str],
|
67
|
+
auth_context: AuthContext,
|
68
|
+
auth_resolve_uid: str,
|
69
|
+
is_auth_scope_universal: bool) -> V:
|
70
|
+
return InMemorySessionValue(
|
71
|
+
auth_provider_name=auth_provider_name,
|
72
|
+
auth_scopes=set(auth_scopes),
|
73
|
+
auth_context=auth_context,
|
74
|
+
auth_resolve_uid=auth_resolve_uid,
|
75
|
+
scoped=is_auth_scope_universal
|
76
|
+
)
|
@@ -0,0 +1,118 @@
|
|
1
|
+
import datetime
|
2
|
+
from abc import ABC, abstractmethod
|
3
|
+
from typing import TypeVar, Generic, List, Set, Optional, Iterable
|
4
|
+
|
5
|
+
from pydantic import BaseModel, Field
|
6
|
+
|
7
|
+
from hyperpocket.auth import AuthProvider
|
8
|
+
from hyperpocket.auth.context import AuthContext
|
9
|
+
from hyperpocket.auth.schema import AuthenticateRequest
|
10
|
+
from hyperpocket.config.session import SessionType
|
11
|
+
|
12
|
+
SESSION_NEAR_EXPIRE_SECONDS = 300
|
13
|
+
SESSION_KEY_DELIMITER = "__"
|
14
|
+
|
15
|
+
|
16
|
+
class BaseSessionValue(BaseModel):
|
17
|
+
auth_provider_name: str = Field(
|
18
|
+
description="The name of the authentication provider used to authenticate the current session")
|
19
|
+
auth_context: Optional[AuthContext] = Field(
|
20
|
+
default=None,
|
21
|
+
description="The authentication context containing the actual session details")
|
22
|
+
scoped: bool = Field(description="Indicates whether the current session is a scoped session")
|
23
|
+
auth_scopes: Optional[Set[str]] = Field(
|
24
|
+
default=None,
|
25
|
+
description="The authentication scopes of the current session, present only for scoped sessions")
|
26
|
+
auth_resolve_uid: Optional[str] = Field(
|
27
|
+
default=None,
|
28
|
+
description="A UID used to asynchronously verify whether the user has completed the authentication process")
|
29
|
+
|
30
|
+
def make_superset_auth_scope(
|
31
|
+
self,
|
32
|
+
other_scopes: Optional[Iterable[str]] = None) -> set[str]:
|
33
|
+
auth_scopes = self.auth_scopes or set()
|
34
|
+
other_scopes = other_scopes or set()
|
35
|
+
return auth_scopes.union(other_scopes)
|
36
|
+
|
37
|
+
def is_auth_applicable(self, auth_provider_name: str, auth_req: AuthenticateRequest) -> bool:
|
38
|
+
return self.auth_provider_name == auth_provider_name \
|
39
|
+
and (not self.scoped or self.auth_scopes.issuperset(auth_req.auth_scopes))
|
40
|
+
|
41
|
+
def is_near_expires(self) -> bool:
|
42
|
+
if self.auth_context.expires_at is not None:
|
43
|
+
now = datetime.datetime.now(tz=datetime.timezone.utc)
|
44
|
+
diff = self.auth_context.expires_at - now
|
45
|
+
if diff.total_seconds() < SESSION_NEAR_EXPIRE_SECONDS:
|
46
|
+
return True
|
47
|
+
|
48
|
+
return False
|
49
|
+
|
50
|
+
|
51
|
+
K = TypeVar('K')
|
52
|
+
V = TypeVar('V', bound=BaseSessionValue)
|
53
|
+
|
54
|
+
|
55
|
+
class SessionStorageInterface(ABC, Generic[K, V]):
|
56
|
+
@abstractmethod
|
57
|
+
def get(self, auth_provider: AuthProvider, thread_id: str, profile: str, **kwargs) -> V:
|
58
|
+
"""
|
59
|
+
Get session
|
60
|
+
|
61
|
+
Args:
|
62
|
+
auth_provider (AuthProvider): auth provider
|
63
|
+
thread_id (str): thread id
|
64
|
+
profile (str): profile name
|
65
|
+
|
66
|
+
Returns:
|
67
|
+
V(BaseSessionValue): Session
|
68
|
+
"""
|
69
|
+
raise NotImplementedError
|
70
|
+
|
71
|
+
@abstractmethod
|
72
|
+
def set(self,
|
73
|
+
auth_provider: AuthProvider,
|
74
|
+
thread_id: str,
|
75
|
+
profile: str,
|
76
|
+
auth_scopes: List[str],
|
77
|
+
auth_resolve_uid: Optional[str],
|
78
|
+
auth_context: Optional[AuthContext],
|
79
|
+
is_auth_scope_universal: bool, **kwargs) -> V:
|
80
|
+
"""
|
81
|
+
Set session, if a session doesn't exist, create new session
|
82
|
+
If set auth_resolve_uid is None and auth_context is not None, created session is regarded as active session.
|
83
|
+
|
84
|
+
Args:
|
85
|
+
auth_provider (AuthProvider): auth provider
|
86
|
+
thread_id (str): thread id
|
87
|
+
profile (str): profile name
|
88
|
+
auth_scopes (List[str]): auth scopes
|
89
|
+
auth_resolve_uid (str): a UID used to verify whether the user has completed the authentication process.
|
90
|
+
if set this value as None, it's regarded as active session
|
91
|
+
auth_context (Optional[AuthContext]): authentication context.
|
92
|
+
in pending session, this value is None. in active session this value shouldn't be None
|
93
|
+
is_auth_scope_universal(bool): a flag to determine whether the session is scoped or not
|
94
|
+
|
95
|
+
Returns:
|
96
|
+
V(BaseSessionValue): Updated session
|
97
|
+
"""
|
98
|
+
raise NotImplementedError
|
99
|
+
|
100
|
+
@abstractmethod
|
101
|
+
def delete(self, auth_provider: AuthProvider, thread_id: str, profile: str, **kwargs) -> bool:
|
102
|
+
"""
|
103
|
+
Delete session
|
104
|
+
|
105
|
+
Args:
|
106
|
+
auth_provider (AuthProvider): auth provider
|
107
|
+
thread_id (str): thread id
|
108
|
+
profile (str): profile name
|
109
|
+
|
110
|
+
Returns:
|
111
|
+
bool: True if the session was deleted, False otherwise
|
112
|
+
"""
|
113
|
+
raise NotImplementedError
|
114
|
+
|
115
|
+
@classmethod
|
116
|
+
@abstractmethod
|
117
|
+
def session_storage_type(cls) -> SessionType:
|
118
|
+
raise NotImplementedError
|
@@ -0,0 +1,126 @@
|
|
1
|
+
import json
|
2
|
+
from typing import List, Optional, Any
|
3
|
+
|
4
|
+
import redis
|
5
|
+
|
6
|
+
from hyperpocket.auth import AuthProvider, AUTH_CONTEXT_MAP
|
7
|
+
from hyperpocket.auth.context import AuthContext
|
8
|
+
from hyperpocket.config.session import SessionConfigRedis
|
9
|
+
from hyperpocket.config.session import SessionType
|
10
|
+
from hyperpocket.session.interface import SessionStorageInterface, SESSION_KEY_DELIMITER, \
|
11
|
+
BaseSessionValue, V, K
|
12
|
+
|
13
|
+
RedisSessionKey = str
|
14
|
+
RedisSessionValue = BaseSessionValue
|
15
|
+
|
16
|
+
|
17
|
+
class RedisSessionStorage(SessionStorageInterface[RedisSessionKey, RedisSessionValue]):
|
18
|
+
def __init__(self, config: SessionConfigRedis):
|
19
|
+
super().__init__()
|
20
|
+
args = config.model_dump()
|
21
|
+
self.client = redis.StrictRedis(**args)
|
22
|
+
|
23
|
+
@classmethod
|
24
|
+
def session_storage_type(cls) -> SessionType:
|
25
|
+
return SessionType.REDIS
|
26
|
+
|
27
|
+
def get(self, auth_provider: AuthProvider, thread_id: str, profile: str, **kwargs) -> Optional[V]:
|
28
|
+
key = self._make_session_key(auth_provider, thread_id, profile)
|
29
|
+
raw_session: Any = self.client.get(key)
|
30
|
+
if raw_session is None:
|
31
|
+
return None
|
32
|
+
|
33
|
+
session = self._deserialize(raw_session)
|
34
|
+
return session
|
35
|
+
|
36
|
+
def set(self, auth_provider: AuthProvider,
|
37
|
+
thread_id: str,
|
38
|
+
profile: str,
|
39
|
+
auth_scopes: List[str],
|
40
|
+
auth_resolve_uid: Optional[str],
|
41
|
+
auth_context: Optional[AuthContext],
|
42
|
+
is_auth_scope_universal: bool, **kwargs) -> V:
|
43
|
+
session = self._make_session(
|
44
|
+
auth_provider_name=auth_provider.name,
|
45
|
+
auth_scopes=auth_scopes,
|
46
|
+
auth_context=auth_context,
|
47
|
+
auth_resolve_uid=auth_resolve_uid,
|
48
|
+
is_auth_scope_universal=is_auth_scope_universal)
|
49
|
+
|
50
|
+
key = self._make_session_key(auth_provider, thread_id, profile)
|
51
|
+
|
52
|
+
raw_session = self._serialize(session)
|
53
|
+
self.client.set(key, raw_session)
|
54
|
+
return session
|
55
|
+
|
56
|
+
def delete(self, auth_provider: AuthProvider, thread_id: str, profile: str, **kwargs) -> bool:
|
57
|
+
key = self._make_session_key(auth_provider, thread_id, profile)
|
58
|
+
return self.client.delete(key) == 1
|
59
|
+
|
60
|
+
@staticmethod
|
61
|
+
def _make_session_key(auth_provider: AuthProvider, thread_id: str, profile: str) -> K:
|
62
|
+
return "{auth_provider}{delimiter}{thread_id}{delimiter}{profile}".format(
|
63
|
+
auth_provider=auth_provider.name,
|
64
|
+
thread_id=thread_id,
|
65
|
+
profile=profile,
|
66
|
+
delimiter=SESSION_KEY_DELIMITER,
|
67
|
+
)
|
68
|
+
|
69
|
+
@staticmethod
|
70
|
+
def _make_session(
|
71
|
+
auth_provider_name: str,
|
72
|
+
auth_scopes: List[str],
|
73
|
+
auth_context: AuthContext,
|
74
|
+
auth_resolve_uid: str,
|
75
|
+
is_auth_scope_universal: bool) -> V:
|
76
|
+
return RedisSessionValue(
|
77
|
+
auth_provider_name=auth_provider_name,
|
78
|
+
auth_scopes=set(auth_scopes),
|
79
|
+
auth_context=auth_context,
|
80
|
+
auth_resolve_uid=auth_resolve_uid,
|
81
|
+
scoped=is_auth_scope_universal
|
82
|
+
)
|
83
|
+
|
84
|
+
@staticmethod
|
85
|
+
def _serialize(session: V) -> str:
|
86
|
+
auth_context_value, auth_context_type = None, None
|
87
|
+
if session.auth_context:
|
88
|
+
auth_context_value = session.auth_context.model_dump()
|
89
|
+
auth_context_type = session.auth_context.__class__.__name__
|
90
|
+
|
91
|
+
auth_scopes = session.auth_scopes
|
92
|
+
if auth_scopes:
|
93
|
+
auth_scopes = list(auth_scopes)
|
94
|
+
|
95
|
+
serialized = {"auth_context_value": auth_context_value,
|
96
|
+
"auth_context_type": auth_context_type,
|
97
|
+
"auth_provider_name": session.auth_provider_name,
|
98
|
+
"scoped": session.scoped,
|
99
|
+
"auth_scopes": auth_scopes,
|
100
|
+
"auth_resolve_uid": session.auth_resolve_uid,
|
101
|
+
}
|
102
|
+
|
103
|
+
return json.dumps(serialized)
|
104
|
+
|
105
|
+
@staticmethod
|
106
|
+
def _deserialize(raw_session: str) -> V:
|
107
|
+
session_dict = json.loads(raw_session)
|
108
|
+
|
109
|
+
auth_context = None
|
110
|
+
if auth_context_type_key := session_dict["auth_context_type"]:
|
111
|
+
auth_context_type = AUTH_CONTEXT_MAP[auth_context_type_key]
|
112
|
+
auth_context_value = session_dict["auth_context_value"]
|
113
|
+
|
114
|
+
auth_context = auth_context_type(**auth_context_value)
|
115
|
+
|
116
|
+
auth_scopes = session_dict["auth_scopes"]
|
117
|
+
if auth_scopes:
|
118
|
+
auth_scopes = set(auth_scopes)
|
119
|
+
|
120
|
+
return RedisSessionValue(
|
121
|
+
auth_provider_name=session_dict["auth_provider_name"],
|
122
|
+
auth_scopes=auth_scopes,
|
123
|
+
auth_resolve_uid=session_dict["auth_resolve_uid"],
|
124
|
+
scoped=session_dict["scoped"],
|
125
|
+
auth_context=auth_context
|
126
|
+
)
|
File without changes
|
@@ -0,0 +1,145 @@
|
|
1
|
+
import unittest
|
2
|
+
|
3
|
+
from hyperpocket.auth import AuthProvider, SlackTokenAuthContext
|
4
|
+
from hyperpocket.config.session import SessionConfigInMemory
|
5
|
+
from hyperpocket.session.in_memory import InMemorySessionStorage, InMemorySessionValue
|
6
|
+
|
7
|
+
|
8
|
+
class TestInMemorySessionStorage(unittest.TestCase):
|
9
|
+
storage: InMemorySessionStorage
|
10
|
+
context: SlackTokenAuthContext
|
11
|
+
|
12
|
+
def setUp(self):
|
13
|
+
self.storage = InMemorySessionStorage(SessionConfigInMemory())
|
14
|
+
self.auth_context = SlackTokenAuthContext(
|
15
|
+
access_token="test",
|
16
|
+
description="test-description",
|
17
|
+
expires_at=None,
|
18
|
+
detail=None,
|
19
|
+
)
|
20
|
+
|
21
|
+
def tearDown(self):
|
22
|
+
del self.storage
|
23
|
+
del self.auth_context
|
24
|
+
|
25
|
+
def test_make_session_key(self):
|
26
|
+
key = self.storage._make_session_key(
|
27
|
+
auth_provider=AuthProvider.SLACK,
|
28
|
+
thread_id="default_thread_id",
|
29
|
+
profile="default_profile"
|
30
|
+
)
|
31
|
+
|
32
|
+
self.assertEqual(key, "SLACK__default_thread_id__default_profile")
|
33
|
+
|
34
|
+
def test_make_session(self):
|
35
|
+
session = self.storage._make_session(
|
36
|
+
auth_provider_name=AuthProvider.SLACK.name,
|
37
|
+
auth_scopes=["scope1", "scope2"],
|
38
|
+
auth_context=self.auth_context,
|
39
|
+
auth_resolve_uid="test-resolve-uid",
|
40
|
+
is_auth_scope_universal=True
|
41
|
+
)
|
42
|
+
|
43
|
+
# then
|
44
|
+
self.assertIsInstance(session, InMemorySessionValue)
|
45
|
+
self.assertEqual(session.auth_provider_name, AuthProvider.SLACK.name)
|
46
|
+
self.assertEqual(session.auth_context.access_token, "test")
|
47
|
+
self.assertEqual(session.auth_context.description, "test-description")
|
48
|
+
|
49
|
+
def test_set(self):
|
50
|
+
session = self.storage.set(
|
51
|
+
auth_provider=AuthProvider.SLACK,
|
52
|
+
thread_id="default_thread_id",
|
53
|
+
profile="default_profile",
|
54
|
+
auth_scopes=["scope1", "scope2"],
|
55
|
+
auth_resolve_uid="test-resolve-uid",
|
56
|
+
auth_context=self.auth_context,
|
57
|
+
is_auth_scope_universal=True
|
58
|
+
)
|
59
|
+
|
60
|
+
self.assertIsInstance(session, InMemorySessionValue)
|
61
|
+
self.assertEqual(session.auth_provider_name, AuthProvider.SLACK.name)
|
62
|
+
self.assertEqual(session.auth_context.access_token, self.auth_context.access_token)
|
63
|
+
self.assertEqual(session.auth_context.description, self.auth_context.description)
|
64
|
+
|
65
|
+
def test_get_existing_data(self):
|
66
|
+
# given
|
67
|
+
self.storage.set(
|
68
|
+
auth_provider=AuthProvider.SLACK,
|
69
|
+
thread_id="default_thread_id",
|
70
|
+
profile="default_profile",
|
71
|
+
auth_scopes=["scope1", "scope2"],
|
72
|
+
auth_resolve_uid="test-resolve-uid",
|
73
|
+
auth_context=self.auth_context,
|
74
|
+
is_auth_scope_universal=True
|
75
|
+
)
|
76
|
+
|
77
|
+
# when
|
78
|
+
session = self.storage.get(
|
79
|
+
auth_provider=AuthProvider.SLACK,
|
80
|
+
thread_id="default_thread_id",
|
81
|
+
profile="default_profile",
|
82
|
+
)
|
83
|
+
|
84
|
+
# then
|
85
|
+
self.assertIsInstance(session, InMemorySessionValue)
|
86
|
+
self.assertEqual(session.auth_provider_name, AuthProvider.SLACK.name)
|
87
|
+
self.assertEqual(session.auth_context.access_token, "test")
|
88
|
+
self.assertEqual(session.auth_context.description, "test-description")
|
89
|
+
|
90
|
+
def test_get_not_existing_data(self):
|
91
|
+
# when
|
92
|
+
session = self.storage.get(
|
93
|
+
auth_provider=AuthProvider.SLACK,
|
94
|
+
thread_id="default_thread_id",
|
95
|
+
profile="default_profile",
|
96
|
+
)
|
97
|
+
|
98
|
+
# then
|
99
|
+
self.assertIsNone(session)
|
100
|
+
|
101
|
+
def test_delete_existing_data(self):
|
102
|
+
# given
|
103
|
+
self.storage.set(
|
104
|
+
auth_provider=AuthProvider.SLACK,
|
105
|
+
thread_id="default_thread_id",
|
106
|
+
profile="default_profile",
|
107
|
+
auth_scopes=["scope1", "scope2"],
|
108
|
+
auth_resolve_uid="test-resolve-uid",
|
109
|
+
auth_context=self.auth_context,
|
110
|
+
is_auth_scope_universal=True
|
111
|
+
)
|
112
|
+
|
113
|
+
# when
|
114
|
+
before_session = self.storage.get(
|
115
|
+
auth_provider=AuthProvider.SLACK,
|
116
|
+
thread_id="default_thread_id",
|
117
|
+
profile="default_profile",
|
118
|
+
)
|
119
|
+
|
120
|
+
deleted = self.storage.delete(
|
121
|
+
auth_provider=AuthProvider.SLACK,
|
122
|
+
thread_id="default_thread_id",
|
123
|
+
profile="default_profile",
|
124
|
+
)
|
125
|
+
|
126
|
+
after_session = self.storage.get(
|
127
|
+
auth_provider=AuthProvider.SLACK,
|
128
|
+
thread_id="default_thread_id",
|
129
|
+
profile="default_profile",
|
130
|
+
)
|
131
|
+
|
132
|
+
# then
|
133
|
+
self.assertTrue(deleted)
|
134
|
+
self.assertIsNotNone(before_session)
|
135
|
+
self.assertIsNone(after_session)
|
136
|
+
|
137
|
+
def test_delete_not_existing_data(self):
|
138
|
+
deleted = self.storage.delete(
|
139
|
+
auth_provider=AuthProvider.SLACK,
|
140
|
+
thread_id="default_thread_id",
|
141
|
+
profile="default_profile",
|
142
|
+
)
|
143
|
+
|
144
|
+
# then
|
145
|
+
self.assertFalse(deleted)
|