hyperpocket 0.1.9__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. hyperpocket/__init__.py +4 -4
  2. hyperpocket/auth/__init__.py +12 -7
  3. hyperpocket/auth/calendly/oauth2_handler.py +24 -17
  4. hyperpocket/auth/calendly/oauth2_schema.py +3 -1
  5. hyperpocket/auth/context.py +2 -1
  6. hyperpocket/auth/github/oauth2_handler.py +13 -8
  7. hyperpocket/auth/github/token_handler.py +27 -21
  8. hyperpocket/auth/google/context.py +1 -3
  9. hyperpocket/auth/google/oauth2_context.py +1 -1
  10. hyperpocket/auth/google/oauth2_handler.py +22 -17
  11. hyperpocket/auth/gumloop/token_context.py +1 -4
  12. hyperpocket/auth/gumloop/token_handler.py +48 -20
  13. hyperpocket/auth/gumloop/token_schema.py +2 -1
  14. hyperpocket/auth/handler.py +21 -6
  15. hyperpocket/auth/linear/token_context.py +2 -5
  16. hyperpocket/auth/linear/token_handler.py +45 -21
  17. hyperpocket/auth/notion/context.py +2 -2
  18. hyperpocket/auth/notion/token_context.py +2 -4
  19. hyperpocket/auth/notion/token_handler.py +45 -21
  20. hyperpocket/auth/notion/token_schema.py +0 -1
  21. hyperpocket/auth/reddit/oauth2_handler.py +9 -10
  22. hyperpocket/auth/reddit/oauth2_schema.py +0 -2
  23. hyperpocket/auth/schema.py +4 -1
  24. hyperpocket/auth/slack/oauth2_context.py +3 -1
  25. hyperpocket/auth/slack/oauth2_handler.py +55 -35
  26. hyperpocket/auth/slack/token_context.py +2 -4
  27. hyperpocket/auth/slack/token_handler.py +42 -19
  28. hyperpocket/builtin.py +4 -2
  29. hyperpocket/cli/__main__.py +4 -2
  30. hyperpocket/cli/auth.py +59 -28
  31. hyperpocket/cli/codegen/auth/auth_context_template.py +3 -2
  32. hyperpocket/cli/codegen/auth/auth_token_context_template.py +3 -2
  33. hyperpocket/cli/codegen/auth/auth_token_handler_template.py +6 -5
  34. hyperpocket/cli/codegen/auth/auth_token_schema_template.py +3 -2
  35. hyperpocket/cli/codegen/auth/server_auth_template.py +3 -2
  36. hyperpocket/cli/pull.py +5 -5
  37. hyperpocket/config/__init__.py +3 -8
  38. hyperpocket/config/auth.py +3 -1
  39. hyperpocket/config/logger.py +20 -15
  40. hyperpocket/config/session.py +4 -2
  41. hyperpocket/config/settings.py +19 -2
  42. hyperpocket/futures/__init__.py +1 -1
  43. hyperpocket/futures/futurestore.py +3 -2
  44. hyperpocket/pocket_auth.py +171 -84
  45. hyperpocket/pocket_core.py +51 -33
  46. hyperpocket/pocket_main.py +122 -93
  47. hyperpocket/prompts.py +2 -2
  48. hyperpocket/repository/__init__.py +1 -1
  49. hyperpocket/repository/lock.py +47 -33
  50. hyperpocket/repository/lockfile.py +2 -2
  51. hyperpocket/repository/repository.py +1 -1
  52. hyperpocket/server/__init__.py +1 -1
  53. hyperpocket/server/auth/github.py +2 -1
  54. hyperpocket/server/auth/linear.py +1 -3
  55. hyperpocket/server/auth/notion.py +2 -5
  56. hyperpocket/server/auth/slack.py +1 -3
  57. hyperpocket/server/auth/token.py +17 -11
  58. hyperpocket/server/proxy.py +29 -13
  59. hyperpocket/server/server.py +75 -31
  60. hyperpocket/server/tool/dto/script.py +15 -10
  61. hyperpocket/server/tool/wasm.py +14 -11
  62. hyperpocket/session/__init__.py +6 -2
  63. hyperpocket/session/in_memory.py +44 -24
  64. hyperpocket/session/interface.py +42 -24
  65. hyperpocket/session/redis.py +48 -31
  66. hyperpocket/tool/__init__.py +10 -10
  67. hyperpocket/tool/function/__init__.py +1 -5
  68. hyperpocket/tool/function/annotation.py +11 -9
  69. hyperpocket/tool/function/tool.py +37 -27
  70. hyperpocket/tool/tool.py +59 -36
  71. hyperpocket/tool/wasm/__init__.py +1 -1
  72. hyperpocket/tool/wasm/browser.py +15 -10
  73. hyperpocket/tool/wasm/invoker.py +16 -16
  74. hyperpocket/tool/wasm/script.py +27 -14
  75. hyperpocket/tool/wasm/templates/__init__.py +22 -15
  76. hyperpocket/tool/wasm/templates/node.py +2 -2
  77. hyperpocket/tool/wasm/templates/python.py +2 -2
  78. hyperpocket/tool/wasm/tool.py +27 -14
  79. hyperpocket/tool_like.py +3 -3
  80. hyperpocket/util/__init__.py +1 -1
  81. hyperpocket/util/extract_func_param_desc_from_docstring.py +33 -14
  82. hyperpocket/util/find_all_leaf_class_in_package.py +4 -3
  83. hyperpocket/util/find_all_subclass_in_package.py +4 -2
  84. hyperpocket/util/flatten_json_schema.py +10 -6
  85. hyperpocket/util/function_to_model.py +33 -12
  86. hyperpocket/util/get_objects_from_subpackage.py +1 -1
  87. hyperpocket/util/json_schema_to_model.py +14 -5
  88. {hyperpocket-0.1.9.dist-info → hyperpocket-0.2.0.dist-info}/METADATA +29 -24
  89. hyperpocket-0.2.0.dist-info/RECORD +137 -0
  90. hyperpocket-0.1.9.dist-info/RECORD +0 -137
  91. {hyperpocket-0.1.9.dist-info → hyperpocket-0.2.0.dist-info}/WHEEL +0 -0
  92. {hyperpocket-0.1.9.dist-info → hyperpocket-0.2.0.dist-info}/entry_points.txt +0 -0
@@ -1,5 +1,6 @@
1
1
  import asyncio
2
- from typing import Any, Optional, List, Union
2
+ from concurrent.futures import ThreadPoolExecutor
3
+ from typing import Any, List, Optional, Union
3
4
 
4
5
  from hyperpocket.config import pocket_logger
5
6
  from hyperpocket.pocket_auth import PocketAuth
@@ -12,30 +13,38 @@ class Pocket(object):
12
13
  server: PocketServer
13
14
  core: PocketCore
14
15
 
15
- def __init__(self,
16
- tools: list[ToolLike],
17
- auth: PocketAuth = None,
18
- lockfile_path: Optional[str] = None,
19
- force_update: bool = False,
20
- use_profile: bool = False):
16
+ def __init__(
17
+ self,
18
+ tools: list[ToolLike],
19
+ auth: PocketAuth = None,
20
+ lockfile_path: Optional[str] = None,
21
+ force_update: bool = False,
22
+ use_profile: bool = False,
23
+ ):
21
24
  self.use_profile = use_profile
22
-
23
25
  self.core = PocketCore(
24
26
  tools=tools,
25
27
  auth=auth,
26
28
  lockfile_path=lockfile_path,
27
- force_update=force_update
29
+ force_update=force_update,
28
30
  )
29
-
30
31
  self.server = PocketServer()
31
32
  self.server.run(self.core)
32
-
33
- def invoke(self,
34
- tool_name: str,
35
- body: Any,
36
- thread_id: str = 'default',
37
- profile: str = 'default',
38
- *args, **kwargs) -> str:
33
+ try:
34
+ self.server.wait_initialized()
35
+ except Exception as e:
36
+ pocket_logger.error(f"Failed to initialize pocket server. error : {e}")
37
+ self._teardown_server()
38
+
39
+ def invoke(
40
+ self,
41
+ tool_name: str,
42
+ body: Any,
43
+ thread_id: str = "default",
44
+ profile: str = "default",
45
+ *args,
46
+ **kwargs,
47
+ ) -> str:
39
48
  """
40
49
  Invoke Tool synchronously
41
50
 
@@ -48,15 +57,19 @@ class Pocket(object):
48
57
  Returns:
49
58
  str: tool result
50
59
  """
51
- result = asyncio.run(self.ainvoke(tool_name, body, thread_id, profile, *args, **kwargs))
52
- return result
60
+ return asyncio.run(
61
+ self.ainvoke(tool_name, body, thread_id, profile, *args, **kwargs)
62
+ )
53
63
 
54
- async def ainvoke(self,
55
- tool_name: str,
56
- body: Any,
57
- thread_id: str = 'default',
58
- profile: str = 'default',
59
- *args, **kwargs) -> str:
64
+ async def ainvoke(
65
+ self,
66
+ tool_name: str,
67
+ body: Any,
68
+ thread_id: str = "default",
69
+ profile: str = "default",
70
+ *args,
71
+ **kwargs,
72
+ ) -> str:
60
73
  """
61
74
  Invoke Tool asynchronously
62
75
 
@@ -79,12 +92,15 @@ class Pocket(object):
79
92
  )
80
93
  return result
81
94
 
82
- def invoke_with_state(self,
83
- tool_name: str,
84
- body: Any,
85
- thread_id: str = 'default',
86
- profile: str = 'default',
87
- *args, **kwargs) -> tuple[str, bool]:
95
+ def invoke_with_state(
96
+ self,
97
+ tool_name: str,
98
+ body: Any,
99
+ thread_id: str = "default",
100
+ profile: str = "default",
101
+ *args,
102
+ **kwargs,
103
+ ) -> tuple[str, bool]:
88
104
  """
89
105
  Invoke Tool with state synchronously
90
106
  State indicates whether this tool is paused or not.
@@ -101,27 +117,33 @@ class Pocket(object):
101
117
  """
102
118
  try:
103
119
  loop = asyncio.new_event_loop()
104
- result = loop.run_until_complete(
105
- self.ainvoke_with_state(tool_name, body, thread_id, profile, *args, **kwargs))
106
-
107
- except RuntimeError as e:
108
- pocket_logger.warning("Can't execute sync def in event loop. use nest-asyncio")
120
+ except RuntimeError:
121
+ pocket_logger.warning(
122
+ "Can't execute sync def in event loop. use nest-asyncio"
123
+ )
109
124
 
110
125
  import nest_asyncio
126
+
111
127
  loop = asyncio.new_event_loop()
112
128
  nest_asyncio.apply(loop=loop)
113
129
 
114
- result = loop.run_until_complete(
115
- self.ainvoke_with_state(tool_name, body, thread_id, profile, *args, **kwargs))
130
+ result = loop.run_until_complete(
131
+ self.ainvoke_with_state(
132
+ tool_name, body, thread_id, profile, *args, **kwargs
133
+ )
134
+ )
116
135
 
117
136
  return result
118
137
 
119
- async def ainvoke_with_state(self,
120
- tool_name: str,
121
- body: Any,
122
- thread_id: str = 'default',
123
- profile: str = 'default',
124
- *args, **kwargs) -> tuple[str, bool]:
138
+ async def ainvoke_with_state(
139
+ self,
140
+ tool_name: str,
141
+ body: Any,
142
+ thread_id: str = "default",
143
+ profile: str = "default",
144
+ *args,
145
+ **kwargs,
146
+ ) -> tuple[str, bool]:
125
147
  """
126
148
  Invoke Tool with state synchronously
127
149
  State indicates whether this tool is paused or not.
@@ -140,10 +162,10 @@ class Pocket(object):
140
162
  PocketServerOperations.CALL,
141
163
  args,
142
164
  {
143
- 'tool_name': tool_name,
144
- 'body': body,
145
- 'thread_id': thread_id,
146
- 'profile': profile,
165
+ "tool_name": tool_name,
166
+ "body": body,
167
+ "thread_id": thread_id,
168
+ "profile": profile,
147
169
  **kwargs,
148
170
  },
149
171
  )
@@ -152,10 +174,11 @@ class Pocket(object):
152
174
 
153
175
  return result, paused
154
176
 
155
- async def initialize_tool_auth(self,
156
- thread_id: str = 'default',
157
- profile: str = 'default',
158
- ) -> dict[str, str]:
177
+ async def initialize_tool_auth(
178
+ self,
179
+ thread_id: str = "default",
180
+ profile: str = "default",
181
+ ) -> dict[str, str]:
159
182
  """
160
183
  Initialize authentication for all tools.
161
184
 
@@ -177,19 +200,17 @@ class Pocket(object):
177
200
  for provider, tools in tool_by_provider.items():
178
201
  tool_name_list = [tool.name for tool in tools]
179
202
  prepare = await self.prepare_in_subprocess(
180
- tool_name=tool_name_list,
181
- thread_id=thread_id,
182
- profile=profile)
203
+ tool_name=tool_name_list, thread_id=thread_id, profile=profile
204
+ )
183
205
 
184
206
  if prepare is not None:
185
207
  prepare_list[provider] = prepare
186
208
 
187
209
  return prepare_list
188
210
 
189
- async def wait_tool_auth(self,
190
- thread_id: str = 'default',
191
- profile: str = 'default'
192
- ) -> bool:
211
+ async def wait_tool_auth(
212
+ self, thread_id: str = "default", profile: str = "default"
213
+ ) -> bool:
193
214
  """
194
215
  Wait until all tool authentications are completed.
195
216
 
@@ -214,10 +235,9 @@ class Pocket(object):
214
235
 
215
236
  waiting_futures.append(
216
237
  self.authenticate_in_subprocess(
217
- tool_name=tools[0].name,
218
- thread_id=thread_id,
219
- profile=profile
220
- ))
238
+ tool_name=tools[0].name, thread_id=thread_id, profile=profile
239
+ )
240
+ )
221
241
 
222
242
  await asyncio.gather(*waiting_futures)
223
243
 
@@ -227,56 +247,65 @@ class Pocket(object):
227
247
  pocket_logger.error("authentication time out.")
228
248
  raise e
229
249
 
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):
250
+ async def prepare_in_subprocess(
251
+ self,
252
+ tool_name: Union[str, List[str]],
253
+ thread_id: str = "default",
254
+ profile: str = "default",
255
+ *args,
256
+ **kwargs,
257
+ ):
235
258
  prepare = await self.server.call_in_subprocess(
236
259
  PocketServerOperations.PREPARE_AUTH,
237
260
  args,
238
261
  {
239
- 'tool_name': tool_name,
240
- 'thread_id': thread_id,
241
- 'profile': profile,
262
+ "tool_name": tool_name,
263
+ "thread_id": thread_id,
264
+ "profile": profile,
242
265
  **kwargs,
243
266
  },
244
267
  )
245
268
 
246
269
  return prepare
247
270
 
248
- async def authenticate_in_subprocess(self,
249
- tool_name: str,
250
- thread_id: str = 'default',
251
- profile: str = 'default',
252
- *args, **kwargs):
271
+ async def authenticate_in_subprocess(
272
+ self,
273
+ tool_name: str,
274
+ thread_id: str = "default",
275
+ profile: str = "default",
276
+ *args,
277
+ **kwargs,
278
+ ):
253
279
  credentials = await self.server.call_in_subprocess(
254
280
  PocketServerOperations.AUTHENTICATE,
255
281
  args,
256
282
  {
257
- 'tool_name': tool_name,
258
- 'thread_id': thread_id,
259
- 'profile': profile,
283
+ "tool_name": tool_name,
284
+ "thread_id": thread_id,
285
+ "profile": profile,
260
286
  **kwargs,
261
287
  },
262
288
  )
263
289
 
264
290
  return credentials
265
291
 
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):
292
+ async def tool_call_in_subprocess(
293
+ self,
294
+ tool_name: str,
295
+ body: Any,
296
+ thread_id: str = "default",
297
+ profile: str = "default",
298
+ *args,
299
+ **kwargs,
300
+ ):
272
301
  result = await self.server.call_in_subprocess(
273
302
  PocketServerOperations.TOOL_CALL,
274
303
  args,
275
304
  {
276
- 'tool_name': tool_name,
277
- 'body': body,
278
- 'thread_id': thread_id,
279
- 'profile': profile,
305
+ "tool_name": tool_name,
306
+ "body": body,
307
+ "thread_id": thread_id,
308
+ "profile": profile,
280
309
  **kwargs,
281
310
  },
282
311
  )
@@ -290,17 +319,17 @@ class Pocket(object):
290
319
  return self
291
320
 
292
321
  def __exit__(self, exc_type, exc_val, exc_tb):
293
- if self.__dict__.get('server'):
322
+ if self.__dict__.get("server"):
294
323
  self.server.teardown()
295
324
 
296
325
  def __del__(self):
297
- if self.__dict__.get('server'):
326
+ if self.__dict__.get("server"):
298
327
  self.server.teardown()
299
328
 
300
329
  def __getstate__(self):
301
330
  state = self.__dict__.copy()
302
- if 'server' in state:
303
- del state['server']
331
+ if "server" in state:
332
+ del state["server"]
304
333
  return state
305
334
 
306
335
  def __setstate__(self, state):
hyperpocket/prompts.py CHANGED
@@ -1,5 +1,5 @@
1
1
  def pocket_extended_tool_description(description: str):
2
- return f'''
2
+ return f"""
3
3
  This tool functions as described in the <tool-description> XML tag:
4
4
  <tool-description>
5
5
  {description}
@@ -10,4 +10,4 @@ Arguments required:
10
10
  - 'profile': The profile of the user invoking the tool. (infer from messages; omit if unknown).
11
11
  Users can request tools to be invoked in specific personas.
12
12
  - 'body': Tool argument passed as a JSON 'body'.
13
- '''
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, eject
3
+ from hyperpocket.repository.repository import eject, pull, sync
4
4
 
5
5
  __all__ = ["Lock", "Lockfile", "pull", "sync", "eject"]
@@ -1,12 +1,13 @@
1
1
  import abc
2
2
  import pathlib
3
3
  import shutil
4
- from typing import Optional, Tuple
4
+ from typing import Optional, Tuple, ClassVar
5
5
 
6
6
  import git
7
7
  from pydantic import BaseModel, Field
8
+ from pydantic.fields import ModelPrivateAttr
8
9
 
9
- from hyperpocket.config import settings, pocket_logger
10
+ from hyperpocket.config import pocket_logger, settings
10
11
 
11
12
 
12
13
  class Lock(BaseModel, abc.ABC):
@@ -34,17 +35,19 @@ class Lock(BaseModel, abc.ABC):
34
35
 
35
36
 
36
37
  class LocalLock(Lock):
37
- tool_source: str = Field(default='local')
38
+ tool_source: str = Field(default="local")
38
39
  tool_path: str
39
40
 
40
41
  def __init__(self, tool_path: str):
41
- super().__init__(tool_source="local", tool_path=str(pathlib.Path(tool_path).resolve()))
42
+ super().__init__(
43
+ tool_source="local", tool_path=str(pathlib.Path(tool_path).resolve())
44
+ )
42
45
 
43
46
  def __str__(self):
44
47
  return f"local\t{self.tool_path}"
45
48
 
46
49
  def key(self):
47
- return self.tool_source, self.tool_path.rstrip('/')
50
+ return self.tool_source, self.tool_path.rstrip("/")
48
51
 
49
52
  def sync(self, **kwargs):
50
53
  pocket_logger.info(f"Syncing path: {self.tool_path} ...")
@@ -55,11 +58,12 @@ class LocalLock(Lock):
55
58
 
56
59
  def toolpkg_path(self) -> pathlib.Path:
57
60
  pocket_pkgs = settings.toolpkg_path
58
- return pocket_pkgs / 'local' / self.tool_path[1:]
61
+ return pocket_pkgs / "local" / self.tool_path[1:]
59
62
 
60
63
 
61
64
  class GitLock(Lock):
62
- tool_source: str = 'git'
65
+ _remote_cache: ClassVar[dict[str, dict[str, str]]]
66
+ tool_source: str = "git"
63
67
  repository_url: str
64
68
  git_ref: str
65
69
  ref_sha: Optional[str] = None
@@ -68,17 +72,17 @@ class GitLock(Lock):
68
72
  return f"git\t{self.repository_url}\t{self.git_ref}\t{self.ref_sha}"
69
73
 
70
74
  def key(self):
71
- return self.tool_source, self.repository_url.rstrip('/'), self.git_ref
75
+ return self.tool_source, self.repository_url.rstrip("/"), self.git_ref
72
76
 
73
77
  def toolpkg_path(self) -> pathlib.Path:
74
78
  if not self.ref_sha:
75
79
  raise ValueError("ref_sha is not set")
76
80
  cleansed_url = self.repository_url
77
- if self.repository_url.startswith('http://'):
81
+ if self.repository_url.startswith("http://"):
78
82
  cleansed_url = self.repository_url[7:]
79
- elif self.repository_url.startswith('https://'):
83
+ elif self.repository_url.startswith("https://"):
80
84
  cleansed_url = self.repository_url[8:]
81
- elif self.repository_url.startswith('git@'):
85
+ elif self.repository_url.startswith("git@"):
82
86
  cleansed_url = self.repository_url[4:]
83
87
  return settings.toolpkg_path / cleansed_url / self.ref_sha
84
88
 
@@ -91,12 +95,16 @@ class GitLock(Lock):
91
95
  to align the local repository with the remote version.
92
96
  """
93
97
  try:
94
- pocket_logger.info(f"Syncing git: {self.repository_url} @ ref: {self.git_ref} ...")
98
+ pocket_logger.info(
99
+ f"Syncing git: {self.repository_url} @ ref: {self.git_ref} ..."
100
+ )
95
101
 
96
102
  # get new sha from refs
97
103
  new_sha = self._get_new_sha_if_exists_in_remote()
98
104
  if new_sha is None:
99
- raise ValueError(f"Could not find ref {self.git_ref} in {self.repository_url}")
105
+ raise ValueError(
106
+ f"Could not find ref {self.git_ref} in {self.repository_url}"
107
+ )
100
108
 
101
109
  # check self.ref_sha should be updated
102
110
  if self.ref_sha != new_sha:
@@ -111,10 +119,10 @@ class GitLock(Lock):
111
119
  # init git repo in local and set origin url
112
120
  repo = git.Repo.init(pkg_version_path)
113
121
  try:
114
- remote = repo.remote('origin')
122
+ remote = repo.remote("origin")
115
123
  remote.set_url(self.repository_url)
116
124
  except ValueError:
117
- remote = repo.create_remote('origin', self.repository_url)
125
+ remote = repo.create_remote("origin", self.repository_url)
118
126
 
119
127
  # check current local commit include new_sha
120
128
  # if not included, fetch and do hard reset
@@ -126,10 +134,12 @@ class GitLock(Lock):
126
134
  if exist_sha is None or exist_sha != self.ref_sha:
127
135
  remote.fetch(depth=1, refspec=self.ref_sha)
128
136
  repo.git.checkout(new_sha)
129
- repo.git.reset('--hard', new_sha)
130
- repo.git.clean('-fd')
137
+ repo.git.reset("--hard", new_sha)
138
+ repo.git.clean("-fd")
131
139
  except Exception as e:
132
- pocket_logger.error(f"failed to sync git: {self.repository_url} @ ref: {self.git_ref}. reason : {e}")
140
+ pocket_logger.error(
141
+ f"failed to sync git: {self.repository_url} @ ref: {self.git_ref}. reason : {e}"
142
+ )
133
143
  raise e
134
144
 
135
145
  def _get_new_sha_if_exists_in_remote(self):
@@ -143,8 +153,8 @@ class GitLock(Lock):
143
153
  refs = git.cmd.Git().ls_remote(self.repository_url)
144
154
 
145
155
  new_sha = None
146
- for r in refs.split('\n'):
147
- sha, ref = r.split('\t')
156
+ for r in refs.split("\n"):
157
+ sha, ref = r.split("\t")
148
158
  if sha == self.ref_sha:
149
159
  new_sha = sha
150
160
  break
@@ -161,16 +171,19 @@ class GitLock(Lock):
161
171
 
162
172
  @classmethod
163
173
  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
+ if not hasattr(cls, "_remote_cache"):
175
+ cls._remote_cache = {}
176
+ if cls._remote_cache.get(repo_url) is None:
177
+ ls_lists = git.cmd.Git().ls_remote(repo_url)
178
+
179
+ branches = {}
180
+ for line in ls_lists.split("\n"):
181
+ sha, ref = line.split("\t")
182
+ if ref.startswith("refs/heads/"):
183
+ branch_name = ref.replace("refs/heads/", "")
184
+ branches[branch_name] = sha
185
+ cls._remote_cache[repo_url] = branches
186
+ return cls._remote_cache[repo_url]
174
187
 
175
188
  @classmethod
176
189
  def parse_repo_url(cls, repo_url: str) -> Tuple[str, str, str]:
@@ -195,7 +208,7 @@ class GitLock(Lock):
195
208
  # Parse base repo URL and remaining path
196
209
  tree_index = repo_path_list.index("tree")
197
210
  base_repo = f"https://github.com/{'/'.join(repo_path_list[:tree_index])}"
198
- sub_path = repo_path_list[tree_index + 1:]
211
+ sub_path = repo_path_list[tree_index + 1 :]
199
212
 
200
213
  # Fetch branch information
201
214
  branches = cls.get_git_branches(base_repo)
@@ -204,14 +217,15 @@ class GitLock(Lock):
204
217
  for idx in range(1, len(sub_path) + 1):
205
218
  branch_name = "/".join(sub_path[:idx])
206
219
  if branch_name in branches:
207
- directory_path = "/".join(sub_path[idx:]) if idx < len(sub_path) else None
220
+ directory_path = (
221
+ "/".join(sub_path[idx:]) if idx < len(sub_path) else None
222
+ )
208
223
  return base_repo, branch_name, directory_path
209
224
 
210
225
  # If no valid branch is found, raise an error
211
226
  raise ValueError("Branch not found in repository")
212
227
 
213
228
  def eject_to_path(self, dest_path: pathlib.Path, src_sub_path: str = None):
214
-
215
229
  # clone the git repository to the target path
216
230
  pocket_logger.info(
217
231
  f"Ejecting git: {self.repository_url} @ ref: {self.git_ref} source in path: {src_sub_path} to {dest_path} ..."
@@ -1,7 +1,7 @@
1
1
  import pathlib
2
2
  from concurrent.futures.thread import ThreadPoolExecutor
3
3
 
4
- from hyperpocket.repository.lock import Lock, LocalLock, GitLock
4
+ from hyperpocket.repository.lock import GitLock, LocalLock, Lock
5
5
 
6
6
 
7
7
  class Lockfile:
@@ -51,7 +51,7 @@ class Lockfile:
51
51
  else:
52
52
  locks = list(self.locks.values())
53
53
  with ThreadPoolExecutor(
54
- max_workers=min(len(locks) + 1, 100), thread_name_prefix="repository_loader"
54
+ max_workers=min(len(locks) + 1, 100), thread_name_prefix="repository_loader"
55
55
  ) as executor:
56
56
  executor.map(lambda lock: lock.sync(force_update=force_update), locks)
57
57
  self.write()
@@ -1,6 +1,6 @@
1
1
  import pathlib
2
2
 
3
- from hyperpocket.repository.lock import LocalLock, GitLock
3
+ from hyperpocket.repository.lock import GitLock, LocalLock
4
4
  from hyperpocket.repository.lockfile import Lockfile
5
5
 
6
6
 
@@ -1,3 +1,3 @@
1
1
  from hyperpocket.server.proxy import add_callback_proxy
2
2
 
3
- __all__ = ["add_callback_proxy"]
3
+ __all__ = ["add_callback_proxy"]
@@ -15,6 +15,7 @@ async def github_oauth2_callback(request: Request, state: str, code: str):
15
15
 
16
16
  return HTMLResponse(content="success")
17
17
 
18
+
18
19
  @github_auth_router.get("/token/callback")
19
20
  async def github_token_callback(request: Request, state: str, token: str):
20
21
  try:
@@ -22,4 +23,4 @@ async def github_token_callback(request: Request, state: str, token: str):
22
23
  except ValueError:
23
24
  return HTMLResponse(content="failed")
24
25
 
25
- return HTMLResponse(content="success")
26
+ return HTMLResponse(content="success")
@@ -3,9 +3,7 @@ from starlette.responses import HTMLResponse
3
3
 
4
4
  from hyperpocket.futures import FutureStore
5
5
 
6
- linear_auth_router = APIRouter(
7
- prefix="/linear"
8
- )
6
+ linear_auth_router = APIRouter(prefix="/linear")
9
7
 
10
8
 
11
9
  @linear_auth_router.get("/token/callback")
@@ -1,12 +1,9 @@
1
-
2
1
  from fastapi import APIRouter
3
2
  from starlette.responses import HTMLResponse
4
3
 
5
4
  from hyperpocket.futures import FutureStore
6
5
 
7
- notion_auth_router = APIRouter(
8
- prefix="/notion"
9
- )
6
+ notion_auth_router = APIRouter(prefix="/notion")
10
7
 
11
8
 
12
9
  @notion_auth_router.get("/token/callback")
@@ -16,4 +13,4 @@ async def notion_token_callback(state: str, token: str):
16
13
  except ValueError:
17
14
  return HTMLResponse(content="failed")
18
15
 
19
- return HTMLResponse(content="success")
16
+ return HTMLResponse(content="success")
@@ -3,9 +3,7 @@ from starlette.responses import HTMLResponse
3
3
 
4
4
  from hyperpocket.futures import FutureStore
5
5
 
6
- slack_auth_router = APIRouter(
7
- prefix="/slack"
8
- )
6
+ slack_auth_router = APIRouter(prefix="/slack")
9
7
 
10
8
 
11
9
  @slack_auth_router.get("/oauth2/callback")