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.
Files changed (131) hide show
  1. hyperpocket/__init__.py +7 -0
  2. hyperpocket/auth/README.KR.md +309 -0
  3. hyperpocket/auth/README.md +323 -0
  4. hyperpocket/auth/__init__.py +24 -0
  5. hyperpocket/auth/calendly/__init__.py +0 -0
  6. hyperpocket/auth/calendly/context.py +13 -0
  7. hyperpocket/auth/calendly/oauth2_context.py +25 -0
  8. hyperpocket/auth/calendly/oauth2_handler.py +146 -0
  9. hyperpocket/auth/calendly/oauth2_schema.py +16 -0
  10. hyperpocket/auth/context.py +38 -0
  11. hyperpocket/auth/github/__init__.py +0 -0
  12. hyperpocket/auth/github/context.py +13 -0
  13. hyperpocket/auth/github/oauth2_context.py +25 -0
  14. hyperpocket/auth/github/oauth2_handler.py +143 -0
  15. hyperpocket/auth/github/oauth2_schema.py +16 -0
  16. hyperpocket/auth/github/token_context.py +12 -0
  17. hyperpocket/auth/github/token_handler.py +79 -0
  18. hyperpocket/auth/github/token_schema.py +9 -0
  19. hyperpocket/auth/google/__init__.py +0 -0
  20. hyperpocket/auth/google/context.py +15 -0
  21. hyperpocket/auth/google/oauth2_context.py +31 -0
  22. hyperpocket/auth/google/oauth2_handler.py +137 -0
  23. hyperpocket/auth/google/oauth2_schema.py +18 -0
  24. hyperpocket/auth/handler.py +171 -0
  25. hyperpocket/auth/linear/__init__.py +0 -0
  26. hyperpocket/auth/linear/context.py +15 -0
  27. hyperpocket/auth/linear/token_context.py +15 -0
  28. hyperpocket/auth/linear/token_handler.py +68 -0
  29. hyperpocket/auth/linear/token_schema.py +9 -0
  30. hyperpocket/auth/provider.py +16 -0
  31. hyperpocket/auth/schema.py +19 -0
  32. hyperpocket/auth/slack/__init__.py +0 -0
  33. hyperpocket/auth/slack/context.py +15 -0
  34. hyperpocket/auth/slack/oauth2_context.py +40 -0
  35. hyperpocket/auth/slack/oauth2_handler.py +151 -0
  36. hyperpocket/auth/slack/oauth2_schema.py +40 -0
  37. hyperpocket/auth/slack/tests/__init__.py +0 -0
  38. hyperpocket/auth/slack/tests/test_oauth2_handler.py +32 -0
  39. hyperpocket/auth/slack/tests/test_token_handler.py +23 -0
  40. hyperpocket/auth/slack/token_context.py +14 -0
  41. hyperpocket/auth/slack/token_handler.py +64 -0
  42. hyperpocket/auth/slack/token_schema.py +9 -0
  43. hyperpocket/auth/tests/__init__.py +0 -0
  44. hyperpocket/auth/tests/test_google_oauth2_handler.py +147 -0
  45. hyperpocket/auth/tests/test_slack_oauth2_handler.py +147 -0
  46. hyperpocket/auth/tests/test_slack_token_handler.py +66 -0
  47. hyperpocket/cli/__init__.py +0 -0
  48. hyperpocket/cli/__main__.py +12 -0
  49. hyperpocket/cli/pull.py +18 -0
  50. hyperpocket/cli/sync.py +17 -0
  51. hyperpocket/config/__init__.py +9 -0
  52. hyperpocket/config/auth.py +36 -0
  53. hyperpocket/config/git.py +17 -0
  54. hyperpocket/config/logger.py +81 -0
  55. hyperpocket/config/session.py +35 -0
  56. hyperpocket/config/settings.py +62 -0
  57. hyperpocket/constants.py +0 -0
  58. hyperpocket/curated_tools.py +10 -0
  59. hyperpocket/external/__init__.py +7 -0
  60. hyperpocket/external/github_client.py +19 -0
  61. hyperpocket/futures/__init__.py +7 -0
  62. hyperpocket/futures/futurestore.py +48 -0
  63. hyperpocket/pocket_auth.py +344 -0
  64. hyperpocket/pocket_main.py +351 -0
  65. hyperpocket/prompts.py +15 -0
  66. hyperpocket/repository/__init__.py +5 -0
  67. hyperpocket/repository/lock.py +156 -0
  68. hyperpocket/repository/lockfile.py +56 -0
  69. hyperpocket/repository/repository.py +18 -0
  70. hyperpocket/server/__init__.py +3 -0
  71. hyperpocket/server/auth/__init__.py +15 -0
  72. hyperpocket/server/auth/calendly.py +16 -0
  73. hyperpocket/server/auth/github.py +25 -0
  74. hyperpocket/server/auth/google.py +16 -0
  75. hyperpocket/server/auth/linear.py +18 -0
  76. hyperpocket/server/auth/slack.py +28 -0
  77. hyperpocket/server/auth/token.py +51 -0
  78. hyperpocket/server/proxy.py +63 -0
  79. hyperpocket/server/server.py +178 -0
  80. hyperpocket/server/tool/__init__.py +10 -0
  81. hyperpocket/server/tool/dto/__init__.py +0 -0
  82. hyperpocket/server/tool/dto/script.py +15 -0
  83. hyperpocket/server/tool/wasm.py +31 -0
  84. hyperpocket/session/README.KR.md +62 -0
  85. hyperpocket/session/README.md +61 -0
  86. hyperpocket/session/__init__.py +4 -0
  87. hyperpocket/session/in_memory.py +76 -0
  88. hyperpocket/session/interface.py +118 -0
  89. hyperpocket/session/redis.py +126 -0
  90. hyperpocket/session/tests/__init__.py +0 -0
  91. hyperpocket/session/tests/test_in_memory.py +145 -0
  92. hyperpocket/session/tests/test_redis.py +151 -0
  93. hyperpocket/tests/__init__.py +0 -0
  94. hyperpocket/tests/test_pocket.py +118 -0
  95. hyperpocket/tests/test_pocket_auth.py +982 -0
  96. hyperpocket/tool/README.KR.md +68 -0
  97. hyperpocket/tool/README.md +75 -0
  98. hyperpocket/tool/__init__.py +13 -0
  99. hyperpocket/tool/builtins/__init__.py +0 -0
  100. hyperpocket/tool/builtins/example/__init__.py +0 -0
  101. hyperpocket/tool/builtins/example/add_tool.py +18 -0
  102. hyperpocket/tool/function/README.KR.md +159 -0
  103. hyperpocket/tool/function/README.md +169 -0
  104. hyperpocket/tool/function/__init__.py +9 -0
  105. hyperpocket/tool/function/annotation.py +30 -0
  106. hyperpocket/tool/function/tool.py +87 -0
  107. hyperpocket/tool/tests/__init__.py +0 -0
  108. hyperpocket/tool/tests/test_function_tool.py +266 -0
  109. hyperpocket/tool/tool.py +106 -0
  110. hyperpocket/tool/wasm/README.KR.md +144 -0
  111. hyperpocket/tool/wasm/README.md +144 -0
  112. hyperpocket/tool/wasm/__init__.py +3 -0
  113. hyperpocket/tool/wasm/browser.py +63 -0
  114. hyperpocket/tool/wasm/invoker.py +41 -0
  115. hyperpocket/tool/wasm/script.py +82 -0
  116. hyperpocket/tool/wasm/templates/__init__.py +28 -0
  117. hyperpocket/tool/wasm/templates/node.py +87 -0
  118. hyperpocket/tool/wasm/templates/python.py +75 -0
  119. hyperpocket/tool/wasm/tool.py +147 -0
  120. hyperpocket/util/__init__.py +1 -0
  121. hyperpocket/util/extract_func_param_desc_from_docstring.py +97 -0
  122. hyperpocket/util/find_all_leaf_class_in_package.py +17 -0
  123. hyperpocket/util/find_all_subclass_in_package.py +29 -0
  124. hyperpocket/util/flatten_json_schema.py +45 -0
  125. hyperpocket/util/function_to_model.py +46 -0
  126. hyperpocket/util/get_objects_from_subpackage.py +28 -0
  127. hyperpocket/util/json_schema_to_model.py +69 -0
  128. hyperpocket-0.0.1.dist-info/METADATA +304 -0
  129. hyperpocket-0.0.1.dist-info/RECORD +131 -0
  130. hyperpocket-0.0.1.dist-info/WHEEL +4 -0
  131. hyperpocket-0.0.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,10 @@
1
+ from fastapi import APIRouter
2
+
3
+ from hyperpocket.server.tool.wasm import wasm_tool_router
4
+
5
+ tool_router = APIRouter(
6
+ prefix="/tools",
7
+ )
8
+ tool_router.include_router(wasm_tool_router)
9
+
10
+ __all__ = ["tool_router"]
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,4 @@
1
+ from hyperpocket.session.interface import SessionStorageInterface
2
+ from hyperpocket.util.find_all_leaf_class_in_package import find_all_leaf_class_in_package
3
+
4
+ SESSION_STORAGE_LIST = find_all_leaf_class_in_package("hyperpocket.session", SessionStorageInterface)
@@ -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)