joltio 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,24 @@
1
+ name: Publish joltio
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - joltio-v*
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ environment: pypi
12
+ permissions:
13
+ id-token: write
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: '3.12'
19
+ - name: Build joltio from the tag
20
+ run: |
21
+ python -m pip install build
22
+ python -m build --sdist --wheel --outdir dist
23
+ - name: Publish joltio to PyPI
24
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -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/
joltio-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.5
2
+ Name: joltio
3
+ Version: 0.1.0
4
+ Summary: Python client for Joltio Data
5
+ Project-URL: Homepage, https://joltio.app/data
6
+ Project-URL: Documentation, https://joltio.app/data/docs
7
+ Project-URL: Repository, https://github.com/datons/python-joltio
8
+ Project-URL: Issues, https://github.com/datons/python-joltio/issues
9
+ Author-email: Jesús López <jesus.lopez@datons.com>
10
+ License-Expression: MIT
11
+ Keywords: api,data,datons,electricity,energy,esios,spain
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Scientific/Engineering
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: httpx>=0.27
24
+ Requires-Dist: polars>=1.0
25
+ Requires-Dist: pydantic>=2.0
26
+ Provides-Extra: pandas
27
+ Requires-Dist: pandas>=2.0; extra == 'pandas'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # joltio
31
+
32
+ Python client for [Joltio Data](https://joltio.app/data). The canonical endpoint is `https://api.joltio.app/data`.
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install joltio
38
+ ```
39
+
40
+ ## Quick start
41
+
42
+ ```python
43
+ from joltio import Client
44
+
45
+ client = Client(api_key="jol_live_...")
46
+
47
+ # Query the electricity market clearing price
48
+ df = client.data.query(
49
+ "SELECT datetime, price "
50
+ "FROM omie.prices "
51
+ "WHERE country = 'ES' AND datetime >= now() - INTERVAL 7 DAY "
52
+ "LIMIT 100"
53
+ )
54
+
55
+ # Dataset metadata (schema, programs, stats)
56
+ meta = client.data.metadata()
57
+
58
+ # Search for units, companies, technologies
59
+ results = client.data.search("iberdrola")
60
+ ```
61
+
62
+ ## Authentication
63
+
64
+ Request a personal Data key from your Joltio workspace administrator.
65
+
66
+ Pass it directly or set `JOLTIO_API_KEY`. `DATONS_API_KEY` remains a fallback during migration.
67
+
68
+ ```bash
69
+ export JOLTIO_API_KEY="jol_live_..."
70
+ ```
71
+
72
+ ```python
73
+ from joltio import Client
74
+
75
+ client = Client() # picks up JOLTIO_API_KEY
76
+ ```
joltio-0.1.0/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # joltio
2
+
3
+ Python client for [Joltio Data](https://joltio.app/data). The canonical endpoint is `https://api.joltio.app/data`.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install joltio
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```python
14
+ from joltio import Client
15
+
16
+ client = Client(api_key="jol_live_...")
17
+
18
+ # Query the electricity market clearing price
19
+ df = client.data.query(
20
+ "SELECT datetime, price "
21
+ "FROM omie.prices "
22
+ "WHERE country = 'ES' AND datetime >= now() - INTERVAL 7 DAY "
23
+ "LIMIT 100"
24
+ )
25
+
26
+ # Dataset metadata (schema, programs, stats)
27
+ meta = client.data.metadata()
28
+
29
+ # Search for units, companies, technologies
30
+ results = client.data.search("iberdrola")
31
+ ```
32
+
33
+ ## Authentication
34
+
35
+ Request a personal Data key from your Joltio workspace administrator.
36
+
37
+ Pass it directly or set `JOLTIO_API_KEY`. `DATONS_API_KEY` remains a fallback during migration.
38
+
39
+ ```bash
40
+ export JOLTIO_API_KEY="jol_live_..."
41
+ ```
42
+
43
+ ```python
44
+ from joltio import Client
45
+
46
+ client = Client() # picks up JOLTIO_API_KEY
47
+ ```
@@ -0,0 +1,56 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "joltio"
7
+ version = "0.1.0"
8
+ description = "Python client for Joltio Data"
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
+ "polars>=1.0",
31
+ "pydantic>=2.0",
32
+ ]
33
+
34
+ [project.optional-dependencies]
35
+ pandas = ["pandas>=2.0"]
36
+
37
+ [project.scripts]
38
+ joltio = "joltio.cli:main"
39
+
40
+ [project.urls]
41
+ Homepage = "https://joltio.app/data"
42
+ Documentation = "https://joltio.app/data/docs"
43
+ Repository = "https://github.com/datons/python-joltio"
44
+ Issues = "https://github.com/datons/python-joltio/issues"
45
+
46
+ [tool.hatch.build.targets.wheel]
47
+ packages = ["src/joltio"]
48
+
49
+ [tool.pytest.ini_options]
50
+ testpaths = ["tests"]
51
+ pythonpath = ["src"]
52
+
53
+ [dependency-groups]
54
+ dev = [
55
+ "pytest>=9.0.2",
56
+ ]
@@ -0,0 +1,19 @@
1
+ """Joltio — Python client for Joltio Data."""
2
+
3
+ from joltio.client import Client
4
+ from joltio.exceptions import (
5
+ AuthenticationError,
6
+ DatonsError,
7
+ JoltioError,
8
+ QueryError,
9
+ RateLimitError,
10
+ )
11
+
12
+ __all__ = [
13
+ "Client",
14
+ "AuthenticationError",
15
+ "DatonsError",
16
+ "JoltioError",
17
+ "QueryError",
18
+ "RateLimitError",
19
+ ]
@@ -0,0 +1,111 @@
1
+ """Joltio CLI — query data and manage authentication.
2
+
3
+ Usage:
4
+ joltio auth set <KEY> Save API key to ~/.config/joltio/config.toml
5
+ joltio auth show Show the saved key (masked)
6
+ joltio auth remove Remove the saved key
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import sys
14
+
15
+ from joltio.config import read_api_key, remove_api_key, write_api_key
16
+
17
+ def _data(args: argparse.Namespace) -> None:
18
+ from joltio import Client
19
+ with Client() as client:
20
+ if args.data_command == 'query':
21
+ result = client.data.query_raw(args.sql).model_dump()
22
+ elif args.data_command == 'coverage':
23
+ result = client.data.coverage(args.table)
24
+ elif args.data_command == 'search':
25
+ result = client.data.search(args.query).model_dump()
26
+ else:
27
+ result = client.data.metadata().model_dump()
28
+ print(json.dumps(result, default=str))
29
+
30
+
31
+ def _auth_set(args: argparse.Namespace) -> None:
32
+ write_api_key(args.key)
33
+ masked = args.key[:8] + "..." + args.key[-4:] if len(args.key) > 12 else args.key[:4] + "..."
34
+ print(f"API key saved: {masked}")
35
+
36
+
37
+ def _auth_show(_args: argparse.Namespace) -> None:
38
+ key = read_api_key()
39
+ if not key:
40
+ print("No API key configured.")
41
+ print("Set one with: joltio auth set <KEY>")
42
+ sys.exit(1)
43
+ masked = key[:8] + "..." + key[-4:] if len(key) > 12 else key[:4] + "..."
44
+ print(f"API key: {masked}")
45
+
46
+
47
+ def _auth_remove(_args: argparse.Namespace) -> None:
48
+ if remove_api_key():
49
+ print("API key removed.")
50
+ else:
51
+ print("No API key to remove.")
52
+
53
+
54
+ def main(argv: list[str] | None = None) -> None:
55
+ """CLI entry point."""
56
+ parser = argparse.ArgumentParser(
57
+ prog="joltio",
58
+ description="Joltio Data CLI",
59
+ )
60
+ subparsers = parser.add_subparsers(dest="command")
61
+
62
+ data_parser = subparsers.add_parser('data', help='Query Joltio Data')
63
+ data_sub = data_parser.add_subparsers(dest='data_command', required=True)
64
+ query = data_sub.add_parser('query')
65
+ query.add_argument('sql')
66
+ query.set_defaults(func=_data)
67
+ search = data_sub.add_parser('search')
68
+ search.add_argument('query')
69
+ search.set_defaults(func=_data)
70
+ coverage = data_sub.add_parser('coverage')
71
+ coverage.add_argument('--table')
72
+ coverage.set_defaults(func=_data)
73
+ metadata = data_sub.add_parser('metadata')
74
+ metadata.set_defaults(func=_data)
75
+
76
+ # joltio auth
77
+ auth_parser = subparsers.add_parser("auth", help="Manage API key authentication")
78
+ auth_sub = auth_parser.add_subparsers(dest="auth_command")
79
+
80
+ # joltio auth set <KEY>
81
+ set_parser = auth_sub.add_parser("set", help="Save API key")
82
+ set_parser.add_argument("key", help="Your Joltio API key")
83
+ set_parser.set_defaults(func=_auth_set)
84
+
85
+ # joltio auth show
86
+ show_parser = auth_sub.add_parser("show", help="Show saved API key (masked)")
87
+ show_parser.set_defaults(func=_auth_show)
88
+
89
+ # joltio auth remove
90
+ remove_parser = auth_sub.add_parser("remove", help="Remove saved API key")
91
+ remove_parser.set_defaults(func=_auth_remove)
92
+
93
+ args = parser.parse_args(argv)
94
+
95
+ if not args.command:
96
+ parser.print_help()
97
+ sys.exit(0)
98
+
99
+ if args.command == "auth" and not args.auth_command:
100
+ auth_parser.print_help()
101
+ sys.exit(0)
102
+
103
+ if hasattr(args, "func"):
104
+ args.func(args)
105
+ else:
106
+ parser.print_help()
107
+ sys.exit(1)
108
+
109
+
110
+ if __name__ == "__main__":
111
+ main()
@@ -0,0 +1,142 @@
1
+ """Joltio 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 joltio.exceptions import AuthenticationError, DatonsError, QueryError, RateLimitError
14
+
15
+ DEFAULT_BASE_URL = "https://api.joltio.app"
16
+ DEFAULT_TIMEOUT = 30.0
17
+
18
+
19
+ class Client:
20
+ """Client for Joltio Data APIs.
21
+
22
+ Usage::
23
+
24
+ from joltio import Client
25
+
26
+ client = Client(api_key="jol_live_...")
27
+ df = client.data.query("SELECT unit, energy FROM esios.archives_i90 WHERE program='PDBF' LIMIT 10")
28
+
29
+ Or with context manager::
30
+
31
+ with Client(api_key="jol_live_...") as client:
32
+ df = client.data.query("SELECT ...")
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ api_key: str | None = None,
38
+ *,
39
+ token: str | None = None,
40
+ base_url: str = DEFAULT_BASE_URL,
41
+ timeout: float = DEFAULT_TIMEOUT,
42
+ ):
43
+ self.token = api_key or token or os.getenv("JOLTIO_API_KEY") or os.getenv("DATONS_API_KEY")
44
+ if not self.token:
45
+ from joltio.config import read_api_key
46
+
47
+ self.token = read_api_key()
48
+ if not self.token:
49
+ raise DatonsError(
50
+ "API key required. Pass api_key=, set JOLTIO_API_KEY, or run: joltio auth set <KEY>"
51
+ )
52
+
53
+ self.base_url = base_url.rstrip("/")
54
+ self.timeout = timeout
55
+
56
+ self._http = httpx.Client(
57
+ base_url=self.base_url,
58
+ headers={
59
+ "X-API-Key": self.token,
60
+ "User-Agent": "python-joltio/0.1.0",
61
+ },
62
+ timeout=self.timeout,
63
+ )
64
+
65
+ # Lazy-initialized managers
66
+ self._esios: Any = None
67
+
68
+ @property
69
+ def data(self):
70
+ return self.esios
71
+
72
+ @property
73
+ def esios(self):
74
+ """Access ESIOS preprocessed data (I90, market programs)."""
75
+ if self._esios is None:
76
+ from joltio.esios.manager import EsiosDataManager
77
+
78
+ self._esios = EsiosDataManager(self)
79
+ return self._esios
80
+
81
+ # -- HTTP primitives (used by managers) ------------------------------------
82
+
83
+ def get(self, path: str, params: dict[str, Any] | None = None) -> dict:
84
+ """Issue a GET request."""
85
+ return self._request("GET", path, params=params)
86
+
87
+ def post(self, path: str, json: dict[str, Any] | None = None) -> dict:
88
+ """Issue a POST request."""
89
+ return self._request("POST", path, json=json)
90
+
91
+ def _request(
92
+ self,
93
+ method: str,
94
+ path: str,
95
+ params: dict[str, Any] | None = None,
96
+ json: dict[str, Any] | None = None,
97
+ ) -> dict:
98
+ """Execute an HTTP request with error handling."""
99
+ try:
100
+ response = self._http.request(method, path, params=params, json=json)
101
+ except httpx.ConnectError as exc:
102
+ raise DatonsError(f"Connection failed: {exc}") from exc
103
+ except httpx.TimeoutException as exc:
104
+ raise DatonsError(f"Request timed out: {exc}") from exc
105
+
106
+ if response.status_code == 401:
107
+ raise AuthenticationError()
108
+ if response.status_code == 429:
109
+ retry_after = response.headers.get("Retry-After")
110
+ # Parse structured error body from server
111
+ try:
112
+ body = response.json()
113
+ detail = body.get("detail", body)
114
+ except Exception:
115
+ detail = None
116
+ tier = detail.get("tier") if isinstance(detail, dict) else None
117
+ raise RateLimitError(
118
+ int(retry_after) if retry_after else None,
119
+ tier=tier,
120
+ detail=detail,
121
+ )
122
+ if response.status_code >= 400:
123
+ detail = response.text[:500]
124
+ raise QueryError(response.status_code, detail)
125
+
126
+ return response.json()
127
+
128
+ # -- Lifecycle -------------------------------------------------------------
129
+
130
+ def close(self) -> None:
131
+ """Close the underlying HTTP connection."""
132
+ self._http.close()
133
+
134
+ def __enter__(self) -> Client:
135
+ return self
136
+
137
+ def __exit__(self, *args: Any) -> None:
138
+ self.close()
139
+
140
+ def __repr__(self) -> str:
141
+ masked = self.token[:8] + "..." if self.token else "None"
142
+ return f"Client(token='{masked}', base_url='{self.base_url}')"
@@ -0,0 +1,91 @@
1
+ """Configuration file management for Joltio CLI.
2
+
3
+ Reads and writes API keys to ~/.config/joltio/config.toml.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ from pathlib import Path
10
+
11
+ CONFIG_DIR = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "joltio"
12
+ CONFIG_FILE = CONFIG_DIR / "config.toml"
13
+
14
+
15
+ def _parse_toml(text: str) -> dict[str, dict[str, str]]:
16
+ """Minimal TOML parser — handles [section] and key = "value" pairs."""
17
+ result: dict[str, dict[str, str]] = {}
18
+ current_section: str | None = None
19
+ for line in text.splitlines():
20
+ stripped = line.strip()
21
+ if not stripped or stripped.startswith("#"):
22
+ continue
23
+ if stripped.startswith("[") and stripped.endswith("]"):
24
+ current_section = stripped[1:-1].strip()
25
+ result.setdefault(current_section, {})
26
+ elif "=" in stripped and current_section is not None:
27
+ key, _, value = stripped.partition("=")
28
+ value = value.strip().strip('"').strip("'")
29
+ result[current_section][key.strip()] = value
30
+ return result
31
+
32
+
33
+ def _write_toml(data: dict[str, dict[str, str]]) -> str:
34
+ """Serialize a dict of sections to minimal TOML."""
35
+ lines: list[str] = []
36
+ for section, entries in data.items():
37
+ lines.append(f"[{section}]")
38
+ for key, value in entries.items():
39
+ lines.append(f'{key} = "{value}"')
40
+ lines.append("")
41
+ return "\n".join(lines)
42
+
43
+
44
+ def read_api_key() -> str | None:
45
+ """Read the API key from the config file, or None if not set."""
46
+ if not CONFIG_FILE.exists():
47
+ return None
48
+ try:
49
+ text = CONFIG_FILE.read_text(encoding="utf-8")
50
+ except OSError:
51
+ return None
52
+ data = _parse_toml(text)
53
+ return data.get("auth", {}).get("api_key") or None
54
+
55
+
56
+ def write_api_key(api_key: str) -> None:
57
+ """Write the API key to the config file, preserving other sections."""
58
+ data: dict[str, dict[str, str]] = {}
59
+ if CONFIG_FILE.exists():
60
+ try:
61
+ text = CONFIG_FILE.read_text(encoding="utf-8")
62
+ data = _parse_toml(text)
63
+ except OSError:
64
+ pass
65
+ data.setdefault("auth", {})
66
+ data["auth"]["api_key"] = api_key
67
+
68
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
69
+ CONFIG_FILE.write_text(_write_toml(data), encoding="utf-8")
70
+ CONFIG_FILE.chmod(0o600)
71
+
72
+
73
+ def remove_api_key() -> bool:
74
+ """Remove the API key from the config file. Returns True if it existed."""
75
+ if not CONFIG_FILE.exists():
76
+ return False
77
+ try:
78
+ text = CONFIG_FILE.read_text(encoding="utf-8")
79
+ except OSError:
80
+ return False
81
+ data = _parse_toml(text)
82
+ if "auth" not in data or "api_key" not in data["auth"]:
83
+ return False
84
+ del data["auth"]["api_key"]
85
+ if not data["auth"]:
86
+ del data["auth"]
87
+ if data:
88
+ CONFIG_FILE.write_text(_write_toml(data), encoding="utf-8")
89
+ else:
90
+ CONFIG_FILE.unlink(missing_ok=True)
91
+ return True
@@ -0,0 +1,21 @@
1
+ """ESIOS Data — preprocessed Spanish electricity market data from ClickHouse."""
2
+
3
+ from joltio.esios.manager import EsiosDataManager
4
+ from joltio.esios.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
+ ]