pse-opendata 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.
Files changed (60) hide show
  1. pse_opendata-0.2.0/CHANGELOG.md +43 -0
  2. pse_opendata-0.2.0/CITATION.cff +31 -0
  3. pse_opendata-0.2.0/LICENSE +21 -0
  4. pse_opendata-0.2.0/MANIFEST.in +3 -0
  5. pse_opendata-0.2.0/PKG-INFO +194 -0
  6. pse_opendata-0.2.0/README.md +156 -0
  7. pse_opendata-0.2.0/pyproject.toml +76 -0
  8. pse_opendata-0.2.0/setup.cfg +4 -0
  9. pse_opendata-0.2.0/src/pse_opendata/__init__.py +110 -0
  10. pse_opendata-0.2.0/src/pse_opendata/__main__.py +3 -0
  11. pse_opendata-0.2.0/src/pse_opendata/_http.py +126 -0
  12. pse_opendata-0.2.0/src/pse_opendata/_version.py +1 -0
  13. pse_opendata-0.2.0/src/pse_opendata/cache.py +108 -0
  14. pse_opendata-0.2.0/src/pse_opendata/catalog.py +590 -0
  15. pse_opendata-0.2.0/src/pse_opendata/cli.py +150 -0
  16. pse_opendata-0.2.0/src/pse_opendata/client.py +455 -0
  17. pse_opendata-0.2.0/src/pse_opendata/exceptions.py +39 -0
  18. pse_opendata-0.2.0/src/pse_opendata/frames.py +139 -0
  19. pse_opendata-0.2.0/src/pse_opendata/legacy_csv.py +119 -0
  20. pse_opendata-0.2.0/src/pse_opendata/py.typed +0 -0
  21. pse_opendata-0.2.0/src/pse_opendata/stitch.py +75 -0
  22. pse_opendata-0.2.0/src/pse_opendata.egg-info/PKG-INFO +194 -0
  23. pse_opendata-0.2.0/src/pse_opendata.egg-info/SOURCES.txt +58 -0
  24. pse_opendata-0.2.0/src/pse_opendata.egg-info/dependency_links.txt +1 -0
  25. pse_opendata-0.2.0/src/pse_opendata.egg-info/entry_points.txt +2 -0
  26. pse_opendata-0.2.0/src/pse_opendata.egg-info/requires.txt +15 -0
  27. pse_opendata-0.2.0/src/pse_opendata.egg-info/top_level.txt +1 -0
  28. pse_opendata-0.2.0/tests/conftest.py +174 -0
  29. pse_opendata-0.2.0/tests/fixtures/README.md +13 -0
  30. pse_opendata-0.2.0/tests/fixtures/api/cmbp-tp_2025-03-30.json +1 -0
  31. pse_opendata-0.2.0/tests/fixtures/api/cmbp-tp_2025-10-26.json +1 -0
  32. pse_opendata-0.2.0/tests/fixtures/api/his-wlk-cal_2024-06-13_2024-06-14.json +1 -0
  33. pse_opendata-0.2.0/tests/fixtures/api/his-wlk-cal_2024-06-14_2024-06-27.json +1 -0
  34. pse_opendata-0.2.0/tests/fixtures/api/his-wlk-cal_2026-10-26_partial.json +1 -0
  35. pse_opendata-0.2.0/tests/fixtures/api/popmb-rmb_2025-10-02_first_hour.json +1 -0
  36. pse_opendata-0.2.0/tests/fixtures/api/poze-redoze_2024-09-24_first_rows.json +1 -0
  37. pse_opendata-0.2.0/tests/fixtures/api/przeplywy-mocy_2025-10-02_first_interval.json +1 -0
  38. pse_opendata-0.2.0/tests/fixtures/api/rce-pln_2025-03-30.json +1 -0
  39. pse_opendata-0.2.0/tests/fixtures/api/rce-pln_2025-09-30_2025-10-01.json +1 -0
  40. pse_opendata-0.2.0/tests/fixtures/api/rce-pln_2025-10-02_paged.json +1 -0
  41. pse_opendata-0.2.0/tests/fixtures/api/rce-pln_2025-10-26.json +1 -0
  42. pse_opendata-0.2.0/tests/fixtures/legacy/PL_CENY_ROZL_RB_2019-01-01_2019-01-03.csv +73 -0
  43. pse_opendata-0.2.0/tests/fixtures/legacy/PL_CENY_ROZL_RB_2023-10-28_2023-10-30.csv +74 -0
  44. pse_opendata-0.2.0/tests/fixtures/legacy/PL_CENY_RYN_EN_2023-10-28_2023-10-30.csv +74 -0
  45. pse_opendata-0.2.0/tests/fixtures/legacy/PL_GEN_WIATR_2020-04-09_2020-04-11.csv +73 -0
  46. pse_opendata-0.2.0/tests/fixtures/legacy/PL_GEN_WIATR_2023-10-28_2023-10-30.csv +74 -0
  47. pse_opendata-0.2.0/tests/fixtures/legacy/PL_WYK_KSE_2008-01-01_2008-01-03_404.csv +32 -0
  48. pse_opendata-0.2.0/tests/fixtures/legacy/PL_WYK_KSE_2010-01-01_2010-01-03.csv +73 -0
  49. pse_opendata-0.2.0/tests/fixtures/legacy/PL_WYK_KSE_2023-03-25_2023-03-27.csv +72 -0
  50. pse_opendata-0.2.0/tests/fixtures/legacy/PL_WYK_KSE_2023-10-28_2023-10-30.csv +74 -0
  51. pse_opendata-0.2.0/tests/fixtures/legacy/PL_WYK_KSE_2024-05-29_2024-06-13.csv +385 -0
  52. pse_opendata-0.2.0/tests/live/test_live.py +64 -0
  53. pse_opendata-0.2.0/tests/test_cache.py +56 -0
  54. pse_opendata-0.2.0/tests/test_catalog.py +53 -0
  55. pse_opendata-0.2.0/tests/test_cli.py +72 -0
  56. pse_opendata-0.2.0/tests/test_client.py +289 -0
  57. pse_opendata-0.2.0/tests/test_frames.py +153 -0
  58. pse_opendata-0.2.0/tests/test_history.py +98 -0
  59. pse_opendata-0.2.0/tests/test_http.py +119 -0
  60. pse_opendata-0.2.0/tests/test_legacy.py +154 -0
@@ -0,0 +1,43 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses [semantic versioning](https://semver.org/).
5
+
6
+ ## [0.2.0] - 2026-10-30
7
+
8
+ First release on PyPI.
9
+
10
+ ### Added
11
+
12
+ - `Client.loads`: one `MonthLoad` per month loaded (cached, downloaded or stale, with the download time and the
13
+ number of rows), e.g. to show data freshness on a dashboard.
14
+ - `CITATION.cff` and Zenodo metadata.
15
+
16
+ ### Changed
17
+
18
+ - Installation from PyPI (`pip install pse-opendata`) in the README and the documentation.
19
+
20
+ ## [0.1.0] - 2026-10-26
21
+
22
+ First version, published on GitHub only (commit 8b01ec8), not on PyPI.
23
+
24
+ ### Added
25
+
26
+ - `get(report, start, end)` for 16 reports of the PSE reports API (prices, balancing market, power system), returning
27
+ frames with `start_utc`, `end_utc`, `start_local`, `end_local` and `business_date` on every row.
28
+ - Convenience functions `rce`, `sdac`, `fundamentals`, `imbalance_prices` and `balancing_capacity`.
29
+ - Report catalogue with titles, field descriptions, units, resolution, first date, revision window and PSE sources,
30
+ checked on 2026-10-26; unconfirmed units are marked unverified.
31
+ - Legacy CSV export of www.pse.pl (`legacy`): `PL_WYK_KSE`, `PL_GEN_WIATR`, `PL_CENY_RYN_EN`, `PL_CENY_ROZL_RB`,
32
+ hourly until 2024-06-13, with DST hour labels, changing columns and rate limits handled.
33
+ - `history` for hourly `rce`, `demand`, `wind` and `pv` series stitched across the switch to the API on 2024-06-14,
34
+ with optional rescaling (`splice_factor`).
35
+ - Monthly Parquet cache with per-report revision windows, offline and stale modes; retries with backoff, request
36
+ spacing, explicit exceptions and logging.
37
+ - Day-by-day single-page requests for `popmb-rmb`, whose keyset paging skips rows.
38
+ - Command line interface `pse-opendata` (`get`, `legacy`, `history`, `reports`, `cache`).
39
+ - Offline tests on recorded PSE responses, optional live tests, ruff, mypy, GitHub Actions for Python 3.11-3.13,
40
+ MkDocs documentation and a release workflow for PyPI Trusted Publishing (disabled).
41
+
42
+ [0.2.0]: https://github.com/kwwaleszczynski-del/pse-opendata/releases/tag/v0.2.0
43
+ [0.1.0]: https://github.com/kwwaleszczynski-del/pse-opendata/tree/8b01ec882b6f3af9c9ea91308293dda64108d705
@@ -0,0 +1,31 @@
1
+ cff-version: 1.2.0
2
+ message: "If you use this software, please cite it using the metadata below."
3
+ type: software
4
+ title: "pse-opendata: Python client for the public data of PSE, the Polish transmission system operator"
5
+ abstract: >-
6
+ An unofficial Python client for the public data of PSE S.A., the Polish transmission system operator: the
7
+ reports API (market prices RCE and SDAC, demand and generation, the balancing market, cross-border flows and
8
+ redispatch of renewables) and the legacy CSV export with the history before June 2024. It returns tidy pandas
9
+ frames with explicit interval start and end times in UTC and Polish local time, caches monthly Parquet files
10
+ with per-report revision windows and handles the pitfalls of the sources: interval-end time stamps, daylight
11
+ saving days, hourly products repeated in quarter-hours, republished settlement data, rate limits and keyset
12
+ paging that skips repeated balancing capacity offers. Not affiliated with PSE S.A.
13
+ authors:
14
+ - family-names: "Waleszczyński"
15
+ given-names: "Konrad"
16
+ version: 0.2.0
17
+ date-released: "2026-10-30"
18
+ license: MIT
19
+ repository-code: "https://github.com/kwwaleszczynski-del/pse-opendata"
20
+ url: "https://kwwaleszczynski-del.github.io/pse-opendata/"
21
+ keywords:
22
+ - electricity market
23
+ - power system
24
+ - open data
25
+ - Poland
26
+ - PSE
27
+ - transmission system operator
28
+ - balancing market
29
+ - day-ahead prices
30
+ - renewable energy
31
+ - pandas
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Konrad Waleszczyński
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ include CHANGELOG.md
2
+ include CITATION.cff
3
+ recursive-include tests *.py *.json *.csv *.md
@@ -0,0 +1,194 @@
1
+ Metadata-Version: 2.4
2
+ Name: pse-opendata
3
+ Version: 0.2.0
4
+ Summary: Unofficial Python client for the public data of PSE, the Polish transmission system operator: reports API and legacy CSV export, as tidy pandas frames with explicit interval times.
5
+ Author: Konrad Waleszczyński
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/kwwaleszczynski-del/pse-opendata
8
+ Project-URL: Documentation, https://kwwaleszczynski-del.github.io/pse-opendata/
9
+ Project-URL: Changelog, https://github.com/kwwaleszczynski-del/pse-opendata/blob/main/CHANGELOG.md
10
+ Keywords: pse,poland,electricity,power-system,energy-market,rce,balancing-market,open-data,pandas
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
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
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: pandas>=2.1
25
+ Requires-Dist: pyarrow>=14
26
+ Requires-Dist: requests>=2.31
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=8; extra == "dev"
29
+ Requires-Dist: ruff>=0.6; extra == "dev"
30
+ Requires-Dist: mypy>=1.10; extra == "dev"
31
+ Requires-Dist: types-requests; extra == "dev"
32
+ Requires-Dist: build>=1.2; extra == "dev"
33
+ Provides-Extra: docs
34
+ Requires-Dist: mkdocs>=1.6; extra == "docs"
35
+ Requires-Dist: mkdocs-material>=9.5; extra == "docs"
36
+ Requires-Dist: mkdocstrings[python]>=0.26; extra == "docs"
37
+ Dynamic: license-file
38
+
39
+ # pse-opendata
40
+
41
+ [![tests](https://github.com/kwwaleszczynski-del/pse-opendata/actions/workflows/ci.yml/badge.svg)](https://github.com/kwwaleszczynski-del/pse-opendata/actions/workflows/ci.yml)
42
+ [![docs](https://img.shields.io/badge/docs-github%20pages-blue)](https://kwwaleszczynski-del.github.io/pse-opendata/)
43
+ [![PyPI](https://img.shields.io/pypi/v/pse-opendata.svg)](https://pypi.org/project/pse-opendata/)
44
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
45
+
46
+ Unofficial Python client for the public data of **PSE S.A.**, the Polish transmission system operator: market
47
+ prices (RCE, SDAC), demand and generation, the balancing market (imbalance prices, balancing capacity, offers),
48
+ cross-border flows and redispatch of renewables. It returns tidy `pandas` frames with explicit interval start and
49
+ end times in UTC and Polish local time, caches monthly Parquet files and also reads the legacy CSV export that holds
50
+ the history before June 2024.
51
+
52
+ > This is an independent open-source project. It is not affiliated with, endorsed by or supported by PSE S.A.
53
+
54
+ ## Why
55
+
56
+ PSE publishes its data openly at [api.raporty.pse.pl](https://api.raporty.pse.pl) (from 14 June 2024) and, for
57
+ earlier years, through the CSV export of [www.pse.pl](https://www.pse.pl/raporty-historyczne). Both are easy to
58
+ download and easy to get subtly wrong:
59
+
60
+ - `dtime_utc` is the **end** of each interval, not the start; local `dtime` reads `02a:15` on the autumn DST day;
61
+ - the API pages with `$first` and `nextLink`, at most 100,000 rows per page, and its keyset paging **skips rows**
62
+ of the balancing capacity offers (`popmb-rmb`) at page boundaries;
63
+ - RCE and SDAC were hourly products until 30 September 2025: the API repeats the hourly price in four quarter-hours;
64
+ - settlement reports are republished for months after the fact (imbalance volumes up to ~230 days later);
65
+ - the legacy export is slow, rate-limited, takes one month per request, labels the extra autumn hour `2A`, changes
66
+ its columns over the years and does not overlap with the API, whose demand sits ~0.7% below it at the switch.
67
+
68
+ `pse-opendata` handles these once, with tests on recorded responses, instead of a copy of a small client in every
69
+ analysis project. Details: [time and data pitfalls](https://kwwaleszczynski-del.github.io/pse-opendata/pitfalls/).
70
+
71
+ ## Install
72
+
73
+ Python 3.11 or newer:
74
+
75
+ ```bash
76
+ pip install pse-opendata
77
+ ```
78
+
79
+ ## Quick start
80
+
81
+ ```python
82
+ import pse_opendata as pse
83
+
84
+ rce = pse.rce("2025-01-01", "2025-01-31") # RCE price, PLN/MWh, 15-minute rows
85
+ mix = pse.fundamentals("2025-06-01", "2025-06-30", fields=["demand", "wi", "pv"]) # MW
86
+ cen = pse.imbalance_prices("2025-07-01", "2025-07-31") # imbalance price CEN and balancing energy prices
87
+ cap = pse.balancing_capacity("2025-07-01", "2025-07-31") # FCR/aFRR/mFRRd/RR prices and MW, hourly
88
+ sdac = pse.get("csdac-pln", "2025-10-01", "2025-10-07") # any catalogued report by name
89
+
90
+ demand = pse.history("demand", "2024-01-01", "2024-12-31") # legacy export + API, hourly, one series
91
+ ```
92
+
93
+ Every frame has the same time columns, one row per interval:
94
+
95
+ | column | meaning |
96
+ |---|---|
97
+ | `start_utc`, `end_utc` | interval start and end, `datetime64[ns, UTC]` |
98
+ | `start_local`, `end_local` | the same instants in `Europe/Warsaw` (DST days are unambiguous) |
99
+ | `business_date` | trading day in Polish local time (`datetime.date`) |
100
+
101
+ followed by row keys (for reports with several rows per interval) and the fields of the report, with PSE's field
102
+ names (e.g. `rce_pln`, `demand`, `wi`, `pv`, `cen_cost`). Dates are business days, inclusive.
103
+
104
+ From the command line:
105
+
106
+ ```bash
107
+ pse-opendata get rce-pln --start 2025-01-01 --end 2025-01-31 --out rce.parquet
108
+ pse-opendata get his-wlk-cal --start 2025-06-01 --fields demand,wi,pv --out mix.csv
109
+ pse-opendata history demand --start 2024-01-01 --end 2024-12-31 --out demand.parquet
110
+ pse-opendata reports
111
+ ```
112
+
113
+ ## Catalogue
114
+
115
+ | Report | Content | Resolution | From |
116
+ |---|---|---|---|
117
+ | `rce-pln` | RCE, market price of electricity | 15 min (hourly product until 2025-09-30) | 2024-06-14 |
118
+ | `csdac-pln` | SDAC day-ahead price for Poland | 15 min (hourly product until 2025-09-30) | 2024-06-14 |
119
+ | `crb-rozl` | Imbalance price CEN and balancing energy prices | 15 min | 2024-06-14 |
120
+ | `en-rozl` | Imbalance energy volumes | 15 min | 2024-06-14 |
121
+ | `eb-rozl` | Activated balancing energy, incl. aFRR platform | 15 min | 2024-06-14 |
122
+ | `cmbp-tp`, `mbp-tp` | Balancing capacity prices and volumes, primary procurement | 1 h | 2024-06-14 |
123
+ | `cmbu-tu`, `mbu-tu` | Balancing capacity prices and volumes, supplementary procurement | 15 min | 2024-06-14 |
124
+ | `popmb-rmb` | Anonymous portfolio offers for balancing capacity | 1 h, many rows | 2024-06-14 |
125
+ | `his-wlk-cal` | Demand, generation by unit type, wind, PV, exchange | 15 min | 2024-06-14 |
126
+ | `kse-load` | Forecast and actual load | 15 min | 2024-06-14 |
127
+ | `pdgopkd` | Day-ahead coordinated plan (forecast demand, wind, PV, reserves) | 15 min | 2024-06-14 |
128
+ | `poze-redoze` | Non-market redispatch of wind and PV | 15 min | 2024-09-24 |
129
+ | `przeplywy-mocy` | Physical cross-border flows per section | 15 min, many rows | 2024-06-14 |
130
+ | `gen-jw` | Generation per unit (≥ 100 MW) | 15 min, many rows | 2024-06-14 |
131
+
132
+ Legacy export (hourly, until 2024-06-13): `PL_WYK_KSE` (demand, JWCD/nJWCD generation, exchange, from 2009),
133
+ `PL_GEN_WIATR` (wind from 2012-05-26, PV from 2020-04-10), `PL_CENY_RYN_EN` (RCE, from 2018),
134
+ `PL_CENY_ROZL_RB` (imbalance prices CRO of the old balancing market, from 2009). The legacy server is slow (about a
135
+ minute per month, one month per request), so long histories take a while the first time and are then cached.
136
+ Field descriptions, units, sources and notes for each report:
137
+ [report catalogue](https://kwwaleszczynski-del.github.io/pse-opendata/reports/). Other endpoints of the API can
138
+ be read raw with `pse.Client().records(endpoint, filter=...)`.
139
+
140
+ ## Cache and reliability
141
+
142
+ - Whole months are cached as Parquet files (`$PSE_OPENDATA_CACHE` or the user cache directory; `Client(cache_dir=...)`
143
+ to choose, `cache_dir=None` to switch off). A month is reused only once it was downloaded after the report's
144
+ revision window (3 days for prices, up to 240 days for settlement volumes); the legacy history never changes.
145
+ - Requests retry with exponential backoff on network errors, HTTP 429 and 5xx (honouring `Retry-After`) and are
146
+ spaced (0.2 s for the API, 15 s for the legacy export).
147
+ - `offline=True` reads the cache only; `allow_stale=True` falls back to a cached copy when a download fails.
148
+ - Errors are explicit: `PseApiError` (with the API's message), `UnknownReportError`, `UnknownFieldError`,
149
+ `CacheMissError`, `LegacyFormatError`. Logging goes to the `pse_opendata` logger.
150
+
151
+ ## Data source and terms of use
152
+
153
+ All data come from public reports of **PSE S.A.** (Polskie Sieci Elektroenergetyczne): the reports API
154
+ `https://api.raporty.pse.pl/api` and the export of www.pse.pl. No key is needed. This package does not include or
155
+ redistribute PSE data, except small samples of real responses used as test fixtures (see
156
+ [tests/fixtures/README.md](tests/fixtures/README.md)).
157
+
158
+ PSE publishes conditions for re-using public sector information
159
+ ([BIP, "Ponowne wykorzystywanie informacji sektora publicznego"](https://www.pse.pl/bip/ponowne-wykorzystanie-informacji-publicznej),
160
+ entry changed on 14 October 2026, checked on 26 October 2026). They allow commercial and non-commercial re-use of
161
+ information published in the BIP and on www.pse.pl free of charge, on condition that you name the source and the date
162
+ the information was obtained (e.g. "Informacja pozyskana od PSE S.A. według stanu strony www.pse.pl na dzień
163
+ [data]"), remove the PSE logo, keep unchanged information unchanged and, when you pass on processed information, tell
164
+ recipients that it was changed and how, without misleading them. PSE accepts no liability for processed information,
165
+ and some materials may be protected by copyright or database rights. The conditions are written for www.pse.pl;
166
+ the reports API is run by the same operator. When you publish results built with this package, add the attribution
167
+ yourself, for example:
168
+
169
+ > Information obtained from www.pse.pl (PSE S.A. report `rce-pln`), state as of [date]. The information has been
170
+ > processed in whole or in part.
171
+
172
+ ## Documentation and development
173
+
174
+ Documentation: <https://kwwaleszczynski-del.github.io/pse-opendata/> (installation, examples, catalogue, time
175
+ pitfalls, API reference). To work on the package:
176
+
177
+ ```bash
178
+ python -m venv .venv
179
+ .venv/Scripts/python -m pip install -e ".[dev,docs]" # Linux/macOS: .venv/bin/python
180
+ pytest -q # offline, recorded responses
181
+ pytest -q --live tests/live # optional: calls the real PSE servers
182
+ ruff check .
183
+ mypy
184
+ mkdocs build --strict
185
+ ```
186
+
187
+ ## Citation
188
+
189
+ If you use the package in published work, please cite it; see [`CITATION.cff`](CITATION.cff).
190
+
191
+ ## License
192
+
193
+ Code: MIT, see [LICENSE](LICENSE). Data: PSE S.A. public reports, see
194
+ [Data source and terms of use](#data-source-and-terms-of-use).
@@ -0,0 +1,156 @@
1
+ # pse-opendata
2
+
3
+ [![tests](https://github.com/kwwaleszczynski-del/pse-opendata/actions/workflows/ci.yml/badge.svg)](https://github.com/kwwaleszczynski-del/pse-opendata/actions/workflows/ci.yml)
4
+ [![docs](https://img.shields.io/badge/docs-github%20pages-blue)](https://kwwaleszczynski-del.github.io/pse-opendata/)
5
+ [![PyPI](https://img.shields.io/pypi/v/pse-opendata.svg)](https://pypi.org/project/pse-opendata/)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
+
8
+ Unofficial Python client for the public data of **PSE S.A.**, the Polish transmission system operator: market
9
+ prices (RCE, SDAC), demand and generation, the balancing market (imbalance prices, balancing capacity, offers),
10
+ cross-border flows and redispatch of renewables. It returns tidy `pandas` frames with explicit interval start and
11
+ end times in UTC and Polish local time, caches monthly Parquet files and also reads the legacy CSV export that holds
12
+ the history before June 2024.
13
+
14
+ > This is an independent open-source project. It is not affiliated with, endorsed by or supported by PSE S.A.
15
+
16
+ ## Why
17
+
18
+ PSE publishes its data openly at [api.raporty.pse.pl](https://api.raporty.pse.pl) (from 14 June 2024) and, for
19
+ earlier years, through the CSV export of [www.pse.pl](https://www.pse.pl/raporty-historyczne). Both are easy to
20
+ download and easy to get subtly wrong:
21
+
22
+ - `dtime_utc` is the **end** of each interval, not the start; local `dtime` reads `02a:15` on the autumn DST day;
23
+ - the API pages with `$first` and `nextLink`, at most 100,000 rows per page, and its keyset paging **skips rows**
24
+ of the balancing capacity offers (`popmb-rmb`) at page boundaries;
25
+ - RCE and SDAC were hourly products until 30 September 2025: the API repeats the hourly price in four quarter-hours;
26
+ - settlement reports are republished for months after the fact (imbalance volumes up to ~230 days later);
27
+ - the legacy export is slow, rate-limited, takes one month per request, labels the extra autumn hour `2A`, changes
28
+ its columns over the years and does not overlap with the API, whose demand sits ~0.7% below it at the switch.
29
+
30
+ `pse-opendata` handles these once, with tests on recorded responses, instead of a copy of a small client in every
31
+ analysis project. Details: [time and data pitfalls](https://kwwaleszczynski-del.github.io/pse-opendata/pitfalls/).
32
+
33
+ ## Install
34
+
35
+ Python 3.11 or newer:
36
+
37
+ ```bash
38
+ pip install pse-opendata
39
+ ```
40
+
41
+ ## Quick start
42
+
43
+ ```python
44
+ import pse_opendata as pse
45
+
46
+ rce = pse.rce("2025-01-01", "2025-01-31") # RCE price, PLN/MWh, 15-minute rows
47
+ mix = pse.fundamentals("2025-06-01", "2025-06-30", fields=["demand", "wi", "pv"]) # MW
48
+ cen = pse.imbalance_prices("2025-07-01", "2025-07-31") # imbalance price CEN and balancing energy prices
49
+ cap = pse.balancing_capacity("2025-07-01", "2025-07-31") # FCR/aFRR/mFRRd/RR prices and MW, hourly
50
+ sdac = pse.get("csdac-pln", "2025-10-01", "2025-10-07") # any catalogued report by name
51
+
52
+ demand = pse.history("demand", "2024-01-01", "2024-12-31") # legacy export + API, hourly, one series
53
+ ```
54
+
55
+ Every frame has the same time columns, one row per interval:
56
+
57
+ | column | meaning |
58
+ |---|---|
59
+ | `start_utc`, `end_utc` | interval start and end, `datetime64[ns, UTC]` |
60
+ | `start_local`, `end_local` | the same instants in `Europe/Warsaw` (DST days are unambiguous) |
61
+ | `business_date` | trading day in Polish local time (`datetime.date`) |
62
+
63
+ followed by row keys (for reports with several rows per interval) and the fields of the report, with PSE's field
64
+ names (e.g. `rce_pln`, `demand`, `wi`, `pv`, `cen_cost`). Dates are business days, inclusive.
65
+
66
+ From the command line:
67
+
68
+ ```bash
69
+ pse-opendata get rce-pln --start 2025-01-01 --end 2025-01-31 --out rce.parquet
70
+ pse-opendata get his-wlk-cal --start 2025-06-01 --fields demand,wi,pv --out mix.csv
71
+ pse-opendata history demand --start 2024-01-01 --end 2024-12-31 --out demand.parquet
72
+ pse-opendata reports
73
+ ```
74
+
75
+ ## Catalogue
76
+
77
+ | Report | Content | Resolution | From |
78
+ |---|---|---|---|
79
+ | `rce-pln` | RCE, market price of electricity | 15 min (hourly product until 2025-09-30) | 2024-06-14 |
80
+ | `csdac-pln` | SDAC day-ahead price for Poland | 15 min (hourly product until 2025-09-30) | 2024-06-14 |
81
+ | `crb-rozl` | Imbalance price CEN and balancing energy prices | 15 min | 2024-06-14 |
82
+ | `en-rozl` | Imbalance energy volumes | 15 min | 2024-06-14 |
83
+ | `eb-rozl` | Activated balancing energy, incl. aFRR platform | 15 min | 2024-06-14 |
84
+ | `cmbp-tp`, `mbp-tp` | Balancing capacity prices and volumes, primary procurement | 1 h | 2024-06-14 |
85
+ | `cmbu-tu`, `mbu-tu` | Balancing capacity prices and volumes, supplementary procurement | 15 min | 2024-06-14 |
86
+ | `popmb-rmb` | Anonymous portfolio offers for balancing capacity | 1 h, many rows | 2024-06-14 |
87
+ | `his-wlk-cal` | Demand, generation by unit type, wind, PV, exchange | 15 min | 2024-06-14 |
88
+ | `kse-load` | Forecast and actual load | 15 min | 2024-06-14 |
89
+ | `pdgopkd` | Day-ahead coordinated plan (forecast demand, wind, PV, reserves) | 15 min | 2024-06-14 |
90
+ | `poze-redoze` | Non-market redispatch of wind and PV | 15 min | 2024-09-24 |
91
+ | `przeplywy-mocy` | Physical cross-border flows per section | 15 min, many rows | 2024-06-14 |
92
+ | `gen-jw` | Generation per unit (≥ 100 MW) | 15 min, many rows | 2024-06-14 |
93
+
94
+ Legacy export (hourly, until 2024-06-13): `PL_WYK_KSE` (demand, JWCD/nJWCD generation, exchange, from 2009),
95
+ `PL_GEN_WIATR` (wind from 2012-05-26, PV from 2020-04-10), `PL_CENY_RYN_EN` (RCE, from 2018),
96
+ `PL_CENY_ROZL_RB` (imbalance prices CRO of the old balancing market, from 2009). The legacy server is slow (about a
97
+ minute per month, one month per request), so long histories take a while the first time and are then cached.
98
+ Field descriptions, units, sources and notes for each report:
99
+ [report catalogue](https://kwwaleszczynski-del.github.io/pse-opendata/reports/). Other endpoints of the API can
100
+ be read raw with `pse.Client().records(endpoint, filter=...)`.
101
+
102
+ ## Cache and reliability
103
+
104
+ - Whole months are cached as Parquet files (`$PSE_OPENDATA_CACHE` or the user cache directory; `Client(cache_dir=...)`
105
+ to choose, `cache_dir=None` to switch off). A month is reused only once it was downloaded after the report's
106
+ revision window (3 days for prices, up to 240 days for settlement volumes); the legacy history never changes.
107
+ - Requests retry with exponential backoff on network errors, HTTP 429 and 5xx (honouring `Retry-After`) and are
108
+ spaced (0.2 s for the API, 15 s for the legacy export).
109
+ - `offline=True` reads the cache only; `allow_stale=True` falls back to a cached copy when a download fails.
110
+ - Errors are explicit: `PseApiError` (with the API's message), `UnknownReportError`, `UnknownFieldError`,
111
+ `CacheMissError`, `LegacyFormatError`. Logging goes to the `pse_opendata` logger.
112
+
113
+ ## Data source and terms of use
114
+
115
+ All data come from public reports of **PSE S.A.** (Polskie Sieci Elektroenergetyczne): the reports API
116
+ `https://api.raporty.pse.pl/api` and the export of www.pse.pl. No key is needed. This package does not include or
117
+ redistribute PSE data, except small samples of real responses used as test fixtures (see
118
+ [tests/fixtures/README.md](tests/fixtures/README.md)).
119
+
120
+ PSE publishes conditions for re-using public sector information
121
+ ([BIP, "Ponowne wykorzystywanie informacji sektora publicznego"](https://www.pse.pl/bip/ponowne-wykorzystanie-informacji-publicznej),
122
+ entry changed on 14 October 2026, checked on 26 October 2026). They allow commercial and non-commercial re-use of
123
+ information published in the BIP and on www.pse.pl free of charge, on condition that you name the source and the date
124
+ the information was obtained (e.g. "Informacja pozyskana od PSE S.A. według stanu strony www.pse.pl na dzień
125
+ [data]"), remove the PSE logo, keep unchanged information unchanged and, when you pass on processed information, tell
126
+ recipients that it was changed and how, without misleading them. PSE accepts no liability for processed information,
127
+ and some materials may be protected by copyright or database rights. The conditions are written for www.pse.pl;
128
+ the reports API is run by the same operator. When you publish results built with this package, add the attribution
129
+ yourself, for example:
130
+
131
+ > Information obtained from www.pse.pl (PSE S.A. report `rce-pln`), state as of [date]. The information has been
132
+ > processed in whole or in part.
133
+
134
+ ## Documentation and development
135
+
136
+ Documentation: <https://kwwaleszczynski-del.github.io/pse-opendata/> (installation, examples, catalogue, time
137
+ pitfalls, API reference). To work on the package:
138
+
139
+ ```bash
140
+ python -m venv .venv
141
+ .venv/Scripts/python -m pip install -e ".[dev,docs]" # Linux/macOS: .venv/bin/python
142
+ pytest -q # offline, recorded responses
143
+ pytest -q --live tests/live # optional: calls the real PSE servers
144
+ ruff check .
145
+ mypy
146
+ mkdocs build --strict
147
+ ```
148
+
149
+ ## Citation
150
+
151
+ If you use the package in published work, please cite it; see [`CITATION.cff`](CITATION.cff).
152
+
153
+ ## License
154
+
155
+ Code: MIT, see [LICENSE](LICENSE). Data: PSE S.A. public reports, see
156
+ [Data source and terms of use](#data-source-and-terms-of-use).
@@ -0,0 +1,76 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pse-opendata"
7
+ dynamic = ["version"]
8
+ description = "Unofficial Python client for the public data of PSE, the Polish transmission system operator: reports API and legacy CSV export, as tidy pandas frames with explicit interval times."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Konrad Waleszczyński" }]
14
+ keywords = ["pse", "poland", "electricity", "power-system", "energy-market", "rce", "balancing-market", "open-data", "pandas"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Science/Research",
18
+ "Intended Audience :: Developers",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Topic :: Scientific/Engineering",
25
+ "Typing :: Typed",
26
+ ]
27
+ dependencies = [
28
+ "pandas>=2.1",
29
+ "pyarrow>=14",
30
+ "requests>=2.31",
31
+ ]
32
+
33
+ [project.optional-dependencies]
34
+ dev = ["pytest>=8", "ruff>=0.6", "mypy>=1.10", "types-requests", "build>=1.2"]
35
+ docs = ["mkdocs>=1.6", "mkdocs-material>=9.5", "mkdocstrings[python]>=0.26"]
36
+
37
+ [project.urls]
38
+ Homepage = "https://github.com/kwwaleszczynski-del/pse-opendata"
39
+ Documentation = "https://kwwaleszczynski-del.github.io/pse-opendata/"
40
+ Changelog = "https://github.com/kwwaleszczynski-del/pse-opendata/blob/main/CHANGELOG.md"
41
+
42
+ [project.scripts]
43
+ pse-opendata = "pse_opendata.cli:main"
44
+
45
+ [tool.setuptools.dynamic]
46
+ version = { attr = "pse_opendata._version.__version__" }
47
+
48
+ [tool.setuptools.packages.find]
49
+ where = ["src"]
50
+
51
+ [tool.setuptools.package-data]
52
+ pse_opendata = ["py.typed"]
53
+
54
+ [tool.pytest.ini_options]
55
+ testpaths = ["tests"]
56
+ pythonpath = ["src"]
57
+ markers = ["live: calls the real PSE servers (run with --live)"]
58
+ # raised inside pandas 2.x with numpy >= 2.3, not by this package
59
+ filterwarnings = ["ignore:The 'generic' unit for NumPy timedelta:DeprecationWarning"]
60
+
61
+ [tool.ruff]
62
+ line-length = 120
63
+ target-version = "py311"
64
+ src = ["src", "tests", "scripts"]
65
+
66
+ [tool.ruff.lint]
67
+ select = ["E", "F", "W", "I", "B", "UP", "SIM"]
68
+
69
+ [tool.ruff.lint.isort]
70
+ known-first-party = ["pse_opendata"]
71
+ known-local-folder = ["conftest"]
72
+
73
+ [tool.mypy]
74
+ python_version = "3.11"
75
+ ignore_missing_imports = true
76
+ files = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,110 @@
1
+ """pse-opendata: unofficial client for the public data of PSE, the Polish transmission system operator.
2
+
3
+ Quick start:
4
+ import pse_opendata as pse
5
+ rce = pse.rce("2025-01-01", "2025-01-31") # RCE price, 15-minute rows
6
+ sys = pse.get("his-wlk-cal", "2025-06-01", fields=["demand", "wi", "pv"])
7
+ demand = pse.history("demand", "2019-01-01", "2025-12-31") # legacy export + API, hourly
8
+
9
+ Module-level functions use a shared default :class:`Client` (default cache directory, see
10
+ :mod:`pse_opendata.cache`); pass ``client=Client(...)`` to any of them to change the settings.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ from typing import Any
17
+
18
+ import pandas as pd
19
+
20
+ from pse_opendata._version import __version__
21
+ from pse_opendata.catalog import LEGACY_REPORTS, REPORTS, Field, LegacyReport, Report, get_legacy_report, get_report
22
+ from pse_opendata.client import Client, DateLike, MonthLoad
23
+ from pse_opendata.exceptions import (
24
+ CacheMissError,
25
+ LegacyFormatError,
26
+ PseApiError,
27
+ PseError,
28
+ UnknownFieldError,
29
+ UnknownReportError,
30
+ )
31
+ from pse_opendata.frames import TIME_COLUMNS, to_hourly
32
+ from pse_opendata.stitch import splice_factor
33
+
34
+ logging.getLogger("pse_opendata").addHandler(logging.NullHandler())
35
+
36
+ __all__ = [
37
+ "LEGACY_REPORTS", "REPORTS", "TIME_COLUMNS", "CacheMissError", "Client", "Field", "LegacyFormatError",
38
+ "LegacyReport", "MonthLoad", "PseApiError", "PseError", "Report", "UnknownFieldError", "UnknownReportError",
39
+ "__version__", "balancing_capacity", "default_client", "fundamentals", "get", "get_legacy_report", "get_report",
40
+ "history", "imbalance_prices", "legacy", "rce", "reports", "sdac", "splice_factor", "to_hourly",
41
+ ]
42
+
43
+ _default: Client | None = None
44
+
45
+
46
+ def default_client() -> Client:
47
+ """The shared client used by the module-level functions (created on first use)."""
48
+ global _default
49
+ if _default is None:
50
+ _default = Client()
51
+ return _default
52
+
53
+
54
+ def get(report: str, start: DateLike, end: DateLike | None = None, *, client: Client | None = None,
55
+ **kwargs: Any) -> pd.DataFrame:
56
+ """Data of a report of the reports API; see :meth:`Client.get`."""
57
+ return (client or default_client()).get(report, start, end, **kwargs)
58
+
59
+
60
+ def legacy(code: str, start: DateLike, end: DateLike | None = None, *, client: Client | None = None,
61
+ **kwargs: Any) -> pd.DataFrame:
62
+ """Hourly data of the legacy CSV export; see :meth:`Client.legacy`."""
63
+ return (client or default_client()).legacy(code, start, end, **kwargs)
64
+
65
+
66
+ def history(series: str, start: DateLike, end: DateLike | None = None, *, client: Client | None = None,
67
+ **kwargs: Any) -> pd.DataFrame:
68
+ """Hourly series stitched from the legacy export and the API; see :meth:`Client.history`."""
69
+ return (client or default_client()).history(series, start, end, **kwargs)
70
+
71
+
72
+ def rce(start: DateLike, end: DateLike | None = None, *, client: Client | None = None, **kwargs: Any) -> pd.DataFrame:
73
+ """RCE price (``rce-pln``); see :meth:`Client.rce`."""
74
+ return (client or default_client()).rce(start, end, **kwargs)
75
+
76
+
77
+ def sdac(start: DateLike, end: DateLike | None = None, *, client: Client | None = None, **kwargs: Any) -> pd.DataFrame:
78
+ """SDAC price for Poland (``csdac-pln``); see :meth:`Client.sdac`."""
79
+ return (client or default_client()).sdac(start, end, **kwargs)
80
+
81
+
82
+ def fundamentals(start: DateLike, end: DateLike | None = None, *, client: Client | None = None,
83
+ **kwargs: Any) -> pd.DataFrame:
84
+ """Demand, generation and exchange (``his-wlk-cal``); see :meth:`Client.fundamentals`."""
85
+ return (client or default_client()).fundamentals(start, end, **kwargs)
86
+
87
+
88
+ def imbalance_prices(start: DateLike, end: DateLike | None = None, *, client: Client | None = None,
89
+ **kwargs: Any) -> pd.DataFrame:
90
+ """Imbalance and balancing energy prices (``crb-rozl``); see :meth:`Client.imbalance_prices`."""
91
+ return (client or default_client()).imbalance_prices(start, end, **kwargs)
92
+
93
+
94
+ def balancing_capacity(start: DateLike, end: DateLike | None = None, *, client: Client | None = None,
95
+ **kwargs: Any) -> pd.DataFrame:
96
+ """Balancing capacity prices and volumes; see :meth:`Client.balancing_capacity`."""
97
+ return (client or default_client()).balancing_capacity(start, end, **kwargs)
98
+
99
+
100
+ def reports(legacy: bool = False) -> pd.DataFrame:
101
+ """The catalogue as a frame: one row per report of the reports API (or of the legacy export)."""
102
+ if legacy:
103
+ rows = [{"code": r.code, "title": r.title, "title_pl": r.title_pl, "resolution": r.resolution,
104
+ "first_date": r.first_date, "last_date": r.last_date, "fields": ", ".join(r.field_names),
105
+ "page": r.page} for r in LEGACY_REPORTS.values()]
106
+ else:
107
+ rows = [{"name": r.name, "title": r.title, "title_pl": r.title_pl, "group": r.group,
108
+ "resolution": r.resolution, "first_date": r.first_date, "revision_days": r.revision_days,
109
+ "fields": ", ".join(r.field_names), "verified": r.verified, "url": r.url} for r in REPORTS.values()]
110
+ return pd.DataFrame(rows)
@@ -0,0 +1,3 @@
1
+ from pse_opendata.cli import main
2
+
3
+ raise SystemExit(main())