Skip to content

Commit 8d3b15f

Browse files
committed
feat: Initial release — Datons Python client
Client for Datons data APIs with ESIOS Data as first product module. - Client with API key auth, lazy-loaded product managers - EsiosDataManager: query (→ DataFrame), metadata, search, dimensions - Pydantic models aligned with the ESIOS Data API
0 parents  commit 8d3b15f

13 files changed

Lines changed: 1380 additions & 0 deletions

File tree

.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
__pycache__/
2+
*.py[cod]
3+
*$py.class
4+
*.egg-info/
5+
dist/
6+
build/
7+
.venv/
8+
.env
9+
*.egg
10+
.pytest_cache/
11+
.ruff_cache/

CLAUDE.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# python-datons
2+
3+
Python client for Datons data APIs (`pip install datons`).
4+
5+
## Structure
6+
7+
- `src/datons/client.py` — Central `Client` with shared auth and HTTP
8+
- `src/datons/esios_data/` — ESIOS preprocessed data manager (first product)
9+
- Future products go in `src/datons/<product_name>/`
10+
11+
## Running tests
12+
13+
```bash
14+
uv run pytest
15+
```
16+
17+
## Backend
18+
19+
The ESIOS Data API runs on droplet-ts at `~/git-repositories/mcps/esios-data/`, proxied via nginx at `mcp.datons.com/esios-data`.

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# datons
2+
3+
Python client for [Datons](https://datons.com) data APIs.
4+
5+
## Installation
6+
7+
```bash
8+
pip install datons
9+
```
10+
11+
## Quick start
12+
13+
```python
14+
from datons import Client
15+
16+
client = Client(token="esd_live_...")
17+
18+
# Query preprocessed I90 market data
19+
df = client.esios_data.query(
20+
"SELECT unit, datetime, energy, price "
21+
"FROM operational_data_15min "
22+
"WHERE program = 'PDBF' AND date >= '2025-01-01' "
23+
"LIMIT 100"
24+
)
25+
26+
# Dataset metadata (schema, programs, stats)
27+
meta = client.esios_data.metadata()
28+
29+
# Search for units, companies, technologies
30+
results = client.esios_data.search("iberdrola")
31+
```
32+
33+
## Authentication
34+
35+
Get your API key at [datons.com/apps/esios-data](https://datons.com/apps/esios-data).
36+
37+
Pass it directly or set the `DATONS_API_KEY` environment variable:
38+
39+
```bash
40+
export DATONS_API_KEY="esd_live_..."
41+
```
42+
43+
```python
44+
from datons import Client
45+
46+
client = Client() # picks up DATONS_API_KEY
47+
```

pyproject.toml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
[build-system]
2+
requires = ["hatchling"]
3+
build-backend = "hatchling.build"
4+
5+
[project]
6+
name = "datons"
7+
version = "0.1.0"
8+
description = "Python client for Datons data APIs"
9+
readme = "README.md"
10+
license = "MIT"
11+
requires-python = ">=3.10"
12+
authors = [
13+
{ name = "Jesús López", email = "jesus.lopez@datons.com" },
14+
]
15+
keywords = ["datons", "energy", "esios", "electricity", "spain", "api", "data"]
16+
classifiers = [
17+
"Development Status :: 3 - Alpha",
18+
"Intended Audience :: Science/Research",
19+
"Programming Language :: Python :: 3",
20+
"Programming Language :: Python :: 3.10",
21+
"Programming Language :: Python :: 3.11",
22+
"Programming Language :: Python :: 3.12",
23+
"Programming Language :: Python :: 3.13",
24+
"License :: OSI Approved :: MIT License",
25+
"Operating System :: OS Independent",
26+
"Topic :: Scientific/Engineering",
27+
]
28+
dependencies = [
29+
"httpx>=0.27",
30+
"pandas>=2.0",
31+
"pydantic>=2.0",
32+
]
33+
34+
[project.urls]
35+
Homepage = "https://datons.com"
36+
Documentation = "https://datons.com/apps/esios-data/docs"
37+
Repository = "https://github.com/datons/python-datons"
38+
Issues = "https://github.com/datons/python-datons/issues"
39+
40+
[tool.hatch.build.targets.wheel]
41+
packages = ["src/datons"]
42+
43+
[tool.pytest.ini_options]
44+
testpaths = ["tests"]
45+
pythonpath = ["src"]
46+
47+
[dependency-groups]
48+
dev = [
49+
"pytest>=9.0.2",
50+
]

src/datons/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""Datons — Python client for Datons data APIs."""
2+
3+
from datons.client import Client
4+
from datons.exceptions import (
5+
AuthenticationError,
6+
DatonsError,
7+
QueryError,
8+
RateLimitError,
9+
)
10+
11+
__all__ = [
12+
"Client",
13+
"AuthenticationError",
14+
"DatonsError",
15+
"QueryError",
16+
"RateLimitError",
17+
]

src/datons/client.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
"""Datons API client.
2+
3+
Central entry point that lazily initializes product-specific managers.
4+
"""
5+
6+
from __future__ import annotations
7+
8+
import os
9+
from typing import Any
10+
11+
import httpx
12+
13+
from datons.exceptions import AuthenticationError, DatonsError, QueryError, RateLimitError
14+
15+
DEFAULT_BASE_URL = "https://mcp.datons.com"
16+
DEFAULT_TIMEOUT = 30.0
17+
18+
19+
class Client:
20+
"""Client for Datons data APIs.
21+
22+
Usage::
23+
24+
from datons import Client
25+
26+
client = Client(token="esd_live_...")
27+
df = client.esios_data.query("SELECT unit, energy FROM operational_data_15min WHERE program='PDBF' LIMIT 10")
28+
29+
Or with context manager::
30+
31+
with Client(token="esd_live_...") as client:
32+
df = client.esios_data.query("SELECT ...")
33+
"""
34+
35+
def __init__(
36+
self,
37+
token: str | None = None,
38+
*,
39+
base_url: str = DEFAULT_BASE_URL,
40+
timeout: float = DEFAULT_TIMEOUT,
41+
):
42+
self.token = token or os.getenv("DATONS_API_KEY")
43+
if not self.token:
44+
raise DatonsError(
45+
"API key required. Pass token= or set DATONS_API_KEY env var."
46+
)
47+
48+
self.base_url = base_url.rstrip("/")
49+
self.timeout = timeout
50+
51+
self._http = httpx.Client(
52+
base_url=self.base_url,
53+
headers={
54+
"X-API-Key": self.token,
55+
"User-Agent": "python-datons/0.1.0",
56+
},
57+
timeout=self.timeout,
58+
)
59+
60+
# Lazy-initialized managers
61+
self._esios_data: Any = None
62+
63+
@property
64+
def esios_data(self):
65+
"""Access ESIOS preprocessed data (I90, market programs)."""
66+
if self._esios_data is None:
67+
from datons.esios_data.manager import EsiosDataManager
68+
69+
self._esios_data = EsiosDataManager(self)
70+
return self._esios_data
71+
72+
# -- HTTP primitives (used by managers) ------------------------------------
73+
74+
def get(self, path: str, params: dict[str, Any] | None = None) -> dict:
75+
"""Issue a GET request."""
76+
return self._request("GET", path, params=params)
77+
78+
def post(self, path: str, json: dict[str, Any] | None = None) -> dict:
79+
"""Issue a POST request."""
80+
return self._request("POST", path, json=json)
81+
82+
def _request(
83+
self,
84+
method: str,
85+
path: str,
86+
params: dict[str, Any] | None = None,
87+
json: dict[str, Any] | None = None,
88+
) -> dict:
89+
"""Execute an HTTP request with error handling."""
90+
try:
91+
response = self._http.request(method, path, params=params, json=json)
92+
except httpx.ConnectError as exc:
93+
raise DatonsError(f"Connection failed: {exc}") from exc
94+
except httpx.TimeoutException as exc:
95+
raise DatonsError(f"Request timed out: {exc}") from exc
96+
97+
if response.status_code == 401:
98+
raise AuthenticationError()
99+
if response.status_code == 429:
100+
retry_after = response.headers.get("Retry-After")
101+
raise RateLimitError(int(retry_after) if retry_after else None)
102+
if response.status_code >= 400:
103+
detail = response.text[:500]
104+
raise QueryError(response.status_code, detail)
105+
106+
return response.json()
107+
108+
# -- Lifecycle -------------------------------------------------------------
109+
110+
def close(self) -> None:
111+
"""Close the underlying HTTP connection."""
112+
self._http.close()
113+
114+
def __enter__(self) -> Client:
115+
return self
116+
117+
def __exit__(self, *args: Any) -> None:
118+
self.close()
119+
120+
def __repr__(self) -> str:
121+
masked = self.token[:8] + "..." if self.token else "None"
122+
return f"Client(token='{masked}', base_url='{self.base_url}')"

src/datons/esios_data/__init__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""ESIOS Data — preprocessed Spanish electricity market data from ClickHouse."""
2+
3+
from datons.esios_data.manager import EsiosDataManager
4+
from datons.esios_data.models import (
5+
ColumnInfo,
6+
DimensionResult,
7+
MetadataResult,
8+
ProgramInfo,
9+
QueryResult,
10+
SearchResult,
11+
)
12+
13+
__all__ = [
14+
"EsiosDataManager",
15+
"ColumnInfo",
16+
"DimensionResult",
17+
"MetadataResult",
18+
"ProgramInfo",
19+
"QueryResult",
20+
"SearchResult",
21+
]

0 commit comments

Comments
 (0)