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.
@@ -0,0 +1,3 @@
1
+ from importlib.metadata import version
2
+
3
+ __version__ = version("hydopt-client")
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)
@@ -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
+ ]
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any