xyberos-github 0.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.
- xyberos_github-0.1.0/PKG-INFO +56 -0
- xyberos_github-0.1.0/README.md +46 -0
- xyberos_github-0.1.0/pyproject.toml +23 -0
- xyberos_github-0.1.0/setup.cfg +4 -0
- xyberos_github-0.1.0/tests/test_client.py +73 -0
- xyberos_github-0.1.0/tests/test_plugin.py +36 -0
- xyberos_github-0.1.0/xyberos_github/__init__.py +6 -0
- xyberos_github-0.1.0/xyberos_github/client.py +95 -0
- xyberos_github-0.1.0/xyberos_github/http.py +57 -0
- xyberos_github-0.1.0/xyberos_github/plugin.py +64 -0
- xyberos_github-0.1.0/xyberos_github.egg-info/PKG-INFO +56 -0
- xyberos_github-0.1.0/xyberos_github.egg-info/SOURCES.txt +14 -0
- xyberos_github-0.1.0/xyberos_github.egg-info/dependency_links.txt +1 -0
- xyberos_github-0.1.0/xyberos_github.egg-info/entry_points.txt +2 -0
- xyberos_github-0.1.0/xyberos_github.egg-info/requires.txt +1 -0
- xyberos_github-0.1.0/xyberos_github.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xyberos-github
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: GitHub REST plugin (RFC-0019, M7): user/repo/issue tools for Xyberos agents
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Keywords: xyberos,plugin,github,tool
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: xyberos>=1.0
|
|
10
|
+
|
|
11
|
+
# xyberos-github
|
|
12
|
+
|
|
13
|
+
**GitHub REST plugin — RFC-0019, M7 (community wave).** User / repository /
|
|
14
|
+
issue tools for Xyberos agents, via the GitHub REST API v3. Stdlib-only
|
|
15
|
+
(`urllib`) with an injectable transport for tests.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install -e ./github
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from xyberos import create_app
|
|
27
|
+
from xyberos_github import GithubPlugin
|
|
28
|
+
|
|
29
|
+
app = create_app()
|
|
30
|
+
app.load_plugin(GithubPlugin()) # token from GITHUB_TOKEN (optional for public reads)
|
|
31
|
+
|
|
32
|
+
app.tools.execute("github_get_user", None, username="octocat")
|
|
33
|
+
app.tools.execute("github_list_repos", None, username="octocat", per_page=30)
|
|
34
|
+
app.tools.execute("github_create_issue", None, owner="o", repo="r", title="Bug", body="...")
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Tools
|
|
38
|
+
|
|
39
|
+
| Tool | Notes |
|
|
40
|
+
| ---- | ----- |
|
|
41
|
+
| `github_get_user(username)` | public profile |
|
|
42
|
+
| `github_list_repos(username, per_page=30)` | public repositories |
|
|
43
|
+
| `github_create_issue(owner, repo, title, body="")` | requires `GITHUB_TOKEN` |
|
|
44
|
+
|
|
45
|
+
## Tests
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install pytest
|
|
49
|
+
pytest tests/
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Canned responses via an injectable transport — no network.
|
|
53
|
+
|
|
54
|
+
## Ship location
|
|
55
|
+
|
|
56
|
+
Plugin (`xyberos.plugins` entry point) — community wave (M7).
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# xyberos-github
|
|
2
|
+
|
|
3
|
+
**GitHub REST plugin — RFC-0019, M7 (community wave).** User / repository /
|
|
4
|
+
issue tools for Xyberos agents, via the GitHub REST API v3. Stdlib-only
|
|
5
|
+
(`urllib`) with an injectable transport for tests.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install -e ./github
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from xyberos import create_app
|
|
17
|
+
from xyberos_github import GithubPlugin
|
|
18
|
+
|
|
19
|
+
app = create_app()
|
|
20
|
+
app.load_plugin(GithubPlugin()) # token from GITHUB_TOKEN (optional for public reads)
|
|
21
|
+
|
|
22
|
+
app.tools.execute("github_get_user", None, username="octocat")
|
|
23
|
+
app.tools.execute("github_list_repos", None, username="octocat", per_page=30)
|
|
24
|
+
app.tools.execute("github_create_issue", None, owner="o", repo="r", title="Bug", body="...")
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Tools
|
|
28
|
+
|
|
29
|
+
| Tool | Notes |
|
|
30
|
+
| ---- | ----- |
|
|
31
|
+
| `github_get_user(username)` | public profile |
|
|
32
|
+
| `github_list_repos(username, per_page=30)` | public repositories |
|
|
33
|
+
| `github_create_issue(owner, repo, title, body="")` | requires `GITHUB_TOKEN` |
|
|
34
|
+
|
|
35
|
+
## Tests
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install pytest
|
|
39
|
+
pytest tests/
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Canned responses via an injectable transport — no network.
|
|
43
|
+
|
|
44
|
+
## Ship location
|
|
45
|
+
|
|
46
|
+
Plugin (`xyberos.plugins` entry point) — community wave (M7).
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "xyberos-github"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "GitHub REST plugin (RFC-0019, M7): user/repo/issue tools for Xyberos agents"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = {text = "Apache-2.0"}
|
|
12
|
+
dependencies = ["xyberos>=1.0"]
|
|
13
|
+
keywords = ["xyberos", "plugin", "github", "tool"]
|
|
14
|
+
|
|
15
|
+
[project.entry-points."xyberos.plugins"]
|
|
16
|
+
github = "xyberos_github.plugin:plugin"
|
|
17
|
+
|
|
18
|
+
[tool.setuptools]
|
|
19
|
+
packages = ["xyberos_github"]
|
|
20
|
+
|
|
21
|
+
[tool.pytest.ini_options]
|
|
22
|
+
testpaths = ["tests"]
|
|
23
|
+
pythonpath = ["."]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Tests for the GitHub REST client (injectable transport, no network)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from xyberos.exceptions.provider import ProviderError
|
|
7
|
+
|
|
8
|
+
from xyberos_github import GithubClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _fake_request():
|
|
12
|
+
calls: list[tuple[str, str, dict]] = []
|
|
13
|
+
|
|
14
|
+
def request(method, url, **kwargs):
|
|
15
|
+
calls.append((method, url, kwargs))
|
|
16
|
+
if url.endswith("/users/octocat"):
|
|
17
|
+
return 200, {
|
|
18
|
+
"login": "octocat", "name": "The Octocat",
|
|
19
|
+
"public_repos": 8, "html_url": "https://github.com/octocat",
|
|
20
|
+
}
|
|
21
|
+
if url.endswith("/users/octocat/repos"):
|
|
22
|
+
return 200, [
|
|
23
|
+
{"full_name": "octocat/Hello-World", "html_url": "https://github.com/octocat/Hello-World", "language": "Ruby"}
|
|
24
|
+
]
|
|
25
|
+
if url.endswith("/repos/octocat/Hello-World/issues"):
|
|
26
|
+
return 201, {"number": 1, "html_url": "https://github.com/octocat/Hello-World/issues/1", "state": "open"}
|
|
27
|
+
return 404, {"message": "not found"}
|
|
28
|
+
|
|
29
|
+
return request, calls
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_get_user():
|
|
33
|
+
request, calls = _fake_request()
|
|
34
|
+
client = GithubClient(token="t", request=request)
|
|
35
|
+
result = client.get_user("octocat")
|
|
36
|
+
assert result == {
|
|
37
|
+
"login": "octocat", "name": "The Octocat",
|
|
38
|
+
"public_repos": 8, "html_url": "https://github.com/octocat",
|
|
39
|
+
}
|
|
40
|
+
assert calls[0][0] == "GET"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_list_repos_sends_query():
|
|
44
|
+
request, calls = _fake_request()
|
|
45
|
+
client = GithubClient(token="t", request=request)
|
|
46
|
+
repos = client.list_repos("octocat", per_page=5)
|
|
47
|
+
assert repos[0]["full_name"] == "octocat/Hello-World"
|
|
48
|
+
assert calls[0][2]["query"] == {"per_page": 5}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_create_issue_sends_bearer():
|
|
52
|
+
request, calls = _fake_request()
|
|
53
|
+
client = GithubClient(token="secret", request=request)
|
|
54
|
+
result = client.create_issue("octocat", "Hello-World", "Bug", "Details")
|
|
55
|
+
assert result["number"] == 1
|
|
56
|
+
assert calls[0][2]["headers"]["Authorization"] == "Bearer secret"
|
|
57
|
+
assert calls[0][2]["json_body"] == {"title": "Bug", "body": "Details"}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_create_issue_requires_token():
|
|
61
|
+
request, _ = _fake_request()
|
|
62
|
+
client = GithubClient(token=None, request=request)
|
|
63
|
+
with pytest.raises(ProviderError, match="token"):
|
|
64
|
+
client.create_issue("o", "r", "Bug")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_http_error_raises():
|
|
68
|
+
def request(method, url, **kwargs):
|
|
69
|
+
return 401, {"message": "Bad credentials"}
|
|
70
|
+
|
|
71
|
+
client = GithubClient(token="t", request=request)
|
|
72
|
+
with pytest.raises(ProviderError, match="401"):
|
|
73
|
+
client.get_user("octocat")
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Tests for loading the GitHub plugin into a Xyberos app."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from xyberos import create_app
|
|
6
|
+
|
|
7
|
+
from xyberos_github import GithubPlugin
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _fake_request():
|
|
11
|
+
def request(method, url, **kwargs):
|
|
12
|
+
if url.endswith("/users/octocat"):
|
|
13
|
+
return 200, {"login": "octocat", "public_repos": 8, "html_url": "https://github.com/octocat"}
|
|
14
|
+
return 404, {"message": "not found"}
|
|
15
|
+
|
|
16
|
+
return request
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_plugin_conforms_to_contract():
|
|
20
|
+
plugin = GithubPlugin()
|
|
21
|
+
assert plugin.name == "github"
|
|
22
|
+
assert callable(plugin.register) and callable(plugin.unregister)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_plugin_registers_and_executes():
|
|
26
|
+
app = create_app()
|
|
27
|
+
app.load_plugin(GithubPlugin(token="t", request=_fake_request()))
|
|
28
|
+
assert "github_get_user" in app.tools.names
|
|
29
|
+
assert "github_list_repos" in app.tools.names
|
|
30
|
+
assert "github_create_issue" in app.tools.names
|
|
31
|
+
|
|
32
|
+
result = app.tools.execute("github_get_user", None, username="octocat")
|
|
33
|
+
assert result["login"] == "octocat"
|
|
34
|
+
|
|
35
|
+
app.unload_plugin("github")
|
|
36
|
+
assert "github_get_user" not in app.tools.names
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""A minimal GitHub REST client (stdlib ``urllib``, injectable transport)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from xyberos.exceptions.provider import ProviderError
|
|
9
|
+
|
|
10
|
+
from .http import RequestTransport, default_request
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _raise_for_status(status: int, body: Any) -> None:
|
|
14
|
+
if 200 <= status < 300:
|
|
15
|
+
return
|
|
16
|
+
message = body if isinstance(body, str) else str(body)
|
|
17
|
+
raise ProviderError(f"GitHub API returned HTTP {status}: {message[:200]}")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class GithubClient:
|
|
21
|
+
"""Thin client for the GitHub REST API v3."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
token: str | None = None,
|
|
26
|
+
*,
|
|
27
|
+
base_url: str = "https://api.github.com",
|
|
28
|
+
request: RequestTransport | None = None,
|
|
29
|
+
timeout: float = 30.0,
|
|
30
|
+
) -> None:
|
|
31
|
+
self._token = token if token is not None else os.getenv("GITHUB_TOKEN")
|
|
32
|
+
self._base_url = base_url.rstrip("/")
|
|
33
|
+
self._request = request or default_request
|
|
34
|
+
self._timeout = timeout
|
|
35
|
+
|
|
36
|
+
# -- users --------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
def get_user(self, username: str) -> dict[str, Any]:
|
|
39
|
+
status, body = self._request(
|
|
40
|
+
"GET",
|
|
41
|
+
f"{self._base_url}/users/{username}",
|
|
42
|
+
headers=self._headers(),
|
|
43
|
+
timeout=self._timeout,
|
|
44
|
+
)
|
|
45
|
+
_raise_for_status(status, body)
|
|
46
|
+
return {
|
|
47
|
+
"login": body.get("login"),
|
|
48
|
+
"name": body.get("name"),
|
|
49
|
+
"public_repos": body.get("public_repos"),
|
|
50
|
+
"html_url": body.get("html_url"),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# -- repositories -------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
def list_repos(self, username: str, *, per_page: int = 30) -> list[dict[str, Any]]:
|
|
56
|
+
status, body = self._request(
|
|
57
|
+
"GET",
|
|
58
|
+
f"{self._base_url}/users/{username}/repos",
|
|
59
|
+
query={"per_page": per_page},
|
|
60
|
+
headers=self._headers(),
|
|
61
|
+
timeout=self._timeout,
|
|
62
|
+
)
|
|
63
|
+
_raise_for_status(status, body)
|
|
64
|
+
return [
|
|
65
|
+
{"full_name": repo.get("full_name"), "html_url": repo.get("html_url"), "language": repo.get("language")}
|
|
66
|
+
for repo in body
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
# -- issues -------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
def create_issue(self, owner: str, repo: str, title: str, body: str = "") -> dict[str, Any]:
|
|
72
|
+
self._require_auth("create_issue")
|
|
73
|
+
status, body_ = self._request(
|
|
74
|
+
"POST",
|
|
75
|
+
f"{self._base_url}/repos/{owner}/{repo}/issues",
|
|
76
|
+
json_body={"title": title, "body": body},
|
|
77
|
+
headers=self._headers(),
|
|
78
|
+
timeout=self._timeout,
|
|
79
|
+
)
|
|
80
|
+
_raise_for_status(status, body_)
|
|
81
|
+
return {"number": body_.get("number"), "html_url": body_.get("html_url"), "state": body_.get("state")}
|
|
82
|
+
|
|
83
|
+
# -- internals ----------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
def _headers(self) -> dict[str, str]:
|
|
86
|
+
headers = {"Accept": "application/vnd.github+json", "User-Agent": "xyberos-github"}
|
|
87
|
+
if self._token:
|
|
88
|
+
headers["Authorization"] = f"Bearer {self._token}"
|
|
89
|
+
return headers
|
|
90
|
+
|
|
91
|
+
def _require_auth(self, action: str) -> None:
|
|
92
|
+
if not self._token:
|
|
93
|
+
raise ProviderError(
|
|
94
|
+
f"GitHub '{action}' requires a token (set GITHUB_TOKEN)"
|
|
95
|
+
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""A tiny stdlib HTTP helper (no third-party deps).
|
|
2
|
+
|
|
3
|
+
``default_request`` performs one HTTP request with ``urllib`` and returns
|
|
4
|
+
``(status, body)`` where ``body`` is parsed JSON when the response is JSON,
|
|
5
|
+
otherwise raw text. Injectable so tests run without a network.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import urllib.error
|
|
12
|
+
import urllib.parse
|
|
13
|
+
import urllib.request
|
|
14
|
+
from typing import Any, Callable
|
|
15
|
+
|
|
16
|
+
#: (method, url, *, json_body, headers, query, timeout) -> (status, body)
|
|
17
|
+
RequestTransport = Callable[..., tuple[int, Any]]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def default_request(
|
|
21
|
+
method: str,
|
|
22
|
+
url: str,
|
|
23
|
+
*,
|
|
24
|
+
json_body: Any = None,
|
|
25
|
+
headers: dict[str, str] | None = None,
|
|
26
|
+
query: dict[str, Any] | None = None,
|
|
27
|
+
timeout: float = 30.0,
|
|
28
|
+
) -> tuple[int, Any]:
|
|
29
|
+
"""Send one request and return ``(status, parsed_json_or_text)``."""
|
|
30
|
+
final_url = url
|
|
31
|
+
if query:
|
|
32
|
+
separator = "&" if "?" in url else "?"
|
|
33
|
+
final_url = url + separator + urllib.parse.urlencode(query)
|
|
34
|
+
|
|
35
|
+
data: bytes | None = None
|
|
36
|
+
request_headers = dict(headers or {})
|
|
37
|
+
if json_body is not None:
|
|
38
|
+
data = json.dumps(json_body).encode("utf-8")
|
|
39
|
+
request_headers.setdefault("Content-Type", "application/json")
|
|
40
|
+
|
|
41
|
+
request = urllib.request.Request(
|
|
42
|
+
final_url, data=data, headers=request_headers, method=method
|
|
43
|
+
)
|
|
44
|
+
try:
|
|
45
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
46
|
+
raw = response.read()
|
|
47
|
+
content_type = response.headers.get("Content-Type", "")
|
|
48
|
+
except urllib.error.HTTPError as exc:
|
|
49
|
+
return exc.code, exc.read().decode("utf-8", errors="replace")
|
|
50
|
+
|
|
51
|
+
text = raw.decode("utf-8", errors="replace")
|
|
52
|
+
if "application/json" in content_type or text.lstrip().startswith("{"):
|
|
53
|
+
try:
|
|
54
|
+
return 200, json.loads(text)
|
|
55
|
+
except json.JSONDecodeError:
|
|
56
|
+
return 200, text
|
|
57
|
+
return 200, text
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""GitHub plugin entry point (RFC-0019, M7)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, cast
|
|
6
|
+
|
|
7
|
+
from xyberos.contracts import Plugin, Tool
|
|
8
|
+
from xyberos.tools import FunctionTool
|
|
9
|
+
|
|
10
|
+
from .client import GithubClient
|
|
11
|
+
from .http import RequestTransport
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _pop_tool(registry: Any, name: str) -> None:
|
|
15
|
+
unregister = getattr(registry, "unregister", None)
|
|
16
|
+
if callable(unregister):
|
|
17
|
+
unregister(name)
|
|
18
|
+
return
|
|
19
|
+
store = getattr(registry, "_tools", None)
|
|
20
|
+
if isinstance(store, dict):
|
|
21
|
+
cast(dict[str, Any], store).pop(name, None)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class GithubPlugin(Plugin):
|
|
25
|
+
"""Registers GitHub tools (user / repos / issues)."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, token: str | None = None, *, request: RequestTransport | None = None) -> None:
|
|
28
|
+
self._client = GithubClient(token, request=request)
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def name(self) -> str:
|
|
32
|
+
return "github"
|
|
33
|
+
|
|
34
|
+
def tools(self) -> list[Tool]:
|
|
35
|
+
client = self._client
|
|
36
|
+
|
|
37
|
+
def _get_user(username: str) -> dict[str, Any]:
|
|
38
|
+
return client.get_user(username)
|
|
39
|
+
|
|
40
|
+
def _list_repos(username: str, per_page: int = 30) -> list[dict[str, Any]]:
|
|
41
|
+
return client.list_repos(username, per_page=per_page)
|
|
42
|
+
|
|
43
|
+
def _create_issue(owner: str, repo: str, title: str, body: str = "") -> dict[str, Any]:
|
|
44
|
+
return client.create_issue(owner, repo, title, body)
|
|
45
|
+
|
|
46
|
+
return [
|
|
47
|
+
FunctionTool("github_get_user", _get_user, description="Get a GitHub user's public profile."),
|
|
48
|
+
FunctionTool("github_list_repos", _list_repos, description="List a GitHub user's public repositories."),
|
|
49
|
+
FunctionTool("github_create_issue", _create_issue, description="Create a GitHub issue on a repository."),
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
def register(self, kernel: object) -> None:
|
|
53
|
+
registry = kernel.resolve("tools")
|
|
54
|
+
for tool in self.tools():
|
|
55
|
+
registry.register(tool)
|
|
56
|
+
|
|
57
|
+
def unregister(self, kernel: object) -> None:
|
|
58
|
+
registry = kernel.resolve("tools")
|
|
59
|
+
for tool in self.tools():
|
|
60
|
+
_pop_tool(registry, tool.name)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
#: Auto-discovered by ``app.load_entry_points()``.
|
|
64
|
+
plugin = GithubPlugin()
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xyberos-github
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: GitHub REST plugin (RFC-0019, M7): user/repo/issue tools for Xyberos agents
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Keywords: xyberos,plugin,github,tool
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: xyberos>=1.0
|
|
10
|
+
|
|
11
|
+
# xyberos-github
|
|
12
|
+
|
|
13
|
+
**GitHub REST plugin — RFC-0019, M7 (community wave).** User / repository /
|
|
14
|
+
issue tools for Xyberos agents, via the GitHub REST API v3. Stdlib-only
|
|
15
|
+
(`urllib`) with an injectable transport for tests.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install -e ./github
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from xyberos import create_app
|
|
27
|
+
from xyberos_github import GithubPlugin
|
|
28
|
+
|
|
29
|
+
app = create_app()
|
|
30
|
+
app.load_plugin(GithubPlugin()) # token from GITHUB_TOKEN (optional for public reads)
|
|
31
|
+
|
|
32
|
+
app.tools.execute("github_get_user", None, username="octocat")
|
|
33
|
+
app.tools.execute("github_list_repos", None, username="octocat", per_page=30)
|
|
34
|
+
app.tools.execute("github_create_issue", None, owner="o", repo="r", title="Bug", body="...")
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Tools
|
|
38
|
+
|
|
39
|
+
| Tool | Notes |
|
|
40
|
+
| ---- | ----- |
|
|
41
|
+
| `github_get_user(username)` | public profile |
|
|
42
|
+
| `github_list_repos(username, per_page=30)` | public repositories |
|
|
43
|
+
| `github_create_issue(owner, repo, title, body="")` | requires `GITHUB_TOKEN` |
|
|
44
|
+
|
|
45
|
+
## Tests
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install pytest
|
|
49
|
+
pytest tests/
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Canned responses via an injectable transport — no network.
|
|
53
|
+
|
|
54
|
+
## Ship location
|
|
55
|
+
|
|
56
|
+
Plugin (`xyberos.plugins` entry point) — community wave (M7).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
tests/test_client.py
|
|
4
|
+
tests/test_plugin.py
|
|
5
|
+
xyberos_github/__init__.py
|
|
6
|
+
xyberos_github/client.py
|
|
7
|
+
xyberos_github/http.py
|
|
8
|
+
xyberos_github/plugin.py
|
|
9
|
+
xyberos_github.egg-info/PKG-INFO
|
|
10
|
+
xyberos_github.egg-info/SOURCES.txt
|
|
11
|
+
xyberos_github.egg-info/dependency_links.txt
|
|
12
|
+
xyberos_github.egg-info/entry_points.txt
|
|
13
|
+
xyberos_github.egg-info/requires.txt
|
|
14
|
+
xyberos_github.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
xyberos>=1.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
xyberos_github
|