metagraphed 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,7 @@
1
+ dist/
2
+ build/
3
+ *.egg-info/
4
+ __pycache__/
5
+ .venv/
6
+ .pytest_cache/
7
+ uv.lock
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.4
2
+ Name: metagraphed
3
+ Version: 0.1.0
4
+ Summary: Thin Python client for metagraphed — the operational + integration registry for Bittensor subnets (api.metagraph.sh).
5
+ Project-URL: Homepage, https://metagraph.sh
6
+ Project-URL: Repository, https://github.com/JSONbored/metagraphed
7
+ Author: JSONbored
8
+ License: MIT
9
+ Keywords: api-client,bittensor,metagraph,registry,subnet
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Typing :: Typed
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+
17
+ # metagraphed (Python)
18
+
19
+ Thin, dependency-free Python client for **metagraphed** — the operational +
20
+ integration registry for Bittensor subnets at `https://api.metagraph.sh`.
21
+
22
+ It mirrors the [npm client](https://www.npmjs.com/package/@jsonbored/metagraphed):
23
+ one generic GET helper over the uniform, read-only API surface, returning the
24
+ parsed `{ ok, schema_version, data, meta }` envelope. Stdlib only — no transitive
25
+ dependencies.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install metagraphed
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ ```python
36
+ from metagraphed import MetagraphedClient, metagraphed_fetch
37
+
38
+ client = MetagraphedClient() # base_url defaults to https://api.metagraph.sh
39
+
40
+ # List subnets (query params; None values are dropped)
41
+ subnets = client.fetch(
42
+ "/api/v1/subnets",
43
+ query={"limit": 10, "sort": "completeness_score", "order": "desc"},
44
+ )
45
+ print(subnets["data"][0]["name"])
46
+
47
+ # One subnet by netuid (path params)
48
+ detail = client.fetch("/api/v1/subnets/{netuid}", path_params={"netuid": 7})
49
+
50
+ # Which subnets are buildable? (integration readiness lives in the agent catalog)
51
+ catalog = client.fetch("/api/v1/agent-catalog")
52
+
53
+ # Point at the frontend origin instead, ad hoc:
54
+ metagraphed_fetch("/api/v1/health", base_url="https://metagraph.sh")
55
+ ```
56
+
57
+ Every response is the standard envelope:
58
+
59
+ ```python
60
+ {"ok": True, "schema_version": 1, "data": ..., "meta": {...}}
61
+ ```
62
+
63
+ On a network failure or non-2xx response, a `MetagraphedError` is raised (with
64
+ `.status` for HTTP errors).
65
+
66
+ ## Versioning & stability
67
+
68
+ Tracks the public `/api/v1` contract; changes are additive within v1. See the
69
+ backend's [API stability policy](https://github.com/JSONbored/metagraphed/blob/main/docs/api-stability.md).
70
+
71
+ ## License
72
+
73
+ MIT
@@ -0,0 +1,39 @@
1
+ # Publishing `metagraphed` to PyPI
2
+
3
+ Releases go out via **OIDC Trusted Publishing** — no API token is ever created
4
+ or stored. The `.github/workflows/publish-python.yml` workflow builds the wheel +
5
+ sdist in an unprivileged job and publishes from a separate privileged job, so no
6
+ third-party code runs while the OIDC token is live.
7
+
8
+ ## One-time bootstrap (owner, no placeholder upload needed)
9
+
10
+ 1. **Confirm the project name is free** at <https://pypi.org/project/metagraphed/>
11
+ (a 404 means available). If it's taken, change `[project].name` in
12
+ `python/pyproject.toml`, the `pip install` command in the README, and the
13
+ PyPI Project Name field below to e.g. `metagraphed-client`.
14
+
15
+ 2. **Add a PyPI pending publisher** at
16
+ <https://pypi.org/manage/account/publishing/> → "Add a new pending publisher"
17
+ with EXACTLY:
18
+ - PyPI Project Name: `metagraphed`
19
+ - Owner: `JSONbored`
20
+ - Repository name: `metagraphed`
21
+ - Workflow name: `publish-python.yml`
22
+ - Environment name: `pypi-production`
23
+
24
+ 3. **Create the GitHub Environment** in repo Settings → Environments → New
25
+ environment named EXACTLY `pypi-production` (optionally add required reviewers
26
+ for a manual approval gate before each publish).
27
+
28
+ That's it — no token, no bootstrap upload. The pending publisher converts to a
29
+ normal Trusted Publisher on the first successful publish.
30
+
31
+ ## Cutting a release
32
+
33
+ 1. Bump `[project].version` in `python/pyproject.toml` (strict semver, no `v`
34
+ prefix) on `main`.
35
+ 2. Actions → **Publish Python SDK** → Run workflow.
36
+
37
+ The release gate (`scripts/validate-python-release.sh`) refuses to run off
38
+ `main`, requires strict semver, and aborts if the git tag `python-v<version>` or
39
+ the PyPI version already exists.
@@ -0,0 +1,57 @@
1
+ # metagraphed (Python)
2
+
3
+ Thin, dependency-free Python client for **metagraphed** — the operational +
4
+ integration registry for Bittensor subnets at `https://api.metagraph.sh`.
5
+
6
+ It mirrors the [npm client](https://www.npmjs.com/package/@jsonbored/metagraphed):
7
+ one generic GET helper over the uniform, read-only API surface, returning the
8
+ parsed `{ ok, schema_version, data, meta }` envelope. Stdlib only — no transitive
9
+ dependencies.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install metagraphed
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```python
20
+ from metagraphed import MetagraphedClient, metagraphed_fetch
21
+
22
+ client = MetagraphedClient() # base_url defaults to https://api.metagraph.sh
23
+
24
+ # List subnets (query params; None values are dropped)
25
+ subnets = client.fetch(
26
+ "/api/v1/subnets",
27
+ query={"limit": 10, "sort": "completeness_score", "order": "desc"},
28
+ )
29
+ print(subnets["data"][0]["name"])
30
+
31
+ # One subnet by netuid (path params)
32
+ detail = client.fetch("/api/v1/subnets/{netuid}", path_params={"netuid": 7})
33
+
34
+ # Which subnets are buildable? (integration readiness lives in the agent catalog)
35
+ catalog = client.fetch("/api/v1/agent-catalog")
36
+
37
+ # Point at the frontend origin instead, ad hoc:
38
+ metagraphed_fetch("/api/v1/health", base_url="https://metagraph.sh")
39
+ ```
40
+
41
+ Every response is the standard envelope:
42
+
43
+ ```python
44
+ {"ok": True, "schema_version": 1, "data": ..., "meta": {...}}
45
+ ```
46
+
47
+ On a network failure or non-2xx response, a `MetagraphedError` is raised (with
48
+ `.status` for HTTP errors).
49
+
50
+ ## Versioning & stability
51
+
52
+ Tracks the public `/api/v1` contract; changes are additive within v1. See the
53
+ backend's [API stability policy](https://github.com/JSONbored/metagraphed/blob/main/docs/api-stability.md).
54
+
55
+ ## License
56
+
57
+ MIT
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "metagraphed"
7
+ version = "0.1.0"
8
+ description = "Thin Python client for metagraphed — the operational + integration registry for Bittensor subnets (api.metagraph.sh)."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.9"
12
+ authors = [{ name = "JSONbored" }]
13
+ keywords = ["bittensor", "metagraph", "subnet", "registry", "api-client"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ "Typing :: Typed",
19
+ ]
20
+
21
+ [project.urls]
22
+ Homepage = "https://metagraph.sh"
23
+ Repository = "https://github.com/JSONbored/metagraphed"
24
+
25
+ [tool.hatch.build.targets.wheel]
26
+ packages = ["src/metagraphed"]
@@ -0,0 +1,17 @@
1
+ """metagraphed — thin Python client for the Bittensor subnet registry API."""
2
+
3
+ from .client import (
4
+ DEFAULT_BASE_URL,
5
+ MetagraphedClient,
6
+ MetagraphedError,
7
+ metagraphed_fetch,
8
+ )
9
+
10
+ __version__ = "0.1.0"
11
+ __all__ = [
12
+ "DEFAULT_BASE_URL",
13
+ "MetagraphedClient",
14
+ "MetagraphedError",
15
+ "metagraphed_fetch",
16
+ "__version__",
17
+ ]
@@ -0,0 +1,105 @@
1
+ """Thin Python client for the metagraphed API (https://api.metagraph.sh).
2
+
3
+ Dependency-free (stdlib ``urllib`` only). Mirrors the generated TypeScript
4
+ client: one generic GET helper over the uniform, read-only API surface,
5
+ returning the parsed ``{ ok, schema_version, data, meta }`` envelope as a dict.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import re
12
+ import urllib.error
13
+ import urllib.parse
14
+ import urllib.request
15
+ from typing import Any, Mapping, Optional
16
+
17
+ DEFAULT_BASE_URL = "https://api.metagraph.sh"
18
+ _PATH_PARAM = re.compile(r"\{([^{}]+)\}")
19
+
20
+
21
+ class MetagraphedError(Exception):
22
+ """Raised on a network failure or a non-2xx HTTP response."""
23
+
24
+ def __init__(self, message: str, *, status: Optional[int] = None) -> None:
25
+ super().__init__(message)
26
+ self.status = status
27
+
28
+
29
+ def _interpolate(path: str, path_params: Optional[Mapping[str, Any]]) -> str:
30
+ params = path_params or {}
31
+
32
+ def repl(match: "re.Match[str]") -> str:
33
+ name = match.group(1)
34
+ if name not in params or params[name] is None:
35
+ raise MetagraphedError(f"Missing path parameter: {name}")
36
+ return urllib.parse.quote(str(params[name]), safe="")
37
+
38
+ return _PATH_PARAM.sub(repl, path)
39
+
40
+
41
+ def metagraphed_fetch(
42
+ path: str,
43
+ *,
44
+ base_url: str = DEFAULT_BASE_URL,
45
+ path_params: Optional[Mapping[str, Any]] = None,
46
+ query: Optional[Mapping[str, Any]] = None,
47
+ headers: Optional[Mapping[str, str]] = None,
48
+ timeout: float = 30.0,
49
+ ) -> Any:
50
+ """GET ``path`` against metagraphed and return the parsed JSON envelope.
51
+
52
+ ``path`` may contain ``{name}`` segments filled from ``path_params``.
53
+ ``query`` values that are ``None`` are dropped. Raises
54
+ :class:`MetagraphedError` on a network failure or a non-2xx response.
55
+ """
56
+ url = base_url.rstrip("/") + _interpolate(path, path_params)
57
+ if query:
58
+ pairs = [(key, value) for key, value in query.items() if value is not None]
59
+ if pairs:
60
+ url += "?" + urllib.parse.urlencode(pairs, doseq=True)
61
+
62
+ request = urllib.request.Request(url, method="GET")
63
+ request.add_header("Accept", "application/json")
64
+ for key, value in (headers or {}).items():
65
+ request.add_header(key, value)
66
+
67
+ try:
68
+ with urllib.request.urlopen(request, timeout=timeout) as response:
69
+ body = response.read().decode("utf-8")
70
+ except urllib.error.HTTPError as error:
71
+ raise MetagraphedError(
72
+ f"GET {url} failed: HTTP {error.code}", status=error.code
73
+ ) from error
74
+ except urllib.error.URLError as error:
75
+ raise MetagraphedError(f"GET {url} failed: {error.reason}") from error
76
+
77
+ return json.loads(body)
78
+
79
+
80
+ class MetagraphedClient:
81
+ """Convenience wrapper binding a ``base_url`` + default ``timeout``."""
82
+
83
+ def __init__(
84
+ self, base_url: str = DEFAULT_BASE_URL, *, timeout: float = 30.0
85
+ ) -> None:
86
+ self.base_url = base_url
87
+ self.timeout = timeout
88
+
89
+ def fetch(
90
+ self,
91
+ path: str,
92
+ *,
93
+ path_params: Optional[Mapping[str, Any]] = None,
94
+ query: Optional[Mapping[str, Any]] = None,
95
+ headers: Optional[Mapping[str, str]] = None,
96
+ ) -> Any:
97
+ """GET ``path`` using this client's ``base_url`` + ``timeout``."""
98
+ return metagraphed_fetch(
99
+ path,
100
+ base_url=self.base_url,
101
+ path_params=path_params,
102
+ query=query,
103
+ headers=headers,
104
+ timeout=self.timeout,
105
+ )
File without changes
@@ -0,0 +1,89 @@
1
+ """Hermetic tests for the metagraphed client (urllib mocked, no network)."""
2
+
3
+ import json
4
+ import unittest
5
+ import urllib.error
6
+ from unittest import mock
7
+
8
+ from metagraphed import MetagraphedClient, MetagraphedError, metagraphed_fetch
9
+
10
+
11
+ class _FakeResponse:
12
+ def __init__(self, payload):
13
+ self._body = json.dumps(payload).encode("utf-8")
14
+
15
+ def read(self):
16
+ return self._body
17
+
18
+ def __enter__(self):
19
+ return self
20
+
21
+ def __exit__(self, *exc):
22
+ return False
23
+
24
+
25
+ class ClientTest(unittest.TestCase):
26
+ def test_interpolates_path_params_and_sets_accept(self):
27
+ captured = {}
28
+
29
+ def fake_urlopen(request, timeout=None):
30
+ captured["url"] = request.full_url
31
+ captured["accept"] = request.get_header("Accept")
32
+ return _FakeResponse({"ok": True, "data": {"netuid": 7}})
33
+
34
+ with mock.patch("urllib.request.urlopen", fake_urlopen):
35
+ out = metagraphed_fetch(
36
+ "/api/v1/subnets/{netuid}", path_params={"netuid": 7}
37
+ )
38
+
39
+ self.assertEqual(captured["url"], "https://api.metagraph.sh/api/v1/subnets/7")
40
+ self.assertEqual(captured["accept"], "application/json")
41
+ self.assertEqual(out["data"]["netuid"], 7)
42
+
43
+ def test_missing_path_param_raises(self):
44
+ with self.assertRaises(MetagraphedError):
45
+ metagraphed_fetch("/api/v1/subnets/{netuid}")
46
+
47
+ def test_drops_none_query_values_and_encodes(self):
48
+ captured = {}
49
+
50
+ def fake_urlopen(request, timeout=None):
51
+ captured["url"] = request.full_url
52
+ return _FakeResponse({"ok": True})
53
+
54
+ with mock.patch("urllib.request.urlopen", fake_urlopen):
55
+ metagraphed_fetch(
56
+ "/api/v1/search",
57
+ query={"q": "image gen", "cursor": None, "limit": 5},
58
+ )
59
+
60
+ self.assertIn("q=image+gen", captured["url"])
61
+ self.assertIn("limit=5", captured["url"])
62
+ self.assertNotIn("cursor", captured["url"])
63
+
64
+ def test_base_url_override(self):
65
+ captured = {}
66
+
67
+ def fake_urlopen(request, timeout=None):
68
+ captured["url"] = request.full_url
69
+ return _FakeResponse({"ok": True})
70
+
71
+ with mock.patch("urllib.request.urlopen", fake_urlopen):
72
+ MetagraphedClient(base_url="https://metagraph.sh").fetch("/api/v1/health")
73
+
74
+ self.assertTrue(
75
+ captured["url"].startswith("https://metagraph.sh/api/v1/health")
76
+ )
77
+
78
+ def test_http_error_becomes_metagraphed_error(self):
79
+ def fake_urlopen(request, timeout=None):
80
+ raise urllib.error.HTTPError(request.full_url, 404, "Not Found", {}, None)
81
+
82
+ with mock.patch("urllib.request.urlopen", fake_urlopen):
83
+ with self.assertRaises(MetagraphedError) as ctx:
84
+ metagraphed_fetch("/api/v1/subnets/{netuid}", path_params={"netuid": 9999})
85
+ self.assertEqual(ctx.exception.status, 404)
86
+
87
+
88
+ if __name__ == "__main__":
89
+ unittest.main()