tyba-client 0.4.13__py3-none-any.whl → 0.4.16__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.

Potentially problematic release.


This version of tyba-client might be problematic. Click here for more details.

tyba_client/client.py CHANGED
@@ -2,9 +2,15 @@ import pandas as pd
2
2
  import typing as t
3
3
  from requests import Response
4
4
 
5
- from tyba_client.models import GenerationModel, PVStorageModel, StandaloneStorageModel
6
5
  from tyba_client.forecast import Forecast
7
- from generation_models import JobModel
6
+ from generation_models import JobModel, GenerationModel, PVStorageModel, StandaloneStorageModel
7
+
8
+ from generation_models.v0_output_schema import (
9
+ GenerationModelResults,
10
+ PVStorageModelResults,
11
+ StandaloneStorageModelSimpleResults,
12
+ StandaloneStorageModelWithDownstreamResults
13
+ )
8
14
  import json
9
15
  import requests
10
16
  import time
@@ -12,25 +18,104 @@ from structlog import get_logger
12
18
  from typing import Callable
13
19
 
14
20
  from tyba_client.operations import Operations
21
+ from pydantic import BaseModel, Field
22
+ from enum import Enum
15
23
 
16
24
  logger = get_logger()
17
25
 
18
26
 
19
- class Ancillary(object):
20
- """_"""
27
+ V0Results = t.Union[
28
+ GenerationModelResults,
29
+ PVStorageModelResults,
30
+ StandaloneStorageModelSimpleResults,
31
+ StandaloneStorageModelWithDownstreamResults
32
+ ]
33
+
34
+ class Market(str, Enum):
35
+ """Indicator for which market to pull pricing data for"""
36
+ RT = "realtime"
37
+ """Indicates pricing data for the Real Time (RT) Market is desired"""
38
+ DA = "dayahead"
39
+ """Indicates pricing data for the Day Ahead (DA) Market is desired"""
40
+
41
+
42
+ class AncillaryService(str, Enum):
43
+ """Indicator for which service to pull pricing data for"""
44
+ REGULATION_UP = "Regulation Up"
45
+ """Indicates pricing data for the Regulation Up service is desired"""
46
+ REGULATION_DOWN = "Regulation Down"
47
+ """Indicates pricing data for the Regulation Down service is desired"""
48
+ RESERVES = "Reserves"
49
+ """Indicates pricing data for the Reserves service is desired"""
50
+ ECRS = "ECRS"
51
+ """Indicates pricing data for the ERCOT Contingency Reserve Service is desired"""
21
52
 
22
- def __init__(self, services):
53
+ class Ancillary(object):
54
+ """Interface for accessing Tyba's historical *ancillary* price data"""
55
+ def __init__(self, services: 'Services'):
23
56
  self.services = services
24
57
 
25
58
  def get(self, route, params=None):
26
59
  return self.services.get(f"ancillary/{route}", params=params)
27
60
 
28
- def get_pricing_regions(self, *, iso, service, market):
29
- """_"""
61
+ def get_pricing_regions(self, *, iso: str, service: AncillaryService, market: Market) -> Response:
62
+ """Get the name and available year ranges for all ancillary service pricing regions that meet the ISO,
63
+ service and market criteria.
64
+
65
+ :param iso: ISO name. Possible values can be found by calling :meth:`Services.get_all_isos`
66
+ :param service: specifies which ancillary service to pull prices for
67
+ :param market: specifies whether to pull day ahead or real time prices for the given service
68
+ :return: :class:`~requests.Response` containing an **array** of JSON objects with schema
69
+ :class:`AncillaryRegionData`. For example:
70
+
71
+ .. code:: python
72
+
73
+ [
74
+ {
75
+ 'region': 'Pacific Northwest - SP15',
76
+ 'start_year': 2010,
77
+ 'end_year': 2025
78
+ },
79
+ {
80
+ 'region': 'WAPA',
81
+ ...
82
+ },
83
+ ...
84
+ ]
85
+
86
+ """
30
87
  return self.get("regions", {"iso": iso, "service": service, "market": market})
31
88
 
32
- def get_prices(self, *, iso, service, market, region, start_year, end_year):
33
- """_"""
89
+ def get_prices(self, *, iso: str, service: AncillaryService, market: Market, region: str, start_year: int, end_year: int) -> Response:
90
+ """Get price time series data for a single region/service combination
91
+
92
+ :param iso: ISO name. Possible values can be found by calling :meth:`Services.get_all_isos`
93
+ :param service: specifies which ancillary service to pull prices for
94
+ :param market: specifies whether to pull day ahead or real time prices for the given service
95
+ :param region: specific region within the ISO to pull prices for. Possible values can be found by calling
96
+ :meth:`get_pricing_regions`
97
+ :param start_year: the year prices should start
98
+ :param end_year: the year prices should end
99
+ :return: :class:`~requests.Response` containing a JSON object with schema :class:`PriceTimeSeries`. For example:
100
+
101
+ .. code:: python
102
+
103
+ {
104
+ 'prices': [
105
+ 61.7929,
106
+ 58.1359,
107
+ 61.4939,
108
+ ....
109
+ ],
110
+ 'datetimes': [
111
+ '2022-01-01T00:00:00Z',
112
+ '2022-01-01T01:00:00Z',
113
+ '2022-01-01T02:00:00Z',
114
+ ....
115
+ ],
116
+ }
117
+
118
+ """
34
119
  return self.get(
35
120
  "prices",
36
121
  {
@@ -45,9 +130,9 @@ class Ancillary(object):
45
130
 
46
131
 
47
132
  class LMP(object):
48
- """_"""
49
-
50
- def __init__(self, services):
133
+ """Interface for accessing Tyba's historical *energy* price data
134
+ """
135
+ def __init__(self, services: 'Services'):
51
136
  self.services = services
52
137
  self._route_base = "lmp"
53
138
 
@@ -57,12 +142,75 @@ class LMP(object):
57
142
  def post(self, route, json):
58
143
  return self.services.post(f"{self._route_base}/{route}", json=json)
59
144
 
60
- def get_all_nodes(self, *, iso):
61
- """_"""
145
+ def get_all_nodes(self, *, iso: str) -> requests.Response:
146
+ """Get node names, IDs and other metadata for all nodes within the given ISO territory.
147
+
148
+ :param iso: ISO name. Possible values can be found by calling :meth:`Services.get_all_isos`
149
+ :return: :class:`~requests.Response` containing an **array** of JSON objects with schema
150
+ :class:`NodeData`. For example:
151
+
152
+ .. code:: python
153
+
154
+ [
155
+ {'da_end_year': 2025,
156
+ 'rt_end_year': 2025,
157
+ 'rt_start_year': 2023,
158
+ 'name': 'CLAP_WWRSR1-APND',
159
+ 'id': '10017280350',
160
+ 'da_start_year': 2023,
161
+ 'zone': 'SDGE',
162
+ 'type': 'GENERATOR'},
163
+ {'da_start_year': 2015,
164
+ 'rt_end_year': 2025,
165
+ 'zone': '',
166
+ 'name': 'ELCENTRO_2_N001:IVLY2',
167
+ 'type': 'SPTIE',
168
+ 'substation': '',
169
+ 'da_end_year': 2025,
170
+ 'id': '10003899356',
171
+ 'rt_start_year': 2015},
172
+ ...
173
+ ]
174
+
175
+ """
62
176
  return self.get("nodes", {"iso": iso})
63
177
 
64
- def get_prices(self, *, node_ids, market, start_year, end_year):
65
- """_"""
178
+ def get_prices(self, *, node_ids: list[str], market: Market, start_year: int, end_year: int) -> requests.Response:
179
+ """Get price time series data for a list of node IDs
180
+
181
+ :param node_ids: list of IDs for which prices are desired
182
+ - Maximum length is 8 IDS
183
+ :param market: specifies whether to pull day ahead or real time market prices
184
+ :param start_year: the year prices should start
185
+ :param end_year: the year prices should end
186
+ :return: :class:`~requests.Response` containing a JSON object whose keys are node IDs and whose values
187
+ are objects with schema :class:`PriceTimeSeries`. For example:
188
+
189
+ .. code:: python
190
+
191
+ {
192
+ '10000802793': {
193
+ 'prices': [
194
+ 61.7929,
195
+ 58.1359,
196
+ 61.4939,
197
+ ....
198
+ ],
199
+ 'datetimes': [
200
+ '2022-01-01T00:00:00',
201
+ '2022-01-01T01:00:00',
202
+ '2022-01-01T02:00:00',
203
+ ....
204
+ ],
205
+ ...
206
+ },
207
+ '20000004677': {
208
+ ...
209
+ },
210
+ ...
211
+ }
212
+
213
+ """
66
214
  return self.get(
67
215
  "prices",
68
216
  {
@@ -73,7 +221,45 @@ class LMP(object):
73
221
  },
74
222
  )
75
223
 
76
- def search_nodes(self, location: t.Optional[str] = None, node_name_filter: t.Optional[str] = None, iso_override: t.Optional[str] = None):
224
+ def search_nodes(self, location: t.Optional[str] = None, node_name_filter: t.Optional[str] = None,
225
+ iso_override: t.Optional[str] = None) -> Response:
226
+ """Get a list of matching nodes based on search criteria. Multiple search criteria (e.g. `location`
227
+ and `node_name_filter`) can be applied in a single request.
228
+
229
+ :param location: location information. There are 3 possible forms:
230
+
231
+ - city/state, e.g. `'dallas, tx'`
232
+ - address, e.g. `'12345 Anywhere Street, Anywhere, TX 12345'`
233
+ - latitude and longitude, e.g. `'29.760427, -95.369804'`
234
+
235
+ :param node_name_filter: partial node name with which to perform a pattern-match, e.g. `'HB_'`
236
+ :param iso_override: ISO signifier, used to constrain search to a single ISO. When equal to ``None``, all ISOs
237
+ are searched based on other criteria. Possible values can be found by calling :meth:`Services.get_all_isos`
238
+ :return: :class:`~requests.Response` containing a JSON object. If matching nodes are found, a `'nodes'` item
239
+ will contain an **array** of objects with schema :class:`NodeSearchData`. If no matching nodes are found,
240
+ an error code will be returned. As an example, a successful search result might look like:
241
+
242
+ .. code:: python
243
+
244
+ {
245
+ "nodes": [
246
+ {
247
+ "node/name": "HB_BUSAVG",
248
+ "node/id": "10000698380",
249
+ "node/iso": "ERCOT",
250
+ "node/lat": 30.850714,
251
+ "node/lng": -97.877628,
252
+ "node/distance-meters": 500.67458
253
+ },
254
+ {
255
+ "node/name": ...,
256
+ ...
257
+ },
258
+ ...
259
+ ]
260
+ }
261
+
262
+ """
77
263
  return self.get(route="search-nodes",
78
264
  params={"location": location,
79
265
  "node_name_filter": node_name_filter,
@@ -81,12 +267,15 @@ class LMP(object):
81
267
 
82
268
 
83
269
  class Services(object):
84
- """_"""
270
+ """Interface for accessing Tyba's historical price data
271
+ """
272
+ def __init__(self, client: 'Client'):
85
273
 
86
- def __init__(self, client):
87
274
  self.client = client
88
- self.ancillary = Ancillary(self)
89
- self.lmp = LMP(self)
275
+ self.ancillary: Ancillary = Ancillary(self)
276
+ """Interface for accessing Tyba's historical *ancillary* price data"""
277
+ self.lmp: LMP = LMP(self)
278
+ """Interface for accessing Tyba's historical *energy* price data"""
90
279
  self._route_base = "services"
91
280
 
92
281
  def get(self, route, params=None):
@@ -95,30 +284,48 @@ class Services(object):
95
284
  def post(self, route, json):
96
285
  return self.client.post(f"{self._route_base}/{route}", json=json)
97
286
 
98
- def get_all_isos(self):
99
- """_"""
287
+ def get_all_isos(self) -> requests.Response:
288
+ """Get of list of all independent system operators and regional transmission operators (generally all referred
289
+ to as ISOs) represented in Tyba's historical price data
290
+
291
+ :return: :class:`~requests.Response` containing JSON array of strings of the available ISO names
292
+ """
100
293
  return self.get("isos")
101
294
 
102
295
 
103
296
  class Client(object):
104
- """Tyba valuation client class"""
297
+ """High level interface for interacting with Tyba's API.
298
+
299
+ :param personal_access_token: required for using the python client/API, contact Tyba to obtain
300
+ """
105
301
 
106
302
  DEFAULT_OPTIONS = {"version": "0.1"}
107
303
 
108
304
  def __init__(
109
305
  self,
110
- personal_access_token,
111
- host="https://dev.tybaenergy.com",
112
- request_args=None,
306
+ personal_access_token: str,
307
+ host: str = "https://dev.tybaenergy.com",
308
+ request_args: t.Optional[dict] = None,
113
309
  ):
114
- """A :class:`Client` object for interacting with Tyba's API."""
115
310
  self.personal_access_token = personal_access_token
116
311
  self.host = host
117
- self.services = Services(self)
118
- self.forecast = Forecast(self)
312
+ self.services: Services = Services(self)
313
+ """Interface for accessing Tyba's historical price data"""
314
+ self.forecast: Forecast = Forecast(self)
315
+ """Interface for accessing Tyba's historical price data"""
119
316
  self.operations = Operations(self)
120
317
  self.request_args = {} if request_args is None else request_args
121
318
 
319
+ @property
320
+ def ancillary(self) -> Ancillary:
321
+ """Shortcut to :class:`client.services.ancillary <Ancillary>`"""
322
+ return self.services.ancillary
323
+
324
+ @property
325
+ def lmp(self) -> LMP:
326
+ """Shortcut to :class:`client.services.lmp <LMP>`"""
327
+ return self.services.lmp
328
+
122
329
  def _auth_header(self):
123
330
  return self.personal_access_token
124
331
 
@@ -141,29 +348,60 @@ class Client(object):
141
348
  **self.request_args,
142
349
  )
143
350
 
144
- def schedule_pv(self, pv_model: GenerationModel):
351
+ def schedule_pv(self, pv_model: GenerationModel) -> Response:
145
352
  model_json_dict = pv_model.to_dict()
146
353
  return self.post("schedule-pv", json=model_json_dict)
147
354
 
148
- def schedule_storage(self, storage_model: StandaloneStorageModel):
355
+ def schedule_storage(self, storage_model: StandaloneStorageModel) -> Response:
149
356
  model_json_dict = storage_model.to_dict()
150
357
  return self.post("schedule-storage", json=model_json_dict)
151
358
 
152
- def schedule_pv_storage(self, pv_storage_model: PVStorageModel):
359
+ def schedule_pv_storage(self, pv_storage_model: PVStorageModel) -> Response:
153
360
  model_json_dict = pv_storage_model.to_dict()
154
361
  return self.post("schedule-pv-storage", json=model_json_dict)
155
362
 
156
- def schedule(self, model: JobModel):
157
- """_"""
363
+ def schedule(self, model: JobModel) -> Response:
364
+ """Schedule a model simulation based on the given inputs
365
+
366
+ :param model: a class instance of one of the model classes, e.g.
367
+ :class:`~generation_models.generation_models.StandaloneStorageModel`. Contains all required inputs for
368
+ running a simulation
369
+ :return: :class:`~requests.Response` whose status code indicates whether the model was successfully scheduled.
370
+ If successful, the response will contain a JSON object with an ``'id'`` for the scheduled model run. This id
371
+ can be used with the :meth:`get_status` and :meth:`wait_on_result` endpoints to retrieve status updates and
372
+ model results. The presence of issues can be easily checked by calling the
373
+ :meth:`~requests.Response.raise_for_status` method of the response object. For example:
374
+
375
+ .. code:: python
376
+
377
+ resp = client.schedule(pv)
378
+ resp.raise-for_status() # this will raise an error if the model was not successfully scheduled
379
+ id_ = resp.json()["id"]
380
+ res = client.wait_on_result(id_)
381
+
382
+ """
158
383
  return self.post("schedule-job", json=model.dict())
159
384
 
160
385
  def get_status(self, run_id: str):
161
- """_"""
386
+ """Check the status and retrieve the results of a scheduled model simulation. If a simulation has not
387
+ completed, this endpoint returns the simulation status/progress. If the simulation has completed, it
388
+ returns the model results.
389
+
390
+ :param run_id: ID of the scheduled model simulation
391
+ :return: :class:`~requests.Response` containing a JSON object with schema :class:`ModelStatus`
392
+ """
162
393
  url = "get-status/" + run_id
163
394
  return self.get(url)
164
395
 
165
396
  def get_status_v1(self, run_id: str):
166
- """_"""
397
+ """`Deprecated, please use` :meth:`get_status`, `which will support additional results schemas in the near
398
+ future`. Identical to :meth:`get_status`, but returns results for completed simulations in the "V1"
399
+ SLD-style schema
400
+
401
+ :param run_id: ID of the scheduled model simulation
402
+ :return: :class:`~requests.Response` containing a JSON object with schema :class:`ModelStatus` (except with a
403
+ different schema for :attr:`~ModelStatus.result`)
404
+ """
167
405
  return self.get(f"get-status/{run_id}", params={"fmt": "v1"})
168
406
 
169
407
  @staticmethod
@@ -172,7 +410,7 @@ class Client(object):
172
410
  wait_time: int,
173
411
  log_progress: bool,
174
412
  getter: Callable[[str], Response],
175
- ):
413
+ ) -> dict:
176
414
  while True:
177
415
  resp = getter(run_id)
178
416
  resp.raise_for_status()
@@ -190,8 +428,16 @@ class Client(object):
190
428
 
191
429
  def wait_on_result(
192
430
  self, run_id: str, wait_time: int = 5, log_progress: bool = False
193
- ):
194
- """_"""
431
+ ) -> dict:
432
+ """Poll for simulation status and, once complete, return the model results
433
+
434
+ :param run_id: ID of the scheduled model simulation
435
+ :param wait_time: time in seconds to wait between polling/updates
436
+ :param log_progress: indicate whether updates/progress should be logged/displayed. If ``True``, will
437
+ report both :attr:`~ModelStatus.status` and :attr:`~ModelStatus.progress` information
438
+ :return: results dictionary equivalent to :attr:`ModelStatus.result` returned by :meth:`get_status`, with the
439
+ exact schema depending on the model inputs
440
+ """
195
441
  return self._wait_on_result(
196
442
  run_id, wait_time, log_progress, getter=self.get_status
197
443
  )
@@ -199,7 +445,16 @@ class Client(object):
199
445
  def wait_on_result_v1(
200
446
  self, run_id: str, wait_time: int = 5, log_progress: bool = False
201
447
  ):
202
- """_"""
448
+ """`Deprecated, please use :meth:`wait_on_result`, which will support additional results schemas in the near
449
+ future`. Identical to :meth:`wait_on_result`, but returns results for completed simulations in the "V1"
450
+ SLD-style schema
451
+
452
+ :param run_id: ID of the scheduled model simulation
453
+ :param wait_time: time in seconds to wait between polling/updates
454
+ :param log_progress: indicate whether updates/progress should be logged/displayed. If ``True``, will
455
+ report both :attr:`~ModelStatus.status` and :attr:`~ModelStatus.progress` information
456
+ :return: results dictionary with "V1" SLD-style schema
457
+ """
203
458
  res = self._wait_on_result(
204
459
  run_id, wait_time, log_progress, getter=self.get_status_v1
205
460
  )
@@ -207,6 +462,7 @@ class Client(object):
207
462
 
208
463
 
209
464
  def parse_v1_result(res: dict):
465
+ """:meta private:"""
210
466
  return {
211
467
  "hourly": pd.concat(
212
468
  {k: pd.DataFrame(v) for k, v in res["hourly"].items()}, axis=1
@@ -216,4 +472,112 @@ def parse_v1_result(res: dict):
216
472
 
217
473
 
218
474
  class UnknownRunId(ValueError):
475
+ """:meta private:"""
219
476
  pass
477
+
478
+
479
+ class NodeType(str, Enum):
480
+ """Indicator of which type of physical infrastructure is associated with a particular market node"""
481
+ GENERATOR = "GENERATOR"
482
+ """Not sure"""
483
+ SPTIE = "SPTIE"
484
+ """Not sure"""
485
+ LOAD = "LOAD"
486
+ """Not sure"""
487
+ INTERTIE = "INTERTIE"
488
+ """Not sure"""
489
+ AGGREGATE = "AGGREGATE"
490
+ """Not sure"""
491
+ HUB = "HUB"
492
+ """Not sure"""
493
+ NA = "N/A"
494
+ """Not sure"""
495
+
496
+
497
+ class NodeData(BaseModel):
498
+ """Schema for node metadata"""
499
+ name: str
500
+ """Name of the node"""
501
+ id: str
502
+ """ID of the node"""
503
+ zone: str
504
+ """Zone where the node is located within the ISO territory"""
505
+ type: NodeType
506
+ """Identifier that indicates physical infrastructure associated with this node"""
507
+ da_start_year: float
508
+ """First year in the Day Ahead (DA) market price dataset for this node"""
509
+ da_end_year: float
510
+ """Final year in the Day Ahead (DA) market price dataset for this node"""
511
+ rt_start_year: int
512
+ """First year in the Real Time (RT) market price dataset for this node"""
513
+ rt_end_year: int
514
+ """Final year in the Real Time (RT) market price dataset for this node"""
515
+ substation: t.Optional[str] = None
516
+ """Indicator of the grid substation associated with this node (not always present)"""
517
+
518
+ class PriceTimeSeries(BaseModel):
519
+ """Schema for pricing data associated with a particular energy price node or ancillary pricing region"""
520
+ datetimes: list[str]
521
+ """Beginning-of-interval datetimes for the hourly pricing given in local time.
522
+
523
+ - For energy prices, the datetimes are timezone-naive (no timezone identifier) but given in the local timezone
524
+ (i.e. including Daylight Savings Time or DST). E.g. The start of the year 2022 in ERCOT is given as
525
+ `'2022-01-01T00:00:00'` as opposed to `'2022-01-01T00:00:00-6:00'`. Leap days are represented by a single hour,
526
+ which should be dropped as a post-processing step.
527
+ - For ancillary prices, the datetimes are in local standard time (i.e. not including DST) but appear to be in
528
+ UTC ("Z" timezone identifier). E.g. The start of the year 2022 in ERCOT is given as `'2022-01-01T00:00:00Z'` and
529
+ not `'2022-01-01T00:00:00-6:00'`. Leap days are not included.
530
+
531
+ """
532
+ prices: list[float]
533
+ """Average hourly settlement prices for hours represented by :attr:`datetimes`.
534
+ """
535
+
536
+ class NodeSearchData(BaseModel):
537
+ """Schema for search-specific node metadata. The `(name 'xxxx')` to the right of the field names can be ignored"""
538
+ node_name: str = Field(..., alias='node/name')
539
+ """Name of the node"""
540
+ node_id: str = Field(..., alias='node/id')
541
+ """ID of the node"""
542
+ node_iso: str = Field(..., alias='node/iso')
543
+ """ISO that the node belongs to"""
544
+ node_longitude: float = Field(..., alias='node/lng')
545
+ """longitude of the point on the electrical grid associated with the node"""
546
+ node_latitude: float = Field(..., alias='node/lat')
547
+ """latitude of the point on the electrical grid associated with the node"""
548
+ node_distance_meters: t.Optional[float] = Field(default=None, alias='node/distance-meters')
549
+ """Distance from the node to the `location` parameter passed to :meth:`~LMP.search_nodes`. Not present if
550
+ `location` is not given.
551
+ """
552
+
553
+
554
+ class AncillaryRegionData(BaseModel):
555
+ """Schema for ancillary region metadata"""
556
+ region: str
557
+ """Name of the region"""
558
+ start_year: int
559
+ """First year in price dataset for the region and specified service"""
560
+ da_end_year: int
561
+ """Final year in price dataset for the region and specified service"""
562
+
563
+
564
+ class ModelStatus(BaseModel):
565
+ """Schema for model status and results"""
566
+ status: str
567
+ """Status of the scheduled run. Possible values are explained in
568
+ :ref:`Tyba Model Run Status Codes <status_codes>`"""
569
+ progress: t.Optional[str]
570
+ """Percentage value indicating the progress towards completing a model simulation. Only present if :attr:`status`
571
+ is not ``'complete'``.
572
+
573
+ - Note that in some cases the simulation may involve multiple optimization iterations, and the progress may appear
574
+ to start over as each additional iteration is undertaken
575
+
576
+ """
577
+ result: t.Optional[V0Results]
578
+ """Model simulation results dictionary with schema defined depending on the model inputs, e.g. scheduling a
579
+ :class:`~generation_models.generation_models.PVGenerationModel` will return a dictionary with
580
+ schema :class:`~generation_models.v0_output_schema.GenerationModelResults`. Only present if
581
+ :attr:`status` is ``'complete'``"""
582
+
583
+
tyba_client/forecast.py CHANGED
@@ -3,6 +3,7 @@ from typing import List, Optional
3
3
 
4
4
 
5
5
  class Forecast(object):
6
+ """-"""
6
7
  def __init__(self, client):
7
8
  self.client = client
8
9
 
@@ -22,6 +23,18 @@ class Forecast(object):
22
23
  prediction_lead_time_mins=None,
23
24
  horizon_mins=None,
24
25
  ):
26
+ """
27
+
28
+ :param object_name:
29
+ :param product:
30
+ :param start_time:
31
+ :param end_time:
32
+ :param forecast_type:
33
+ :param predictions_per_hour:
34
+ :param prediction_lead_time_mins:
35
+ :param horizon_mins:
36
+ :return:
37
+ """
25
38
  return self.get(
26
39
  "most_recent_forecast",
27
40
  params={
@@ -49,6 +62,19 @@ class Forecast(object):
49
62
  horizon_mins=None,
50
63
 
51
64
  ):
65
+ """
66
+
67
+ :param object_name:
68
+ :param product:
69
+ :param start_time:
70
+ :param end_time:
71
+ :param quantiles:
72
+ :param forecast_type:
73
+ :param predictions_per_hour:
74
+ :param prediction_lead_time_mins:
75
+ :param horizon_mins:
76
+ :return:
77
+ """
52
78
  return self.get(
53
79
  "most_recent_probabilistic_forecast",
54
80
  params={
@@ -78,6 +104,21 @@ class Forecast(object):
78
104
  prediction_lead_time_mins=None,
79
105
  horizon_mins=None,
80
106
  ):
107
+ """
108
+
109
+ :param object_name:
110
+ :param product:
111
+ :param start_time:
112
+ :param end_time:
113
+ :param days_ago:
114
+ :param before_time:
115
+ :param exact_vintage:
116
+ :param forecast_type:
117
+ :param predictions_per_hour:
118
+ :param prediction_lead_time_mins:
119
+ :param horizon_mins:
120
+ :return:
121
+ """
81
122
  return self.get(
82
123
  "vintaged_forecast",
83
124
  params={
@@ -110,6 +151,22 @@ class Forecast(object):
110
151
  prediction_lead_time_mins=None,
111
152
  horizon_mins=None,
112
153
  ):
154
+ """
155
+
156
+ :param object_name:
157
+ :param product:
158
+ :param start_time:
159
+ :param end_time:
160
+ :param quantiles:
161
+ :param days_ago:
162
+ :param before_time:
163
+ :param exact_vintage:
164
+ :param forecast_type:
165
+ :param predictions_per_hour:
166
+ :param prediction_lead_time_mins:
167
+ :param horizon_mins:
168
+ :return:
169
+ """
113
170
  return self.get(
114
171
  "vintaged_probabilistic_forecast",
115
172
  params={
@@ -139,6 +196,18 @@ class Forecast(object):
139
196
  prediction_lead_time_mins=None,
140
197
  horizon_mins=None,
141
198
  ):
199
+ """
200
+
201
+ :param object_name:
202
+ :param product:
203
+ :param vintage_start_time:
204
+ :param vintage_end_time:
205
+ :param forecast_type:
206
+ :param predictions_per_hour:
207
+ :param prediction_lead_time_mins:
208
+ :param horizon_mins:
209
+ :return:
210
+ """
142
211
  return self.get(
143
212
  "forecasts_by_vintage",
144
213
  params={
@@ -165,6 +234,19 @@ class Forecast(object):
165
234
  prediction_lead_time_mins=None,
166
235
  horizon_mins=None,
167
236
  ):
237
+ """
238
+
239
+ :param object_name:
240
+ :param product:
241
+ :param quantiles:
242
+ :param vintage_start_time:
243
+ :param vintage_end_time:
244
+ :param forecast_type:
245
+ :param predictions_per_hour:
246
+ :param prediction_lead_time_mins:
247
+ :param horizon_mins:
248
+ :return:
249
+ """
168
250
  return self.get(
169
251
  "probabilistic_forecasts_by_vintage",
170
252
  params={
@@ -184,6 +266,14 @@ class Forecast(object):
184
266
  self, object_name: str, product: str, start_time: datetime, end_time: datetime,
185
267
  predictions_per_hour: Optional[int] = None
186
268
  ):
269
+ """
270
+
271
+ :param object_name:
272
+ :param product:
273
+ :param start_time:
274
+ :param end_time:
275
+ :return:
276
+ """
187
277
  return self.get(
188
278
  "actuals",
189
279
  params={
tyba_client/io.py CHANGED
@@ -5,6 +5,11 @@ from generation_models import (
5
5
  ONDEfficiencyCurve,
6
6
  ONDTemperatureDerateCurve,
7
7
  )
8
+ from warnings import warn
9
+
10
+ warn("""tyba_client.io is deprecated in favor of using generation_models.utils.pvsyst_readers. Tyba will
11
+ cease to support tyba_client.io on 6/1/2025. See https://docs.tybaenergy.com/api/index.html or reach out
12
+ to us for help migrating.""", FutureWarning)
8
13
 
9
14
 
10
15
  def read_pvsyst_file(path: str) -> dict:
@@ -53,7 +58,7 @@ def pv_module_from_pan(
53
58
  bifacial_ground_clearance_height=1.0,
54
59
  bifacial_transmission_factor: float = 0.013,
55
60
  ) -> PVModuleMermoudLejeune:
56
- """_"""
61
+ """DEPRECATED. See :func:`generation_models.utils.pvsyst_readers.pv_module_from_pan`."""
57
62
  pan_blob = read_pvsyst_file(pan_file)
58
63
  data = pan_blob["PVObject_"]["items"]
59
64
  commercial = data["PVObject_Commercial"]["items"]
@@ -103,7 +108,7 @@ def pv_module_from_pan(
103
108
 
104
109
 
105
110
  def inverter_from_ond(ond_file: str, includes_xfmr: bool = True) -> ONDInverter:
106
- """_"""
111
+ """DEPRECATED. See :func:`generation_models.utils.pvsyst_readers.inverter_from_ond`."""
107
112
  ond = read_pvsyst_file(ond_file)
108
113
  data = ond["PVObject_"]["items"]
109
114
  converter = data["Converter"]["items"]
tyba_client/operations.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import datetime
2
+ import json
2
3
  from datetime import date
3
- from typing import Optional
4
+ from typing import Optional, Literal
4
5
 
5
6
 
6
7
  class Operations(object):
@@ -12,6 +13,10 @@ class Operations(object):
12
13
  response.raise_for_status()
13
14
  return response.text
14
15
 
16
+
17
+ def post(self, route, json):
18
+ return self.client.post(f"operations/{route}", json=json)
19
+
15
20
  def performance_report(
16
21
  self,
17
22
  start_date: date,
@@ -82,3 +87,58 @@ class Operations(object):
82
87
  if org_id:
83
88
  params["org_id"] = org_id
84
89
  return self.get("internal_api/assets", params=params)
90
+
91
+ def set_asset_overrides(
92
+ self,
93
+ asset_names: list[str],
94
+ field: str,
95
+ aggregation: Literal["global", "single_day", "single_day_hourly", "12x24"],
96
+ values,
97
+ service: Optional[str] = None,
98
+ date: Optional[datetime.date] = None,
99
+ ):
100
+ assumption = {
101
+ "field": field,
102
+ "data": {"aggregation":aggregation }
103
+ }
104
+ match aggregation:
105
+ case "12x24":
106
+ assumption["data"] = {"aggregation": "12x24"} | values
107
+ case "single_day_hourly":
108
+ assumption["data"] = {
109
+ "aggregation": "single_day_hourly",
110
+ "date": date,
111
+ "values": values
112
+ }
113
+ case "single_day":
114
+ assumption["data"] = {
115
+ "aggregation": "single_day_hourly",
116
+ "values": values,
117
+ "date": date
118
+ }
119
+ case "global":
120
+ assumption["data"] = {
121
+ "aggregation": "global",
122
+ "value": values
123
+ }
124
+ if service:
125
+ assumption["service"] = service
126
+ request_data = {"asset_names": asset_names, "assumption": assumption}
127
+
128
+ res = self.post("internal_api/assets/override/", json=request_data)
129
+ if res.status_code != 200:
130
+ return {
131
+ "status_code": res.status_code,
132
+ "reason": res.reason,
133
+ "message": res.text
134
+ }
135
+ else:
136
+ return json.loads(res.text)
137
+
138
+
139
+
140
+ def overrides_schema(
141
+ self,
142
+ ):
143
+
144
+ return json.loads(self.get("internal_api/overrides_schema"))
@@ -8,6 +8,12 @@ from generation_models import SolarResource, SolarResourceTimeSeries
8
8
  from requests.exceptions import HTTPError
9
9
  import typing as t
10
10
  from io import StringIO
11
+ from warnings import warn
12
+
13
+
14
+ warn("""tyba_client.solar_resource is deprecated in favor of using generation_models.utils.psm_readers. Tyba will
15
+ cease to support tyba_client.solar_resource on 6/1/2025. See https://docs.tybaenergy.com/api/index.html or reach out
16
+ to us for help migrating.""", FutureWarning)
11
17
 
12
18
 
13
19
  @dataclass
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: tyba-client
3
- Version: 0.4.13
3
+ Version: 0.4.16
4
4
  Summary: A Python API client for the Tyba Public API
5
5
  License: MIT
6
6
  Author: Tyler Nisonoff
@@ -19,6 +19,7 @@ Requires-Dist: generation-models (>=0.10.6,<0.11.0)
19
19
  Requires-Dist: marshmallow (>=3.12.1,<4.0.0)
20
20
  Requires-Dist: pandas (>=1.3.2,<2.0.0) ; python_version < "3.9"
21
21
  Requires-Dist: pandas (>=2.0.0,<3.0.0) ; python_version >= "3.9"
22
+ Requires-Dist: pydantic (>=2.10.6,<3.0.0)
22
23
  Requires-Dist: requests (>=2.25.1,<3.0.0)
23
24
  Requires-Dist: structlog (>=24.1.0,<25.0.0)
24
25
  Description-Content-Type: text/markdown
@@ -0,0 +1,12 @@
1
+ tyba_client/__init__.py,sha256=42STGor_9nKYXumfeV5tiyD_M8VdcddX7CEexmibPBk,22
2
+ tyba_client/client.py,sha256=RAvjZkjWDDeY4eJXD2oELF4E1DOa02AqAcvWftsxISY,23295
3
+ tyba_client/forecast.py,sha256=P9GuKPrTCQpdxatSPCck5dezfMIe7otCjOiylyPVh-s,8383
4
+ tyba_client/io.py,sha256=0FVRtUjSDzyWYBvIMSJH7bCclCRqD2Y-pbcsJ-Ufyo0,6226
5
+ tyba_client/models.py,sha256=NOo39qStMkWBhfLPkUu37MaKMe3E2yFc5KAzUnFy96M,17569
6
+ tyba_client/operations.py,sha256=cOAyeV2vz_6FEY4jlOPsMNLmJ8TvIaZgfyYJjMZEejg,4175
7
+ tyba_client/solar_resource.py,sha256=0Jte2cZpE-USHeW-qac4AN1mI2tmrC-VSahnbs4v3Bs,3683
8
+ tyba_client/utils.py,sha256=n4tUBGlQIwxbLqJcQRiAIbIJA0DaLSjbAxakhukqaeE,876
9
+ tyba_client-0.4.16.dist-info/LICENSE,sha256=LbMfEdjEK-IRzvCfdEBhn9UCANze0Rc7hWrQTEj_xvU,1079
10
+ tyba_client-0.4.16.dist-info/METADATA,sha256=v4flCDkfvS6JTnCAEEsNmVkywi0FFNJ5MImlCPtDgyE,1324
11
+ tyba_client-0.4.16.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
12
+ tyba_client-0.4.16.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.1
2
+ Generator: poetry-core 2.1.3
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,21 +0,0 @@
1
- The MIT License (MIT)
2
-
3
- Copyright (c) 2021 Tyba Energy.
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
@@ -1,13 +0,0 @@
1
- LICENSE,sha256=LbMfEdjEK-IRzvCfdEBhn9UCANze0Rc7hWrQTEj_xvU,1079
2
- tyba_client/__init__.py,sha256=42STGor_9nKYXumfeV5tiyD_M8VdcddX7CEexmibPBk,22
3
- tyba_client/client.py,sha256=6hFeAF5rptn6b1Az6sM8j1b2mLN04k_Qc_LOsdZRsBQ,6576
4
- tyba_client/forecast.py,sha256=ehtIZB_RrhXbic5iadm8gGTD5YEZAB0agrFqj61A2mk,6245
5
- tyba_client/io.py,sha256=iyiQrpM1xg9XAoXlU6JvNB1uxRK77keJ8gAG-fefV2U,5783
6
- tyba_client/models.py,sha256=NOo39qStMkWBhfLPkUu37MaKMe3E2yFc5KAzUnFy96M,17569
7
- tyba_client/operations.py,sha256=WkAr_e-15C0wIzP9l_a0uRJGqhObUIEG_iDWo_17TqU,2316
8
- tyba_client/solar_resource.py,sha256=oUD6-qUJhiIjrHbSzSL0X4asGgU4lRBocyy0MkA3keY,3379
9
- tyba_client/utils.py,sha256=n4tUBGlQIwxbLqJcQRiAIbIJA0DaLSjbAxakhukqaeE,876
10
- tyba_client-0.4.13.dist-info/LICENSE,sha256=LbMfEdjEK-IRzvCfdEBhn9UCANze0Rc7hWrQTEj_xvU,1079
11
- tyba_client-0.4.13.dist-info/METADATA,sha256=wab_EgjfsdNK4Ja0vSpJX_L9w1DKGK_J7MDXdXHVYl4,1282
12
- tyba_client-0.4.13.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
13
- tyba_client-0.4.13.dist-info/RECORD,,
File without changes