sunsightenergy 0.3.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.
- sunsightenergy-0.3.0/.gitignore +56 -0
- sunsightenergy-0.3.0/PKG-INFO +310 -0
- sunsightenergy-0.3.0/README.md +280 -0
- sunsightenergy-0.3.0/examples/quickstart.ipynb +183 -0
- sunsightenergy-0.3.0/pyproject.toml +48 -0
- sunsightenergy-0.3.0/src/sunsightenergy/__init__.py +36 -0
- sunsightenergy-0.3.0/src/sunsightenergy/_util.py +162 -0
- sunsightenergy-0.3.0/src/sunsightenergy/client.py +1030 -0
- sunsightenergy-0.3.0/src/sunsightenergy/exceptions.py +44 -0
- sunsightenergy-0.3.0/src/sunsightenergy/version.py +1 -0
- sunsightenergy-0.3.0/tests/test_sdk.py +275 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Dependencies
|
|
2
|
+
node_modules/
|
|
3
|
+
__pycache__/
|
|
4
|
+
*.pyc
|
|
5
|
+
.venv/
|
|
6
|
+
venv/
|
|
7
|
+
temp/
|
|
8
|
+
# Build
|
|
9
|
+
dist/
|
|
10
|
+
build/
|
|
11
|
+
*.egg-info/
|
|
12
|
+
|
|
13
|
+
# Environment — only .env.local files are gitignored (secrets)
|
|
14
|
+
# .env.dev and .env.prod contain non-secret config and ARE committed
|
|
15
|
+
.env
|
|
16
|
+
.env.local
|
|
17
|
+
*.env.local
|
|
18
|
+
|
|
19
|
+
# IDE
|
|
20
|
+
.vscode/
|
|
21
|
+
.idea/
|
|
22
|
+
*.swp
|
|
23
|
+
|
|
24
|
+
# Claude Code — personal overrides (stay out of git); .claude/settings.json IS committed
|
|
25
|
+
.claude/settings.local.json
|
|
26
|
+
|
|
27
|
+
# OS
|
|
28
|
+
.DS_Store
|
|
29
|
+
Thumbs.db
|
|
30
|
+
|
|
31
|
+
# Logs
|
|
32
|
+
*.log
|
|
33
|
+
|
|
34
|
+
# Testing
|
|
35
|
+
coverage/
|
|
36
|
+
htmlcov/
|
|
37
|
+
.pytest_cache/
|
|
38
|
+
|
|
39
|
+
# uv
|
|
40
|
+
.python-version
|
|
41
|
+
|
|
42
|
+
# TypeScript incremental build cache (generated by `tsc -b` during builds)
|
|
43
|
+
*.tsbuildinfo
|
|
44
|
+
|
|
45
|
+
# Ruff cache
|
|
46
|
+
.ruff_cache/
|
|
47
|
+
|
|
48
|
+
# Throwaway local backend env files used for manual smoke testing. Some
|
|
49
|
+
# variants (.env.smoketest.aws/.real) hold real AWS creds copied in by hand —
|
|
50
|
+
# never let any dotted suffix of these slip into git.
|
|
51
|
+
.env.smoketest
|
|
52
|
+
.env.smoketest.*
|
|
53
|
+
*.env.smoketest
|
|
54
|
+
|
|
55
|
+
# Local S3 fixture data used for smoke testing client-api against fake buckets.
|
|
56
|
+
data-pipeline/.local_s3/
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: sunsightenergy
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Python client for the Sunsight Energy data API
|
|
5
|
+
Project-URL: Homepage, https://sunsightenergy.com
|
|
6
|
+
Project-URL: Documentation, https://docs.sunsightenergy.com
|
|
7
|
+
Project-URL: Repository, https://github.com/Sunsightenergy/sunsight-platform
|
|
8
|
+
Author-email: Sunsight Energy <support@sunsightenergy.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: electricity,energy,ercot,iso,lmp,market,pjm,sunsight
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: pandas>=2.0.0
|
|
22
|
+
Requires-Dist: pyarrow>=14.0.0
|
|
23
|
+
Requires-Dist: requests>=2.31.0
|
|
24
|
+
Requires-Dist: tqdm>=4.66.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
27
|
+
Provides-Extra: polars
|
|
28
|
+
Requires-Dist: polars>=0.20.0; extra == 'polars'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# Sunsight Energy Python SDK
|
|
32
|
+
|
|
33
|
+
Python client for the [Sunsight Energy](https://sunsightenergy.com) data API:
|
|
34
|
+
real-time and historical data for US/Canadian ISOs (ERCOT, PJM, CAISO, SPP,
|
|
35
|
+
MISO, NYISO, ISONE, IESO), weather and satellite imagery.
|
|
36
|
+
|
|
37
|
+
You ask for **a dataset and a time range**. The SDK handles paging, your
|
|
38
|
+
plan's date-range limit, retries and file formats, and hands back a pandas or
|
|
39
|
+
polars DataFrame, parquet files on disk, or images in a folder.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install sunsightenergy # pandas
|
|
45
|
+
pip install "sunsightenergy[polars]" # + polars output
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Python 3.10+. Works in Jupyter, Google Colab, scripts and pipelines.
|
|
49
|
+
|
|
50
|
+
## Quick start
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import sunsightenergy as ss
|
|
54
|
+
|
|
55
|
+
c = ss.Client("ss_live_...") # or set SUNSIGHT_API_KEY and use ss.Client()
|
|
56
|
+
|
|
57
|
+
df = c.get("ercot_load", "2026-09-01", "2026-10-01")
|
|
58
|
+
df.plot(x="interval_end_utc", y="load")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Create a key in the portal under **API Platform → API Keys**.
|
|
62
|
+
|
|
63
|
+
## Coming from Grid Status
|
|
64
|
+
|
|
65
|
+
The client has gridstatusio's methods with the same arguments, so existing
|
|
66
|
+
code only needs a new import and key:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from sunsightenergy import Client as GridStatusClient
|
|
70
|
+
|
|
71
|
+
client = GridStatusClient("ss_live_...")
|
|
72
|
+
df = client.get_dataset(
|
|
73
|
+
dataset="ercot_load",
|
|
74
|
+
start="2026-10-04",
|
|
75
|
+
end="2026-10-05",
|
|
76
|
+
timezone="market",
|
|
77
|
+
)
|
|
78
|
+
client.list_datasets(filter_term="lmp")
|
|
79
|
+
client.get_dataset_metadata("ercot_load")
|
|
80
|
+
client.get_api_usage()
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`get_dataset` takes every Grid Status argument: `publish_time_start`,
|
|
84
|
+
`publish_time_end`, `publish_time` ("latest", "latest_report" or a
|
|
85
|
+
timestamp), `columns`, `filter_column` / `filter_value` / `filter_operator`
|
|
86
|
+
(including "in"), `resample` / `resample_by` / `resample_function`, `limit`,
|
|
87
|
+
`page_size`, `timezone`, `verbose` and `return_format` ("pandas", "polars",
|
|
88
|
+
"python"). Differences:
|
|
89
|
+
|
|
90
|
+
- `timezone` defaults to `"market"` (Grid Status defaults to UTC). Pass
|
|
91
|
+
`timezone="UTC"` for Grid Status's default output.
|
|
92
|
+
- For forecasts, `start`/`end` are delivery times as on Grid Status; the
|
|
93
|
+
client reads publications from up to 7 days before `start`. Pass
|
|
94
|
+
`publish_time_start` for forecasts published further ahead.
|
|
95
|
+
- `get_daily_peak_report` isn't available.
|
|
96
|
+
|
|
97
|
+
## Loading data
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
# pandas (default), polars, or a pyarrow Table
|
|
101
|
+
df = c.get("pjm_load", "2026-09-01", "2026-10-01")
|
|
102
|
+
pdf = c.get("pjm_load", "2026-09-01", "2026-10-01", output="polars")
|
|
103
|
+
|
|
104
|
+
# One market day: ERCOT's operating day 2026-10-04, in Central time
|
|
105
|
+
df = c.get("ercot_load", day="2026-10-04")
|
|
106
|
+
|
|
107
|
+
# Fewer columns, a filter, a row limit, another timezone
|
|
108
|
+
df = c.get(
|
|
109
|
+
"pjm_load", "2026-09-01", "2026-09-08",
|
|
110
|
+
columns=["interval_start_utc", "pjm_rto"],
|
|
111
|
+
filter=("pjm_rto", ">", 90000), # ("col", value) means "="; a list value means "in"
|
|
112
|
+
limit=1000,
|
|
113
|
+
timezone="UTC", # "market" (default), "UTC", "CST"-style or any IANA name
|
|
114
|
+
)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
- `start` is inclusive and `end` exclusive. They accept strings, `date`,
|
|
118
|
+
`datetime` or `pd.Timestamp`. **A value without a timezone is read in
|
|
119
|
+
`timezone`, which defaults to the market's own clock** (Central for ERCOT
|
|
120
|
+
and SPP, Eastern for PJM, NYISO and ISO-NE, Pacific for CAISO, fixed EST
|
|
121
|
+
for MISO and IESO). So `"2026-10-04"` on an ERCOT dataset is midnight
|
|
122
|
+
Central. `end` defaults to now; `day=` is one whole day.
|
|
123
|
+
- Interval datasets filter on `interval_start_utc`, so a day is the intervals
|
|
124
|
+
that start in it (as on Grid Status).
|
|
125
|
+
- Every `*_utc` column stays UTC and gets a `*_local` twin in the query
|
|
126
|
+
timezone. `timezone="UTC"` returns only the UTC columns.
|
|
127
|
+
- Long ranges are split into the windows your plan allows and fetched in
|
|
128
|
+
parallel where your plan allows, with one progress bar.
|
|
129
|
+
- Timestamps come back as real timezone-aware datetimes; numbers as numbers.
|
|
130
|
+
|
|
131
|
+
### Large ranges: save to disk
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
paths = c.download("pjm_load", "2025-01-01", "2026-01-01", dir="data/")
|
|
135
|
+
# data/pjm_load/2025-01-01.parquet, data/pjm_load/2025-01-02.parquet, ...
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
- One parquet file per day in the query timezone (the market's by default).
|
|
139
|
+
The folder is created if it doesn't exist.
|
|
140
|
+
- **Every run fetches every day again and overwrites its file**, so the
|
|
141
|
+
folder always matches the server. A day that no longer has data has its
|
|
142
|
+
old file removed.
|
|
143
|
+
- Read the folder back with any tool:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
import pandas as pd, polars as pl
|
|
147
|
+
pd.read_parquet("data/pjm_load")
|
|
148
|
+
pl.scan_parquet("data/pjm_load/*.parquet").collect()
|
|
149
|
+
# DuckDB: SELECT * FROM read_parquet('data/pjm_load/*.parquet')
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Process piece by piece
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
for chunk in c.stream("pjm_load", "2025-01-01", "2026-01-01"):
|
|
156
|
+
print(len(chunk)) # one DataFrame per window; memory stays flat
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Forecasts
|
|
160
|
+
|
|
161
|
+
A forecast is published many times (ERCOT's 7-day load forecast: every hour),
|
|
162
|
+
so each delivery hour has many predictions. `start`/`end` select
|
|
163
|
+
**publications**: you get every forecast published in `[start, end)`, each
|
|
164
|
+
with its full horizon and its `publish_time_utc`.
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
ds = "ercot_load_forecast_by_forecast_zone"
|
|
168
|
+
c.info(ds) # kind: forecast; time column (start/end): publish_time_utc
|
|
169
|
+
|
|
170
|
+
all_ = c.get(ds, "2026-10-01", "2026-10-02") # 24 publications x 7-day horizon
|
|
171
|
+
best = c.get(ds, "2026-10-01", "2026-10-02", vintage="latest") # newest forecast per delivery hour
|
|
172
|
+
then = c.get(ds, as_of="2026-10-01T09:00Z", # the forecast as it stood at 09:00,
|
|
173
|
+
start="2026-10-02", end="2026-10-03") # cut to delivery on Oct 2
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
- `vintage="latest"` keeps, for each delivery interval, the newest publication
|
|
177
|
+
in the window. Rows still carry `publish_time_utc`, so you see which won.
|
|
178
|
+
- `as_of=` returns one whole publication: the newest at or before that time
|
|
179
|
+
(looking back up to 7 days). `start`/`end` are optional and cut its
|
|
180
|
+
delivery times. Use it for backtests: "what did we know at 9am?"
|
|
181
|
+
- `download()` saves forecasts per **publish** day.
|
|
182
|
+
- Snapshot datasets (reference lists, outage snapshots) work the same way.
|
|
183
|
+
|
|
184
|
+
## IoT sensors
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
c.devices() # device_id + when it last checked in
|
|
188
|
+
df = c.get("sensor_readings", "2026-10-02", "2026-10-03",
|
|
189
|
+
devices=["sunsight-001"], # default: all devices
|
|
190
|
+
images=True) # adds image_url: the sky photo for that reading
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Device data is stored as hundreds of small files per device-day, so each
|
|
194
|
+
request covers at most 7 days (`get()` splits longer ranges for you).
|
|
195
|
+
`images=True` counts each distinct photo against your monthly files & images
|
|
196
|
+
quota.
|
|
197
|
+
|
|
198
|
+
## Raw files
|
|
199
|
+
|
|
200
|
+
Original source files, byte for byte (PJM eDART outage reports, PJM
|
|
201
|
+
workbooks):
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
f = c.files("pjm_transmission_outages_raw", "2026-10-02", "2026-10-03",
|
|
205
|
+
source="linesout", dir="raw/") # source= is optional: default all
|
|
206
|
+
f[["captured_at", "source", "filename", "local_path"]]
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Same columns and options as `images()`, plus `source`. Files share the
|
|
210
|
+
monthly files & images quota.
|
|
211
|
+
|
|
212
|
+
## Images
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
# A table of images with download links (links last ~15 minutes)
|
|
216
|
+
imgs = c.images("goes19_regional_southcentral_truecolor", "2026-09-30", "2026-10-01")
|
|
217
|
+
|
|
218
|
+
# Also save them: imgs/goes19_regional_southcentral_truecolor/2026-09-30/<file>.jpg
|
|
219
|
+
imgs = c.images("goes19_regional_southcentral_truecolor", "2026-09-30", "2026-10-01", dir="imgs/")
|
|
220
|
+
imgs[["captured_at", "local_path"]]
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Columns: `captured_at` (UTC), `filename`, `size_bytes`, `url` and, with
|
|
224
|
+
`dir=`, `local_path`. Saving runs in parallel, skips files you already have,
|
|
225
|
+
and recovers when a link expires mid-run. On networks that block direct
|
|
226
|
+
storage links, pass `via="api"`.
|
|
227
|
+
|
|
228
|
+
## Finding datasets
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
c.datasets() # everything your key can see
|
|
232
|
+
c.datasets(search="ercot load") # every word must match id, name, description or tags
|
|
233
|
+
c.datasets(queryable=True) # only what get() can serve today
|
|
234
|
+
|
|
235
|
+
c.info("pjm_load") # columns + types, time column, data range
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
A dataset with `queryable == False` has a `reason`, e.g. it is a file or
|
|
239
|
+
image dataset (use `files()` / `images()`).
|
|
240
|
+
|
|
241
|
+
## Errors
|
|
242
|
+
|
|
243
|
+
| Exception | When |
|
|
244
|
+
|---|---|
|
|
245
|
+
| `AuthError` | The key is missing or invalid. |
|
|
246
|
+
| `ForbiddenError` | Your plan or key doesn't allow it: a range over the plan limit, image history on the free plan, a dataset outside the key's scope, a revoked key. |
|
|
247
|
+
| `NotFoundError` | No such dataset. |
|
|
248
|
+
| `NotQueryableError` | The dataset exists, but `get()` can't serve it yet (see `reason`). |
|
|
249
|
+
| `QuotaExceededError` | This month's rows or images are used up. Raised immediately, never retried. |
|
|
250
|
+
| `RateLimitError` | Still over your plan's concurrent-request or requests-per-minute limit after waiting ~5 minutes. |
|
|
251
|
+
| `SunsightError` (code `too_large`, HTTP 413) | Even an hour of the range is too much data for one request (the SDK splits larger windows in half on its own). Use fewer columns. |
|
|
252
|
+
|
|
253
|
+
All of them subclass `ss.SunsightError` and include the server's message, the
|
|
254
|
+
HTTP `status_code`, and a machine-readable `code` such as `range_exceeded`,
|
|
255
|
+
`not_scoped`, `quota_rows`, `quota_images` or `rate_limited`:
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
try:
|
|
259
|
+
df = c.get("pjm_load", "2026-01-01", "2026-03-01")
|
|
260
|
+
except ss.ForbiddenError as e:
|
|
261
|
+
if e.code == "range_exceeded":
|
|
262
|
+
...
|
|
263
|
+
```
|
|
264
|
+
Network errors and server errors are retried (3 times by default).
|
|
265
|
+
|
|
266
|
+
## Plan limits
|
|
267
|
+
|
|
268
|
+
Each plan limits:
|
|
269
|
+
|
|
270
|
+
| Plan | Requests at a time | Requests per minute | Days per request | Rows per month | Image history |
|
|
271
|
+
|---|---|---|---|---|---|
|
|
272
|
+
| Free | 1 | 60 | 7 | 500,000 | latest 1 day |
|
|
273
|
+
| Starter | 3 | 300 | 90 | 5,000,000 | full |
|
|
274
|
+
| Pro | 10 | 1,200 | 365 | 50,000,000 | full |
|
|
275
|
+
| Enterprise | 50 | 6,000 | 366 | unlimited | full |
|
|
276
|
+
|
|
277
|
+
The SDK reads your limits automatically (`c.usage()` shows them, plus rows
|
|
278
|
+
used this month), so a "days per request" limit never shows up in your code.
|
|
279
|
+
If you hit the per-minute limit, the SDK waits until the next minute and
|
|
280
|
+
carries on (one log line says so). Every response carries
|
|
281
|
+
`X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`.
|
|
282
|
+
|
|
283
|
+
## Notebooks, scripts and pipelines
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
c = ss.Client(
|
|
287
|
+
progress=None, # None: bars in notebooks/terminals, off when output is redirected
|
|
288
|
+
log_level="INFO", # one line per call; "DEBUG" for every request; "WARNING" for quiet
|
|
289
|
+
)
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
- `SUNSIGHT_API_KEY`: the key, so it stays out of your notebook.
|
|
293
|
+
- `SUNSIGHT_BASE_URL`: point at another environment (e.g. `http://localhost:8001`).
|
|
294
|
+
- `SUNSIGHT_PROGRESS=0`: turn progress bars off everywhere (CI, cron).
|
|
295
|
+
- Logs go to the standard `logging` logger named `sunsightenergy`. If your app
|
|
296
|
+
already configures logging, the SDK adds no handler of its own.
|
|
297
|
+
|
|
298
|
+
## Without the SDK
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
curl -H "X-API-Key: ss_live_..." \
|
|
302
|
+
"https://api.sunsightenergy.com/v1/datasets/pjm_load/query?start_time=2026-09-01&end_time=2026-09-02"
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Add `return_format=csv` or `return_format=parquet` for those formats. The
|
|
306
|
+
next page's cursor is in the `X-Next-Cursor` response header.
|
|
307
|
+
|
|
308
|
+
## License
|
|
309
|
+
|
|
310
|
+
MIT
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# Sunsight Energy Python SDK
|
|
2
|
+
|
|
3
|
+
Python client for the [Sunsight Energy](https://sunsightenergy.com) data API:
|
|
4
|
+
real-time and historical data for US/Canadian ISOs (ERCOT, PJM, CAISO, SPP,
|
|
5
|
+
MISO, NYISO, ISONE, IESO), weather and satellite imagery.
|
|
6
|
+
|
|
7
|
+
You ask for **a dataset and a time range**. The SDK handles paging, your
|
|
8
|
+
plan's date-range limit, retries and file formats, and hands back a pandas or
|
|
9
|
+
polars DataFrame, parquet files on disk, or images in a folder.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install sunsightenergy # pandas
|
|
15
|
+
pip install "sunsightenergy[polars]" # + polars output
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Python 3.10+. Works in Jupyter, Google Colab, scripts and pipelines.
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
import sunsightenergy as ss
|
|
24
|
+
|
|
25
|
+
c = ss.Client("ss_live_...") # or set SUNSIGHT_API_KEY and use ss.Client()
|
|
26
|
+
|
|
27
|
+
df = c.get("ercot_load", "2026-09-01", "2026-10-01")
|
|
28
|
+
df.plot(x="interval_end_utc", y="load")
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Create a key in the portal under **API Platform → API Keys**.
|
|
32
|
+
|
|
33
|
+
## Coming from Grid Status
|
|
34
|
+
|
|
35
|
+
The client has gridstatusio's methods with the same arguments, so existing
|
|
36
|
+
code only needs a new import and key:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from sunsightenergy import Client as GridStatusClient
|
|
40
|
+
|
|
41
|
+
client = GridStatusClient("ss_live_...")
|
|
42
|
+
df = client.get_dataset(
|
|
43
|
+
dataset="ercot_load",
|
|
44
|
+
start="2026-10-04",
|
|
45
|
+
end="2026-10-05",
|
|
46
|
+
timezone="market",
|
|
47
|
+
)
|
|
48
|
+
client.list_datasets(filter_term="lmp")
|
|
49
|
+
client.get_dataset_metadata("ercot_load")
|
|
50
|
+
client.get_api_usage()
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`get_dataset` takes every Grid Status argument: `publish_time_start`,
|
|
54
|
+
`publish_time_end`, `publish_time` ("latest", "latest_report" or a
|
|
55
|
+
timestamp), `columns`, `filter_column` / `filter_value` / `filter_operator`
|
|
56
|
+
(including "in"), `resample` / `resample_by` / `resample_function`, `limit`,
|
|
57
|
+
`page_size`, `timezone`, `verbose` and `return_format` ("pandas", "polars",
|
|
58
|
+
"python"). Differences:
|
|
59
|
+
|
|
60
|
+
- `timezone` defaults to `"market"` (Grid Status defaults to UTC). Pass
|
|
61
|
+
`timezone="UTC"` for Grid Status's default output.
|
|
62
|
+
- For forecasts, `start`/`end` are delivery times as on Grid Status; the
|
|
63
|
+
client reads publications from up to 7 days before `start`. Pass
|
|
64
|
+
`publish_time_start` for forecasts published further ahead.
|
|
65
|
+
- `get_daily_peak_report` isn't available.
|
|
66
|
+
|
|
67
|
+
## Loading data
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
# pandas (default), polars, or a pyarrow Table
|
|
71
|
+
df = c.get("pjm_load", "2026-09-01", "2026-10-01")
|
|
72
|
+
pdf = c.get("pjm_load", "2026-09-01", "2026-10-01", output="polars")
|
|
73
|
+
|
|
74
|
+
# One market day: ERCOT's operating day 2026-10-04, in Central time
|
|
75
|
+
df = c.get("ercot_load", day="2026-10-04")
|
|
76
|
+
|
|
77
|
+
# Fewer columns, a filter, a row limit, another timezone
|
|
78
|
+
df = c.get(
|
|
79
|
+
"pjm_load", "2026-09-01", "2026-09-08",
|
|
80
|
+
columns=["interval_start_utc", "pjm_rto"],
|
|
81
|
+
filter=("pjm_rto", ">", 90000), # ("col", value) means "="; a list value means "in"
|
|
82
|
+
limit=1000,
|
|
83
|
+
timezone="UTC", # "market" (default), "UTC", "CST"-style or any IANA name
|
|
84
|
+
)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
- `start` is inclusive and `end` exclusive. They accept strings, `date`,
|
|
88
|
+
`datetime` or `pd.Timestamp`. **A value without a timezone is read in
|
|
89
|
+
`timezone`, which defaults to the market's own clock** (Central for ERCOT
|
|
90
|
+
and SPP, Eastern for PJM, NYISO and ISO-NE, Pacific for CAISO, fixed EST
|
|
91
|
+
for MISO and IESO). So `"2026-10-04"` on an ERCOT dataset is midnight
|
|
92
|
+
Central. `end` defaults to now; `day=` is one whole day.
|
|
93
|
+
- Interval datasets filter on `interval_start_utc`, so a day is the intervals
|
|
94
|
+
that start in it (as on Grid Status).
|
|
95
|
+
- Every `*_utc` column stays UTC and gets a `*_local` twin in the query
|
|
96
|
+
timezone. `timezone="UTC"` returns only the UTC columns.
|
|
97
|
+
- Long ranges are split into the windows your plan allows and fetched in
|
|
98
|
+
parallel where your plan allows, with one progress bar.
|
|
99
|
+
- Timestamps come back as real timezone-aware datetimes; numbers as numbers.
|
|
100
|
+
|
|
101
|
+
### Large ranges: save to disk
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
paths = c.download("pjm_load", "2025-01-01", "2026-01-01", dir="data/")
|
|
105
|
+
# data/pjm_load/2025-01-01.parquet, data/pjm_load/2025-01-02.parquet, ...
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
- One parquet file per day in the query timezone (the market's by default).
|
|
109
|
+
The folder is created if it doesn't exist.
|
|
110
|
+
- **Every run fetches every day again and overwrites its file**, so the
|
|
111
|
+
folder always matches the server. A day that no longer has data has its
|
|
112
|
+
old file removed.
|
|
113
|
+
- Read the folder back with any tool:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
import pandas as pd, polars as pl
|
|
117
|
+
pd.read_parquet("data/pjm_load")
|
|
118
|
+
pl.scan_parquet("data/pjm_load/*.parquet").collect()
|
|
119
|
+
# DuckDB: SELECT * FROM read_parquet('data/pjm_load/*.parquet')
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Process piece by piece
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
for chunk in c.stream("pjm_load", "2025-01-01", "2026-01-01"):
|
|
126
|
+
print(len(chunk)) # one DataFrame per window; memory stays flat
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Forecasts
|
|
130
|
+
|
|
131
|
+
A forecast is published many times (ERCOT's 7-day load forecast: every hour),
|
|
132
|
+
so each delivery hour has many predictions. `start`/`end` select
|
|
133
|
+
**publications**: you get every forecast published in `[start, end)`, each
|
|
134
|
+
with its full horizon and its `publish_time_utc`.
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
ds = "ercot_load_forecast_by_forecast_zone"
|
|
138
|
+
c.info(ds) # kind: forecast; time column (start/end): publish_time_utc
|
|
139
|
+
|
|
140
|
+
all_ = c.get(ds, "2026-10-01", "2026-10-02") # 24 publications x 7-day horizon
|
|
141
|
+
best = c.get(ds, "2026-10-01", "2026-10-02", vintage="latest") # newest forecast per delivery hour
|
|
142
|
+
then = c.get(ds, as_of="2026-10-01T09:00Z", # the forecast as it stood at 09:00,
|
|
143
|
+
start="2026-10-02", end="2026-10-03") # cut to delivery on Oct 2
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
- `vintage="latest"` keeps, for each delivery interval, the newest publication
|
|
147
|
+
in the window. Rows still carry `publish_time_utc`, so you see which won.
|
|
148
|
+
- `as_of=` returns one whole publication: the newest at or before that time
|
|
149
|
+
(looking back up to 7 days). `start`/`end` are optional and cut its
|
|
150
|
+
delivery times. Use it for backtests: "what did we know at 9am?"
|
|
151
|
+
- `download()` saves forecasts per **publish** day.
|
|
152
|
+
- Snapshot datasets (reference lists, outage snapshots) work the same way.
|
|
153
|
+
|
|
154
|
+
## IoT sensors
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
c.devices() # device_id + when it last checked in
|
|
158
|
+
df = c.get("sensor_readings", "2026-10-02", "2026-10-03",
|
|
159
|
+
devices=["sunsight-001"], # default: all devices
|
|
160
|
+
images=True) # adds image_url: the sky photo for that reading
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Device data is stored as hundreds of small files per device-day, so each
|
|
164
|
+
request covers at most 7 days (`get()` splits longer ranges for you).
|
|
165
|
+
`images=True` counts each distinct photo against your monthly files & images
|
|
166
|
+
quota.
|
|
167
|
+
|
|
168
|
+
## Raw files
|
|
169
|
+
|
|
170
|
+
Original source files, byte for byte (PJM eDART outage reports, PJM
|
|
171
|
+
workbooks):
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
f = c.files("pjm_transmission_outages_raw", "2026-10-02", "2026-10-03",
|
|
175
|
+
source="linesout", dir="raw/") # source= is optional: default all
|
|
176
|
+
f[["captured_at", "source", "filename", "local_path"]]
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Same columns and options as `images()`, plus `source`. Files share the
|
|
180
|
+
monthly files & images quota.
|
|
181
|
+
|
|
182
|
+
## Images
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
# A table of images with download links (links last ~15 minutes)
|
|
186
|
+
imgs = c.images("goes19_regional_southcentral_truecolor", "2026-09-30", "2026-10-01")
|
|
187
|
+
|
|
188
|
+
# Also save them: imgs/goes19_regional_southcentral_truecolor/2026-09-30/<file>.jpg
|
|
189
|
+
imgs = c.images("goes19_regional_southcentral_truecolor", "2026-09-30", "2026-10-01", dir="imgs/")
|
|
190
|
+
imgs[["captured_at", "local_path"]]
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Columns: `captured_at` (UTC), `filename`, `size_bytes`, `url` and, with
|
|
194
|
+
`dir=`, `local_path`. Saving runs in parallel, skips files you already have,
|
|
195
|
+
and recovers when a link expires mid-run. On networks that block direct
|
|
196
|
+
storage links, pass `via="api"`.
|
|
197
|
+
|
|
198
|
+
## Finding datasets
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
c.datasets() # everything your key can see
|
|
202
|
+
c.datasets(search="ercot load") # every word must match id, name, description or tags
|
|
203
|
+
c.datasets(queryable=True) # only what get() can serve today
|
|
204
|
+
|
|
205
|
+
c.info("pjm_load") # columns + types, time column, data range
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
A dataset with `queryable == False` has a `reason`, e.g. it is a file or
|
|
209
|
+
image dataset (use `files()` / `images()`).
|
|
210
|
+
|
|
211
|
+
## Errors
|
|
212
|
+
|
|
213
|
+
| Exception | When |
|
|
214
|
+
|---|---|
|
|
215
|
+
| `AuthError` | The key is missing or invalid. |
|
|
216
|
+
| `ForbiddenError` | Your plan or key doesn't allow it: a range over the plan limit, image history on the free plan, a dataset outside the key's scope, a revoked key. |
|
|
217
|
+
| `NotFoundError` | No such dataset. |
|
|
218
|
+
| `NotQueryableError` | The dataset exists, but `get()` can't serve it yet (see `reason`). |
|
|
219
|
+
| `QuotaExceededError` | This month's rows or images are used up. Raised immediately, never retried. |
|
|
220
|
+
| `RateLimitError` | Still over your plan's concurrent-request or requests-per-minute limit after waiting ~5 minutes. |
|
|
221
|
+
| `SunsightError` (code `too_large`, HTTP 413) | Even an hour of the range is too much data for one request (the SDK splits larger windows in half on its own). Use fewer columns. |
|
|
222
|
+
|
|
223
|
+
All of them subclass `ss.SunsightError` and include the server's message, the
|
|
224
|
+
HTTP `status_code`, and a machine-readable `code` such as `range_exceeded`,
|
|
225
|
+
`not_scoped`, `quota_rows`, `quota_images` or `rate_limited`:
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
try:
|
|
229
|
+
df = c.get("pjm_load", "2026-01-01", "2026-03-01")
|
|
230
|
+
except ss.ForbiddenError as e:
|
|
231
|
+
if e.code == "range_exceeded":
|
|
232
|
+
...
|
|
233
|
+
```
|
|
234
|
+
Network errors and server errors are retried (3 times by default).
|
|
235
|
+
|
|
236
|
+
## Plan limits
|
|
237
|
+
|
|
238
|
+
Each plan limits:
|
|
239
|
+
|
|
240
|
+
| Plan | Requests at a time | Requests per minute | Days per request | Rows per month | Image history |
|
|
241
|
+
|---|---|---|---|---|---|
|
|
242
|
+
| Free | 1 | 60 | 7 | 500,000 | latest 1 day |
|
|
243
|
+
| Starter | 3 | 300 | 90 | 5,000,000 | full |
|
|
244
|
+
| Pro | 10 | 1,200 | 365 | 50,000,000 | full |
|
|
245
|
+
| Enterprise | 50 | 6,000 | 366 | unlimited | full |
|
|
246
|
+
|
|
247
|
+
The SDK reads your limits automatically (`c.usage()` shows them, plus rows
|
|
248
|
+
used this month), so a "days per request" limit never shows up in your code.
|
|
249
|
+
If you hit the per-minute limit, the SDK waits until the next minute and
|
|
250
|
+
carries on (one log line says so). Every response carries
|
|
251
|
+
`X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`.
|
|
252
|
+
|
|
253
|
+
## Notebooks, scripts and pipelines
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
c = ss.Client(
|
|
257
|
+
progress=None, # None: bars in notebooks/terminals, off when output is redirected
|
|
258
|
+
log_level="INFO", # one line per call; "DEBUG" for every request; "WARNING" for quiet
|
|
259
|
+
)
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
- `SUNSIGHT_API_KEY`: the key, so it stays out of your notebook.
|
|
263
|
+
- `SUNSIGHT_BASE_URL`: point at another environment (e.g. `http://localhost:8001`).
|
|
264
|
+
- `SUNSIGHT_PROGRESS=0`: turn progress bars off everywhere (CI, cron).
|
|
265
|
+
- Logs go to the standard `logging` logger named `sunsightenergy`. If your app
|
|
266
|
+
already configures logging, the SDK adds no handler of its own.
|
|
267
|
+
|
|
268
|
+
## Without the SDK
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
curl -H "X-API-Key: ss_live_..." \
|
|
272
|
+
"https://api.sunsightenergy.com/v1/datasets/pjm_load/query?start_time=2026-09-01&end_time=2026-09-02"
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Add `return_format=csv` or `return_format=parquet` for those formats. The
|
|
276
|
+
next page's cursor is in the `X-Next-Cursor` response header.
|
|
277
|
+
|
|
278
|
+
## License
|
|
279
|
+
|
|
280
|
+
MIT
|