dchub 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.
- dchub/__init__.py +5 -0
- dchub/client.py +154 -0
- dchub-0.1.0.dist-info/METADATA +60 -0
- dchub-0.1.0.dist-info/RECORD +6 -0
- dchub-0.1.0.dist-info/WHEEL +5 -0
- dchub-0.1.0.dist-info/top_level.txt +1 -0
dchub/__init__.py
ADDED
dchub/client.py
ADDED
|
@@ -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,6 @@
|
|
|
1
|
+
dchub/__init__.py,sha256=vxv_lrcBK-8z0JbLQH7x45oiXigfSSplt66Iq8sNINc,150
|
|
2
|
+
dchub/client.py,sha256=VjsaVkVCOfLQ6Fj2quOsB9_IORxLPUk4GuPIs8NM85g,6028
|
|
3
|
+
dchub-0.1.0.dist-info/METADATA,sha256=YDNPb-AGza2TYiPpMg6ykvNc93fzLd_hLerohNP_gCE,2228
|
|
4
|
+
dchub-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
5
|
+
dchub-0.1.0.dist-info/top_level.txt,sha256=MleyxETg_be7Dvso8yPCSXbeyvW_FFoUazCaSEztOVY,6
|
|
6
|
+
dchub-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dchub
|