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.
- joltio-0.1.0/.github/workflows/joltio-publish.yml +24 -0
- joltio-0.1.0/.gitignore +11 -0
- joltio-0.1.0/PKG-INFO +76 -0
- joltio-0.1.0/README.md +47 -0
- joltio-0.1.0/pyproject.toml +56 -0
- joltio-0.1.0/src/joltio/__init__.py +19 -0
- joltio-0.1.0/src/joltio/cli.py +111 -0
- joltio-0.1.0/src/joltio/client.py +142 -0
- joltio-0.1.0/src/joltio/config.py +91 -0
- joltio-0.1.0/src/joltio/esios/__init__.py +21 -0
- joltio-0.1.0/src/joltio/esios/manager.py +237 -0
- joltio-0.1.0/src/joltio/esios/models.py +121 -0
- joltio-0.1.0/src/joltio/exceptions.py +57 -0
- joltio-0.1.0/tests/test_joltio.py +22 -0
- joltio-0.1.0/uv.lock +850 -0
|
@@ -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
|
joltio-0.1.0/.gitignore
ADDED
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
|
+
]
|