energyscope-client 0.2.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,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: energyscope-client
3
+ Version: 0.2.0
4
+ Summary: Python client for EnergyScope energy market data (Arrow Flight)
5
+ Author-email: David Linton <contact@energyscope.io>
6
+ License: MIT
7
+ Project-URL: Homepage, https://energyscope.io
8
+ Project-URL: Documentation, https://energyscope.io/connect
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: pyarrow>=14.0
12
+ Requires-Dist: polars>=0.20
13
+ Provides-Extra: demo
14
+ Requires-Dist: streamlit>=1.30; extra == "demo"
15
+ Requires-Dist: plotly>=5.0; extra == "demo"
16
+
17
+ Python client for EnergyScope (energyscope.io) — energy market time series over Arrow Flight. `import energyscope as es; es.Client("YOUR_API_KEY").get("PET.RWTC.D", start="2024-01-01")`
@@ -0,0 +1,6 @@
1
+ """EnergyScope Python client — Arrow Flight interface to energy market data."""
2
+
3
+ from .client import Client, SERVERS
4
+
5
+ __all__ = ["Client", "SERVERS"]
6
+ __version__ = "0.1.0"
@@ -0,0 +1,227 @@
1
+ """EnergyScope Flight client."""
2
+
3
+ import json
4
+ from typing import Optional
5
+
6
+ import pyarrow as pa
7
+ import pyarrow.flight as flight
8
+ import polars as pl
9
+
10
+ SERVERS = {
11
+ "data": "grpc+tls://data.energyscope.io:443",
12
+ "s0": "grpc://s0.energyscope.io:8815",
13
+ "s1": "grpc://s1.energyscope.io:8815",
14
+ "s2": "grpc://s2.energyscope.io:8815",
15
+ "aws": "grpc://18.198.51.233:8815",
16
+ "local": "grpc://localhost:8815",
17
+ "lan": "grpc://192.168.132.91:8815",
18
+ "vps": "grpc://147.93.89.98:8815",
19
+ }
20
+
21
+
22
+ class Client:
23
+ """Arrow Flight client for EnergyScope.
24
+
25
+ Usage:
26
+ import energyscope as es
27
+ client = es.Client() # default: data (Cloudflare)
28
+ client = es.Client(server="s0") # MS-02 direct
29
+ client = es.Client(server="local") # localhost
30
+
31
+ df = client.get("PET.RWTC.D", start="2024-01-01")
32
+ df = client.get(["PET.RWTC.D", "PET.RBRTE.D"], start="2024-01-01")
33
+ results = client.search("crude oil")
34
+ latest = client.latest("PET.RWTC.D")
35
+ health = client.health()
36
+ """
37
+
38
+ def __init__(self, api_key: Optional[str] = None, server: str = "data", address: Optional[str] = None):
39
+ """Connect to an EnergyScope Flight server.
40
+
41
+ Args:
42
+ api_key: API key for authentication (optional for demo/dev)
43
+ server: Preset name ("data", "s0", "s1", "s2", "local", "usb4", "vps")
44
+ address: Full address override (e.g. "grpc://myserver:8815")
45
+ """
46
+ if address:
47
+ self._address = address
48
+ elif server in SERVERS:
49
+ self._address = SERVERS[server]
50
+ else:
51
+ raise ValueError(f"Unknown server '{server}'. Choose from: {list(SERVERS.keys())}")
52
+
53
+ self._api_key = api_key
54
+ self._client: Optional[flight.FlightClient] = None
55
+
56
+ @property
57
+ def address(self) -> str:
58
+ return self._address
59
+
60
+ def _get_client(self) -> flight.FlightClient:
61
+ if self._client is None:
62
+ self._client = flight.connect(self._address)
63
+ if self._api_key:
64
+ # Handshake returns a bearer token header pair for subsequent calls
65
+ self._auth_header = self._client.authenticate_basic_token(self._api_key, "")
66
+ else:
67
+ self._auth_header = None
68
+ return self._client
69
+
70
+ def _call_opts(self) -> flight.FlightCallOptions:
71
+ """Get call options with auth header if authenticated."""
72
+ if self._auth_header:
73
+ return flight.FlightCallOptions(headers=[self._auth_header])
74
+ return flight.FlightCallOptions()
75
+
76
+ def _action(self, action_type: str, body: bytes = b"") -> str:
77
+ """Execute a Flight action and return the response body as string."""
78
+ client = self._get_client()
79
+ action = flight.Action(action_type, body)
80
+ results = list(client.do_action(action, self._call_opts()))
81
+ if results:
82
+ return results[0].body.to_pybytes().decode()
83
+ return ""
84
+
85
+ def get(
86
+ self,
87
+ series: str | list[str],
88
+ start: Optional[str] = None,
89
+ end: Optional[str] = None,
90
+ pivot: bool = False,
91
+ fill: Optional[str] = None,
92
+ ) -> pl.DataFrame:
93
+ """Fetch time series data.
94
+
95
+ Args:
96
+ series: Series ID or list of IDs (e.g. "PET.RWTC.D")
97
+ start: Start date "YYYY-MM-DD" (optional)
98
+ end: End date "YYYY-MM-DD" (optional)
99
+ pivot: If True, pivot to wide format (dates as rows, series as columns)
100
+ fill: Fill strategy for pivoted data. Options:
101
+ "ffill" — forward-fill last known value (like Bloomberg BDH)
102
+ "null" — leave nulls (default)
103
+ A number (e.g. 0) — fill with that value
104
+
105
+ Returns:
106
+ Polars DataFrame. Long format (series_id, period, value) by default,
107
+ or wide format (period as index, one column per series) if pivot=True.
108
+ """
109
+ if isinstance(series, str):
110
+ series = [series]
111
+
112
+ ticket_data = {"series": series}
113
+ if start:
114
+ ticket_data["start"] = start
115
+ if end:
116
+ ticket_data["end"] = end
117
+
118
+ client = self._get_client()
119
+ ticket = flight.Ticket(json.dumps(ticket_data).encode())
120
+ reader = client.do_get(ticket, self._call_opts())
121
+ table = reader.read_all()
122
+ df = pl.from_arrow(table)
123
+
124
+ if pivot and len(df) > 0:
125
+ df = df.pivot(on="series_id", index="period", values="value").sort("period")
126
+ if fill == "ffill":
127
+ df = df.fill_null(strategy="forward")
128
+ elif fill is not None and fill != "null":
129
+ try:
130
+ df = df.fill_null(float(fill))
131
+ except (ValueError, TypeError):
132
+ pass
133
+
134
+ return df
135
+
136
+ def get_arrow(
137
+ self,
138
+ series: str | list[str],
139
+ start: Optional[str] = None,
140
+ end: Optional[str] = None,
141
+ ) -> pa.Table:
142
+ """Fetch time series data as Arrow table (zero-copy).
143
+
144
+ Same as get() but returns pyarrow.Table instead of pandas DataFrame.
145
+ """
146
+ if isinstance(series, str):
147
+ series = [series]
148
+
149
+ ticket_data = {"series": series}
150
+ if start:
151
+ ticket_data["start"] = start
152
+ if end:
153
+ ticket_data["end"] = end
154
+
155
+ client = self._get_client()
156
+ ticket = flight.Ticket(json.dumps(ticket_data).encode())
157
+ reader = client.do_get(ticket, self._call_opts())
158
+ return reader.read_all()
159
+
160
+ def search(self, query: str) -> list[dict]:
161
+ """Search series metadata by keyword.
162
+
163
+ Returns:
164
+ List of dicts with: series_id, name, f (frequency), units
165
+ """
166
+ body = self._action("search", query.encode())
167
+ return json.loads(body) if body else []
168
+
169
+ def latest(self, series: str | list[str]) -> list[dict]:
170
+ """Get latest value for one or more series.
171
+
172
+ Returns:
173
+ List of dicts with: series_id, period, value, name, units
174
+ """
175
+ if isinstance(series, str):
176
+ series = [series]
177
+ body = self._action("latest", json.dumps({"series": series}).encode())
178
+ return json.loads(body) if body else []
179
+
180
+ def health(self) -> dict:
181
+ """Check server health.
182
+
183
+ Returns:
184
+ Dict with: status, series_count, data_rows
185
+ """
186
+ body = self._action("healthcheck")
187
+ return json.loads(body) if body else {}
188
+
189
+ def browse(self, category_id: Optional[int] = None) -> dict | list:
190
+ """Browse category tree.
191
+
192
+ Args:
193
+ category_id: Category ID to browse into, or None for roots.
194
+
195
+ Returns:
196
+ Root list or category dict with children and series.
197
+ """
198
+ req = {"id": category_id} if category_id is not None else {}
199
+ body = self._action("browse", json.dumps(req).encode())
200
+ return json.loads(body) if body else []
201
+
202
+ def max_date(self, series: str | list[str]) -> Optional[str]:
203
+ """Get the latest data date across series.
204
+
205
+ Returns:
206
+ Date string "YYYY-MM-DD" or None
207
+ """
208
+ if isinstance(series, str):
209
+ series = [series]
210
+ body = self._action("max_date", json.dumps({"series": series}).encode())
211
+ result = json.loads(body) if body else {}
212
+ return result.get("max_date")
213
+
214
+ def close(self):
215
+ """Close the connection."""
216
+ if self._client is not None:
217
+ self._client.close()
218
+ self._client = None
219
+
220
+ def __enter__(self):
221
+ return self
222
+
223
+ def __exit__(self, *args):
224
+ self.close()
225
+
226
+ def __repr__(self):
227
+ return f"energyscope.Client('{self._address}')"
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: energyscope-client
3
+ Version: 0.2.0
4
+ Summary: Python client for EnergyScope energy market data (Arrow Flight)
5
+ Author-email: David Linton <contact@energyscope.io>
6
+ License: MIT
7
+ Project-URL: Homepage, https://energyscope.io
8
+ Project-URL: Documentation, https://energyscope.io/connect
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: pyarrow>=14.0
12
+ Requires-Dist: polars>=0.20
13
+ Provides-Extra: demo
14
+ Requires-Dist: streamlit>=1.30; extra == "demo"
15
+ Requires-Dist: plotly>=5.0; extra == "demo"
16
+
17
+ Python client for EnergyScope (energyscope.io) — energy market time series over Arrow Flight. `import energyscope as es; es.Client("YOUR_API_KEY").get("PET.RWTC.D", start="2024-01-01")`
@@ -0,0 +1,8 @@
1
+ pyproject.toml
2
+ energyscope/__init__.py
3
+ energyscope/client.py
4
+ energyscope_client.egg-info/PKG-INFO
5
+ energyscope_client.egg-info/SOURCES.txt
6
+ energyscope_client.egg-info/dependency_links.txt
7
+ energyscope_client.egg-info/requires.txt
8
+ energyscope_client.egg-info/top_level.txt
@@ -0,0 +1,6 @@
1
+ pyarrow>=14.0
2
+ polars>=0.20
3
+
4
+ [demo]
5
+ streamlit>=1.30
6
+ plotly>=5.0
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ # PyPI name is energyscope-client (the bare "energyscope" name belongs to
7
+ # EPFL's energy-system modeling tool); the IMPORT name stays energyscope.
8
+ name = "energyscope-client"
9
+ version = "0.2.0"
10
+ description = "Python client for EnergyScope energy market data (Arrow Flight)"
11
+ readme = { text = "Python client for EnergyScope (energyscope.io) — energy market time series over Arrow Flight. `import energyscope as es; es.Client(\"YOUR_API_KEY\").get(\"PET.RWTC.D\", start=\"2024-01-01\")`", content-type = "text/markdown" }
12
+ requires-python = ">=3.10"
13
+ license = { text = "MIT" }
14
+ authors = [{ name = "David Linton", email = "contact@energyscope.io" }]
15
+ dependencies = [
16
+ "pyarrow>=14.0",
17
+ "polars>=0.20",
18
+ ]
19
+
20
+ [project.urls]
21
+ Homepage = "https://energyscope.io"
22
+ Documentation = "https://energyscope.io/connect"
23
+
24
+ [project.optional-dependencies]
25
+ demo = ["streamlit>=1.30", "plotly>=5.0"]
26
+
27
+ [tool.setuptools]
28
+ packages = ["energyscope"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+