github-haystack 2.0.0__tar.gz → 2.1.0__tar.gz

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 (49) hide show
  1. {github_haystack-2.0.0 → github_haystack-2.1.0}/CHANGELOG.md +23 -0
  2. {github_haystack-2.0.0 → github_haystack-2.1.0}/PKG-INFO +2 -1
  3. github_haystack-2.1.0/pydoc/config_docusaurus.yml +18 -0
  4. {github_haystack-2.0.0 → github_haystack-2.1.0}/pyproject.toml +16 -3
  5. {github_haystack-2.0.0 → github_haystack-2.1.0}/src/haystack_integrations/components/connectors/github/file_editor.py +1 -1
  6. {github_haystack-2.0.0 → github_haystack-2.1.0}/src/haystack_integrations/components/connectors/github/issue_commenter.py +1 -1
  7. {github_haystack-2.0.0 → github_haystack-2.1.0}/src/haystack_integrations/components/connectors/github/issue_viewer.py +1 -1
  8. {github_haystack-2.0.0 → github_haystack-2.1.0}/src/haystack_integrations/components/connectors/github/pr_creator.py +6 -1
  9. {github_haystack-2.0.0 → github_haystack-2.1.0}/src/haystack_integrations/components/connectors/github/repo_forker.py +1 -1
  10. {github_haystack-2.0.0 → github_haystack-2.1.0}/src/haystack_integrations/components/connectors/github/repo_viewer.py +1 -1
  11. {github_haystack-2.0.0 → github_haystack-2.1.0}/src/haystack_integrations/tools/github/file_editor_tool.py +1 -1
  12. {github_haystack-2.0.0 → github_haystack-2.1.0}/src/haystack_integrations/tools/github/issue_commenter_tool.py +1 -1
  13. {github_haystack-2.0.0 → github_haystack-2.1.0}/src/haystack_integrations/tools/github/issue_viewer_tool.py +1 -1
  14. {github_haystack-2.0.0 → github_haystack-2.1.0}/src/haystack_integrations/tools/github/pr_creator_tool.py +1 -1
  15. {github_haystack-2.0.0 → github_haystack-2.1.0}/src/haystack_integrations/tools/github/repo_forker_tool.py +1 -1
  16. {github_haystack-2.0.0 → github_haystack-2.1.0}/src/haystack_integrations/tools/github/repo_viewer_tool.py +1 -1
  17. {github_haystack-2.0.0 → github_haystack-2.1.0}/src/haystack_integrations/tools/github/utils.py +2 -2
  18. {github_haystack-2.0.0 → github_haystack-2.1.0}/tests/test_file_editor.py +91 -0
  19. {github_haystack-2.0.0 → github_haystack-2.1.0}/tests/test_file_editor_tool.py +7 -0
  20. {github_haystack-2.0.0 → github_haystack-2.1.0}/tests/test_pr_creator.py +79 -0
  21. {github_haystack-2.0.0 → github_haystack-2.1.0}/tests/test_repo_forker.py +54 -0
  22. {github_haystack-2.0.0 → github_haystack-2.1.0}/tests/test_repo_viewer.py +49 -0
  23. github_haystack-2.1.0/tests/test_utils.py +63 -0
  24. github_haystack-2.0.0/pydoc/config_docusaurus.yml +0 -33
  25. {github_haystack-2.0.0 → github_haystack-2.1.0}/.gitignore +0 -0
  26. {github_haystack-2.0.0 → github_haystack-2.1.0}/LICENSE.txt +0 -0
  27. {github_haystack-2.0.0 → github_haystack-2.1.0}/README.md +0 -0
  28. {github_haystack-2.0.0 → github_haystack-2.1.0}/src/haystack_integrations/components/connectors/github/__init__.py +0 -0
  29. {github_haystack-2.0.0 → github_haystack-2.1.0}/src/haystack_integrations/components/connectors/py.typed +0 -0
  30. {github_haystack-2.0.0 → github_haystack-2.1.0}/src/haystack_integrations/prompts/github/__init__.py +0 -0
  31. {github_haystack-2.0.0 → github_haystack-2.1.0}/src/haystack_integrations/prompts/github/context_prompt.py +0 -0
  32. {github_haystack-2.0.0 → github_haystack-2.1.0}/src/haystack_integrations/prompts/github/file_editor_prompt.py +0 -0
  33. {github_haystack-2.0.0 → github_haystack-2.1.0}/src/haystack_integrations/prompts/github/issue_commenter_prompt.py +0 -0
  34. {github_haystack-2.0.0 → github_haystack-2.1.0}/src/haystack_integrations/prompts/github/issue_viewer_prompt.py +0 -0
  35. {github_haystack-2.0.0 → github_haystack-2.1.0}/src/haystack_integrations/prompts/github/pr_creator_prompt.py +0 -0
  36. {github_haystack-2.0.0 → github_haystack-2.1.0}/src/haystack_integrations/prompts/github/repo_forker_prompt.py +0 -0
  37. {github_haystack-2.0.0 → github_haystack-2.1.0}/src/haystack_integrations/prompts/github/repo_viewer_prompt.py +0 -0
  38. {github_haystack-2.0.0 → github_haystack-2.1.0}/src/haystack_integrations/prompts/github/system_prompt.py +0 -0
  39. {github_haystack-2.0.0 → github_haystack-2.1.0}/src/haystack_integrations/prompts/py.typed +0 -0
  40. {github_haystack-2.0.0 → github_haystack-2.1.0}/src/haystack_integrations/tools/github/__init__.py +0 -0
  41. {github_haystack-2.0.0 → github_haystack-2.1.0}/src/haystack_integrations/tools/py.typed +0 -0
  42. {github_haystack-2.0.0 → github_haystack-2.1.0}/tests/__init__.py +0 -0
  43. {github_haystack-2.0.0 → github_haystack-2.1.0}/tests/test_issue_commenter.py +0 -0
  44. {github_haystack-2.0.0 → github_haystack-2.1.0}/tests/test_issue_commenter_tool.py +0 -0
  45. {github_haystack-2.0.0 → github_haystack-2.1.0}/tests/test_issue_viewer.py +0 -0
  46. {github_haystack-2.0.0 → github_haystack-2.1.0}/tests/test_issue_viewer_tool.py +0 -0
  47. {github_haystack-2.0.0 → github_haystack-2.1.0}/tests/test_pr_creator_tool.py +0 -0
  48. {github_haystack-2.0.0 → github_haystack-2.1.0}/tests/test_repo_forker_tool.py +0 -0
  49. {github_haystack-2.0.0 → github_haystack-2.1.0}/tests/test_repo_viewer_tool.py +0 -0
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## [integrations/github-v2.0.0] - 2026-01-12
4
+
5
+ ### 📚 Documentation
6
+
7
+ - Add pydoc configurations for Docusaurus (#2411)
8
+
9
+ ### ⚙️ CI
10
+
11
+ - Change pytest command (#2475)
12
+
13
+ ### 🧹 Chores
14
+
15
+ - Remove Readme API CI workflow and configs (#2573)
16
+ - Fix nightly test (#2580)
17
+ - Make fmt command more forgiving (#2671)
18
+ - [**breaking**] Github - drop Python 3.9 and use X|Y typing (#2705)
19
+
20
+ ### 🌀 Miscellaneous
21
+
22
+ - Adopt PEP 585 type hinting (part 3) (#2510)
23
+ - Chore: Fix tool validation test failures in github integration (#2659)
24
+ - Fix: Fix github integration unit tests (#2703)
25
+
3
26
  ## [integrations/github-v1.2.1] - 2025-10-20
4
27
 
5
28
  ### 🚀 Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github-haystack
3
- Version: 2.0.0
3
+ Version: 2.1.0
4
4
  Summary: Haystack components for interacting with GitHub repositories
5
5
  Project-URL: Source, https://github.com/deepset-ai/haystack-core-integrations/github
6
6
  Project-URL: Documentation, https://github.com/deepset-ai/haystack-core-integrations/blob/main/integrations/github/README.md
@@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.10
15
15
  Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
18
19
  Classifier: Programming Language :: Python :: Implementation :: CPython
19
20
  Classifier: Programming Language :: Python :: Implementation :: PyPy
20
21
  Requires-Python: >=3.10
@@ -0,0 +1,18 @@
1
+ loaders:
2
+ - modules:
3
+ - haystack_integrations.components.connectors.github.file_editor
4
+ - haystack_integrations.components.connectors.github.issue_commenter
5
+ - haystack_integrations.components.connectors.github.issue_viewer
6
+ - haystack_integrations.components.connectors.github.pr_creator
7
+ - haystack_integrations.components.connectors.github.repo_viewer
8
+ - haystack_integrations.components.connectors.github.repo_forker
9
+ search_path: [../src]
10
+ processors:
11
+ - type: filter
12
+ documented_only: true
13
+ skip_empty_modules: true
14
+ renderer:
15
+ description: GitHub integration for Haystack
16
+ id: integrations-github
17
+ filename: github.md
18
+ title: GitHub
@@ -19,6 +19,7 @@ classifiers = [
19
19
  "Programming Language :: Python :: 3.11",
20
20
  "Programming Language :: Python :: 3.12",
21
21
  "Programming Language :: Python :: 3.13",
22
+ "Programming Language :: Python :: 3.14",
22
23
  "Programming Language :: Python :: Implementation :: CPython",
23
24
  "Programming Language :: Python :: Implementation :: PyPy",
24
25
  ]
@@ -45,7 +46,7 @@ installer = "uv"
45
46
  dependencies = ["haystack-pydoc-tools", "ruff"]
46
47
 
47
48
  [tool.hatch.envs.default.scripts]
48
- docs = ["pydoc-markdown pydoc/config_docusaurus.yml"]
49
+ docs = ["haystack-pydoc pydoc/config_docusaurus.yml"]
49
50
  fmt = "ruff check --fix {args}; ruff format {args}"
50
51
  fmt-check = "ruff check {args} && ruff format --check {args}"
51
52
 
@@ -63,7 +64,8 @@ dependencies = [
63
64
  unit = 'pytest -m "not integration" {args:tests}'
64
65
  integration = 'pytest -m "integration" {args:tests}'
65
66
  all = 'pytest {args:tests}'
66
- cov-retry = 'pytest --cov=haystack_integrations --reruns 3 --reruns-delay 30 -x {args:tests}'
67
+ unit-cov-retry = 'pytest --cov=haystack_integrations --reruns 3 --reruns-delay 30 -x -m "not integration" {args:tests}'
68
+ integration-cov-append-retry = 'pytest --cov=haystack_integrations --cov-append --reruns 3 --reruns-delay 30 -x -m "integration" {args:tests}'
67
69
 
68
70
  types = """mypy -p haystack_integrations.components.connectors.github \
69
71
  -p haystack_integrations.prompts.github \
@@ -81,9 +83,17 @@ line-length = 120
81
83
  [tool.ruff.lint]
82
84
  select = [
83
85
  "A",
86
+ "ANN",
84
87
  "ARG",
85
88
  "B",
86
89
  "C",
90
+ "D102", # Missing docstring in public method
91
+ "D103", # Missing docstring in public function
92
+ "D205", # 1 blank line required between summary line and description
93
+ "D209", # Closing triple quotes go to new line
94
+ "D213", # summary lines must be positioned on the second physical line of the docstring
95
+ "D417", # Missing argument descriptions in the docstring
96
+ "D419", # Docstring is empty
87
97
  "DTZ",
88
98
  "E",
89
99
  "EM",
@@ -121,6 +131,8 @@ ignore = [
121
131
  # Misc
122
132
  "B008",
123
133
  "S101",
134
+ # Allow `Any` - used legitimately for dynamic types and SDK boundaries
135
+ "ANN401",
124
136
  ]
125
137
 
126
138
  [tool.ruff.lint.isort]
@@ -131,13 +143,14 @@ ban-relative-imports = "parents"
131
143
 
132
144
  [tool.ruff.lint.per-file-ignores]
133
145
  # Tests can use magic values, assertions, and relative imports
134
- "tests/**/*" = ["PLR2004", "S101", "TID252"]
146
+ "tests/**/*" = ["D", "PLR2004", "S101", "TID252", "ANN"]
135
147
  # Ignore RUF001 for all files in the prompts directory
136
148
  "src/haystack_integrations/components/prompts/**/*" = ["RUF001"]
137
149
 
138
150
  [tool.coverage.run]
139
151
  source = ["haystack_integrations"]
140
152
  branch = true
153
+ relative_files = true
141
154
  parallel = false
142
155
 
143
156
 
@@ -82,7 +82,7 @@ class GitHubFileEditor:
82
82
  repo: str | None = None,
83
83
  branch: str = "main",
84
84
  raise_on_failure: bool = True,
85
- ):
85
+ ) -> None:
86
86
  """
87
87
  Initialize the component.
88
88
 
@@ -41,7 +41,7 @@ class GitHubIssueCommenter:
41
41
  github_token: Secret = Secret.from_env_var("GITHUB_TOKEN"),
42
42
  raise_on_failure: bool = True,
43
43
  retry_attempts: int = 2,
44
- ):
44
+ ) -> None:
45
45
  """
46
46
  Initialize the component.
47
47
 
@@ -40,7 +40,7 @@ class GitHubIssueViewer:
40
40
  github_token: Secret | None = None,
41
41
  raise_on_failure: bool = True,
42
42
  retry_attempts: int = 2,
43
- ):
43
+ ) -> None:
44
44
  """
45
45
  Initialize the component.
46
46
 
@@ -38,7 +38,12 @@ class GitHubPRCreator:
38
38
  ```
39
39
  """
40
40
 
41
- def __init__(self, *, github_token: Secret = Secret.from_env_var("GITHUB_TOKEN"), raise_on_failure: bool = True):
41
+ def __init__(
42
+ self,
43
+ *,
44
+ github_token: Secret = Secret.from_env_var("GITHUB_TOKEN"),
45
+ raise_on_failure: bool = True,
46
+ ) -> None:
42
47
  """
43
48
  Initialize the component.
44
49
 
@@ -48,7 +48,7 @@ class GitHubRepoForker:
48
48
  poll_interval: int = 2,
49
49
  auto_sync: bool = True,
50
50
  create_branch: bool = True,
51
- ):
51
+ ) -> None:
52
52
  """
53
53
  Initialize the component.
54
54
 
@@ -76,7 +76,7 @@ class GitHubRepoViewer:
76
76
  max_file_size: int = 1_000_000, # 1MB default limit
77
77
  repo: str | None = None,
78
78
  branch: str = "main",
79
- ):
79
+ ) -> None:
80
80
  """
81
81
  Initialize the component.
82
82
 
@@ -31,7 +31,7 @@ class GitHubFileEditorTool(ComponentTool):
31
31
  outputs_to_string: dict[str, str | Callable[[Any], str]] | None = None,
32
32
  inputs_from_state: dict[str, str] | None = None,
33
33
  outputs_to_state: dict[str, dict[str, str | Callable]] | None = None,
34
- ):
34
+ ) -> None:
35
35
  """
36
36
  Initialize the GitHub file editor tool.
37
37
 
@@ -30,7 +30,7 @@ class GitHubIssueCommenterTool(ComponentTool):
30
30
  outputs_to_string: dict[str, str | Callable[[Any], str]] | None = None,
31
31
  inputs_from_state: dict[str, str] | None = None,
32
32
  outputs_to_state: dict[str, dict[str, str | Callable]] | None = None,
33
- ):
33
+ ) -> None:
34
34
  """
35
35
  Initialize the GitHub issue commenter tool.
36
36
 
@@ -30,7 +30,7 @@ class GitHubIssueViewerTool(ComponentTool):
30
30
  outputs_to_string: dict[str, str | Callable[[Any], str]] | None = None,
31
31
  inputs_from_state: dict[str, str] | None = None,
32
32
  outputs_to_state: dict[str, dict[str, str | Callable]] | None = None,
33
- ):
33
+ ) -> None:
34
34
  """
35
35
  Initialize the GitHub issue viewer tool.
36
36
 
@@ -29,7 +29,7 @@ class GitHubPRCreatorTool(ComponentTool):
29
29
  outputs_to_string: dict[str, str | Callable[[Any], str]] | None = None,
30
30
  inputs_from_state: dict[str, str] | None = None,
31
31
  outputs_to_state: dict[str, dict[str, str | Callable]] | None = None,
32
- ):
32
+ ) -> None:
33
33
  """
34
34
  Initialize the GitHub PR creator tool.
35
35
 
@@ -29,7 +29,7 @@ class GitHubRepoForkerTool(ComponentTool):
29
29
  outputs_to_string: dict[str, str | Callable[[Any], str]] | None = None,
30
30
  inputs_from_state: dict[str, str] | None = None,
31
31
  outputs_to_state: dict[str, dict[str, str | Callable]] | None = None,
32
- ):
32
+ ) -> None:
33
33
  """
34
34
  Initialize the GitHub Repo Forker tool.
35
35
 
@@ -32,7 +32,7 @@ class GitHubRepoViewerTool(ComponentTool):
32
32
  outputs_to_string: dict[str, str | Callable[[Any], str]] | None = None,
33
33
  inputs_from_state: dict[str, str] | None = None,
34
34
  outputs_to_state: dict[str, dict[str, str | Callable]] | None = None,
35
- ):
35
+ ) -> None:
36
36
  """
37
37
  Initialize the GitHub repository viewer tool.
38
38
 
@@ -21,7 +21,7 @@ def message_handler(documents: list[Document], max_length: int = 150_000) -> str
21
21
  result_str = ""
22
22
  for document in documents:
23
23
  if document.meta["type"] in ["file", "dir", "error"]:
24
- result_str += document.content or "" + "\n"
24
+ result_str += (document.content or "") + "\n"
25
25
  else:
26
26
  result_str += f"File Content for {document.meta['path']}\n\n"
27
27
  result_str += document.content or ""
@@ -29,7 +29,7 @@ def message_handler(documents: list[Document], max_length: int = 150_000) -> str
29
29
  if len(result_str) > max_length:
30
30
  result_str = result_str[:max_length] + "...(large file can't be fully displayed)"
31
31
 
32
- return result_str
32
+ return result_str.strip()
33
33
 
34
34
 
35
35
  def serialize_handlers(
@@ -280,3 +280,94 @@ class TestGitHubFileEditor:
280
280
  assert "Authorization" not in headers
281
281
  assert headers["Accept"] == "application/vnd.github.v3+json"
282
282
  assert headers["User-Agent"] == "Haystack/GitHubFileEditor"
283
+
284
+ @pytest.mark.parametrize(
285
+ "file_b64,original,expected",
286
+ [
287
+ ("SGVsbG8gV29ybGQ=", "missing", "Error: Original string not found in file"),
288
+ ("SGVsbG8gSGVsbG8=", "Hello", "Error: Original string appears multiple times. Please provide more context"),
289
+ ],
290
+ )
291
+ @patch("requests.get")
292
+ def test_run_edit_string_edge_cases(self, mock_get, file_b64, original, expected, monkeypatch):
293
+ monkeypatch.setenv("GITHUB_TOKEN", "test-token")
294
+ mock_get.return_value.json.return_value = {"content": file_b64, "sha": "abc123"}
295
+ mock_get.return_value.raise_for_status.return_value = None
296
+
297
+ editor = GitHubFileEditor()
298
+ result = editor.run(
299
+ command=Command.EDIT,
300
+ payload={"path": "f.txt", "original": original, "replacement": "X", "message": "m"},
301
+ repo="owner/repo",
302
+ branch="main",
303
+ )
304
+ assert result["result"] == expected
305
+
306
+ @patch("requests.get")
307
+ def test_run_undo_not_last_user(self, mock_get, monkeypatch):
308
+ monkeypatch.setenv("GITHUB_TOKEN", "test-token")
309
+
310
+ mock_get.return_value.raise_for_status.return_value = None
311
+ mock_get.return_value.json.side_effect = [
312
+ [{"author": {"login": "different_user"}, "sha": "sha1"}],
313
+ {"login": "another_user"},
314
+ ]
315
+
316
+ editor = GitHubFileEditor()
317
+ result = editor.run(command=Command.UNDO, payload={"message": "m"}, repo="owner/repo", branch="main")
318
+ assert result["result"] == "Error: Last commit was not made by the current user"
319
+
320
+ @pytest.mark.parametrize(
321
+ "kwargs,expected_substring",
322
+ [
323
+ ({"command": Command.EDIT, "payload": {}}, "Error: No repository specified"),
324
+ (
325
+ {"command": "bogus", "payload": {}, "repo": "owner/repo"},
326
+ None,
327
+ ),
328
+ ],
329
+ )
330
+ def test_run_validation_errors(self, kwargs, expected_substring, monkeypatch):
331
+ monkeypatch.setenv("GITHUB_TOKEN", "test-token")
332
+ editor = GitHubFileEditor()
333
+ if expected_substring is None:
334
+ with pytest.raises(ValueError):
335
+ editor.run(**kwargs)
336
+ else:
337
+ result = editor.run(**kwargs)
338
+ assert expected_substring in result["result"]
339
+
340
+ @patch("requests.get")
341
+ def test_run_command_as_string(self, mock_get, monkeypatch):
342
+ monkeypatch.setenv("GITHUB_TOKEN", "test-token")
343
+ mock_get.return_value.json.return_value = {"content": "SGVsbG8=", "sha": "abc"}
344
+ mock_get.return_value.raise_for_status.return_value = None
345
+
346
+ editor = GitHubFileEditor(repo="owner/repo")
347
+ with patch.object(editor, "_update_file", return_value=True):
348
+ result = editor.run(
349
+ command="EDIT",
350
+ payload={"path": "f.txt", "original": "Hello", "replacement": "Hi", "message": "m"},
351
+ )
352
+ assert result["result"] == "Edit successful"
353
+
354
+ @pytest.mark.parametrize(
355
+ "command,payload",
356
+ [
357
+ (Command.UNDO, {"message": "m"}),
358
+ (Command.CREATE, {"path": "n.txt", "content": "x", "message": "m"}),
359
+ (Command.DELETE, {"path": "n.txt", "message": "m"}),
360
+ ],
361
+ )
362
+ @patch("requests.get")
363
+ def test_run_command_error_handling_no_raise(self, mock_get, command, payload, monkeypatch):
364
+ monkeypatch.setenv("GITHUB_TOKEN", "test-token")
365
+ mock_get.side_effect = requests.RequestException("API Error")
366
+
367
+ editor = GitHubFileEditor(raise_on_failure=False)
368
+ with (
369
+ patch("requests.put", side_effect=requests.RequestException("API Error")),
370
+ patch("requests.delete", side_effect=requests.RequestException("API Error")),
371
+ ):
372
+ result = editor.run(command=command, payload=payload, repo="owner/repo", branch="main")
373
+ assert "Error: API Error" in result["result"]
@@ -205,6 +205,13 @@ class TestGitHubFileEditorTool:
205
205
  "connection_type_validation": True,
206
206
  }
207
207
 
208
+ # Compatibility with newer versions of Haystack that include these parameters
209
+ for key in ["confirmation_strategies", "required_variables", "user_prompt"]:
210
+ if key in pipeline_dict["components"]["agent"]["init_parameters"]:
211
+ expected_dict["components"]["agent"]["init_parameters"][key] = pipeline_dict["components"]["agent"][
212
+ "init_parameters"
213
+ ][key]
214
+
208
215
  assert pipeline_dict == expected_dict
209
216
 
210
217
  deserialized_pipeline = Pipeline.from_dict(pipeline_dict)
@@ -143,3 +143,82 @@ class TestGitHubPRCreator:
143
143
  assert "Authorization" not in headers
144
144
  assert headers["Accept"] == "application/vnd.github.v3+json"
145
145
  assert headers["User-Agent"] == "Haystack/GitHubPRCreator"
146
+
147
+ def test_parse_issue_url_invalid(self, monkeypatch):
148
+ monkeypatch.setenv("GITHUB_TOKEN", "test-token")
149
+ pr_creator = GitHubPRCreator()
150
+ with pytest.raises(ValueError, match="Invalid GitHub issue URL format"):
151
+ pr_creator._parse_issue_url("https://github.com/owner/repo/pull/1")
152
+
153
+ @patch("requests.get")
154
+ def test_run_returns_error_when_fork_not_found(self, mock_get, monkeypatch):
155
+ monkeypatch.setenv("GITHUB_TOKEN", "test-token")
156
+ mock_get.return_value.json.return_value = {"login": "test_user"}
157
+ mock_get.return_value.raise_for_status.return_value = None
158
+
159
+ pr_creator = GitHubPRCreator()
160
+ with patch.object(pr_creator, "_check_fork_exists", return_value=False):
161
+ result = pr_creator.run(
162
+ issue_url="https://github.com/owner/repo/issues/456",
163
+ title="Test PR",
164
+ branch="feature-branch",
165
+ base="main",
166
+ )
167
+ assert result == {"result": "Error: Fork not found at test_user/repo"}
168
+
169
+ @pytest.mark.parametrize(
170
+ "fork_payload,raises_request,expected",
171
+ [
172
+ ({"fork": True}, False, True),
173
+ ({"fork": False}, False, False),
174
+ ({}, False, False),
175
+ (None, True, False),
176
+ ],
177
+ )
178
+ @patch("requests.get")
179
+ def test_check_fork_exists(self, mock_get, fork_payload, raises_request, expected, monkeypatch):
180
+ monkeypatch.setenv("GITHUB_TOKEN", "test-token")
181
+ if raises_request:
182
+ mock_get.side_effect = requests.RequestException("boom")
183
+ else:
184
+ mock_get.return_value.json.return_value = fork_payload
185
+ mock_get.return_value.raise_for_status.return_value = None
186
+
187
+ pr_creator = GitHubPRCreator()
188
+ assert pr_creator._check_fork_exists("repo", "test_user") is expected
189
+
190
+ @pytest.mark.parametrize("raise_on_failure", [True, False])
191
+ @patch("requests.post")
192
+ def test_create_fork(self, mock_post, raise_on_failure, monkeypatch):
193
+ monkeypatch.setenv("GITHUB_TOKEN", "test-token")
194
+ mock_post.return_value.json.return_value = {"owner": {"login": "test_user"}}
195
+ mock_post.return_value.raise_for_status.return_value = None
196
+
197
+ pr_creator = GitHubPRCreator(raise_on_failure=raise_on_failure)
198
+ assert pr_creator._create_fork("owner", "repo") == "test_user"
199
+
200
+ mock_post.side_effect = requests.RequestException("boom")
201
+ if raise_on_failure:
202
+ with pytest.raises(RuntimeError, match="Failed to create fork"):
203
+ pr_creator._create_fork("owner", "repo")
204
+ else:
205
+ assert pr_creator._create_fork("owner", "repo") is None
206
+
207
+ @pytest.mark.parametrize("raise_on_failure", [True, False])
208
+ @patch("requests.post")
209
+ @patch("requests.get")
210
+ def test_create_branch(self, mock_get, mock_post, raise_on_failure, monkeypatch):
211
+ monkeypatch.setenv("GITHUB_TOKEN", "test-token")
212
+ mock_get.return_value.json.return_value = {"object": {"sha": "abc123"}}
213
+ mock_get.return_value.raise_for_status.return_value = None
214
+ mock_post.return_value.raise_for_status.return_value = None
215
+
216
+ pr_creator = GitHubPRCreator(raise_on_failure=raise_on_failure)
217
+ assert pr_creator._create_branch("owner", "repo", "feature", "main") is True
218
+
219
+ mock_get.side_effect = requests.RequestException("boom")
220
+ if raise_on_failure:
221
+ with pytest.raises(RuntimeError, match="Failed to create branch"):
222
+ pr_creator._create_branch("owner", "repo", "feature", "main")
223
+ else:
224
+ assert pr_creator._create_branch("owner", "repo", "feature", "main") is False
@@ -272,3 +272,57 @@ class TestGitHubRepoForker:
272
272
  assert "Authorization" not in headers
273
273
  assert headers["Accept"] == "application/vnd.github.v3+json"
274
274
  assert headers["User-Agent"] == "Haystack/GitHubRepoForker"
275
+
276
+ @pytest.mark.parametrize(
277
+ "status_code,raises,expected",
278
+ [
279
+ (200, False, True),
280
+ (404, False, False),
281
+ (None, True, False),
282
+ ],
283
+ )
284
+ @patch("requests.get")
285
+ def test_check_fork_status(self, mock_get, status_code, raises, expected, monkeypatch):
286
+ monkeypatch.setenv("GITHUB_TOKEN", "test-token")
287
+ if raises:
288
+ mock_get.side_effect = requests.RequestException("boom")
289
+ else:
290
+ mock_get.return_value.status_code = status_code
291
+
292
+ forker = GitHubRepoForker()
293
+ assert forker._check_fork_status("test_user/repo") is expected
294
+
295
+ def test_get_existing_repository_returns_none_on_exception(self, monkeypatch):
296
+ monkeypatch.setenv("GITHUB_TOKEN", "test-token")
297
+
298
+ forker = GitHubRepoForker()
299
+ with (
300
+ patch.object(forker, "_get_authenticated_user", return_value="test_user"),
301
+ patch("requests.get", side_effect=requests.RequestException("boom")),
302
+ ):
303
+ assert forker._get_existing_repository("repo") is None
304
+
305
+ @pytest.mark.parametrize("succeeds_within_timeout", [True, False])
306
+ def test_run_wait_for_completion(self, succeeds_within_timeout, monkeypatch):
307
+ monkeypatch.setenv("GITHUB_TOKEN", "test-token")
308
+
309
+ forker = GitHubRepoForker(
310
+ wait_for_completion=True,
311
+ max_wait_seconds=5 if succeeds_within_timeout else 0,
312
+ poll_interval=0,
313
+ create_branch=False,
314
+ )
315
+ with (
316
+ patch.object(forker, "_parse_github_url", return_value=("owner", "repo", "123")),
317
+ patch.object(forker, "_get_authenticated_user", return_value="test_user"),
318
+ patch.object(forker, "_get_existing_repository", return_value=None),
319
+ patch.object(forker, "_create_fork", return_value="test_user/repo"),
320
+ patch.object(forker, "_check_fork_status", side_effect=[False, True]),
321
+ patch("time.sleep", lambda _: None),
322
+ ):
323
+ if succeeds_within_timeout:
324
+ result = forker.run(url="https://github.com/owner/repo/issues/123")
325
+ assert result == {"repo": "test_user/repo", "issue_branch": None}
326
+ else:
327
+ with pytest.raises(TimeoutError):
328
+ forker.run(url="https://github.com/owner/repo/issues/123")
@@ -186,3 +186,52 @@ class TestGitHubRepoViewer:
186
186
  assert "Authorization" not in headers
187
187
  assert headers["Accept"] == "application/vnd.github.v3+json"
188
188
  assert headers["User-Agent"] == "Haystack/GitHubRepoViewer"
189
+
190
+ def test_run_no_repo_raises(self, monkeypatch):
191
+ monkeypatch.setenv("GITHUB_TOKEN", "test-token")
192
+ viewer = GitHubRepoViewer()
193
+ with pytest.raises(ValueError, match="Repository not provided"):
194
+ viewer.run(path="README.md")
195
+
196
+ @pytest.mark.parametrize(
197
+ "file_payload,error_substring",
198
+ [
199
+ (
200
+ {
201
+ "name": "big.bin",
202
+ "path": "big.bin",
203
+ "size": 2_000_000,
204
+ "html_url": "https://x",
205
+ "content": "",
206
+ "encoding": "base64",
207
+ },
208
+ "exceeds limit",
209
+ ),
210
+ (
211
+ {
212
+ "name": "raw.txt",
213
+ "path": "raw.txt",
214
+ "size": 5,
215
+ "html_url": "https://x",
216
+ "content": "Hello",
217
+ "encoding": "utf-8",
218
+ },
219
+ None,
220
+ ),
221
+ ],
222
+ )
223
+ @patch("requests.get")
224
+ def test_run_file_size_and_encoding(self, mock_get, file_payload, error_substring, monkeypatch):
225
+ monkeypatch.setenv("GITHUB_TOKEN", "test-token")
226
+ mock_get.return_value.json.return_value = file_payload
227
+ mock_get.return_value.raise_for_status.return_value = None
228
+
229
+ viewer = GitHubRepoViewer(raise_on_failure=False)
230
+ result = viewer.run(repo="owner/repo", path=file_payload["path"])
231
+ doc = result["documents"][0]
232
+ if error_substring is None:
233
+ assert doc.content == "Hello"
234
+ assert doc.meta["type"] == "file_content"
235
+ else:
236
+ assert doc.meta["type"] == "error"
237
+ assert error_substring in doc.content
@@ -0,0 +1,63 @@
1
+ # SPDX-FileCopyrightText: 2023-present deepset GmbH <info@deepset.ai>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ import pytest
5
+ from haystack import Document
6
+
7
+ from haystack_integrations.tools.github.utils import message_handler, serialize_handlers
8
+
9
+
10
+ class TestMessageHandler:
11
+ @pytest.mark.parametrize(
12
+ "documents,expected",
13
+ [
14
+ (
15
+ [Document(content="error message", meta={"type": "error"})],
16
+ "error message",
17
+ ),
18
+ (
19
+ [
20
+ Document(content="docs", meta={"type": "dir"}),
21
+ Document(content="README.md", meta={"type": "file"}),
22
+ ],
23
+ "docs\nREADME.md",
24
+ ),
25
+ (
26
+ [Document(content="print('hi')", meta={"type": "file_content", "path": "main.py"})],
27
+ "File Content for main.py\n\nprint('hi')",
28
+ ),
29
+ ],
30
+ )
31
+ def test_message_handler_renders_documents(self, documents, expected):
32
+ assert message_handler(documents) == expected
33
+
34
+ def test_message_handler_truncates_to_max_length(self):
35
+ big_content = "x" * 200
36
+ doc = Document(content=big_content, meta={"type": "file_content", "path": "main.py"})
37
+ result = message_handler([doc], max_length=50)
38
+ assert result.endswith("...(large file can't be fully displayed)")
39
+ assert len(result) == 50 + len("...(large file can't be fully displayed)")
40
+
41
+
42
+ class TestSerializeHandlers:
43
+ @pytest.mark.parametrize(
44
+ "outputs_to_state,outputs_to_string,error_match",
45
+ [
46
+ (
47
+ {"documents": {"handler": "not_callable"}},
48
+ None,
49
+ "outputs_to_state\\[documents\\] is not a callable",
50
+ ),
51
+ (
52
+ None,
53
+ {"handler": "not_callable"},
54
+ "outputs_to_string is not a callable",
55
+ ),
56
+ ],
57
+ )
58
+ def test_serialize_handlers_raises_when_handler_not_callable(
59
+ self, outputs_to_state, outputs_to_string, error_match
60
+ ):
61
+ serialized: dict = {}
62
+ with pytest.raises(ValueError, match=error_match):
63
+ serialize_handlers(serialized, outputs_to_state, outputs_to_string)
@@ -1,33 +0,0 @@
1
- loaders:
2
- - ignore_when_discovered:
3
- - __init__
4
- modules:
5
- - haystack_integrations.components.connectors.github.file_editor
6
- - haystack_integrations.components.connectors.github.issue_commenter
7
- - haystack_integrations.components.connectors.github.issue_viewer
8
- - haystack_integrations.components.connectors.github.pr_creator
9
- - haystack_integrations.components.connectors.github.repo_viewer
10
- - haystack_integrations.components.connectors.github.repo_forker
11
- search_path:
12
- - ../src
13
- type: haystack_pydoc_tools.loaders.CustomPythonLoader
14
- processors:
15
- - do_not_filter_modules: false
16
- documented_only: true
17
- expression: null
18
- skip_empty_modules: true
19
- type: filter
20
- - type: smart
21
- - type: crossref
22
- renderer:
23
- description: GitHub integration for Haystack
24
- id: integrations-github
25
- markdown:
26
- add_member_class_prefix: false
27
- add_method_class_prefix: true
28
- classdef_code_block: false
29
- descriptive_class_title: false
30
- descriptive_module_title: true
31
- filename: github.md
32
- title: GitHub
33
- type: haystack_pydoc_tools.renderers.DocusaurusRenderer