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
@@ -1,86 +1,34 @@
1
1
  import asyncio
2
- import pathlib
3
- from typing import Union, Any, Optional, Callable
2
+ from typing import Any, Optional, List, Union
4
3
 
5
4
  from hyperpocket.config import pocket_logger
6
5
  from hyperpocket.pocket_auth import PocketAuth
7
- from hyperpocket.repository import Lockfile
8
- from hyperpocket.repository.lock import LocalLock, GitLock
6
+ from hyperpocket.pocket_core import PocketCore
9
7
  from hyperpocket.server.server import PocketServer, PocketServerOperations
10
- from hyperpocket.tool import Tool, ToolRequest
11
- from hyperpocket.tool.function.tool import FunctionTool
12
- from hyperpocket.tool.wasm import WasmTool
13
- from hyperpocket.tool.wasm.tool import WasmToolRequest
14
-
15
- ToolLike = Union[Tool, str, Callable, ToolRequest]
8
+ from hyperpocket.tool_like import ToolLike
16
9
 
17
10
 
18
11
  class Pocket(object):
19
- auth: PocketAuth
20
12
  server: PocketServer
21
- tools: dict[str, Tool]
13
+ core: PocketCore
22
14
 
23
15
  def __init__(self,
24
16
  tools: list[ToolLike],
25
17
  auth: PocketAuth = None,
26
18
  lockfile_path: Optional[str] = None,
27
- force_update: bool = False):
28
- if auth is None:
29
- auth = PocketAuth()
30
-
31
- if lockfile_path is None:
32
- lockfile_path = "./pocket.lock"
33
- lockfile_pathlib_path = pathlib.Path(lockfile_path)
34
- lockfile = Lockfile(lockfile_pathlib_path)
35
- tool_likes = []
36
- for tool_like in tools:
37
- if isinstance(tool_like, str):
38
- if pathlib.Path(tool_like).exists():
39
- lock = LocalLock(tool_like)
40
- req = WasmToolRequest(lock, "")
41
- else:
42
- lock = GitLock(repository_url=tool_like, git_ref='HEAD')
43
- req = WasmToolRequest(lock, "")
44
- lockfile.add_lock(lock)
45
- tool_likes.append(req)
46
- elif isinstance(tool_like, WasmToolRequest):
47
- lockfile.add_lock(tool_like.lock)
48
- tool_likes.append(tool_like)
49
- elif isinstance(tool_like, ToolRequest):
50
- raise ValueError(f"unreachable. tool_like:{tool_like}")
51
- elif isinstance(tool_like, WasmTool):
52
- raise ValueError("WasmTool should pass ToolRequest instance instead.")
53
- else:
54
- tool_likes.append(tool_like)
55
- lockfile.sync(force_update=force_update, referenced_only=True)
56
-
57
- self.tools = dict()
58
- for tool_like in tool_likes:
59
- tool = self._load_tool(tool_like, lockfile)
60
- if tool.name in self.tools:
61
- pocket_logger.error(f"Duplicate tool name: {tool.name}.")
62
- raise ValueError(f"Duplicate tool name: {tool.name}")
63
- self.tools[tool.name] = tool
64
-
65
- pocket_logger.info(f"All Tools Loaded successfully. total registered tools : {len(self.tools)}")
66
-
67
- self.auth = auth
19
+ force_update: bool = False,
20
+ use_profile: bool = False):
21
+ self.use_profile = use_profile
22
+
23
+ self.core = PocketCore(
24
+ tools=tools,
25
+ auth=auth,
26
+ lockfile_path=lockfile_path,
27
+ force_update=force_update
28
+ )
29
+
68
30
  self.server = PocketServer()
69
- self.server.run(self)
70
-
71
- def _load_tool(self, tool_like: ToolLike, lockfile: Lockfile) -> Tool:
72
- pocket_logger.info(f"Loading Tool {tool_like}")
73
- if isinstance(tool_like, Tool):
74
- tool = tool_like
75
- elif isinstance(tool_like, ToolRequest):
76
- tool = Tool.from_tool_request(tool_like, lockfile=lockfile)
77
- elif isinstance(tool_like, Callable):
78
- tool = FunctionTool.from_func(tool_like)
79
- else:
80
- raise ValueError(f"Invalid tool type: {type(tool_like)}")
81
-
82
- pocket_logger.info(f"Complete Loading Tool {tool.name}")
83
- return tool
31
+ self.server.run(self.core)
84
32
 
85
33
  def invoke(self,
86
34
  tool_name: str,
@@ -204,131 +152,136 @@ class Pocket(object):
204
152
 
205
153
  return result, paused
206
154
 
207
- # DO NOT EVER THINK ABOUT USING SERVER PROPERTY BELOW HERE
208
- async def acall(self,
209
- tool_name: str,
210
- body: Any,
211
- thread_id: str = 'default',
212
- profile: str = 'default',
213
- *args, **kwargs) -> tuple[str, bool]:
155
+ async def initialize_tool_auth(self,
156
+ thread_id: str = 'default',
157
+ profile: str = 'default',
158
+ ) -> dict[str, str]:
214
159
  """
215
- Invoke tool asynchronously, not that different from `Pocket.invoke`
216
- But this method is called only in subprocess.
160
+ Initialize authentication for all tools.
161
+
162
+ This method prepares all tools that require authentication by retrieving
163
+ their respective authentication URIs.
217
164
 
218
- This function performs the following steps:
219
- 1. `prepare_auth` : preparing the authentication process for the tool if necessary.
220
- 2. `authenticate` : performing authentication that needs to invoke tool.
221
- 3. `tool_call` : Executing tool actually with authentication information.
165
+ If no tool requires authentication, an empty list is returned.
222
166
 
223
167
  Args:
224
- tool_name(str): tool name to invoke
225
- body(Any): tool arguments. should be json format
226
- thread_id(str): thread id
227
- profile(str): profile name
168
+ thread_id (str): The thread id. Defaults to 'default'.
169
+ profile (str): The profile to be used for authentication. Defaults to 'default'.
228
170
 
229
171
  Returns:
230
- tuple[str, bool]: tool result and state.
231
- """
232
- tool = self._tool_instance(tool_name)
233
- if tool.auth is not None:
234
- callback_info = self.prepare_auth(tool_name, thread_id, profile, **kwargs)
235
- if callback_info:
236
- return callback_info, True
237
- # 02. authenticate
238
- credentials = await self.authenticate(tool_name, thread_id, profile, **kwargs)
239
- # 03. call tool
240
- result = await self.tool_call(tool_name, body=body, envs=credentials, **kwargs)
241
- return result, False
242
-
243
- def prepare_auth(self,
244
- tool_name: str,
245
- thread_id: str = 'default',
246
- profile: str = 'default',
247
- **kwargs) -> Optional[str]:
172
+ List[str]: A list of authentication URIs for the tools that require authentication.
248
173
  """
249
- Prepares the authentication process for the tool if necessary.
250
- Returns callback URL and whether the tool requires authentication.
174
+ tool_by_provider = self.core.grouping_tool_by_auth_provider()
251
175
 
252
- Args:
253
- tool_name(str): tool name to invoke
254
- thread_id(str): thread id
255
- profile(str): profile name
176
+ prepare_list = {}
177
+ for provider, tools in tool_by_provider.items():
178
+ tool_name_list = [tool.name for tool in tools]
179
+ prepare = await self.prepare_in_subprocess(
180
+ tool_name=tool_name_list,
181
+ thread_id=thread_id,
182
+ profile=profile)
256
183
 
257
- Returns:
258
- Optional[str]: callback URI if necessary
259
- """
260
- tool = self._tool_instance(tool_name)
261
- if tool.auth is None:
262
- return None
263
-
264
- auth_req = self.auth.make_request(
265
- auth_handler_name=tool.auth.auth_handler,
266
- auth_provider=tool.auth.auth_provider,
267
- auth_scopes=tool.auth.scopes)
268
-
269
- return self.auth.prepare(
270
- auth_req=auth_req,
271
- auth_handler_name=tool.auth.auth_handler,
272
- auth_provider=tool.auth.auth_provider,
273
- thread_id=thread_id,
274
- profile=profile,
275
- **kwargs
276
- )
184
+ if prepare is not None:
185
+ prepare_list[provider] = prepare
186
+
187
+ return prepare_list
277
188
 
278
- async def authenticate(
279
- self,
280
- tool_name: str,
281
- thread_id: str = 'default',
282
- profile: str = 'default',
283
- **kwargs) -> dict[str, str]:
189
+ async def wait_tool_auth(self,
190
+ thread_id: str = 'default',
191
+ profile: str = 'default'
192
+ ) -> bool:
284
193
  """
285
- Authenticates the handler included in the tool and returns credentials.
194
+ Wait until all tool authentications are completed.
195
+
196
+ This method waits until all tools associated with the given
197
+ `thread_id` and `profile` have completed their authentication process.
286
198
 
287
199
  Args:
288
- tool_name(str): tool name to invoke
289
- thread_id(str): thread id
290
- profile(str): profile name
200
+ thread_id (str): The thread id. Defaults to 'default'.
201
+ profile (str): The profile to be used for authentication. Defaults to 'default'.
291
202
 
292
203
  Returns:
293
- dict[str, str]: credentials
204
+ bool: Returns `True` if all tool authentications are successfully completed,
205
+ or `False` if the process was interrupted or failed.
294
206
  """
295
- tool = self._tool_instance(tool_name)
296
- if tool.auth is None:
297
- return {}
298
- auth_req = self.auth.make_request(
299
- auth_handler_name=tool.auth.auth_handler,
300
- auth_provider=tool.auth.auth_provider,
301
- auth_scopes=tool.auth.scopes)
302
- auth_ctx = await self.auth.authenticate_async(
303
- auth_req=auth_req,
304
- auth_handler_name=tool.auth.auth_handler,
305
- auth_provider=tool.auth.auth_provider,
306
- thread_id=thread_id,
307
- profile=profile,
308
- **kwargs,
207
+ try:
208
+ tool_by_provider = self.core.grouping_tool_by_auth_provider()
209
+
210
+ waiting_futures = []
211
+ for provider, tools in tool_by_provider.items():
212
+ if len(tools) == 0:
213
+ continue
214
+
215
+ waiting_futures.append(
216
+ self.authenticate_in_subprocess(
217
+ tool_name=tools[0].name,
218
+ thread_id=thread_id,
219
+ profile=profile
220
+ ))
221
+
222
+ await asyncio.gather(*waiting_futures)
223
+
224
+ return True
225
+
226
+ except asyncio.TimeoutError as e:
227
+ pocket_logger.error("authentication time out.")
228
+ raise e
229
+
230
+ async def prepare_in_subprocess(self,
231
+ tool_name: Union[str, List[str]],
232
+ thread_id: str = 'default',
233
+ profile: str = 'default',
234
+ *args, **kwargs):
235
+ prepare = await self.server.call_in_subprocess(
236
+ PocketServerOperations.PREPARE_AUTH,
237
+ args,
238
+ {
239
+ 'tool_name': tool_name,
240
+ 'thread_id': thread_id,
241
+ 'profile': profile,
242
+ **kwargs,
243
+ },
309
244
  )
310
- return auth_ctx.to_dict()
311
245
 
312
- async def tool_call(self, tool_name: str, **kwargs) -> str:
313
- """
314
- Executing tool actually
246
+ return prepare
315
247
 
316
- Args:
317
- tool_name(str): tool name to invoke
318
- kwargs(dict): keyword arguments. authentication information is passed through this.
248
+ async def authenticate_in_subprocess(self,
249
+ tool_name: str,
250
+ thread_id: str = 'default',
251
+ profile: str = 'default',
252
+ *args, **kwargs):
253
+ credentials = await self.server.call_in_subprocess(
254
+ PocketServerOperations.AUTHENTICATE,
255
+ args,
256
+ {
257
+ 'tool_name': tool_name,
258
+ 'thread_id': thread_id,
259
+ 'profile': profile,
260
+ **kwargs,
261
+ },
262
+ )
319
263
 
320
- Returns:
321
- str: tool result
322
- """
323
- tool = self._tool_instance(tool_name)
324
- try:
325
- return await asyncio.wait_for(tool.ainvoke(**kwargs), timeout=180)
326
- except asyncio.TimeoutError:
327
- pocket_logger.warning("Timeout tool call.")
328
- return "timeout tool call"
264
+ return credentials
329
265
 
330
- def _tool_instance(self, tool_name: str) -> Tool:
331
- return self.tools[tool_name]
266
+ async def tool_call_in_subprocess(self,
267
+ tool_name: str,
268
+ body: Any,
269
+ thread_id: str = 'default',
270
+ profile: str = 'default',
271
+ *args, **kwargs):
272
+ result = await self.server.call_in_subprocess(
273
+ PocketServerOperations.TOOL_CALL,
274
+ args,
275
+ {
276
+ 'tool_name': tool_name,
277
+ 'body': body,
278
+ 'thread_id': thread_id,
279
+ 'profile': profile,
280
+ **kwargs,
281
+ },
282
+ )
283
+
284
+ return result
332
285
 
333
286
  def _teardown_server(self):
334
287
  self.server.teardown()
@@ -338,11 +291,11 @@ class Pocket(object):
338
291
 
339
292
  def __exit__(self, exc_type, exc_val, exc_tb):
340
293
  if self.__dict__.get('server'):
341
- self._teardown_server()
294
+ self.server.teardown()
342
295
 
343
296
  def __del__(self):
344
297
  if self.__dict__.get('server'):
345
- self._teardown_server()
298
+ self.server.teardown()
346
299
 
347
300
  def __getstate__(self):
348
301
  state = self.__dict__.copy()
hyperpocket/prompts.py CHANGED
@@ -1,15 +1,13 @@
1
1
  def pocket_extended_tool_description(description: str):
2
2
  return f'''
3
- This tool performs as stated in the <tool-description></tool-description> XML tag.
3
+ This tool functions as described in the <tool-description> XML tag:
4
4
  <tool-description>
5
5
  {description}
6
6
  </tool-description>
7
7
 
8
- This tool requires arguments as follows:
9
- - 'thread_id': The ID of the chat thread where the tool is invoked. Omitted when unknown.
10
- - 'profile': The profile of the user invoking the tool. Inferred from user's messages.
11
- Users can request tools to be invoked in specific personas, which is called a profile.
12
- If the user's profile name can be inferred from the query, pass it as a string in the 'profile' JSON property.
13
- Omitted when unknown.
14
- - 'body': The argument of the tool. The argument is passed as JSON property 'body' in the JSON schema.
8
+ Arguments required:
9
+ - 'thread_id': Chat thread ID (omit if unknown).
10
+ - 'profile': The profile of the user invoking the tool. (infer from messages; omit if unknown).
11
+ Users can request tools to be invoked in specific personas.
12
+ - 'body': Tool argument passed as a JSON 'body'.
15
13
  '''
@@ -1,5 +1,5 @@
1
1
  from hyperpocket.repository.lock import Lock
2
2
  from hyperpocket.repository.lockfile import Lockfile
3
- from hyperpocket.repository.repository import pull, sync
3
+ from hyperpocket.repository.repository import pull, sync, eject
4
4
 
5
- __all__ = ["Lock", "Lockfile", "pull", "sync"]
5
+ __all__ = ["Lock", "Lockfile", "pull", "sync", "eject"]
@@ -1,7 +1,7 @@
1
1
  import abc
2
2
  import pathlib
3
3
  import shutil
4
- from typing import Optional
4
+ from typing import Optional, Tuple
5
5
 
6
6
  import git
7
7
  from pydantic import BaseModel, Field
@@ -28,6 +28,10 @@ class Lock(BaseModel, abc.ABC):
28
28
  def toolpkg_path(self) -> pathlib.Path:
29
29
  raise NotImplementedError
30
30
 
31
+ def eject_to_path(self, dest_path: pathlib.Path, src_sub_path: str = None):
32
+ ## local locks are already tracked by git
33
+ raise NotImplementedError
34
+
31
35
 
32
36
  class LocalLock(Lock):
33
37
  tool_source: str = Field(default='local')
@@ -154,3 +158,69 @@ class GitLock(Lock):
154
158
  new_sha = sha
155
159
  break
156
160
  return new_sha
161
+
162
+ @classmethod
163
+ def get_git_branches(cls, repo_url):
164
+ ls_lists = git.cmd.Git().ls_remote(repo_url)
165
+
166
+ branches = {}
167
+ for line in ls_lists.split("\n"):
168
+ sha, ref = line.split("\t")
169
+ if ref.startswith("refs/heads/"):
170
+ branch_name = ref.replace("refs/heads/", "")
171
+ branches[branch_name] = sha
172
+
173
+ return branches
174
+
175
+ @classmethod
176
+ def parse_repo_url(cls, repo_url: str) -> Tuple[str, str, str]:
177
+ """
178
+ Parses a GitHub repository URL with optional branch and path information.
179
+
180
+ Returns:
181
+ Tuple[str, str, str]: base_repo, branch_name, directory_path
182
+ """
183
+ if not repo_url.startswith("https://github.com/"):
184
+ raise AttributeError("Only GitHub URLs are supported")
185
+
186
+ # Remove the base URL and split the path
187
+ repo_path = repo_url.removeprefix("https://github.com/")
188
+ repo_path_list = repo_path.split("/")
189
+
190
+ # Check if the URL contains 'tree' (indicating branch and sub-path information)
191
+ if "tree" not in repo_path_list:
192
+ # If no 'tree', return the full repository URL
193
+ return repo_url, "HEAD", ""
194
+
195
+ # Parse base repo URL and remaining path
196
+ tree_index = repo_path_list.index("tree")
197
+ base_repo = f"https://github.com/{'/'.join(repo_path_list[:tree_index])}"
198
+ sub_path = repo_path_list[tree_index + 1:]
199
+
200
+ # Fetch branch information
201
+ branches = cls.get_git_branches(base_repo)
202
+
203
+ # Find branch and sub-directory path
204
+ for idx in range(1, len(sub_path) + 1):
205
+ branch_name = "/".join(sub_path[:idx])
206
+ if branch_name in branches:
207
+ directory_path = "/".join(sub_path[idx:]) if idx < len(sub_path) else None
208
+ return base_repo, branch_name, directory_path
209
+
210
+ # If no valid branch is found, raise an error
211
+ raise ValueError("Branch not found in repository")
212
+
213
+ def eject_to_path(self, dest_path: pathlib.Path, src_sub_path: str = None):
214
+
215
+ # clone the git repository to the target path
216
+ pocket_logger.info(
217
+ f"Ejecting git: {self.repository_url} @ ref: {self.git_ref} source in path: {src_sub_path} to {dest_path} ..."
218
+ )
219
+ if dest_path.exists():
220
+ shutil.rmtree(dest_path)
221
+
222
+ if src_sub_path:
223
+ src_path = self.toolpkg_path() / src_sub_path
224
+ else:
225
+ src_path = self.toolpkg_path()
226
+ shutil.copytree(src_path, dest_path)
@@ -8,19 +8,19 @@ class Lockfile:
8
8
  path: pathlib.Path = None
9
9
  locks: dict[tuple, Lock] = None
10
10
  referenced_locks: set[tuple] = None
11
-
11
+
12
12
  def __init__(self, path: pathlib.Path):
13
13
  self.path = path
14
14
  self.locks = {}
15
15
  self.referenced_locks = set()
16
16
  if self.path.exists():
17
- with open(self.path, 'r') as f:
17
+ with open(self.path, "r") as f:
18
18
  for line in f:
19
- split = line.strip().split('\t')
19
+ split = line.strip().split("\t")
20
20
  source = split[0]
21
- if source == 'local':
21
+ if source == "local":
22
22
  lock = LocalLock(tool_path=split[1])
23
- elif source == 'git':
23
+ elif source == "git":
24
24
  lock = GitLock(
25
25
  repository_url=split[1],
26
26
  git_ref=split[2],
@@ -31,12 +31,17 @@ class Lockfile:
31
31
  self.locks[lock.key()] = lock
32
32
  else:
33
33
  self.path.touch()
34
-
34
+
35
35
  def add_lock(self, lock: Lock):
36
36
  if lock.key() not in self.locks:
37
37
  self.locks[lock.key()] = lock
38
38
  self.referenced_locks.add(lock.key())
39
-
39
+
40
+ def remove_lock(self, key: tuple[str, ...]):
41
+ self.locks.pop(key)
42
+ if key in self.referenced_locks:
43
+ self.referenced_locks.remove(key)
44
+
40
45
  def get_lock(self, key: tuple[str, ...]):
41
46
  return self.locks[key]
42
47
 
@@ -45,12 +50,13 @@ class Lockfile:
45
50
  locks = [self.get_lock(key) for key in self.referenced_locks]
46
51
  else:
47
52
  locks = list(self.locks.values())
48
- with ThreadPoolExecutor(max_workers=min(len(locks), 100), thread_name_prefix="repository_loader") as executor:
49
- executor.map(lambda l: l.sync(force_update=force_update), locks)
53
+ with ThreadPoolExecutor(
54
+ max_workers=min(len(locks) + 1, 100), thread_name_prefix="repository_loader"
55
+ ) as executor:
56
+ executor.map(lambda lock: lock.sync(force_update=force_update), locks)
50
57
  self.write()
51
-
58
+
52
59
  def write(self):
53
- with open(self.path, 'w') as f:
60
+ with open(self.path, "w") as f:
54
61
  for lock in self.locks.values():
55
- f.write(str(lock) + '\n')
56
-
62
+ f.write(str(lock) + "\n")
@@ -12,7 +12,32 @@ def pull(lockfile: Lockfile, urllike: str, git_ref: str):
12
12
  lockfile.add_lock(GitLock(repository_url=urllike, git_ref=git_ref))
13
13
  lockfile.sync(force_update=False)
14
14
  lockfile.write()
15
-
15
+
16
+
16
17
  def sync(lockfile: Lockfile, force_update: bool):
17
18
  lockfile.sync(force_update=force_update)
18
19
  lockfile.write()
20
+
21
+
22
+ def eject(url: str, ref: str, remote_path: str, lockfile: Lockfile):
23
+ path = pathlib.Path(url)
24
+ if path.exists():
25
+ raise ValueError("Local tools are already ejected")
26
+ else:
27
+ lock = lockfile.get_lock(GitLock(repository_url=url, git_ref=ref).key())
28
+
29
+ # first copy source to ejected directory
30
+ working_dir = pathlib.Path.cwd()
31
+ ejected_dir = working_dir / "ejected_tools"
32
+ if not ejected_dir.exists():
33
+ ejected_dir.mkdir()
34
+
35
+ tool_name = pathlib.Path(remote_path).name
36
+ eject_path = ejected_dir / tool_name
37
+ lock.eject_to_path(eject_path, remote_path)
38
+
39
+ # then update lockfile
40
+ lockfile.remove_lock(lock.key())
41
+ lockfile.add_lock(LocalLock(tool_path=str(eject_path)))
42
+ lockfile.sync(force_update=True)
43
+ lockfile.write()
@@ -1,11 +1,5 @@
1
1
  from fastapi import APIRouter
2
2
 
3
- from hyperpocket.server.auth.github import github_auth_router
4
- from hyperpocket.server.auth.google import google_auth_router
5
- from hyperpocket.server.auth.linear import linear_auth_router
6
- from hyperpocket.server.auth.slack import slack_auth_router
7
- from hyperpocket.server.auth.token import token_router
8
- from hyperpocket.server.auth.calendly import calendly_auth_router
9
3
  from hyperpocket.util.get_objects_from_subpackage import get_objects_from_subpackage
10
4
 
11
5
  auth_router = APIRouter(prefix="/auth")
@@ -0,0 +1,16 @@
1
+ from fastapi import APIRouter, Request
2
+ from starlette.responses import HTMLResponse
3
+
4
+ from hyperpocket.futures import FutureStore
5
+
6
+ gumloop_auth_router = APIRouter(prefix="/gumloop")
7
+
8
+
9
+ @gumloop_auth_router.get("/token/callback")
10
+ async def gumloop_oauth2_callback(request: Request, state: str, token: str):
11
+ try:
12
+ FutureStore.resolve_future(state, token)
13
+ except ValueError:
14
+ return HTMLResponse(content="failed")
15
+
16
+ return HTMLResponse(content="success")
@@ -0,0 +1,19 @@
1
+
2
+ from fastapi import APIRouter
3
+ from starlette.responses import HTMLResponse
4
+
5
+ from hyperpocket.futures import FutureStore
6
+
7
+ notion_auth_router = APIRouter(
8
+ prefix="/notion"
9
+ )
10
+
11
+
12
+ @notion_auth_router.get("/token/callback")
13
+ async def notion_token_callback(state: str, token: str):
14
+ try:
15
+ FutureStore.resolve_future(state, token)
16
+ except ValueError:
17
+ return HTMLResponse(content="failed")
18
+
19
+ return HTMLResponse(content="success")
@@ -0,0 +1,16 @@
1
+ from fastapi import APIRouter
2
+ from starlette.responses import HTMLResponse
3
+
4
+ from hyperpocket.futures import FutureStore
5
+
6
+ reddit_auth_router = APIRouter(prefix="/reddit")
7
+
8
+
9
+ @reddit_auth_router.get("/oauth2/callback")
10
+ async def reddit_oauth2_callback(state: str, code: str):
11
+ try:
12
+ FutureStore.resolve_future(state, code)
13
+ except ValueError:
14
+ return HTMLResponse(content="failed")
15
+
16
+ return HTMLResponse(content="success")