pygpt-net 2.6.0.post2__py3-none-any.whl → 2.6.2__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 (98) hide show
  1. pygpt_net/CHANGELOG.txt +8 -0
  2. pygpt_net/__init__.py +3 -3
  3. pygpt_net/app.py +27 -9
  4. pygpt_net/controller/chat/response.py +10 -4
  5. pygpt_net/controller/chat/stream.py +40 -2
  6. pygpt_net/controller/model/editor.py +45 -4
  7. pygpt_net/controller/plugins/plugins.py +25 -0
  8. pygpt_net/controller/presets/editor.py +100 -100
  9. pygpt_net/controller/presets/experts.py +20 -1
  10. pygpt_net/controller/presets/presets.py +5 -4
  11. pygpt_net/controller/ui/mode.py +17 -66
  12. pygpt_net/core/agents/provider.py +2 -1
  13. pygpt_net/core/agents/runner.py +123 -9
  14. pygpt_net/core/agents/runners/helpers.py +3 -2
  15. pygpt_net/core/agents/runners/llama_workflow.py +176 -22
  16. pygpt_net/core/agents/runners/loop.py +22 -13
  17. pygpt_net/core/experts/experts.py +19 -25
  18. pygpt_net/core/idx/chat.py +24 -34
  19. pygpt_net/core/idx/response.py +5 -2
  20. pygpt_net/core/locale/locale.py +73 -45
  21. pygpt_net/core/render/web/body.py +152 -207
  22. pygpt_net/core/render/web/renderer.py +4 -2
  23. pygpt_net/data/config/config.json +3 -3
  24. pygpt_net/data/config/models.json +3 -3
  25. pygpt_net/data/locale/locale.de.ini +12 -8
  26. pygpt_net/data/locale/locale.en.ini +12 -8
  27. pygpt_net/data/locale/locale.es.ini +12 -8
  28. pygpt_net/data/locale/locale.fr.ini +12 -8
  29. pygpt_net/data/locale/locale.it.ini +12 -8
  30. pygpt_net/data/locale/locale.pl.ini +12 -8
  31. pygpt_net/data/locale/locale.uk.ini +12 -8
  32. pygpt_net/data/locale/locale.zh.ini +12 -8
  33. pygpt_net/item/ctx.py +2 -1
  34. pygpt_net/plugin/base/plugin.py +35 -3
  35. pygpt_net/plugin/bitbucket/__init__.py +12 -0
  36. pygpt_net/plugin/bitbucket/config.py +267 -0
  37. pygpt_net/plugin/bitbucket/plugin.py +125 -0
  38. pygpt_net/plugin/bitbucket/worker.py +569 -0
  39. pygpt_net/plugin/cmd_files/worker.py +19 -16
  40. pygpt_net/plugin/facebook/__init__.py +12 -0
  41. pygpt_net/plugin/facebook/config.py +359 -0
  42. pygpt_net/plugin/facebook/plugin.py +114 -0
  43. pygpt_net/plugin/facebook/worker.py +698 -0
  44. pygpt_net/plugin/github/__init__.py +12 -0
  45. pygpt_net/plugin/github/config.py +441 -0
  46. pygpt_net/plugin/github/plugin.py +124 -0
  47. pygpt_net/plugin/github/worker.py +674 -0
  48. pygpt_net/plugin/google/__init__.py +12 -0
  49. pygpt_net/plugin/google/config.py +367 -0
  50. pygpt_net/plugin/google/plugin.py +126 -0
  51. pygpt_net/plugin/google/worker.py +826 -0
  52. pygpt_net/plugin/slack/__init__.py +12 -0
  53. pygpt_net/plugin/slack/config.py +349 -0
  54. pygpt_net/plugin/slack/plugin.py +116 -0
  55. pygpt_net/plugin/slack/worker.py +639 -0
  56. pygpt_net/plugin/telegram/__init__.py +12 -0
  57. pygpt_net/plugin/telegram/config.py +308 -0
  58. pygpt_net/plugin/telegram/plugin.py +118 -0
  59. pygpt_net/plugin/telegram/worker.py +563 -0
  60. pygpt_net/plugin/twitter/__init__.py +12 -0
  61. pygpt_net/plugin/twitter/config.py +491 -0
  62. pygpt_net/plugin/twitter/plugin.py +126 -0
  63. pygpt_net/plugin/twitter/worker.py +837 -0
  64. pygpt_net/provider/agents/base.py +4 -1
  65. pygpt_net/provider/agents/llama_index/codeact_workflow.py +95 -0
  66. pygpt_net/provider/agents/llama_index/legacy/__init__.py +0 -0
  67. pygpt_net/provider/agents/llama_index/{openai.py → legacy/openai.py} +2 -2
  68. pygpt_net/provider/agents/llama_index/{openai_assistant.py → legacy/openai_assistant.py} +37 -5
  69. pygpt_net/provider/agents/llama_index/{planner.py → legacy/planner.py} +3 -3
  70. pygpt_net/provider/agents/llama_index/{react.py → legacy/react.py} +3 -3
  71. pygpt_net/provider/agents/llama_index/openai_workflow.py +52 -0
  72. pygpt_net/provider/agents/llama_index/planner_workflow.py +115 -0
  73. pygpt_net/provider/agents/llama_index/react_workflow.py +6 -4
  74. pygpt_net/provider/agents/llama_index/workflow/__init__.py +0 -0
  75. pygpt_net/provider/agents/llama_index/{codeact_agent_custom.py → workflow/codeact.py} +124 -8
  76. pygpt_net/provider/agents/llama_index/workflow/events.py +24 -0
  77. pygpt_net/provider/agents/llama_index/workflow/openai.py +634 -0
  78. pygpt_net/provider/agents/llama_index/workflow/planner.py +601 -0
  79. pygpt_net/provider/agents/openai/agent.py +1 -0
  80. pygpt_net/provider/agents/openai/agent_b2b.py +2 -0
  81. pygpt_net/provider/agents/openai/agent_planner.py +1 -0
  82. pygpt_net/provider/agents/openai/agent_with_experts.py +1 -0
  83. pygpt_net/provider/agents/openai/agent_with_experts_feedback.py +1 -0
  84. pygpt_net/provider/agents/openai/agent_with_feedback.py +1 -0
  85. pygpt_net/provider/agents/openai/evolve.py +1 -0
  86. pygpt_net/provider/core/preset/patch.py +11 -17
  87. pygpt_net/ui/base/config_dialog.py +4 -0
  88. pygpt_net/ui/dialog/preset.py +34 -77
  89. pygpt_net/ui/layout/toolbox/presets.py +2 -2
  90. pygpt_net/ui/main.py +3 -1
  91. pygpt_net/ui/widget/lists/experts.py +3 -2
  92. {pygpt_net-2.6.0.post2.dist-info → pygpt_net-2.6.2.dist-info}/METADATA +155 -4
  93. {pygpt_net-2.6.0.post2.dist-info → pygpt_net-2.6.2.dist-info}/RECORD +96 -62
  94. pygpt_net/data/config/presets/agent_react_workflow.json +0 -34
  95. pygpt_net/provider/agents/llama_index/code_act.py +0 -58
  96. {pygpt_net-2.6.0.post2.dist-info → pygpt_net-2.6.2.dist-info}/LICENSE +0 -0
  97. {pygpt_net-2.6.0.post2.dist-info → pygpt_net-2.6.2.dist-info}/WHEEL +0 -0
  98. {pygpt_net-2.6.0.post2.dist-info → pygpt_net-2.6.2.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,569 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # ================================================== #
4
+ # This file is a part of PYGPT package #
5
+ # Website: https://pygpt.net #
6
+ # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
+ # MIT License #
8
+ # Created By : Marcin Szczygliński #
9
+ # Updated Date: 2025.08.15 00:00:00 #
10
+ # ================================================== #
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import mimetypes
16
+ import os
17
+ import time
18
+ from typing import Any, Dict, List, Optional, Tuple
19
+ from urllib.parse import quote
20
+
21
+ import requests
22
+ from PySide6.QtCore import Slot
23
+
24
+ from pygpt_net.plugin.base.worker import BaseWorker, BaseSignals
25
+
26
+
27
+ class WorkerSignals(BaseSignals):
28
+ pass
29
+
30
+
31
+ class Worker(BaseWorker):
32
+ """
33
+ Bitbucket Cloud: Auth (Basic App Password or Bearer), Users, Workspaces, Repos,
34
+ Source/Contents (files), Issues, Pull Requests, Search.
35
+ """
36
+
37
+ def __init__(self, *args, **kwargs):
38
+ super(Worker, self).__init__()
39
+ self.signals = WorkerSignals()
40
+ self.args = args
41
+ self.kwargs = kwargs
42
+ self.plugin = None
43
+ self.cmds = None
44
+ self.ctx = None
45
+ self.msg = None
46
+
47
+ # ---------------------- Core runner ----------------------
48
+
49
+ @Slot()
50
+ def run(self):
51
+ try:
52
+ responses = []
53
+ for item in self.cmds:
54
+ if self.is_stopped():
55
+ break
56
+ try:
57
+ response = None
58
+ if item["cmd"] in self.plugin.allowed_cmds and self.plugin.has_cmd(item["cmd"]):
59
+
60
+ # -------- Auth / Setup --------
61
+ if item["cmd"] == "bb_auth_set_mode":
62
+ response = self.cmd_bb_auth_set_mode(item)
63
+ elif item["cmd"] == "bb_set_app_password":
64
+ response = self.cmd_bb_set_app_password(item)
65
+ elif item["cmd"] == "bb_set_bearer":
66
+ response = self.cmd_bb_set_bearer(item)
67
+ elif item["cmd"] == "bb_auth_check":
68
+ response = self.cmd_bb_auth_check(item)
69
+
70
+ # -------- Users / Workspaces --------
71
+ elif item["cmd"] == "bb_me":
72
+ response = self.cmd_bb_me(item)
73
+ elif item["cmd"] == "bb_user_get":
74
+ response = self.cmd_bb_user_get(item)
75
+ elif item["cmd"] == "bb_workspaces_list":
76
+ response = self.cmd_bb_workspaces_list(item)
77
+
78
+ # -------- Repos --------
79
+ elif item["cmd"] == "bb_repos_list":
80
+ response = self.cmd_bb_repos_list(item)
81
+ elif item["cmd"] == "bb_repo_get":
82
+ response = self.cmd_bb_repo_get(item)
83
+ elif item["cmd"] == "bb_repo_create":
84
+ response = self.cmd_bb_repo_create(item)
85
+ elif item["cmd"] == "bb_repo_delete":
86
+ response = self.cmd_bb_repo_delete(item)
87
+
88
+ # -------- Contents (files) --------
89
+ elif item["cmd"] == "bb_contents_get":
90
+ response = self.cmd_bb_contents_get(item)
91
+ elif item["cmd"] == "bb_file_put":
92
+ response = self.cmd_bb_file_put(item)
93
+ elif item["cmd"] == "bb_file_delete":
94
+ response = self.cmd_bb_file_delete(item)
95
+
96
+ # -------- Issues --------
97
+ elif item["cmd"] == "bb_issues_list":
98
+ response = self.cmd_bb_issues_list(item)
99
+ elif item["cmd"] == "bb_issue_create":
100
+ response = self.cmd_bb_issue_create(item)
101
+ elif item["cmd"] == "bb_issue_comment":
102
+ response = self.cmd_bb_issue_comment(item)
103
+ elif item["cmd"] == "bb_issue_update":
104
+ response = self.cmd_bb_issue_update(item)
105
+
106
+ # -------- Pull Requests --------
107
+ elif item["cmd"] == "bb_prs_list":
108
+ response = self.cmd_bb_prs_list(item)
109
+ elif item["cmd"] == "bb_pr_create":
110
+ response = self.cmd_bb_pr_create(item)
111
+ elif item["cmd"] == "bb_pr_merge":
112
+ response = self.cmd_bb_pr_merge(item)
113
+
114
+ # -------- Search --------
115
+ elif item["cmd"] == "bb_search_repos":
116
+ response = self.cmd_bb_search_repos(item)
117
+
118
+ if response:
119
+ responses.append(response)
120
+
121
+ except Exception as e:
122
+ responses.append(self.make_response(item, self.throw_error(e)))
123
+
124
+ if responses:
125
+ self.reply_more(responses)
126
+ if self.msg is not None:
127
+ self.status(self.msg)
128
+ except Exception as e:
129
+ self.error(e)
130
+ finally:
131
+ self.cleanup()
132
+
133
+ # ---------------------- HTTP / Helpers ----------------------
134
+
135
+ def _api_base(self) -> str:
136
+ return (self.plugin.get_option_value("api_base") or "https://api.bitbucket.org/2.0").rstrip("/")
137
+
138
+ def _timeout(self) -> int:
139
+ try:
140
+ return int(self.plugin.get_option_value("http_timeout") or 30)
141
+ except Exception:
142
+ return 30
143
+
144
+ def _now(self) -> int:
145
+ return int(time.time())
146
+
147
+ def _auth_mode(self) -> str:
148
+ # auto | basic | bearer
149
+ mode = (self.plugin.get_option_value("auth_mode") or "auto").strip().lower()
150
+ user = (self.plugin.get_option_value("bb_username") or "").strip()
151
+ app = (self.plugin.get_option_value("bb_app_password") or "").strip()
152
+ tok = (self.plugin.get_option_value("bb_access_token") or "").strip()
153
+ if mode == "basic":
154
+ return "basic"
155
+ if mode == "bearer":
156
+ return "bearer"
157
+ # auto preference: bearer if token present else basic if user+app present
158
+ if tok:
159
+ return "bearer"
160
+ if user and app:
161
+ return "basic"
162
+ return "bearer" if tok else "basic"
163
+
164
+ def _requests_auth(self):
165
+ # Let requests handle Basic header generation
166
+ if self._auth_mode() == "basic":
167
+ user = (self.plugin.get_option_value("bb_username") or "").strip()
168
+ pwd = (self.plugin.get_option_value("bb_app_password") or "").strip()
169
+ if not (user and pwd):
170
+ raise RuntimeError("Basic auth selected but username/app_password missing.")
171
+ return (user, pwd)
172
+ return None
173
+
174
+ def _headers(self) -> Dict[str, str]:
175
+ hdrs = {
176
+ "Accept": "application/json",
177
+ "User-Agent": "pygpt-net-bitbucket-plugin/1.0",
178
+ }
179
+ if self._auth_mode() == "bearer":
180
+ tok = (self.plugin.get_option_value("bb_access_token") or "").strip()
181
+ if not tok:
182
+ raise RuntimeError("Bearer auth selected but access token missing.")
183
+ hdrs["Authorization"] = f"Bearer {tok}"
184
+ return hdrs
185
+
186
+ def _handle_response(self, r: requests.Response) -> dict:
187
+ try:
188
+ data = r.json() if r.content else {}
189
+ except Exception:
190
+ data = {"raw": r.text}
191
+
192
+ if r.status_code not in (200, 201, 202, 204):
193
+ # Bitbucket typical error payload: {"error":{"message":"...","detail":"..."}}
194
+ msg = ""
195
+ if isinstance(data, dict):
196
+ err = data.get("error")
197
+ if isinstance(err, dict):
198
+ msg = err.get("message") or err.get("detail") or ""
199
+ elif isinstance(err, str):
200
+ msg = err
201
+ else:
202
+ msg = data.get("message") or data.get("raw") or ""
203
+ payload = {
204
+ "status": r.status_code,
205
+ "error": msg or (r.text or ""),
206
+ "www-authenticate": r.headers.get("WWW-Authenticate"),
207
+ }
208
+ if r.status_code == 401 and self._auth_mode() == "basic":
209
+ u = (self.plugin.get_option_value("bb_username") or "")
210
+ if "@" in u:
211
+ payload["hint"] = "Use Bitbucket username (handle), not email."
212
+ raise RuntimeError(json.dumps(payload, ensure_ascii=False))
213
+
214
+ # Attach meta
215
+ if isinstance(data, dict):
216
+ data["_meta"] = {
217
+ "status": r.status_code,
218
+ "x-rate-remaining": r.headers.get("x-rate-limit-remaining"),
219
+ "x-rate-reset": r.headers.get("x-rate-limit-reset"),
220
+ "x-rate-limit": r.headers.get("x-rate-limit-limit"),
221
+ }
222
+ return data
223
+
224
+ def _get(self, path: str, params: dict | None = None):
225
+ url = f"{self._api_base()}{path}"
226
+ r = requests.get(url, headers=self._headers(), params=params or {}, timeout=self._timeout(), auth=self._requests_auth())
227
+ return self._handle_response(r)
228
+
229
+ def _delete(self, path: str, params: dict | None = None):
230
+ url = f"{self._api_base()}{path}"
231
+ r = requests.delete(url, headers=self._headers(), params=params or {}, timeout=self._timeout(), auth=self._requests_auth())
232
+ return self._handle_response(r)
233
+
234
+ def _post_json(self, path: str, payload: dict, params: dict | None = None):
235
+ url = f"{self._api_base()}{path}"
236
+ r = requests.post(url, headers=self._headers(), params=params or {}, json=payload or {}, timeout=self._timeout(), auth=self._requests_auth())
237
+ return self._handle_response(r)
238
+
239
+ def _put_json(self, path: str, payload: dict, params: dict | None = None):
240
+ url = f"{self._api_base()}{path}"
241
+ r = requests.put(url, headers=self._headers(), params=params or {}, json=payload or {}, timeout=self._timeout(), auth=self._requests_auth())
242
+ return self._handle_response(r)
243
+
244
+ def _post_form(self, path: str, data: dict | List[tuple] | None = None, files: dict | None = None):
245
+ url = f"{self._api_base()}{path}"
246
+ r = requests.post(url, headers=self._headers(), data=data, files=files, timeout=self._timeout(), auth=self._requests_auth())
247
+ return self._handle_response(r)
248
+
249
+ def _guess_mime(self, path: str) -> str:
250
+ mt, _ = mimetypes.guess_type(path)
251
+ return mt or "application/octet-stream"
252
+
253
+ # ---------------------- Auth commands ----------------------
254
+
255
+ def cmd_bb_auth_set_mode(self, item: dict) -> dict:
256
+ p = item.get("params", {})
257
+ mode = (p.get("mode") or "").strip().lower()
258
+ if mode not in ("auto", "basic", "bearer"):
259
+ return self.make_response(item, "Param 'mode' must be: auto|basic|bearer")
260
+ self.plugin.set_option_value("auth_mode", mode)
261
+ return self.make_response(item, {"ok": True, "mode": mode})
262
+
263
+ def cmd_bb_set_app_password(self, item: dict) -> dict:
264
+ p = item.get("params", {})
265
+ user = (p.get("username") or "").strip()
266
+ app = (p.get("app_password") or "").strip()
267
+ if not (user and app):
268
+ return self.make_response(item, "Params 'username' and 'app_password' required")
269
+ self.plugin.set_option_value("bb_username", user)
270
+ self.plugin.set_option_value("bb_app_password", app)
271
+ if p.get("set_mode"):
272
+ self.plugin.set_option_value("auth_mode", "basic")
273
+ return self.make_response(item, {"ok": True, "mode": self._auth_mode()})
274
+
275
+ def cmd_bb_set_bearer(self, item: dict) -> dict:
276
+ p = item.get("params", {})
277
+ tok = (p.get("access_token") or "").strip()
278
+ if not tok:
279
+ return self.make_response(item, "Param 'access_token' required")
280
+ self.plugin.set_option_value("bb_access_token", tok)
281
+ if p.get("set_mode"):
282
+ self.plugin.set_option_value("auth_mode", "bearer")
283
+ return self.make_response(item, {"ok": True, "mode": self._auth_mode()})
284
+
285
+ def cmd_bb_auth_check(self, item: dict) -> dict:
286
+ # Simple /user ping with current mode
287
+ try:
288
+ res = self._get("/user")
289
+ # Cache username if present
290
+ data = res.get("data") or res
291
+ if isinstance(data, dict):
292
+ uu = data.get("uuid") or (data.get("user") or {}).get("uuid")
293
+ un = data.get("username") or data.get("nickname")
294
+ if uu:
295
+ self.plugin.set_option_value("user_uuid", uu)
296
+ if un:
297
+ self.plugin.set_option_value("username", un)
298
+ return self.make_response(item, {"ok": True, "mode": self._auth_mode(), "user": res})
299
+ except Exception as e:
300
+ return self.make_response(item, self.throw_error(e))
301
+
302
+ # ---------------------- Users / Workspaces ----------------------
303
+
304
+ def cmd_bb_me(self, item: dict) -> dict:
305
+ res = self._get("/user")
306
+ data = res.get("data") or res
307
+ if isinstance(data, dict):
308
+ if data.get("uuid"):
309
+ self.plugin.set_option_value("user_uuid", data["uuid"])
310
+ if data.get("username"):
311
+ self.plugin.set_option_value("username", data["username"])
312
+ return self.make_response(item, res)
313
+
314
+ def cmd_bb_user_get(self, item: dict) -> dict:
315
+ p = item.get("params", {})
316
+ username = p.get("username")
317
+ if not username:
318
+ return self.make_response(item, "Param 'username' required")
319
+ res = self._get(f"/users/{quote(username)}")
320
+ return self.make_response(item, res)
321
+
322
+ def cmd_bb_workspaces_list(self, item: dict) -> dict:
323
+ p = item.get("params", {}) or {}
324
+ params = {}
325
+ if p.get("page"): params["page"] = int(p["page"])
326
+ if p.get("pagelen"): params["pagelen"] = int(p["pagelen"])
327
+ res = self._get("/workspaces", params=params)
328
+ return self.make_response(item, res)
329
+
330
+ # ---------------------- Repositories ----------------------
331
+
332
+ def cmd_bb_repos_list(self, item: dict) -> dict:
333
+ p = item.get("params", {}) or {}
334
+ workspace = p.get("workspace")
335
+ params = {}
336
+ for k in ("role", "q", "sort", "page", "pagelen", "after"):
337
+ if p.get(k) is not None:
338
+ params[k] = p[k]
339
+ if workspace:
340
+ res = self._get(f"/repositories/{quote(workspace)}", params=params)
341
+ else:
342
+ res = self._get("/repositories", params=params)
343
+ return self.make_response(item, res)
344
+
345
+ def cmd_bb_repo_get(self, item: dict) -> dict:
346
+ p = item.get("params", {}) or {}
347
+ ws, repo = p.get("workspace"), p.get("repo")
348
+ if not (ws and repo):
349
+ return self.make_response(item, "Params 'workspace' and 'repo' required")
350
+ res = self._get(f"/repositories/{quote(ws)}/{quote(repo)}")
351
+ return self.make_response(item, res)
352
+
353
+ def cmd_bb_repo_create(self, item: dict) -> dict:
354
+ p = item.get("params", {}) or {}
355
+ ws, repo = p.get("workspace"), p.get("repo")
356
+ if not (ws and repo):
357
+ return self.make_response(item, "Params 'workspace' and 'repo' required")
358
+ payload: Dict[str, Any] = {
359
+ "scm": p.get("scm") or "git",
360
+ "is_private": bool(p.get("is_private", True)),
361
+ }
362
+ if p.get("description"): payload["description"] = p["description"]
363
+ if p.get("project_key"): payload["project"] = {"key": p["project_key"]}
364
+ res = self._post_json(f"/repositories/{quote(ws)}/{quote(repo)}", payload)
365
+ return self.make_response(item, res)
366
+
367
+ def cmd_bb_repo_delete(self, item: dict) -> dict:
368
+ p = item.get("params", {}) or {}
369
+ ws, repo = p.get("workspace"), p.get("repo")
370
+ confirm = bool(p.get("confirm", False))
371
+ if not (ws and repo):
372
+ return self.make_response(item, "Params 'workspace' and 'repo' required")
373
+ if not confirm:
374
+ return self.make_response(item, "Confirm deletion by setting 'confirm': true")
375
+ res = self._delete(f"/repositories/{quote(ws)}/{quote(repo)}")
376
+ return self.make_response(item, res)
377
+
378
+ # ---------------------- Contents (Source API) ----------------------
379
+
380
+ def cmd_bb_contents_get(self, item: dict) -> dict:
381
+ p = item.get("params", {}) or {}
382
+ ws, repo = p.get("workspace"), p.get("repo")
383
+ if not (ws and repo):
384
+ return self.make_response(item, "Params 'workspace' and 'repo' required")
385
+ path = (p.get("path") or "").strip().strip("/")
386
+ ref = (p.get("ref") or "").strip()
387
+ params: Dict[str, Any] = {}
388
+ if p.get("format"): params["format"] = p["format"] # meta|rendered
389
+ if p.get("q"): params["q"] = p["q"]
390
+ if p.get("sort"): params["sort"] = p["sort"]
391
+ if p.get("max_depth") is not None: params["max_depth"] = int(p["max_depth"])
392
+ if ref and path:
393
+ res = self._get(f"/repositories/{quote(ws)}/{quote(repo)}/src/{quote(ref)}/{path}", params=params)
394
+ elif ref and not path:
395
+ res = self._get(f"/repositories/{quote(ws)}/{quote(repo)}/src/{quote(ref)}", params=params)
396
+ elif path:
397
+ res = self._get(f"/repositories/{quote(ws)}/{quote(repo)}/src/{path}", params=params)
398
+ else:
399
+ res = self._get(f"/repositories/{quote(ws)}/{quote(repo)}/src", params=params)
400
+ return self.make_response(item, res)
401
+
402
+ def _read_local_bytes(self, local_path: str) -> bytes:
403
+ local = self.prepare_path(local_path)
404
+ if not os.path.exists(local):
405
+ raise RuntimeError(f"Local file not found: {local}")
406
+ with open(local, "rb") as fh:
407
+ return fh.read()
408
+
409
+ def cmd_bb_file_put(self, item: dict) -> dict:
410
+ p = item.get("params", {}) or {}
411
+ ws, repo, remote_path = p.get("workspace"), p.get("repo"), p.get("path")
412
+ if not (ws and repo and remote_path):
413
+ return self.make_response(item, "Params 'workspace','repo','path' required")
414
+ message = p.get("message") or f"Update {remote_path}"
415
+ branch = p.get("branch")
416
+ parents = p.get("parents")
417
+ content_str = p.get("content")
418
+ local_path = p.get("local_path")
419
+
420
+ files = None
421
+ data: Dict[str, Any] | List[tuple]
422
+ if local_path:
423
+ data_pairs: List[tuple] = [("message", message)]
424
+ if branch: data_pairs.append(("branch", branch))
425
+ if parents: data_pairs.append(("parents", parents))
426
+ b = self._read_local_bytes(local_path)
427
+ mime = self._guess_mime(local_path)
428
+ files = {remote_path: (os.path.basename(local_path), b, mime)}
429
+ data = data_pairs
430
+ elif content_str is not None:
431
+ data = {remote_path: content_str, "message": message}
432
+ if branch: data["branch"] = branch
433
+ if parents: data["parents"] = parents
434
+ else:
435
+ return self.make_response(item, "Provide 'content' or 'local_path'")
436
+
437
+ res = self._post_form(f"/repositories/{quote(ws)}/{quote(repo)}/src", data=data, files=files)
438
+ return self.make_response(item, res)
439
+
440
+ def cmd_bb_file_delete(self, item: dict) -> dict:
441
+ p = item.get("params", {}) or {}
442
+ ws, repo = p.get("workspace"), p.get("repo")
443
+ paths = p.get("paths") or p.get("path")
444
+ if isinstance(paths, str): paths = [paths]
445
+ if not (ws and repo and paths):
446
+ return self.make_response(item, "Params 'workspace','repo','paths' required")
447
+ message = p.get("message") or "Delete files"
448
+ branch = p.get("branch")
449
+ parents = p.get("parents")
450
+ data: List[tuple] = [("message", message)]
451
+ if branch: data.append(("branch", branch))
452
+ if parents: data.append(("parents", parents))
453
+ for path in paths:
454
+ data.append(("files", path))
455
+ res = self._post_form(f"/repositories/{quote(ws)}/{quote(repo)}/src", data=data)
456
+ return self.make_response(item, res)
457
+
458
+ # ---------------------- Issues ----------------------
459
+
460
+ def cmd_bb_issues_list(self, item: dict) -> dict:
461
+ p = item.get("params", {}) or {}
462
+ ws, repo = p.get("workspace"), p.get("repo")
463
+ if not (ws and repo):
464
+ return self.make_response(item, "Params 'workspace' and 'repo' required")
465
+ params: Dict[str, Any] = {}
466
+ for k in ("q", "sort", "page", "pagelen"):
467
+ if p.get(k) is not None: params[k] = p[k]
468
+ if p.get("state"):
469
+ q = params.get("q", "")
470
+ clause = f'state="{p["state"]}"'
471
+ params["q"] = f"{q} AND {clause}" if q else clause
472
+ res = self._get(f"/repositories/{quote(ws)}/{quote(repo)}/issues", params=params)
473
+ return self.make_response(item, res)
474
+
475
+ def cmd_bb_issue_create(self, item: dict) -> dict:
476
+ p = item.get("params", {}) or {}
477
+ ws, repo, title = p.get("workspace"), p.get("repo"), p.get("title")
478
+ if not (ws and repo and title):
479
+ return self.make_response(item, "Params 'workspace','repo','title' required")
480
+ payload: Dict[str, Any] = {"title": title}
481
+ if p.get("content"): payload["content"] = {"raw": p["content"]}
482
+ if p.get("assignee"): payload["assignee"] = {"username": p["assignee"]}
483
+ res = self._post_json(f"/repositories/{quote(ws)}/{quote(repo)}/issues", payload)
484
+ return self.make_response(item, res)
485
+
486
+ def cmd_bb_issue_comment(self, item: dict) -> dict:
487
+ p = item.get("params", {}) or {}
488
+ ws, repo, iid, content = p.get("workspace"), p.get("repo"), p.get("id"), p.get("content")
489
+ if not (ws and repo and iid and content):
490
+ return self.make_response(item, "Params 'workspace','repo','id','content' required")
491
+ payload = {"content": {"raw": content}}
492
+ res = self._post_json(f"/repositories/{quote(ws)}/{quote(repo)}/issues/{int(iid)}/comments", payload)
493
+ return self.make_response(item, res)
494
+
495
+ def cmd_bb_issue_update(self, item: dict) -> dict:
496
+ p = item.get("params", {}) or {}
497
+ ws, repo, iid = p.get("workspace"), p.get("repo"), p.get("id")
498
+ if not (ws and repo and iid):
499
+ return self.make_response(item, "Params 'workspace','repo','id' required")
500
+ payload: Dict[str, Any] = {}
501
+ if p.get("state"): payload["state"] = p["state"]
502
+ if p.get("title"): payload["title"] = p["title"]
503
+ if p.get("content"): payload["content"] = {"raw": p["content"]}
504
+ res = self._put_json(f"/repositories/{quote(ws)}/{quote(repo)}/issues/{int(iid)}", payload)
505
+ return self.make_response(item, res)
506
+
507
+ # ---------------------- Pull Requests ----------------------
508
+
509
+ def cmd_bb_prs_list(self, item: dict) -> dict:
510
+ p = item.get("params", {}) or {}
511
+ ws, repo = p.get("workspace"), p.get("repo")
512
+ if not (ws and repo):
513
+ return self.make_response(item, "Params 'workspace' and 'repo' required")
514
+ params: Dict[str, Any] = {}
515
+ for k in ("state", "page", "pagelen", "q", "sort"):
516
+ if p.get(k) is not None: params[k] = p[k]
517
+ res = self._get(f"/repositories/{quote(ws)}/{quote(repo)}/pullrequests", params=params)
518
+ return self.make_response(item, res)
519
+
520
+ def cmd_bb_pr_create(self, item: dict) -> dict:
521
+ p = item.get("params", {}) or {}
522
+ ws, repo = p.get("workspace"), p.get("repo")
523
+ title, src, dst = p.get("title"), p.get("source_branch"), p.get("destination_branch")
524
+ if not (ws and repo and title and src and dst):
525
+ return self.make_response(item, "Params 'workspace','repo','title','source_branch','destination_branch' required")
526
+ payload: Dict[str, Any] = {
527
+ "title": title,
528
+ "source": {"branch": {"name": src}},
529
+ "destination": {"branch": {"name": dst}},
530
+ }
531
+ if p.get("description"): payload["description"] = p["description"]
532
+ if p.get("draft") is not None: payload["draft"] = bool(p["draft"])
533
+ res = self._post_json(f"/repositories/{quote(ws)}/{quote(repo)}/pullrequests", payload)
534
+ return self.make_response(item, res)
535
+
536
+ def cmd_bb_pr_merge(self, item: dict) -> dict:
537
+ p = item.get("params", {}) or {}
538
+ ws, repo, pr_id = p.get("workspace"), p.get("repo"), p.get("id")
539
+ if not (ws and repo and pr_id):
540
+ return self.make_response(item, "Params 'workspace','repo','id' required")
541
+ payload: Dict[str, Any] = {}
542
+ if p.get("message"): payload["message"] = p["message"]
543
+ if p.get("close_source_branch") is not None: payload["close_source_branch"] = bool(p["close_source_branch"])
544
+ res = self._post_json(f"/repositories/{quote(ws)}/{quote(repo)}/pullrequests/{int(pr_id)}/merge", payload)
545
+ return self.make_response(item, res)
546
+
547
+ # ---------------------- Search ----------------------
548
+
549
+ def cmd_bb_search_repos(self, item: dict) -> dict:
550
+ p = item.get("params", {}) or {}
551
+ params: Dict[str, Any] = {}
552
+ if p.get("q"): params["q"] = p["q"] # BBQL
553
+ if p.get("sort"): params["sort"] = p["sort"]
554
+ if p.get("page"): params["page"] = int(p["page"])
555
+ if p.get("pagelen"): params["pagelen"] = int(p["pagelen"])
556
+ res = self._get("/repositories", params=params)
557
+ return self.make_response(item, res)
558
+
559
+ # ---------------------- FS helpers ----------------------
560
+
561
+ def prepare_path(self, path: str) -> str:
562
+ if path in [".", "./"]:
563
+ return self.plugin.window.core.config.get_user_dir("data")
564
+ if self.is_absolute_path(path):
565
+ return path
566
+ return os.path.join(self.plugin.window.core.config.get_user_dir("data"), path)
567
+
568
+ def is_absolute_path(self, path: str) -> bool:
569
+ return os.path.isabs(path)
@@ -6,7 +6,7 @@
6
6
  # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
7
  # MIT License #
8
8
  # Created By : Marcin Szczygliński #
9
- # Updated Date: 2025.08.11 14:00:00 #
9
+ # Updated Date: 2025.08.14 01:00:00 #
10
10
  # ================================================== #
11
11
 
12
12
  import fnmatch
@@ -442,25 +442,28 @@ class Worker(BaseWorker):
442
442
  dst = self.prepare_path(item["params"]['dst'])
443
443
  self.msg = "Downloading file: {} into {}".format(item["params"]['src'], dst)
444
444
  self.log(self.msg)
445
+ size = 0
445
446
  # Check if src is URL
446
447
  if item["params"]['src'].startswith("http"):
447
448
  src = item["params"]['src']
448
449
  # Download file from URL
449
- req = Request(
450
- url=src,
451
- headers={'User-Agent': 'Mozilla/5.0'},
452
- )
453
- context = ssl.create_default_context()
454
- context.check_hostname = False
455
- context.verify_mode = ssl.CERT_NONE
456
- with urlopen(
457
- req,
458
- context=context,
459
- timeout=5) as response, \
460
- open(dst, 'wb') as out_file:
461
- shutil.copyfileobj(response, out_file)
462
-
463
- size = os.path.getsize(dst)
450
+ try:
451
+ req = Request(
452
+ url=src,
453
+ headers={'User-Agent': 'Mozilla/5.0'},
454
+ )
455
+ context = ssl.create_default_context()
456
+ context.check_hostname = False
457
+ context.verify_mode = ssl.CERT_NONE
458
+ with urlopen(
459
+ req,
460
+ context=context,
461
+ timeout=5) as response, \
462
+ open(dst, 'wb') as out_file:
463
+ shutil.copyfileobj(response, out_file)
464
+ size = os.path.getsize(dst)
465
+ except Exception as e:
466
+ return self.make_response(item, f"Failed to download file: {e}")
464
467
  else:
465
468
  # Handle local file paths
466
469
  src = os.path.join(
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # ================================================== #
4
+ # This file is a part of PYGPT package #
5
+ # Website: https://pygpt.net #
6
+ # GitHub: https://github.com/szczyglis-dev/py-gpt #
7
+ # MIT License #
8
+ # Created By : Marcin Szczygliński #
9
+ # Updated Date: 2025.06.30 02:00:00 #
10
+ # ================================================== #
11
+
12
+ from .plugin import *