xyberos-gitlab 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.
@@ -0,0 +1,56 @@
1
+ Metadata-Version: 2.4
2
+ Name: xyberos-gitlab
3
+ Version: 0.1.0
4
+ Summary: GitLab REST plugin (RFC-0019, M7): project tools for Xyberos agents
5
+ License: Apache-2.0
6
+ Keywords: xyberos,plugin,gitlab,tool
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: xyberos>=1.0
10
+
11
+ # xyberos-gitlab
12
+
13
+ **GitLab REST plugin — RFC-0019, M7 (community wave).** Project tools for
14
+ Xyberos agents via the GitLab REST API v4. Stdlib-only with an injectable
15
+ transport for tests.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pip install -e ./gitlab
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ```python
26
+ from xyberos import create_app
27
+ from xyberos_gitlab import GitlabPlugin
28
+
29
+ app = create_app()
30
+ app.load_plugin(GitlabPlugin()) # token from GITLAB_TOKEN
31
+
32
+ app.tools.execute("gitlab_get_project", None, project="group/repo")
33
+ app.tools.execute("gitlab_list_projects", None, search="xyberos", per_page=20)
34
+ ```
35
+
36
+ ## Tools
37
+
38
+ | Tool | Notes |
39
+ | ---- | ----- |
40
+ | `gitlab_get_project(project)` | project by id or URL-encoded path |
41
+ | `gitlab_list_projects(search="", per_page=20)` | search projects |
42
+
43
+ Requires `GITLAB_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-gitlab
2
+
3
+ **GitLab REST plugin — RFC-0019, M7 (community wave).** Project tools for
4
+ Xyberos agents via the GitLab REST API v4. Stdlib-only with an injectable
5
+ transport for tests.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install -e ./gitlab
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```python
16
+ from xyberos import create_app
17
+ from xyberos_gitlab import GitlabPlugin
18
+
19
+ app = create_app()
20
+ app.load_plugin(GitlabPlugin()) # token from GITLAB_TOKEN
21
+
22
+ app.tools.execute("gitlab_get_project", None, project="group/repo")
23
+ app.tools.execute("gitlab_list_projects", None, search="xyberos", per_page=20)
24
+ ```
25
+
26
+ ## Tools
27
+
28
+ | Tool | Notes |
29
+ | ---- | ----- |
30
+ | `gitlab_get_project(project)` | project by id or URL-encoded path |
31
+ | `gitlab_list_projects(search="", per_page=20)` | search projects |
32
+
33
+ Requires `GITLAB_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-gitlab"
7
+ version = "0.1.0"
8
+ description = "GitLab REST plugin (RFC-0019, M7): project 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", "gitlab", "tool"]
14
+
15
+ [project.entry-points."xyberos.plugins"]
16
+ gitlab = "xyberos_gitlab.plugin:plugin"
17
+
18
+ [tool.setuptools]
19
+ packages = ["xyberos_gitlab"]
20
+
21
+ [tool.pytest.ini_options]
22
+ testpaths = ["tests"]
23
+ pythonpath = ["."]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,41 @@
1
+ """Tests for the GitLab 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_gitlab import GitlabClient
9
+
10
+
11
+ def _fake_request():
12
+ def request(method, url, **kwargs):
13
+ if url.endswith("/projects/group%2Frepo"):
14
+ return 200, {"id": 1, "name": "repo", "path_with_namespace": "group/repo", "web_url": "https://gitlab.com/group/repo"}
15
+ if url.endswith("/projects"):
16
+ return 200, [{"name": "repo", "path_with_namespace": "group/repo", "web_url": "https://gitlab.com/group/repo"}]
17
+ return 404, {"message": "not found"}
18
+
19
+ return request
20
+
21
+
22
+ def test_get_project_encodes_path():
23
+ request = _fake_request()
24
+ client = GitlabClient(token="t", request=request)
25
+ result = client.get_project("group/repo")
26
+ assert result["path_with_namespace"] == "group/repo"
27
+ assert result["web_url"] == "https://gitlab.com/group/repo"
28
+
29
+
30
+ def test_list_projects():
31
+ request = _fake_request()
32
+ client = GitlabClient(token="t", request=request)
33
+ projects = client.list_projects("repo", per_page=5)
34
+ assert projects[0]["name"] == "repo"
35
+
36
+
37
+ def test_requires_token():
38
+ request = _fake_request()
39
+ client = GitlabClient(token=None, request=request)
40
+ with pytest.raises(ProviderError, match="token"):
41
+ client.get_project("g/r")
@@ -0,0 +1,28 @@
1
+ """Tests for loading the GitLab plugin into a Xyberos app."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from xyberos import create_app
6
+
7
+ from xyberos_gitlab import GitlabPlugin
8
+
9
+
10
+ def _fake_request():
11
+ def request(method, url, **kwargs):
12
+ if url.endswith("/projects"):
13
+ return 200, [{"name": "repo", "path_with_namespace": "group/repo", "web_url": "https://gitlab.com/group/repo"}]
14
+ return 404, {"message": "not found"}
15
+
16
+ return request
17
+
18
+
19
+ def test_plugin_registers_and_executes():
20
+ app = create_app()
21
+ app.load_plugin(GitlabPlugin(token="t", request=_fake_request()))
22
+ assert "gitlab_get_project" in app.tools.names
23
+ assert "gitlab_list_projects" in app.tools.names
24
+
25
+ projects = app.tools.execute("gitlab_list_projects", None, search="repo")
26
+ assert projects[0]["name"] == "repo"
27
+
28
+ app.unload_plugin("gitlab")
@@ -0,0 +1,6 @@
1
+ """GitLab REST plugin (RFC-0019, M7)."""
2
+
3
+ from .client import GitlabClient
4
+ from .plugin import GitlabPlugin
5
+
6
+ __all__ = ["GitlabClient", "GitlabPlugin"]
@@ -0,0 +1,70 @@
1
+ """A minimal GitLab REST client (stdlib ``urllib``, injectable transport)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import urllib.parse
7
+ from typing import Any
8
+
9
+ from xyberos.exceptions.provider import ProviderError
10
+
11
+ from .http import RequestTransport, default_request
12
+
13
+
14
+ def _raise_for_status(status: int, body: Any) -> None:
15
+ if 200 <= status < 300:
16
+ return
17
+ message = body if isinstance(body, str) else str(body)
18
+ raise ProviderError(f"GitLab API returned HTTP {status}: {message[:200]}")
19
+
20
+
21
+ class GitlabClient:
22
+ """Thin client for the GitLab REST API v4."""
23
+
24
+ def __init__(
25
+ self,
26
+ token: str | None = None,
27
+ *,
28
+ base_url: str = "https://gitlab.com/api/v4",
29
+ request: RequestTransport | None = None,
30
+ timeout: float = 30.0,
31
+ ) -> None:
32
+ self._token = token if token is not None else os.getenv("GITLAB_TOKEN")
33
+ self._base_url = base_url.rstrip("/")
34
+ self._request = request or default_request
35
+ self._timeout = timeout
36
+
37
+ def get_project(self, project: str) -> dict[str, Any]:
38
+ encoded = urllib.parse.quote(project, safe="")
39
+ status, body = self._request(
40
+ "GET",
41
+ f"{self._base_url}/projects/{encoded}",
42
+ headers=self._headers(),
43
+ timeout=self._timeout,
44
+ )
45
+ _raise_for_status(status, body)
46
+ return {
47
+ "id": body.get("id"),
48
+ "name": body.get("name"),
49
+ "path_with_namespace": body.get("path_with_namespace"),
50
+ "web_url": body.get("web_url"),
51
+ }
52
+
53
+ def list_projects(self, search: str = "", *, per_page: int = 20) -> list[dict[str, Any]]:
54
+ status, body = self._request(
55
+ "GET",
56
+ f"{self._base_url}/projects",
57
+ query={"search": search, "per_page": per_page},
58
+ headers=self._headers(),
59
+ timeout=self._timeout,
60
+ )
61
+ _raise_for_status(status, body)
62
+ return [
63
+ {"name": project.get("name"), "path_with_namespace": project.get("path_with_namespace"), "web_url": project.get("web_url")}
64
+ for project in body
65
+ ]
66
+
67
+ def _headers(self) -> dict[str, str]:
68
+ if not self._token:
69
+ raise ProviderError("GitLab requires a token (set GITLAB_TOKEN)")
70
+ return {"PRIVATE-TOKEN": self._token}
@@ -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,60 @@
1
+ """GitLab 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 GitlabClient
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 GitlabPlugin(Plugin):
25
+ """Registers GitLab tools (projects)."""
26
+
27
+ def __init__(self, token: str | None = None, *, request: RequestTransport | None = None) -> None:
28
+ self._client = GitlabClient(token, request=request)
29
+
30
+ @property
31
+ def name(self) -> str:
32
+ return "gitlab"
33
+
34
+ def tools(self) -> list[Tool]:
35
+ client = self._client
36
+
37
+ def _get_project(project: str) -> dict[str, Any]:
38
+ return client.get_project(project)
39
+
40
+ def _list_projects(search: str = "", per_page: int = 20) -> list[dict[str, Any]]:
41
+ return client.list_projects(search, per_page=per_page)
42
+
43
+ return [
44
+ FunctionTool("gitlab_get_project", _get_project, description="Get a GitLab project by path or id."),
45
+ FunctionTool("gitlab_list_projects", _list_projects, description="Search GitLab projects."),
46
+ ]
47
+
48
+ def register(self, kernel: object) -> None:
49
+ registry = kernel.resolve("tools")
50
+ for tool in self.tools():
51
+ registry.register(tool)
52
+
53
+ def unregister(self, kernel: object) -> None:
54
+ registry = kernel.resolve("tools")
55
+ for tool in self.tools():
56
+ _pop_tool(registry, tool.name)
57
+
58
+
59
+ #: Auto-discovered by ``app.load_entry_points()``.
60
+ plugin = GitlabPlugin()
@@ -0,0 +1,56 @@
1
+ Metadata-Version: 2.4
2
+ Name: xyberos-gitlab
3
+ Version: 0.1.0
4
+ Summary: GitLab REST plugin (RFC-0019, M7): project tools for Xyberos agents
5
+ License: Apache-2.0
6
+ Keywords: xyberos,plugin,gitlab,tool
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: xyberos>=1.0
10
+
11
+ # xyberos-gitlab
12
+
13
+ **GitLab REST plugin — RFC-0019, M7 (community wave).** Project tools for
14
+ Xyberos agents via the GitLab REST API v4. Stdlib-only with an injectable
15
+ transport for tests.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pip install -e ./gitlab
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ```python
26
+ from xyberos import create_app
27
+ from xyberos_gitlab import GitlabPlugin
28
+
29
+ app = create_app()
30
+ app.load_plugin(GitlabPlugin()) # token from GITLAB_TOKEN
31
+
32
+ app.tools.execute("gitlab_get_project", None, project="group/repo")
33
+ app.tools.execute("gitlab_list_projects", None, search="xyberos", per_page=20)
34
+ ```
35
+
36
+ ## Tools
37
+
38
+ | Tool | Notes |
39
+ | ---- | ----- |
40
+ | `gitlab_get_project(project)` | project by id or URL-encoded path |
41
+ | `gitlab_list_projects(search="", per_page=20)` | search projects |
42
+
43
+ Requires `GITLAB_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_gitlab/__init__.py
6
+ xyberos_gitlab/client.py
7
+ xyberos_gitlab/http.py
8
+ xyberos_gitlab/plugin.py
9
+ xyberos_gitlab.egg-info/PKG-INFO
10
+ xyberos_gitlab.egg-info/SOURCES.txt
11
+ xyberos_gitlab.egg-info/dependency_links.txt
12
+ xyberos_gitlab.egg-info/entry_points.txt
13
+ xyberos_gitlab.egg-info/requires.txt
14
+ xyberos_gitlab.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [xyberos.plugins]
2
+ gitlab = xyberos_gitlab.plugin:plugin
@@ -0,0 +1 @@
1
+ xyberos>=1.0
@@ -0,0 +1 @@
1
+ xyberos_gitlab