hydopt-client 0.0.2__py3-none-any.whl
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.
- hydopt_client/__init__.py +3 -0
- hydopt_client/auth.py +17 -0
- hydopt_client/client.py +193 -0
- hydopt_client/models.py +132 -0
- hydopt_client-0.0.2.dist-info/METADATA +68 -0
- hydopt_client-0.0.2.dist-info/RECORD +7 -0
- hydopt_client-0.0.2.dist-info/WHEEL +4 -0
hydopt_client/auth.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from google.auth.transport import requests as google_requests
|
|
2
|
+
from google.oauth2 import id_token
|
|
3
|
+
|
|
4
|
+
SERVER_URL = "https://hydopt-api-131385138719.us-central1.run.app"
|
|
5
|
+
AUDIENCE = "131385138719-463u6bbculfbqd491e2falsocsdr0rca.apps.googleusercontent.com"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TokenProvider:
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
audience: str | None = None,
|
|
12
|
+
) -> None:
|
|
13
|
+
self.audience = audience or AUDIENCE
|
|
14
|
+
|
|
15
|
+
def get_token(self) -> str:
|
|
16
|
+
request = google_requests.Request()
|
|
17
|
+
return id_token.fetch_id_token(request, self.audience)
|
hydopt_client/client.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import http
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from hydopt_client.auth import SERVER_URL, TokenProvider
|
|
9
|
+
from hydopt_client.models import (
|
|
10
|
+
CatchmentRecord,
|
|
11
|
+
HydoptRecord,
|
|
12
|
+
ObjectStatReport,
|
|
13
|
+
Order,
|
|
14
|
+
parse_records,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
FILTER_KEYS = {
|
|
18
|
+
"country": "country",
|
|
19
|
+
"region": "region",
|
|
20
|
+
"locality": "locality",
|
|
21
|
+
"owners": "owners",
|
|
22
|
+
"point_group": "pointGroup",
|
|
23
|
+
"category2": "category2",
|
|
24
|
+
"category3": "category3",
|
|
25
|
+
"value_type": "valueType",
|
|
26
|
+
"data_sources": "dateSources",
|
|
27
|
+
"id": "id",
|
|
28
|
+
"start": "start",
|
|
29
|
+
"end": "end",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class Filters:
|
|
35
|
+
country: list[str] = field(default_factory=list)
|
|
36
|
+
region: list[str] = field(default_factory=list)
|
|
37
|
+
locality: list[str] = field(default_factory=list)
|
|
38
|
+
owners: list[str] = field(default_factory=list)
|
|
39
|
+
point_group: list[str] = field(default_factory=list)
|
|
40
|
+
category2: list[str] = field(default_factory=list)
|
|
41
|
+
category3: list[str] = field(default_factory=list)
|
|
42
|
+
value_type: list[str] = field(default_factory=list)
|
|
43
|
+
data_sources: list[str] = field(default_factory=list)
|
|
44
|
+
id: list[str] = field(default_factory=list)
|
|
45
|
+
start: str | None = None
|
|
46
|
+
end: str | None = None
|
|
47
|
+
|
|
48
|
+
def to_query(self) -> dict[str, str | list[str]]:
|
|
49
|
+
return {
|
|
50
|
+
FILTER_KEYS[name]: values for name, values in vars(self).items() if values
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class GroupBy(StrEnum):
|
|
55
|
+
country = "country"
|
|
56
|
+
region = "region"
|
|
57
|
+
locality = "locality"
|
|
58
|
+
point_group = "pointGroup"
|
|
59
|
+
category2 = "category2"
|
|
60
|
+
category3 = "category3"
|
|
61
|
+
owner = "owner"
|
|
62
|
+
id = "id"
|
|
63
|
+
point_id = "pointId"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Client:
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
url: str | None = None,
|
|
70
|
+
timeout: int = 30,
|
|
71
|
+
http_client: httpx.Client | None = None,
|
|
72
|
+
token_provider: TokenProvider | None = None,
|
|
73
|
+
) -> None:
|
|
74
|
+
self.url = url or SERVER_URL
|
|
75
|
+
self.token_provider = (
|
|
76
|
+
token_provider if token_provider is not None else TokenProvider()
|
|
77
|
+
)
|
|
78
|
+
self.timeout = timeout
|
|
79
|
+
self.http_client = http_client or httpx.Client()
|
|
80
|
+
|
|
81
|
+
def get_token(self) -> str:
|
|
82
|
+
return self.token_provider.get_token()
|
|
83
|
+
|
|
84
|
+
def authorization_header(self) -> dict[str, str]:
|
|
85
|
+
return {"Authorization": f"Bearer {self.get_token()}"}
|
|
86
|
+
|
|
87
|
+
def _request(
|
|
88
|
+
self, method: str, path: str, params: dict[str, Any] | None = None
|
|
89
|
+
) -> httpx.Response:
|
|
90
|
+
response = self.http_client.request(
|
|
91
|
+
method,
|
|
92
|
+
f"{self.url}{path}",
|
|
93
|
+
headers=self.authorization_header(),
|
|
94
|
+
params=params,
|
|
95
|
+
timeout=self.timeout,
|
|
96
|
+
)
|
|
97
|
+
if response.status_code == http.HTTPStatus.UNAUTHORIZED:
|
|
98
|
+
response = self.http_client.request(
|
|
99
|
+
method,
|
|
100
|
+
f"{self.url}{path}",
|
|
101
|
+
headers=self.authorization_header(),
|
|
102
|
+
timeout=self.timeout,
|
|
103
|
+
params=params,
|
|
104
|
+
)
|
|
105
|
+
return response
|
|
106
|
+
|
|
107
|
+
def get_items(
|
|
108
|
+
self,
|
|
109
|
+
path: str,
|
|
110
|
+
filters: Filters | None,
|
|
111
|
+
group_by: list[GroupBy] | None,
|
|
112
|
+
) -> list[HydoptRecord]:
|
|
113
|
+
params: dict[str, str | list[str]] = {}
|
|
114
|
+
if filters:
|
|
115
|
+
params.update(filters.to_query())
|
|
116
|
+
if group_by:
|
|
117
|
+
params["groupby"] = [g.value for g in group_by]
|
|
118
|
+
|
|
119
|
+
response = self._request("GET", path, params=params)
|
|
120
|
+
response.raise_for_status()
|
|
121
|
+
return parse_records(response.json()["items"])
|
|
122
|
+
|
|
123
|
+
def series(
|
|
124
|
+
self,
|
|
125
|
+
asof: str,
|
|
126
|
+
category1: str,
|
|
127
|
+
filters: Filters | None = None,
|
|
128
|
+
group_by: list[GroupBy] | None = None,
|
|
129
|
+
) -> list[HydoptRecord]:
|
|
130
|
+
return self.get_items(f"/v1/series/{asof}/{category1}", filters, group_by)
|
|
131
|
+
|
|
132
|
+
def diff(
|
|
133
|
+
self,
|
|
134
|
+
category1: str,
|
|
135
|
+
from_asof: str,
|
|
136
|
+
to_asof: str,
|
|
137
|
+
filters: Filters | None = None,
|
|
138
|
+
group_by: list[GroupBy] | None = None,
|
|
139
|
+
) -> list[HydoptRecord]:
|
|
140
|
+
return self.get_items(
|
|
141
|
+
f"/v1/diff/{category1}/{from_asof}/{to_asof}", filters, group_by
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def dataset(
|
|
145
|
+
self,
|
|
146
|
+
dataset: str,
|
|
147
|
+
category1: str,
|
|
148
|
+
filters: Filters | None,
|
|
149
|
+
group_by: list[GroupBy] | None,
|
|
150
|
+
) -> list[HydoptRecord]:
|
|
151
|
+
return self.get_items(f"/v1/{dataset}/{category1}", filters, group_by)
|
|
152
|
+
|
|
153
|
+
def static(
|
|
154
|
+
self,
|
|
155
|
+
category1: str,
|
|
156
|
+
filters: Filters | None = None,
|
|
157
|
+
group_by: list[GroupBy] | None = None,
|
|
158
|
+
) -> list[HydoptRecord]:
|
|
159
|
+
return self.dataset("static", category1, filters, group_by)
|
|
160
|
+
|
|
161
|
+
def realized(
|
|
162
|
+
self,
|
|
163
|
+
category1: str,
|
|
164
|
+
filters: Filters | None = None,
|
|
165
|
+
group_by: list[GroupBy] | None = None,
|
|
166
|
+
) -> list[HydoptRecord]:
|
|
167
|
+
return self.dataset("realized", category1, filters, group_by)
|
|
168
|
+
|
|
169
|
+
def list_partitions(self) -> list[str]:
|
|
170
|
+
response = self._request("GET", "/v1/dates")
|
|
171
|
+
response.raise_for_status()
|
|
172
|
+
return response.json()["availableDates"]
|
|
173
|
+
|
|
174
|
+
def catchments(self, asof: str) -> list[CatchmentRecord]:
|
|
175
|
+
response = self._request("GET", f"/v1/catchments/{asof}")
|
|
176
|
+
response.raise_for_status()
|
|
177
|
+
return [
|
|
178
|
+
CatchmentRecord.model_validate(item) for item in response.json()["items"]
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
def object_report(self, source_date: str, target_date: str) -> ObjectStatReport:
|
|
182
|
+
response = self._request(
|
|
183
|
+
"GET", f"/v1/object-report/{source_date}/{target_date}"
|
|
184
|
+
)
|
|
185
|
+
response.raise_for_status()
|
|
186
|
+
return ObjectStatReport.model_validate(response.json())
|
|
187
|
+
|
|
188
|
+
def orders(self, asof: str) -> list[Order]:
|
|
189
|
+
response = self._request("GET", f"/v1/orders/{asof}")
|
|
190
|
+
response.raise_for_status()
|
|
191
|
+
return [
|
|
192
|
+
Order.model_validate(item) for item in response.json().get("orders") or []
|
|
193
|
+
]
|
hydopt_client/models.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from enum import StrEnum
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
5
|
+
from pydantic.alias_generators import to_camel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ProcessingInfo(BaseModel):
|
|
9
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
10
|
+
|
|
11
|
+
group_key: str | None = None
|
|
12
|
+
reduced_group_key: str | None = None
|
|
13
|
+
color: str | None = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class HydoptRecord(BaseModel):
|
|
17
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
18
|
+
|
|
19
|
+
row_num: int
|
|
20
|
+
asof: datetime
|
|
21
|
+
category1: str
|
|
22
|
+
category2: str
|
|
23
|
+
category3: str
|
|
24
|
+
country: str
|
|
25
|
+
data_source: str
|
|
26
|
+
id: str
|
|
27
|
+
locality: str
|
|
28
|
+
name: str
|
|
29
|
+
owner: str
|
|
30
|
+
point_group: str
|
|
31
|
+
region: str
|
|
32
|
+
unit: str
|
|
33
|
+
value_type: str
|
|
34
|
+
x: list[datetime]
|
|
35
|
+
y: list[float]
|
|
36
|
+
processing_info: ProcessingInfo | None = None
|
|
37
|
+
|
|
38
|
+
@field_validator("x", "y", mode="before")
|
|
39
|
+
@classmethod
|
|
40
|
+
def null_to_empty(cls, v: object) -> object:
|
|
41
|
+
return v if v is not None else []
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def parse_records(items: list[dict]) -> list[HydoptRecord]:
|
|
45
|
+
return [HydoptRecord.model_validate(item) for item in items]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class CatchmentRecord(BaseModel):
|
|
49
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
50
|
+
|
|
51
|
+
id: str
|
|
52
|
+
region: str
|
|
53
|
+
locality: str
|
|
54
|
+
polygon: str = Field(alias="shape")
|
|
55
|
+
name: str
|
|
56
|
+
created_time: datetime
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Identity(BaseModel):
|
|
60
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
61
|
+
|
|
62
|
+
asof: datetime
|
|
63
|
+
category1: str
|
|
64
|
+
category2: str
|
|
65
|
+
category3: str
|
|
66
|
+
country: str
|
|
67
|
+
data_source: str
|
|
68
|
+
id: str
|
|
69
|
+
locality: str
|
|
70
|
+
name: str
|
|
71
|
+
owner: str
|
|
72
|
+
point_group: str
|
|
73
|
+
region: str
|
|
74
|
+
unit: str
|
|
75
|
+
value_type: str
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class IndividualReaderStats(BaseModel):
|
|
79
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
80
|
+
|
|
81
|
+
name: str
|
|
82
|
+
size_mb: int
|
|
83
|
+
num_records: int
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class CompareResult(BaseModel):
|
|
87
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
88
|
+
|
|
89
|
+
stat: list[IndividualReaderStats]
|
|
90
|
+
appearing: list[Identity]
|
|
91
|
+
disappearing: list[Identity]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class PartitionComparison(BaseModel):
|
|
95
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
96
|
+
|
|
97
|
+
name: str
|
|
98
|
+
diff: CompareResult
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class MissingPartition(BaseModel):
|
|
102
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
103
|
+
|
|
104
|
+
is_in: str
|
|
105
|
+
name: str
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ObjectStatReport(BaseModel):
|
|
109
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
110
|
+
|
|
111
|
+
source_date: str
|
|
112
|
+
target_date: str
|
|
113
|
+
created_at: datetime
|
|
114
|
+
missing: list[MissingPartition] | None = None
|
|
115
|
+
partition_comparisons: list[PartitionComparison]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class OrderDirection(StrEnum):
|
|
119
|
+
production = "production"
|
|
120
|
+
demand = "demand"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class Order(BaseModel):
|
|
124
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
125
|
+
|
|
126
|
+
id: str
|
|
127
|
+
price: float
|
|
128
|
+
quantity: float
|
|
129
|
+
region: str
|
|
130
|
+
direction: OrderDirection
|
|
131
|
+
start: datetime
|
|
132
|
+
end: datetime
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: hydopt-client
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Python client for the HydOpt hydropower optimization API
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Requires-Dist: google-auth>=2.56.3
|
|
7
|
+
Requires-Dist: httpx>=0.28.1
|
|
8
|
+
Requires-Dist: pydantic>=2.13.4
|
|
9
|
+
Requires-Dist: requests>=2.34.2
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# hydopt-client
|
|
13
|
+
|
|
14
|
+
Python client for the [HydOpt](https://hydopt.io) hydropower optimization API.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
uv add hydopt-client
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Authentication
|
|
23
|
+
|
|
24
|
+
Authenticate with Google Cloud:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
gcloud auth application-default login
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from hydopt_client.client import Client
|
|
34
|
+
|
|
35
|
+
client = Client()
|
|
36
|
+
|
|
37
|
+
# List available partition dates
|
|
38
|
+
dates = client.list_partitions()
|
|
39
|
+
print(dates) # ["2026-01-01", "2026-01-02", ...]
|
|
40
|
+
|
|
41
|
+
# Fetch a time series
|
|
42
|
+
records = client.series(dates[0], "production")
|
|
43
|
+
for record in records:
|
|
44
|
+
print(record.name, record.x[0], record.y[0])
|
|
45
|
+
|
|
46
|
+
# Filter and group results
|
|
47
|
+
from hydopt_client.client import Filters, GroupBy
|
|
48
|
+
|
|
49
|
+
filtered = client.series(
|
|
50
|
+
dates[0],
|
|
51
|
+
"production",
|
|
52
|
+
filters=Filters(country=["norway"]),
|
|
53
|
+
group_by=[GroupBy.region],
|
|
54
|
+
)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Available Methods
|
|
58
|
+
|
|
59
|
+
| Method | Description |
|
|
60
|
+
|--------|-------------|
|
|
61
|
+
| `list_partitions()` | List available partition dates |
|
|
62
|
+
| `series(asof, category1, ...)` | Query time-series data |
|
|
63
|
+
| `diff(category1, from, to, ...)` | Compare two partitions |
|
|
64
|
+
| `static(category1, ...)` | Query static dataset |
|
|
65
|
+
| `realized(category1, ...)` | Query realized dataset |
|
|
66
|
+
| `catchments(asof)` | Fetch catchment geometries |
|
|
67
|
+
| `orders(asof)` | Fetch energy orders |
|
|
68
|
+
| `object_report(source, target)` | Compare two partition objects |
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
hydopt_client/__init__.py,sha256=iHQOlnZ7hvhtmRgX05Y3G-2LobTXpDlRbqXnyNkNWC0,79
|
|
2
|
+
hydopt_client/auth.py,sha256=MgELQa6rhm__Mn4BFZr63z8wfORE_oFdsFFWKCB2aJE,543
|
|
3
|
+
hydopt_client/client.py,sha256=rCED4Pn1WzooEjpX8MvXO4pQTsdwe8aMuJ7eBKRIpMQ,5883
|
|
4
|
+
hydopt_client/models.py,sha256=yYYcrTmhT9eIU1zd2w1aH7ATQ5xDgQZguCP5JU-0Q2M,3035
|
|
5
|
+
hydopt_client-0.0.2.dist-info/METADATA,sha256=Kl9boGQe_Br_vYlsjw_eCKWm6wqSHHEgJXwLLyP1yVU,1643
|
|
6
|
+
hydopt_client-0.0.2.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
7
|
+
hydopt_client-0.0.2.dist-info/RECORD,,
|