strands-coder 1.4.0__tar.gz → 1.4.1__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.
- {strands_coder-1.4.0 → strands_coder-1.4.1}/PKG-INFO +3 -1
- {strands_coder-1.4.0 → strands_coder-1.4.1}/README.md +2 -0
- {strands_coder-1.4.0 → strands_coder-1.4.1}/strands_coder/context.py +19 -13
- strands_coder-1.4.1/tests/test_context_graphql_null.py +133 -0
- {strands_coder-1.4.0 → strands_coder-1.4.1}/.github/workflows/agent.yml +0 -0
- {strands_coder-1.4.0 → strands_coder-1.4.1}/.github/workflows/auto-release.yml +0 -0
- {strands_coder-1.4.0 → strands_coder-1.4.1}/.github/workflows/control.yml +0 -0
- {strands_coder-1.4.0 → strands_coder-1.4.1}/.gitignore +0 -0
- {strands_coder-1.4.0 → strands_coder-1.4.1}/LICENSE +0 -0
- {strands_coder-1.4.0 → strands_coder-1.4.1}/SYSTEM_PROMPT.md +0 -0
- {strands_coder-1.4.0 → strands_coder-1.4.1}/action.yml +0 -0
- {strands_coder-1.4.0 → strands_coder-1.4.1}/docs/CNAME +0 -0
- {strands_coder-1.4.0 → strands_coder-1.4.1}/docs/index.html +0 -0
- {strands_coder-1.4.0 → strands_coder-1.4.1}/pyproject.toml +0 -0
- {strands_coder-1.4.0 → strands_coder-1.4.1}/setup-aws-oidc.sh +0 -0
- {strands_coder-1.4.0 → strands_coder-1.4.1}/strands_coder/__init__.py +0 -0
- {strands_coder-1.4.0 → strands_coder-1.4.1}/strands_coder/agent_runner.py +0 -0
- {strands_coder-1.4.0 → strands_coder-1.4.1}/strands_coder/tools/__init__.py +0 -0
- {strands_coder-1.4.0 → strands_coder-1.4.1}/strands_coder/tools/create_subagent.py +0 -0
- {strands_coder-1.4.0 → strands_coder-1.4.1}/strands_coder/tools/github_tools.py +0 -0
- {strands_coder-1.4.0 → strands_coder-1.4.1}/strands_coder/tools/projects.py +0 -0
- {strands_coder-1.4.0 → strands_coder-1.4.1}/strands_coder/tools/scheduler.py +0 -0
- {strands_coder-1.4.0 → strands_coder-1.4.1}/strands_coder/tools/store_in_kb.py +0 -0
- {strands_coder-1.4.0 → strands_coder-1.4.1}/strands_coder/tools/system_prompt.py +0 -0
- {strands_coder-1.4.0 → strands_coder-1.4.1}/strands_coder/tools/use_github.py +0 -0
- {strands_coder-1.4.0 → strands_coder-1.4.1}/tools/use_langfuse.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: strands-coder
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.1
|
|
4
4
|
Summary: Add an AI assistant to your GitHub repositories that can review code, manage issues, and improve over time.
|
|
5
5
|
Project-URL: Homepage, https://github.com/cagataycali/strands-coder
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/cagataycali/strands-coder/issues
|
|
@@ -261,6 +261,8 @@ from strands import Agent
|
|
|
261
261
|
from strands_coder import use_github, projects, scheduler, store_in_kb
|
|
262
262
|
|
|
263
263
|
# Create agent with GitHub tools
|
|
264
|
+
[](https://github.com/cagataycali/awesome-strands-agents)
|
|
265
|
+
|
|
264
266
|
agent = Agent(
|
|
265
267
|
tools=[use_github, projects, scheduler, store_in_kb],
|
|
266
268
|
system_prompt="You are an autonomous GitHub agent."
|
|
@@ -217,6 +217,8 @@ from strands import Agent
|
|
|
217
217
|
from strands_coder import use_github, projects, scheduler, store_in_kb
|
|
218
218
|
|
|
219
219
|
# Create agent with GitHub tools
|
|
220
|
+
[](https://github.com/cagataycali/awesome-strands-agents)
|
|
221
|
+
|
|
220
222
|
agent = Agent(
|
|
221
223
|
tools=[use_github, projects, scheduler, store_in_kb],
|
|
222
224
|
system_prompt="You are an autonomous GitHub agent."
|
|
@@ -218,18 +218,24 @@ def fetch_github_event_context() -> str:
|
|
|
218
218
|
if "errors" in data:
|
|
219
219
|
print(f"⚠ GraphQL errors: {data['errors']}")
|
|
220
220
|
else:
|
|
221
|
-
|
|
221
|
+
# GraphQL returns null (not {}) for absent nodes - e.g. when the
|
|
222
|
+
# number refers to a PR (issue: null) or the repo/data is null on a
|
|
223
|
+
# partial error. Chained .get(k, {}) does NOT guard against a present
|
|
224
|
+
# key with a null value, so coalesce every nullable node with `or {}`.
|
|
225
|
+
issue_data = (
|
|
226
|
+
((data.get("data") or {}).get("repository") or {}).get("issue") or {}
|
|
227
|
+
)
|
|
222
228
|
|
|
223
229
|
# Comments
|
|
224
|
-
comments = issue_data.get("comments"
|
|
225
|
-
total_comments = issue_data.get("comments"
|
|
230
|
+
comments = (issue_data.get("comments") or {}).get("nodes") or []
|
|
231
|
+
total_comments = (issue_data.get("comments") or {}).get("totalCount", 0)
|
|
226
232
|
|
|
227
233
|
if comments:
|
|
228
234
|
context_parts.append(
|
|
229
235
|
f"\n### 💬 Comments ({total_comments} total)\n"
|
|
230
236
|
)
|
|
231
237
|
for idx, comment in enumerate(comments, 1):
|
|
232
|
-
author = comment.get("author"
|
|
238
|
+
author = (comment.get("author") or {}).get("login", "unknown")
|
|
233
239
|
body = comment.get("body", "")
|
|
234
240
|
created = comment.get("createdAt", "")
|
|
235
241
|
context_parts.append(f"""
|
|
@@ -240,10 +246,10 @@ def fetch_github_event_context() -> str:
|
|
|
240
246
|
""")
|
|
241
247
|
|
|
242
248
|
# Linked PRs/Issues
|
|
243
|
-
timeline = issue_data.get("timelineItems"
|
|
249
|
+
timeline = (issue_data.get("timelineItems") or {}).get("nodes") or []
|
|
244
250
|
linked_items = []
|
|
245
251
|
for item in timeline:
|
|
246
|
-
source = item.get("source"
|
|
252
|
+
source = item.get("source") or {}
|
|
247
253
|
if source:
|
|
248
254
|
num = source.get("number")
|
|
249
255
|
title = source.get("title")
|
|
@@ -367,11 +373,11 @@ def fetch_github_event_context() -> str:
|
|
|
367
373
|
print(f"⚠ GraphQL errors: {data['errors']}")
|
|
368
374
|
else:
|
|
369
375
|
pr_data = (
|
|
370
|
-
data.get("data"
|
|
376
|
+
((data.get("data") or {}).get("repository") or {}).get("pullRequest") or {}
|
|
371
377
|
)
|
|
372
378
|
|
|
373
379
|
# Reviews
|
|
374
|
-
reviews = pr_data.get("reviews"
|
|
380
|
+
reviews = (pr_data.get("reviews") or {}).get("nodes") or []
|
|
375
381
|
total_reviews = pr_data.get("reviews", {}).get("totalCount", 0)
|
|
376
382
|
|
|
377
383
|
if reviews:
|
|
@@ -397,7 +403,7 @@ def fetch_github_event_context() -> str:
|
|
|
397
403
|
f"\n### 💬 Comments ({total_comments} total)\n"
|
|
398
404
|
)
|
|
399
405
|
for idx, comment in enumerate(comments, 1):
|
|
400
|
-
author = comment.get("author"
|
|
406
|
+
author = (comment.get("author") or {}).get("login", "unknown")
|
|
401
407
|
body = comment.get("body", "")
|
|
402
408
|
created = comment.get("createdAt", "")
|
|
403
409
|
context_parts.append(f"""
|
|
@@ -526,15 +532,15 @@ def fetch_github_event_context() -> str:
|
|
|
526
532
|
print(f"⚠ GraphQL errors: {data['errors']}")
|
|
527
533
|
else:
|
|
528
534
|
disc_data = (
|
|
529
|
-
data.get("data"
|
|
535
|
+
((data.get("data") or {}).get("repository") or {}).get("discussion") or {}
|
|
530
536
|
)
|
|
531
|
-
comments = disc_data.get("comments"
|
|
532
|
-
total_comments = disc_data.get("comments"
|
|
537
|
+
comments = (disc_data.get("comments") or {}).get("nodes") or []
|
|
538
|
+
total_comments = (disc_data.get("comments") or {}).get("totalCount", 0)
|
|
533
539
|
|
|
534
540
|
if comments:
|
|
535
541
|
context_parts.append(f"\n### 💬 Replies ({total_comments} total)\n")
|
|
536
542
|
for idx, comment in enumerate(comments, 1):
|
|
537
|
-
author = comment.get("author"
|
|
543
|
+
author = (comment.get("author") or {}).get("login", "unknown")
|
|
538
544
|
body = comment.get("body", "")
|
|
539
545
|
created = comment.get("createdAt", "")
|
|
540
546
|
context_parts.append(f"""
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Regression tests for fetch_github_event_context GraphQL-null handling.
|
|
2
|
+
|
|
3
|
+
GitHub's GraphQL API returns ``null`` (not ``{}``) for an absent node - e.g.
|
|
4
|
+
``repository.issue`` is ``null`` when the queried number is a PR, or when a
|
|
5
|
+
partial error nulls a field. ``dict.get(key, {})`` does NOT guard against a key
|
|
6
|
+
that is present with a ``None`` value, so the old code crashed with::
|
|
7
|
+
|
|
8
|
+
AttributeError: 'NoneType' object has no attribute 'get'
|
|
9
|
+
|
|
10
|
+
at ``issue_data.get("comments", {})``. These tests pin the fix: a null node must
|
|
11
|
+
degrade gracefully (return context built so far), never raise.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
from unittest.mock import patch
|
|
18
|
+
|
|
19
|
+
import importlib.util as _ilu
|
|
20
|
+
import os as _os
|
|
21
|
+
|
|
22
|
+
_CTX_PATH = _os.path.join(
|
|
23
|
+
_os.path.dirname(_os.path.dirname(_os.path.abspath(__file__))),
|
|
24
|
+
"strands_coder",
|
|
25
|
+
"context.py",
|
|
26
|
+
)
|
|
27
|
+
_spec = _ilu.spec_from_file_location("strands_coder_context", _CTX_PATH)
|
|
28
|
+
_mod = _ilu.module_from_spec(_spec)
|
|
29
|
+
_spec.loader.exec_module(_mod)
|
|
30
|
+
fetch_github_event_context = _mod.fetch_github_event_context
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _FakeResponse:
|
|
34
|
+
def __init__(self, payload: dict):
|
|
35
|
+
self._payload = payload
|
|
36
|
+
|
|
37
|
+
def raise_for_status(self) -> None:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
def json(self) -> dict:
|
|
41
|
+
return self._payload
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _ctx(event_name: str, event: dict) -> str:
|
|
45
|
+
return json.dumps(
|
|
46
|
+
{
|
|
47
|
+
"event_name": event_name,
|
|
48
|
+
"event": event,
|
|
49
|
+
"repository": "owner/repo",
|
|
50
|
+
"actor": "tester",
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_issue_node_null_does_not_raise(monkeypatch):
|
|
56
|
+
"""repository.issue == null (number is actually a PR) must not crash."""
|
|
57
|
+
monkeypatch.setenv("GITHUB_CONTEXT", _ctx("issues", {"issue": {"number": 7}, "action": "opened"}))
|
|
58
|
+
monkeypatch.setenv("GITHUB_TOKEN", "x")
|
|
59
|
+
monkeypatch.delenv("PAT_TOKEN", raising=False)
|
|
60
|
+
|
|
61
|
+
with patch("requests.post", return_value=_FakeResponse({"data": {"repository": {"issue": None}}})):
|
|
62
|
+
out = fetch_github_event_context()
|
|
63
|
+
|
|
64
|
+
assert isinstance(out, str)
|
|
65
|
+
assert "owner/repo" in out # raw context header still rendered
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_repository_null_does_not_raise(monkeypatch):
|
|
69
|
+
"""data.repository == null (partial error) must not crash."""
|
|
70
|
+
monkeypatch.setenv("GITHUB_CONTEXT", _ctx("issues", {"issue": {"number": 7}, "action": "opened"}))
|
|
71
|
+
monkeypatch.setenv("GITHUB_TOKEN", "x")
|
|
72
|
+
monkeypatch.delenv("PAT_TOKEN", raising=False)
|
|
73
|
+
|
|
74
|
+
with patch("requests.post", return_value=_FakeResponse({"data": {"repository": None}})):
|
|
75
|
+
out = fetch_github_event_context()
|
|
76
|
+
|
|
77
|
+
assert isinstance(out, str)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_pull_request_node_null_does_not_raise(monkeypatch):
|
|
81
|
+
"""repository.pullRequest == null must not crash."""
|
|
82
|
+
monkeypatch.setenv(
|
|
83
|
+
"GITHUB_CONTEXT",
|
|
84
|
+
_ctx("pull_request", {"pull_request": {"number": 9}, "action": "opened"}),
|
|
85
|
+
)
|
|
86
|
+
monkeypatch.setenv("GITHUB_TOKEN", "x")
|
|
87
|
+
monkeypatch.delenv("PAT_TOKEN", raising=False)
|
|
88
|
+
|
|
89
|
+
with patch("requests.post", return_value=_FakeResponse({"data": {"repository": {"pullRequest": None}}})):
|
|
90
|
+
out = fetch_github_event_context()
|
|
91
|
+
|
|
92
|
+
assert isinstance(out, str)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_discussion_node_null_does_not_raise(monkeypatch):
|
|
96
|
+
"""repository.discussion == null must not crash."""
|
|
97
|
+
monkeypatch.setenv(
|
|
98
|
+
"GITHUB_CONTEXT",
|
|
99
|
+
_ctx("discussion", {"discussion": {"number": 3}, "action": "created"}),
|
|
100
|
+
)
|
|
101
|
+
monkeypatch.setenv("GITHUB_TOKEN", "x")
|
|
102
|
+
monkeypatch.delenv("PAT_TOKEN", raising=False)
|
|
103
|
+
|
|
104
|
+
with patch("requests.post", return_value=_FakeResponse({"data": {"repository": {"discussion": None}}})):
|
|
105
|
+
out = fetch_github_event_context()
|
|
106
|
+
|
|
107
|
+
assert isinstance(out, str)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_null_comment_author_does_not_raise(monkeypatch):
|
|
111
|
+
"""A comment whose author is null (deleted user) must not crash."""
|
|
112
|
+
monkeypatch.setenv("GITHUB_CONTEXT", _ctx("issues", {"issue": {"number": 7}, "action": "opened"}))
|
|
113
|
+
monkeypatch.setenv("GITHUB_TOKEN", "x")
|
|
114
|
+
monkeypatch.delenv("PAT_TOKEN", raising=False)
|
|
115
|
+
|
|
116
|
+
payload = {
|
|
117
|
+
"data": {
|
|
118
|
+
"repository": {
|
|
119
|
+
"issue": {
|
|
120
|
+
"comments": {
|
|
121
|
+
"totalCount": 1,
|
|
122
|
+
"nodes": [{"author": None, "body": "hi", "createdAt": "2026-01-01"}],
|
|
123
|
+
},
|
|
124
|
+
"timelineItems": {"nodes": [{"source": None}]},
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
with patch("requests.post", return_value=_FakeResponse(payload)):
|
|
130
|
+
out = fetch_github_event_context()
|
|
131
|
+
|
|
132
|
+
assert isinstance(out, str)
|
|
133
|
+
assert "@unknown" in out
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|