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.
- energyscope_client-0.2.0/PKG-INFO +17 -0
- energyscope_client-0.2.0/energyscope/__init__.py +6 -0
- energyscope_client-0.2.0/energyscope/client.py +227 -0
- energyscope_client-0.2.0/energyscope_client.egg-info/PKG-INFO +17 -0
- energyscope_client-0.2.0/energyscope_client.egg-info/SOURCES.txt +8 -0
- energyscope_client-0.2.0/energyscope_client.egg-info/dependency_links.txt +1 -0
- energyscope_client-0.2.0/energyscope_client.egg-info/requires.txt +6 -0
- energyscope_client-0.2.0/energyscope_client.egg-info/top_level.txt +1 -0
- energyscope_client-0.2.0/pyproject.toml +28 -0
- energyscope_client-0.2.0/setup.cfg +4 -0
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
energyscope
|
|
@@ -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"]
|