xyberos-github 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ """GitHub REST plugin (RFC-0019, M7)."""
2
+
3
+ from .client import GithubClient
4
+ from .plugin import GithubPlugin
5
+
6
+ __all__ = ["GithubClient", "GithubPlugin"]
@@ -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
+ )
xyberos_github/http.py ADDED
@@ -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,9 @@
1
+ xyberos_github/__init__.py,sha256=iHtpE4G_2B1ULsVkZ6CUjbL5KKRbwvrchUtr2u7iRZQ,152
2
+ xyberos_github/client.py,sha256=6TCcl87PFVX--sCYdP74l_0THZ3knJ9AMIhUuSABzho,3338
3
+ xyberos_github/http.py,sha256=6w9_RJnzXazWN6e3VB5IfBDNvTSqPvYefHfmLOrpFGg,1888
4
+ xyberos_github/plugin.py,sha256=RHOcvpyVWEN5U09jFvvBm6hIAh0FS4HTot5D4tjCQwc,2149
5
+ xyberos_github-0.1.0.dist-info/METADATA,sha256=FV1bbLkeTW4TRoYZDn4ji-GggETN6J7IimcmR-a7oKI,1452
6
+ xyberos_github-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
7
+ xyberos_github-0.1.0.dist-info/entry_points.txt,sha256=bnHRPCdFyB6oKTWirFbRbtck68Yj-W52zVGynGpLQxI,56
8
+ xyberos_github-0.1.0.dist-info/top_level.txt,sha256=WBDAlKP-AyOAxurOU7rpVZfDPV7x_Igdzu17UuKS5bE,15
9
+ xyberos_github-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [xyberos.plugins]
2
+ github = xyberos_github.plugin:plugin
@@ -0,0 +1 @@
1
+ xyberos_github