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 ADDED
@@ -0,0 +1,5 @@
1
+ """DC Hub Python SDK — live data-center, power & gas intelligence over MCP."""
2
+ from .client import DCHub
3
+
4
+ __all__ = ["DCHub"]
5
+ __version__ = "0.1.0"
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ dchub