dchub 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.
- dchub-0.1.0/PKG-INFO +60 -0
- dchub-0.1.0/README.md +46 -0
- dchub-0.1.0/dchub/__init__.py +5 -0
- dchub-0.1.0/dchub/client.py +154 -0
- dchub-0.1.0/dchub.egg-info/PKG-INFO +60 -0
- dchub-0.1.0/dchub.egg-info/SOURCES.txt +10 -0
- dchub-0.1.0/dchub.egg-info/dependency_links.txt +1 -0
- dchub-0.1.0/dchub.egg-info/requires.txt +3 -0
- dchub-0.1.0/dchub.egg-info/top_level.txt +1 -0
- dchub-0.1.0/pyproject.toml +24 -0
- dchub-0.1.0/setup.cfg +4 -0
- dchub-0.1.0/tests/test_client.py +38 -0
dchub-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dchub
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: DC Hub Python SDK — live data-center, power & gas intelligence over MCP.
|
|
5
|
+
Author: DC Hub
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://dchub.cloud
|
|
8
|
+
Project-URL: Repository, https://github.com/azmartone67/dchub-mcp-server
|
|
9
|
+
Keywords: data-center,energy,mcp,grid,infrastructure
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Provides-Extra: test
|
|
13
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
14
|
+
|
|
15
|
+
# dchub — Python SDK
|
|
16
|
+
|
|
17
|
+
Live data-center, power & gas intelligence for AI agents. Hides the MCP JSON-RPC
|
|
18
|
+
handshake (`initialize` → `notifications/initialized` → `tools/call`, SSE
|
|
19
|
+
parsing) behind a thin client. **Zero runtime dependencies** (stdlib only).
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
```bash
|
|
23
|
+
pip install dchub # from this repo: pip install ./sdk/python
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quickstart (5 lines)
|
|
27
|
+
```python
|
|
28
|
+
from dchub import DCHub
|
|
29
|
+
dc = DCHub() # reads DCHUB_API_KEY from env if set
|
|
30
|
+
print(dc.market("northern-virginia")) # market intel
|
|
31
|
+
print(dc.search(state="VA")) # facility search (canonical slugs)
|
|
32
|
+
print(dc.grid(iso="ERCOT")) # live grid intel
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## API
|
|
36
|
+
| Method | Tool | Returns |
|
|
37
|
+
|--------|------|---------|
|
|
38
|
+
| `dc.market(slug)` | `get_market_intel` | by-status counts, operators, recent facilities |
|
|
39
|
+
| `dc.search(q, state, country, limit)` | `search_facilities` | rows w/ canonical slug, provider, location |
|
|
40
|
+
| `dc.grid(iso)` | `get_grid_data` | live demand / mix / headroom |
|
|
41
|
+
| `dc.call(tool, **args)` | *any of 38* | cleaned data payload |
|
|
42
|
+
| `dc.tools()` | `tools/list` | list of 38 tool names |
|
|
43
|
+
|
|
44
|
+
## Auth & tiers
|
|
45
|
+
Set `DCHUB_API_KEY` (sent as `X-API-Key`) for full data:
|
|
46
|
+
```bash
|
|
47
|
+
curl -X POST https://dchub.cloud/api/v1/keys/claim -d '{"client_name":"python-sdk"}'
|
|
48
|
+
export DCHUB_API_KEY=dch_live_...
|
|
49
|
+
```
|
|
50
|
+
On the **free tier** some fields are masked and `grid` returns a gated preview;
|
|
51
|
+
the SDK strips the upsell wrapper and returns the real embedded payload either
|
|
52
|
+
way. Source/citation: https://dchub.cloud (CC-BY-4.0).
|
|
53
|
+
|
|
54
|
+
## Tests
|
|
55
|
+
```bash
|
|
56
|
+
pip install -e ".[test]" && pytest # 5 live, gate-graceful tests
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
> Packaging is configured but **not published** — the maintainer runs
|
|
60
|
+
> `python -m build && twine upload` to ship to PyPI.
|
dchub-0.1.0/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# dchub — Python SDK
|
|
2
|
+
|
|
3
|
+
Live data-center, power & gas intelligence for AI agents. Hides the MCP JSON-RPC
|
|
4
|
+
handshake (`initialize` → `notifications/initialized` → `tools/call`, SSE
|
|
5
|
+
parsing) behind a thin client. **Zero runtime dependencies** (stdlib only).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
```bash
|
|
9
|
+
pip install dchub # from this repo: pip install ./sdk/python
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Quickstart (5 lines)
|
|
13
|
+
```python
|
|
14
|
+
from dchub import DCHub
|
|
15
|
+
dc = DCHub() # reads DCHUB_API_KEY from env if set
|
|
16
|
+
print(dc.market("northern-virginia")) # market intel
|
|
17
|
+
print(dc.search(state="VA")) # facility search (canonical slugs)
|
|
18
|
+
print(dc.grid(iso="ERCOT")) # live grid intel
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## API
|
|
22
|
+
| Method | Tool | Returns |
|
|
23
|
+
|--------|------|---------|
|
|
24
|
+
| `dc.market(slug)` | `get_market_intel` | by-status counts, operators, recent facilities |
|
|
25
|
+
| `dc.search(q, state, country, limit)` | `search_facilities` | rows w/ canonical slug, provider, location |
|
|
26
|
+
| `dc.grid(iso)` | `get_grid_data` | live demand / mix / headroom |
|
|
27
|
+
| `dc.call(tool, **args)` | *any of 38* | cleaned data payload |
|
|
28
|
+
| `dc.tools()` | `tools/list` | list of 38 tool names |
|
|
29
|
+
|
|
30
|
+
## Auth & tiers
|
|
31
|
+
Set `DCHUB_API_KEY` (sent as `X-API-Key`) for full data:
|
|
32
|
+
```bash
|
|
33
|
+
curl -X POST https://dchub.cloud/api/v1/keys/claim -d '{"client_name":"python-sdk"}'
|
|
34
|
+
export DCHUB_API_KEY=dch_live_...
|
|
35
|
+
```
|
|
36
|
+
On the **free tier** some fields are masked and `grid` returns a gated preview;
|
|
37
|
+
the SDK strips the upsell wrapper and returns the real embedded payload either
|
|
38
|
+
way. Source/citation: https://dchub.cloud (CC-BY-4.0).
|
|
39
|
+
|
|
40
|
+
## Tests
|
|
41
|
+
```bash
|
|
42
|
+
pip install -e ".[test]" && pytest # 5 live, gate-graceful tests
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
> Packaging is configured but **not published** — the maintainer runs
|
|
46
|
+
> `python -m build && twine upload` to ship to PyPI.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""DC Hub Python SDK — hides the MCP JSON-RPC handshake.
|
|
2
|
+
|
|
3
|
+
The MCP transport (initialize -> notifications/initialized -> tools/call, with
|
|
4
|
+
SSE response parsing) is wrapped so you can just write:
|
|
5
|
+
|
|
6
|
+
from dchub import DCHub
|
|
7
|
+
dc = DCHub() # reads DCHUB_API_KEY from env if set
|
|
8
|
+
dc.market("northern-virginia")
|
|
9
|
+
dc.search(state="VA")
|
|
10
|
+
dc.grid(iso="ERCOT")
|
|
11
|
+
dc.call("get_market_intel", market="dallas") # any of the 38 tools
|
|
12
|
+
dc.tools() # list tool names
|
|
13
|
+
|
|
14
|
+
Set DCHUB_API_KEY for full-tier data (sent as the X-API-Key header).
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import urllib.request
|
|
21
|
+
|
|
22
|
+
__all__ = ["DCHub"]
|
|
23
|
+
|
|
24
|
+
_DEFAULT_ENDPOINT = "https://dchub.cloud/mcp"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DCHub:
|
|
28
|
+
def __init__(self, api_key: str | None = None, endpoint: str | None = None,
|
|
29
|
+
timeout: int = 60):
|
|
30
|
+
self.endpoint = endpoint or os.environ.get("DCHUB_ENDPOINT", _DEFAULT_ENDPOINT)
|
|
31
|
+
self.api_key = api_key if api_key is not None else os.environ.get("DCHUB_API_KEY")
|
|
32
|
+
self.timeout = timeout
|
|
33
|
+
self._session_id: str | None = None
|
|
34
|
+
|
|
35
|
+
# --- transport ---------------------------------------------------------
|
|
36
|
+
def _headers(self) -> dict:
|
|
37
|
+
h = {"Content-Type": "application/json",
|
|
38
|
+
"Accept": "application/json, text/event-stream"}
|
|
39
|
+
if self.api_key:
|
|
40
|
+
h["X-API-Key"] = self.api_key
|
|
41
|
+
if self._session_id:
|
|
42
|
+
h["Mcp-Session-Id"] = self._session_id
|
|
43
|
+
return h
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def _parse_body(raw: str):
|
|
47
|
+
raw = raw.strip()
|
|
48
|
+
if not raw:
|
|
49
|
+
return None
|
|
50
|
+
if raw.startswith("{"):
|
|
51
|
+
return json.loads(raw)
|
|
52
|
+
for line in raw.splitlines(): # SSE: 'data: {...}'
|
|
53
|
+
line = line.strip()
|
|
54
|
+
if line.startswith("data:"):
|
|
55
|
+
payload = line[len("data:"):].strip()
|
|
56
|
+
if payload.startswith("{"):
|
|
57
|
+
return json.loads(payload)
|
|
58
|
+
raise ValueError(f"Could not parse MCP response body: {raw[:300]}")
|
|
59
|
+
|
|
60
|
+
def _post(self, payload: dict):
|
|
61
|
+
data = json.dumps(payload).encode()
|
|
62
|
+
req = urllib.request.Request(self.endpoint, data=data,
|
|
63
|
+
headers=self._headers(), method="POST")
|
|
64
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
65
|
+
sid = resp.headers.get("Mcp-Session-Id")
|
|
66
|
+
body = resp.read().decode()
|
|
67
|
+
if sid:
|
|
68
|
+
self._session_id = sid
|
|
69
|
+
return self._parse_body(body)
|
|
70
|
+
|
|
71
|
+
def _ensure_session(self):
|
|
72
|
+
if self._session_id:
|
|
73
|
+
return
|
|
74
|
+
self._post({"jsonrpc": "2.0", "id": 1, "method": "initialize",
|
|
75
|
+
"params": {"protocolVersion": "2024-11-05", "capabilities": {},
|
|
76
|
+
"clientInfo": {"name": "dchub-python-sdk", "version": "1.0"}}})
|
|
77
|
+
try:
|
|
78
|
+
self._post({"jsonrpc": "2.0", "method": "notifications/initialized"})
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
# --- payload cleaning --------------------------------------------------
|
|
83
|
+
@staticmethod
|
|
84
|
+
def _clean(result):
|
|
85
|
+
"""Return the real data payload, stripping any free-tier upsell wrapper.
|
|
86
|
+
|
|
87
|
+
`search_facilities` already returns a structured dict; the text tools
|
|
88
|
+
(`get_market_intel`, `get_grid_data`) embed the data as a JSON block
|
|
89
|
+
fenced by `---`. Upsell-only objects (agent_action/agent_claim) are
|
|
90
|
+
skipped.
|
|
91
|
+
"""
|
|
92
|
+
if isinstance(result, dict):
|
|
93
|
+
return result
|
|
94
|
+
if isinstance(result, str):
|
|
95
|
+
for part in result.split("---"):
|
|
96
|
+
part = part.strip()
|
|
97
|
+
if part.startswith("{"):
|
|
98
|
+
try:
|
|
99
|
+
obj = json.loads(part)
|
|
100
|
+
except Exception:
|
|
101
|
+
continue
|
|
102
|
+
if not ({"agent_action", "agent_claim"} & set(obj)):
|
|
103
|
+
return obj
|
|
104
|
+
return {"text": result}
|
|
105
|
+
return result
|
|
106
|
+
|
|
107
|
+
# --- generic call ------------------------------------------------------
|
|
108
|
+
def call(self, tool: str, **arguments):
|
|
109
|
+
"""Call any DC Hub MCP tool; returns the cleaned data payload."""
|
|
110
|
+
self._ensure_session()
|
|
111
|
+
resp = self._post({"jsonrpc": "2.0", "id": 3, "method": "tools/call",
|
|
112
|
+
"params": {"name": tool, "arguments": arguments}})
|
|
113
|
+
if "error" in resp:
|
|
114
|
+
return resp["error"]
|
|
115
|
+
content = resp.get("result", {}).get("content", [])
|
|
116
|
+
parsed = []
|
|
117
|
+
for item in content:
|
|
118
|
+
if item.get("type") == "text":
|
|
119
|
+
txt = item["text"]
|
|
120
|
+
try:
|
|
121
|
+
parsed.append(json.loads(txt))
|
|
122
|
+
except Exception:
|
|
123
|
+
parsed.append(txt)
|
|
124
|
+
else:
|
|
125
|
+
parsed.append(item)
|
|
126
|
+
raw = parsed[0] if len(parsed) == 1 else parsed
|
|
127
|
+
return self._clean(raw)
|
|
128
|
+
|
|
129
|
+
def tools(self) -> list[str]:
|
|
130
|
+
"""List all available tool names."""
|
|
131
|
+
self._ensure_session()
|
|
132
|
+
resp = self._post({"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}})
|
|
133
|
+
return [t["name"] for t in resp.get("result", {}).get("tools", [])]
|
|
134
|
+
|
|
135
|
+
# --- convenience methods ----------------------------------------------
|
|
136
|
+
def market(self, slug: str):
|
|
137
|
+
"""Market intelligence for a market slug, e.g. 'northern-virginia'."""
|
|
138
|
+
return self.call("get_market_intel", market=slug)
|
|
139
|
+
|
|
140
|
+
def search(self, q: str | None = None, state: str | None = None,
|
|
141
|
+
country: str | None = None, limit: int = 5):
|
|
142
|
+
"""Search facilities by free-text / state / country."""
|
|
143
|
+
args = {"limit": limit}
|
|
144
|
+
if q:
|
|
145
|
+
args["q"] = q
|
|
146
|
+
if state:
|
|
147
|
+
args["state"] = state
|
|
148
|
+
if country:
|
|
149
|
+
args["country"] = country
|
|
150
|
+
return self.call("search_facilities", **args)
|
|
151
|
+
|
|
152
|
+
def grid(self, iso: str):
|
|
153
|
+
"""Live grid intelligence for an ISO, e.g. 'ERCOT', 'PJM'."""
|
|
154
|
+
return self.call("get_grid_data", iso=iso)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dchub
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: DC Hub Python SDK — live data-center, power & gas intelligence over MCP.
|
|
5
|
+
Author: DC Hub
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://dchub.cloud
|
|
8
|
+
Project-URL: Repository, https://github.com/azmartone67/dchub-mcp-server
|
|
9
|
+
Keywords: data-center,energy,mcp,grid,infrastructure
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Provides-Extra: test
|
|
13
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
14
|
+
|
|
15
|
+
# dchub — Python SDK
|
|
16
|
+
|
|
17
|
+
Live data-center, power & gas intelligence for AI agents. Hides the MCP JSON-RPC
|
|
18
|
+
handshake (`initialize` → `notifications/initialized` → `tools/call`, SSE
|
|
19
|
+
parsing) behind a thin client. **Zero runtime dependencies** (stdlib only).
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
```bash
|
|
23
|
+
pip install dchub # from this repo: pip install ./sdk/python
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quickstart (5 lines)
|
|
27
|
+
```python
|
|
28
|
+
from dchub import DCHub
|
|
29
|
+
dc = DCHub() # reads DCHUB_API_KEY from env if set
|
|
30
|
+
print(dc.market("northern-virginia")) # market intel
|
|
31
|
+
print(dc.search(state="VA")) # facility search (canonical slugs)
|
|
32
|
+
print(dc.grid(iso="ERCOT")) # live grid intel
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## API
|
|
36
|
+
| Method | Tool | Returns |
|
|
37
|
+
|--------|------|---------|
|
|
38
|
+
| `dc.market(slug)` | `get_market_intel` | by-status counts, operators, recent facilities |
|
|
39
|
+
| `dc.search(q, state, country, limit)` | `search_facilities` | rows w/ canonical slug, provider, location |
|
|
40
|
+
| `dc.grid(iso)` | `get_grid_data` | live demand / mix / headroom |
|
|
41
|
+
| `dc.call(tool, **args)` | *any of 38* | cleaned data payload |
|
|
42
|
+
| `dc.tools()` | `tools/list` | list of 38 tool names |
|
|
43
|
+
|
|
44
|
+
## Auth & tiers
|
|
45
|
+
Set `DCHUB_API_KEY` (sent as `X-API-Key`) for full data:
|
|
46
|
+
```bash
|
|
47
|
+
curl -X POST https://dchub.cloud/api/v1/keys/claim -d '{"client_name":"python-sdk"}'
|
|
48
|
+
export DCHUB_API_KEY=dch_live_...
|
|
49
|
+
```
|
|
50
|
+
On the **free tier** some fields are masked and `grid` returns a gated preview;
|
|
51
|
+
the SDK strips the upsell wrapper and returns the real embedded payload either
|
|
52
|
+
way. Source/citation: https://dchub.cloud (CC-BY-4.0).
|
|
53
|
+
|
|
54
|
+
## Tests
|
|
55
|
+
```bash
|
|
56
|
+
pip install -e ".[test]" && pytest # 5 live, gate-graceful tests
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
> Packaging is configured but **not published** — the maintainer runs
|
|
60
|
+
> `python -m build && twine upload` to ship to PyPI.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dchub
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dchub"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "DC Hub Python SDK — live data-center, power & gas intelligence over MCP."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "DC Hub" }]
|
|
13
|
+
keywords = ["data-center", "energy", "mcp", "grid", "infrastructure"]
|
|
14
|
+
dependencies = [] # stdlib only (urllib) — zero runtime deps
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://dchub.cloud"
|
|
18
|
+
Repository = "https://github.com/azmartone67/dchub-mcp-server"
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
test = ["pytest>=7"]
|
|
22
|
+
|
|
23
|
+
[tool.setuptools]
|
|
24
|
+
packages = ["dchub"]
|
dchub-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Live tests for the DC Hub Python SDK (free tier; gate-graceful)."""
|
|
2
|
+
from dchub import DCHub
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_tools_list_is_38():
|
|
6
|
+
assert len(DCHub().tools()) == 38
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_market_returns_real_data():
|
|
10
|
+
d = DCHub().market("northern-virginia")
|
|
11
|
+
assert isinstance(d, dict)
|
|
12
|
+
# Free tier rotates between the full payload and a 1-result preview gate;
|
|
13
|
+
# accept either (gate-graceful), but when data is present it must be real.
|
|
14
|
+
if "market" in d:
|
|
15
|
+
assert "northern virginia" in d["market"]["name"].lower()
|
|
16
|
+
assert sum(d["by_status"].values()) > 0
|
|
17
|
+
else:
|
|
18
|
+
assert "text" in d # gated preview wrapper, stripped to its text
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_search_returns_canonical_slug():
|
|
22
|
+
d = DCHub().search(state="VA", limit=3)
|
|
23
|
+
rows = d.get("data", [])
|
|
24
|
+
assert rows and all("slug" in r for r in rows)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_clean_strips_upsell_wrapper():
|
|
28
|
+
# A text blob with an upsell-only object then the real payload.
|
|
29
|
+
blob = ('marketing...\n---\n{"agent_action":{"x":1}}\n---\n'
|
|
30
|
+
'{"stats":{"facility_count":739},"success":true}')
|
|
31
|
+
out = DCHub._clean(blob)
|
|
32
|
+
assert out == {"stats": {"facility_count": 739}, "success": True}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_grid_call_does_not_raise():
|
|
36
|
+
# Free tier gates grid to a preview; SDK should still return cleanly.
|
|
37
|
+
g = DCHub().grid("ERCOT")
|
|
38
|
+
assert isinstance(g, dict)
|