Skip to content

sda_api

This is a wrapper to communicate with the EMS SDA API.

SdaApi

Client for the Volue SDA‑EMS GraphQL API.

The client provides high‑level methods for:

  • Discovering available bid addresses
  • Fetching block bid packages
  • Fetching curve bid packages

Authentication is handled via OAuth2 client‑credentials. Credentials are loaded from the environment key configuration and resolved via Azure Key Vault.

Attributes:

Name Type Description
env

Current execution environment (e.g. "nonprod", "prod").

base_url

GraphQL endpoint URL selected based on environment.

client_id

OAuth2 client ID.

client_secret

OAuth2 client secret.

session

Persistent HTTP session.

Example
        from physical_operations_utils.ems_utils.SdaApi import SdaApi

        api = SdaApi()
Source code in physical_operations_utils/ems_utils/SdaApi.py
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
class SdaApi:
    """
    Client for the Volue SDA‑EMS GraphQL API.

    The client provides high‑level methods for:

    - Discovering available bid addresses
    - Fetching block bid packages
    - Fetching curve bid packages

    Authentication is handled via OAuth2 client‑credentials.
    Credentials are loaded from the environment key configuration
    and resolved via Azure Key Vault.

    Attributes:
        env: Current execution environment (e.g. ``"nonprod"``, ``"prod"``).
        base_url: GraphQL endpoint URL selected based on environment.
        client_id: OAuth2 client ID.
        client_secret: OAuth2 client secret.
        session: Persistent HTTP session.

    Example:
        ```python
                from physical_operations_utils.ems_utils.SdaApi import SdaApi

                api = SdaApi()
        ```
    """

    def __init__(self, verify_ssl: bool = True, timeout_s: float = 30.0):
        """
        Initialize an SDA‑EMS API client.

        During initialization the client:

        - Resolves the current environment
        - Loads OAuth2 credentials from Key Vault
        - Selects the correct GraphQL endpoint
        - Creates a persistent HTTP session

        Args:
            verify_ssl: Whether SSL certificates are verified.
            timeout_s: HTTP request timeout in seconds.
        """
        self.verify_ssl = verify_ssl
        self.timeout_s = timeout_s
        self.env = setup_environment()

        sda_cfg = get_keys_yaml_file().get("volue_sda_api", {})
        self.client_id = get_secret(sda_cfg["client_id"])
        self.client_secret = get_secret(sda_cfg["client_secret"])

        self.base_url = (
            BASE_URL_TEST if self.env.lower() == "nonprod" else BASE_URL_PROD
        )

        self.session = requests.Session()
        self.session.headers.update(
            {"Accept": "application/json", "Content-Type": "application/json"}
        )

        self._access_token: Optional[str] = None
        self._access_token_expires_at: float = 0.0

    def _get_access_token(self) -> str:
        now = time.time()
        if self._access_token and now < (self._access_token_expires_at - 60):
            return self._access_token

        r = self.session.post(
            TOKEN_URL,
            data={
                "grant_type": "client_credentials",
                "client_id": self.client_id,
                "client_secret": self.client_secret,
                "scope": SCOPE,
            },
            headers={"Content-Type": "application/x-www-form-urlencoded"},
            timeout=self.timeout_s,
            verify=self.verify_ssl,
        )

        try:
            r.raise_for_status()
        except requests.HTTPError:
            logger.warning(
                "Volue SDA token request failed | url=%s | status=%s | response=%s",
                r.url,
                r.status_code,
                r.text[:500],
            )
            raise

        payload = r.json()
        token = payload.get("access_token")
        expires_in = float(payload.get("expires_in", 3600))

        if not token:
            raise VolueSdaApiError(f"Token response missing access_token: {payload}")

        self._access_token = token
        self._access_token_expires_at = now + expires_in
        return token

    def _post_graphql(self, query: str) -> Dict[str, Any]:
        token = self._get_access_token()
        r = self.session.post(
            self.base_url,
            json={"query": query, "variables": {}},
            headers={"Authorization": f"Bearer {token}"},
            timeout=self.timeout_s,
            verify=self.verify_ssl,
        )

        # retry once on 401
        if r.status_code == 401:
            self._access_token = None
            token = self._get_access_token()
            r = self.session.post(
                self.base_url,
                json={"query": query, "variables": {}},
                headers={"Authorization": f"Bearer {token}"},
                timeout=self.timeout_s,
                verify=self.verify_ssl,
            )

        try:
            r.raise_for_status()
        except requests.HTTPError:
            logger.warning(
                "Volue SDA GraphQL HTTP failed | url=%s | status=%s | response=%s",
                r.url,
                r.status_code,
                r.text[:800],
            )
            raise

        payload = r.json()

        if payload.get("errors"):
            logger.warning(
                "Volue SDA GraphQL errors | errors=%s", str(payload["errors"])[:2000]
            )
            raise VolueSdaApiError(f"GraphQL errors: {payload['errors']}")

        data = payload.get("data")
        if data is None:
            raise VolueSdaApiError(f"GraphQL response missing data: {payload}")

        return data

    def _trading(self, query: str) -> Dict[str, Any]:
        data = self._post_graphql(query)
        trading = data.get("trading")
        if trading is None:
            raise VolueSdaApiError(f"Missing 'trading' in response: {data}")
        return trading

    # Queries

    def bids_addresses(self, *, data_group: str) -> List[str]:
        """
        List available SDA bid addresses for a given data group.

        This method returns all external matrix or bid addresses that
        exist within the specified SDA data group.

        Args:
            data_group: SDA data group identifier
                (e.g. ``"DA_MATRICES_OUT"``).

        Returns:
            List[str]: List of available SDA bid addresses.

        Example:
        ```python
                api = SdaApi()

                addresses = api.bids_addresses(
                    data_group="DA_MATRICES_OUT"
                )
        ```
        """
        q = f"""
        query {{
          trading {{
            bidsAddresses(dataGroup: {_gql(data_group)})
          }}
        }}
        """
        return self._trading(q)["bidsAddresses"]

    def block_bids_package(
        self,
        *,
        address: str,
        data_group: str,
        delivery_date_utc: Optional[str] = None,
        from_delivery_date_utc: Optional[str] = None,
        to_delivery_date_utc: Optional[str] = None,
    ) -> Dict[str, Any]:
        """
        Fetch block bid packages for a given SDA address.

        Exactly one of the following must be provided:
        - ``delivery_date_utc``
        - ``from_delivery_date_utc`` and ``to_delivery_date_utc``

        Args:
            address: SDA bid address.
            data_group: SDA data group.
            delivery_date_utc: Single delivery date (UTC, ISO‑8601).
            from_delivery_date_utc: Start of delivery date range (UTC).
            to_delivery_date_utc: End of delivery date range (UTC).

        Returns:
            Dict[str, Any]: Raw block bid package response.

        Raises:
            ValueError: If no valid delivery date filter is provided.
            VolueSdaApiError: If the API returns an error.

        Example:
        ```python
                api = SdaApi()

                result = api.block_bids_package(
                    address="BORLÄNGE_CHP_MATRIX",
                    data_group="DA_MATRICES_OUT",
                    delivery_date_utc="2025-10-01T22:00Z",
                )

                print(result["success"])
                print(result["bids"])
        ```
        """
        criteria: Dict[str, Any] = {"address": address, "dataGroup": data_group}

        if delivery_date_utc:
            criteria["getBidByDeliveryDate"] = {"deliveryDate": delivery_date_utc}
        elif from_delivery_date_utc and to_delivery_date_utc:
            criteria["getBidsByRange"] = {
                "fromDeliveryDate": from_delivery_date_utc,
                "toDeliveryDate": to_delivery_date_utc,
            }
        else:
            raise ValueError(
                "Provide delivery_date_utc OR (from_delivery_date_utc and to_delivery_date_utc)"
            )

        q = f"""
        query {{
          trading {{
            blockBidsPackage(criteria: {_gql(criteria)}) {{
              success
              errors {{ errorCode message }}
              bids {{
                dataGroup
                address
                deliveryDate
                periodLength
                type
                currency
                bids {{
                  reference
                  linkedTo
                  deliveryStartTimeUtc
                  periodCount
                  price
                  volume
                }}
              }}
            }}
          }}
        }}
        """
        return self._trading(q)["blockBidsPackage"]

    def curve_bids_package(
        self,
        *,
        address: str,
        data_group: str,
        delivery_date_utc: Optional[str] = None,
        from_delivery_date_utc: Optional[str] = None,
        to_delivery_date_utc: Optional[str] = None,
    ) -> Dict[str, Any]:
        """
        Fetch curve bid packages for a given SDA address.

        Exactly one of the following must be provided:
        - ``delivery_date_utc``
        - ``from_delivery_date_utc`` and ``to_delivery_date_utc``

        Args:
            address: SDA bid address.
            data_group: SDA data group.
            delivery_date_utc: Single delivery date (UTC, ISO‑8601).
            from_delivery_date_utc: Start of delivery date range (UTC).
            to_delivery_date_utc: End of delivery date range (UTC).

        Returns:
            Dict[str, Any]: Raw curve bid package response.

        Raises:
            ValueError: If no valid delivery date filter is provided.
            VolueSdaApiError: If the API returns an error.

        Notes:
            The SDA backend may return ``Unexpected Execution Error``
            when no curve bids exist for the given criteria. This should
            be treated as “no data”.

        Example:
            ```python
                api = SdaApi()

                try:
                    result = api.curve_bids_package(
                        address="BORLÄNGE_CHP_MATRIX",
                        data_group="DA_MATRICES_OUT",
                        from_delivery_date_utc="2025-10-01T22:00Z",
                        to_delivery_date_utc="2025-10-02T22:00Z",
                    )
                except VolueSdaApiError:
                    result = {"bids": []}

                for bid in result.get("bids", []):
                    print(bid["deliveryDate"], bid["type"])
            ```
        """
        criteria: Dict[str, Any] = {"address": address, "dataGroup": data_group}

        if delivery_date_utc:
            criteria["getBidByDeliveryDate"] = {"deliveryDate": delivery_date_utc}
        elif from_delivery_date_utc and to_delivery_date_utc:
            criteria["getBidsByRange"] = {
                "fromDeliveryDate": from_delivery_date_utc,
                "toDeliveryDate": to_delivery_date_utc,
            }
        else:
            raise ValueError(
                "Provide delivery_date_utc OR (from_delivery_date_utc and to_delivery_date_utc)"
            )

        q = f"""
        query {{
          trading {{
            curveBidsPackage(criteria: {_gql(criteria)}) {{
              success
              errors {{ errorCode message }}
              bids {{
                address
                deliveryDate
                dataGroup
                type
                periodLength
                currency
                bids {{
                  price
                  deliveryStartTimeUtc
                  periodVolumes
                }}
              }}
            }}
          }}
        }}
        """
        return self._trading(q)["curveBidsPackage"]

    # Mutations

    def update_curve_bids_package(
        self,
        *,
        address: str,
        data_group: str,
        delivery_date: str,
        period_length: str,
        currency: str,
        bid_type: str = "PERIOD_MATRIX_DA",
        bids: List[Dict[str, Any]],
    ) -> Dict[str, Any]:
        """
        Submit (create/update) curve bid packages via the SDA GraphQL mutation.

        Args:
            address: SDA bid address.
            data_group: SDA data group.
            delivery_date: Delivery date (ISO-8601 date, e.g. ``"2026-05-21"``).
            period_length: Period length enum name (e.g. ``"PT15M"``, ``"PT60M"``).
            currency: Currency enum name (e.g. ``"EUR"``).
            bid_type: Bid type enum name (default ``"PERIOD_MATRIX_DA"``).
            bids: List of bid dicts, each with ``price`` (float),
                ``deliveryStartTimeUtc`` (str), and ``periodVolumes`` (list of float).

        Returns:
            Dict[str, Any]: Raw mutation response containing ``value`` and ``errors``.

        Raises:
            VolueSdaApiError: If the API returns GraphQL errors.

        Example:
            ```python
                api = SdaApi()

                result = api.update_curve_bids_package(
                    address="DA_MLK_TEST_MATRIX",
                    data_group="MLK_DA_MATRIX",
                    delivery_date="2026-05-21",
                    period_length="PT15M",
                    currency="EUR",
                    bid_type="PERIOD_MATRIX_DA",
                    bids=[{
                        "price": 2.0,
                        "deliveryStartTimeUtc": "2026-05-20T23:00:00.000Z",
                        "periodVolumes": [-13.20, -13.20, ...],
                    }],
                )
            ```
        """
        input_package = {
            "address": address,
            "dataGroup": data_group,
            "deliveryDate": delivery_date,
            "periodLength": _GqlEnum(period_length),
            "currency": _GqlEnum(currency),
            "type": _GqlEnum(bid_type),
            "bids": bids,
        }

        q = f"""
        mutation {{
          trading {{
            updateCurveBidsPackage(input: {_gql([input_package])}) {{
              value {{
                address
                dataGroup
                success
              }}
              errors {{
                code: __typename
                ... on Error {{ message }}
                ... on RestRequestError {{ message }}
              }}
            }}
          }}
        }}
        """
        return self._trading(q)["updateCurveBidsPackage"]

__init__(verify_ssl=True, timeout_s=30.0)

Initialize an SDA‑EMS API client.

During initialization the client:

  • Resolves the current environment
  • Loads OAuth2 credentials from Key Vault
  • Selects the correct GraphQL endpoint
  • Creates a persistent HTTP session

Parameters:

Name Type Description Default
verify_ssl bool

Whether SSL certificates are verified.

True
timeout_s float

HTTP request timeout in seconds.

30.0
Source code in physical_operations_utils/ems_utils/SdaApi.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
def __init__(self, verify_ssl: bool = True, timeout_s: float = 30.0):
    """
    Initialize an SDA‑EMS API client.

    During initialization the client:

    - Resolves the current environment
    - Loads OAuth2 credentials from Key Vault
    - Selects the correct GraphQL endpoint
    - Creates a persistent HTTP session

    Args:
        verify_ssl: Whether SSL certificates are verified.
        timeout_s: HTTP request timeout in seconds.
    """
    self.verify_ssl = verify_ssl
    self.timeout_s = timeout_s
    self.env = setup_environment()

    sda_cfg = get_keys_yaml_file().get("volue_sda_api", {})
    self.client_id = get_secret(sda_cfg["client_id"])
    self.client_secret = get_secret(sda_cfg["client_secret"])

    self.base_url = (
        BASE_URL_TEST if self.env.lower() == "nonprod" else BASE_URL_PROD
    )

    self.session = requests.Session()
    self.session.headers.update(
        {"Accept": "application/json", "Content-Type": "application/json"}
    )

    self._access_token: Optional[str] = None
    self._access_token_expires_at: float = 0.0

bids_addresses(*, data_group)

List available SDA bid addresses for a given data group.

This method returns all external matrix or bid addresses that exist within the specified SDA data group.

Parameters:

Name Type Description Default
data_group str

SDA data group identifier (e.g. "DA_MATRICES_OUT").

required

Returns:

Type Description
List[str]

List[str]: List of available SDA bid addresses.

Example:

        api = SdaApi()

        addresses = api.bids_addresses(
            data_group="DA_MATRICES_OUT"
        )
Source code in physical_operations_utils/ems_utils/SdaApi.py
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
def bids_addresses(self, *, data_group: str) -> List[str]:
    """
    List available SDA bid addresses for a given data group.

    This method returns all external matrix or bid addresses that
    exist within the specified SDA data group.

    Args:
        data_group: SDA data group identifier
            (e.g. ``"DA_MATRICES_OUT"``).

    Returns:
        List[str]: List of available SDA bid addresses.

    Example:
    ```python
            api = SdaApi()

            addresses = api.bids_addresses(
                data_group="DA_MATRICES_OUT"
            )
    ```
    """
    q = f"""
    query {{
      trading {{
        bidsAddresses(dataGroup: {_gql(data_group)})
      }}
    }}
    """
    return self._trading(q)["bidsAddresses"]

block_bids_package(*, address, data_group, delivery_date_utc=None, from_delivery_date_utc=None, to_delivery_date_utc=None)

Fetch block bid packages for a given SDA address.

Exactly one of the following must be provided: - delivery_date_utc - from_delivery_date_utc and to_delivery_date_utc

Parameters:

Name Type Description Default
address str

SDA bid address.

required
data_group str

SDA data group.

required
delivery_date_utc Optional[str]

Single delivery date (UTC, ISO‑8601).

None
from_delivery_date_utc Optional[str]

Start of delivery date range (UTC).

None
to_delivery_date_utc Optional[str]

End of delivery date range (UTC).

None

Returns:

Type Description
Dict[str, Any]

Dict[str, Any]: Raw block bid package response.

Raises:

Type Description
ValueError

If no valid delivery date filter is provided.

VolueSdaApiError

If the API returns an error.

Example:

        api = SdaApi()

        result = api.block_bids_package(
            address="BORLÄNGE_CHP_MATRIX",
            data_group="DA_MATRICES_OUT",
            delivery_date_utc="2025-10-01T22:00Z",
        )

        print(result["success"])
        print(result["bids"])
Source code in physical_operations_utils/ems_utils/SdaApi.py
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
def block_bids_package(
    self,
    *,
    address: str,
    data_group: str,
    delivery_date_utc: Optional[str] = None,
    from_delivery_date_utc: Optional[str] = None,
    to_delivery_date_utc: Optional[str] = None,
) -> Dict[str, Any]:
    """
    Fetch block bid packages for a given SDA address.

    Exactly one of the following must be provided:
    - ``delivery_date_utc``
    - ``from_delivery_date_utc`` and ``to_delivery_date_utc``

    Args:
        address: SDA bid address.
        data_group: SDA data group.
        delivery_date_utc: Single delivery date (UTC, ISO‑8601).
        from_delivery_date_utc: Start of delivery date range (UTC).
        to_delivery_date_utc: End of delivery date range (UTC).

    Returns:
        Dict[str, Any]: Raw block bid package response.

    Raises:
        ValueError: If no valid delivery date filter is provided.
        VolueSdaApiError: If the API returns an error.

    Example:
    ```python
            api = SdaApi()

            result = api.block_bids_package(
                address="BORLÄNGE_CHP_MATRIX",
                data_group="DA_MATRICES_OUT",
                delivery_date_utc="2025-10-01T22:00Z",
            )

            print(result["success"])
            print(result["bids"])
    ```
    """
    criteria: Dict[str, Any] = {"address": address, "dataGroup": data_group}

    if delivery_date_utc:
        criteria["getBidByDeliveryDate"] = {"deliveryDate": delivery_date_utc}
    elif from_delivery_date_utc and to_delivery_date_utc:
        criteria["getBidsByRange"] = {
            "fromDeliveryDate": from_delivery_date_utc,
            "toDeliveryDate": to_delivery_date_utc,
        }
    else:
        raise ValueError(
            "Provide delivery_date_utc OR (from_delivery_date_utc and to_delivery_date_utc)"
        )

    q = f"""
    query {{
      trading {{
        blockBidsPackage(criteria: {_gql(criteria)}) {{
          success
          errors {{ errorCode message }}
          bids {{
            dataGroup
            address
            deliveryDate
            periodLength
            type
            currency
            bids {{
              reference
              linkedTo
              deliveryStartTimeUtc
              periodCount
              price
              volume
            }}
          }}
        }}
      }}
    }}
    """
    return self._trading(q)["blockBidsPackage"]

curve_bids_package(*, address, data_group, delivery_date_utc=None, from_delivery_date_utc=None, to_delivery_date_utc=None)

Fetch curve bid packages for a given SDA address.

Exactly one of the following must be provided: - delivery_date_utc - from_delivery_date_utc and to_delivery_date_utc

Parameters:

Name Type Description Default
address str

SDA bid address.

required
data_group str

SDA data group.

required
delivery_date_utc Optional[str]

Single delivery date (UTC, ISO‑8601).

None
from_delivery_date_utc Optional[str]

Start of delivery date range (UTC).

None
to_delivery_date_utc Optional[str]

End of delivery date range (UTC).

None

Returns:

Type Description
Dict[str, Any]

Dict[str, Any]: Raw curve bid package response.

Raises:

Type Description
ValueError

If no valid delivery date filter is provided.

VolueSdaApiError

If the API returns an error.

Notes

The SDA backend may return Unexpected Execution Error when no curve bids exist for the given criteria. This should be treated as “no data”.

Example
    api = SdaApi()

    try:
        result = api.curve_bids_package(
            address="BORLÄNGE_CHP_MATRIX",
            data_group="DA_MATRICES_OUT",
            from_delivery_date_utc="2025-10-01T22:00Z",
            to_delivery_date_utc="2025-10-02T22:00Z",
        )
    except VolueSdaApiError:
        result = {"bids": []}

    for bid in result.get("bids", []):
        print(bid["deliveryDate"], bid["type"])
Source code in physical_operations_utils/ems_utils/SdaApi.py
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
def curve_bids_package(
    self,
    *,
    address: str,
    data_group: str,
    delivery_date_utc: Optional[str] = None,
    from_delivery_date_utc: Optional[str] = None,
    to_delivery_date_utc: Optional[str] = None,
) -> Dict[str, Any]:
    """
    Fetch curve bid packages for a given SDA address.

    Exactly one of the following must be provided:
    - ``delivery_date_utc``
    - ``from_delivery_date_utc`` and ``to_delivery_date_utc``

    Args:
        address: SDA bid address.
        data_group: SDA data group.
        delivery_date_utc: Single delivery date (UTC, ISO‑8601).
        from_delivery_date_utc: Start of delivery date range (UTC).
        to_delivery_date_utc: End of delivery date range (UTC).

    Returns:
        Dict[str, Any]: Raw curve bid package response.

    Raises:
        ValueError: If no valid delivery date filter is provided.
        VolueSdaApiError: If the API returns an error.

    Notes:
        The SDA backend may return ``Unexpected Execution Error``
        when no curve bids exist for the given criteria. This should
        be treated as “no data”.

    Example:
        ```python
            api = SdaApi()

            try:
                result = api.curve_bids_package(
                    address="BORLÄNGE_CHP_MATRIX",
                    data_group="DA_MATRICES_OUT",
                    from_delivery_date_utc="2025-10-01T22:00Z",
                    to_delivery_date_utc="2025-10-02T22:00Z",
                )
            except VolueSdaApiError:
                result = {"bids": []}

            for bid in result.get("bids", []):
                print(bid["deliveryDate"], bid["type"])
        ```
    """
    criteria: Dict[str, Any] = {"address": address, "dataGroup": data_group}

    if delivery_date_utc:
        criteria["getBidByDeliveryDate"] = {"deliveryDate": delivery_date_utc}
    elif from_delivery_date_utc and to_delivery_date_utc:
        criteria["getBidsByRange"] = {
            "fromDeliveryDate": from_delivery_date_utc,
            "toDeliveryDate": to_delivery_date_utc,
        }
    else:
        raise ValueError(
            "Provide delivery_date_utc OR (from_delivery_date_utc and to_delivery_date_utc)"
        )

    q = f"""
    query {{
      trading {{
        curveBidsPackage(criteria: {_gql(criteria)}) {{
          success
          errors {{ errorCode message }}
          bids {{
            address
            deliveryDate
            dataGroup
            type
            periodLength
            currency
            bids {{
              price
              deliveryStartTimeUtc
              periodVolumes
            }}
          }}
        }}
      }}
    }}
    """
    return self._trading(q)["curveBidsPackage"]

update_curve_bids_package(*, address, data_group, delivery_date, period_length, currency, bid_type='PERIOD_MATRIX_DA', bids)

Submit (create/update) curve bid packages via the SDA GraphQL mutation.

Parameters:

Name Type Description Default
address str

SDA bid address.

required
data_group str

SDA data group.

required
delivery_date str

Delivery date (ISO-8601 date, e.g. "2026-05-21").

required
period_length str

Period length enum name (e.g. "PT15M", "PT60M").

required
currency str

Currency enum name (e.g. "EUR").

required
bid_type str

Bid type enum name (default "PERIOD_MATRIX_DA").

'PERIOD_MATRIX_DA'
bids List[Dict[str, Any]]

List of bid dicts, each with price (float), deliveryStartTimeUtc (str), and periodVolumes (list of float).

required

Returns:

Type Description
Dict[str, Any]

Dict[str, Any]: Raw mutation response containing value and errors.

Raises:

Type Description
VolueSdaApiError

If the API returns GraphQL errors.

Example
    api = SdaApi()

    result = api.update_curve_bids_package(
        address="DA_MLK_TEST_MATRIX",
        data_group="MLK_DA_MATRIX",
        delivery_date="2026-05-21",
        period_length="PT15M",
        currency="EUR",
        bid_type="PERIOD_MATRIX_DA",
        bids=[{
            "price": 2.0,
            "deliveryStartTimeUtc": "2026-05-20T23:00:00.000Z",
            "periodVolumes": [-13.20, -13.20, ...],
        }],
    )
Source code in physical_operations_utils/ems_utils/SdaApi.py
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
def update_curve_bids_package(
    self,
    *,
    address: str,
    data_group: str,
    delivery_date: str,
    period_length: str,
    currency: str,
    bid_type: str = "PERIOD_MATRIX_DA",
    bids: List[Dict[str, Any]],
) -> Dict[str, Any]:
    """
    Submit (create/update) curve bid packages via the SDA GraphQL mutation.

    Args:
        address: SDA bid address.
        data_group: SDA data group.
        delivery_date: Delivery date (ISO-8601 date, e.g. ``"2026-05-21"``).
        period_length: Period length enum name (e.g. ``"PT15M"``, ``"PT60M"``).
        currency: Currency enum name (e.g. ``"EUR"``).
        bid_type: Bid type enum name (default ``"PERIOD_MATRIX_DA"``).
        bids: List of bid dicts, each with ``price`` (float),
            ``deliveryStartTimeUtc`` (str), and ``periodVolumes`` (list of float).

    Returns:
        Dict[str, Any]: Raw mutation response containing ``value`` and ``errors``.

    Raises:
        VolueSdaApiError: If the API returns GraphQL errors.

    Example:
        ```python
            api = SdaApi()

            result = api.update_curve_bids_package(
                address="DA_MLK_TEST_MATRIX",
                data_group="MLK_DA_MATRIX",
                delivery_date="2026-05-21",
                period_length="PT15M",
                currency="EUR",
                bid_type="PERIOD_MATRIX_DA",
                bids=[{
                    "price": 2.0,
                    "deliveryStartTimeUtc": "2026-05-20T23:00:00.000Z",
                    "periodVolumes": [-13.20, -13.20, ...],
                }],
            )
        ```
    """
    input_package = {
        "address": address,
        "dataGroup": data_group,
        "deliveryDate": delivery_date,
        "periodLength": _GqlEnum(period_length),
        "currency": _GqlEnum(currency),
        "type": _GqlEnum(bid_type),
        "bids": bids,
    }

    q = f"""
    mutation {{
      trading {{
        updateCurveBidsPackage(input: {_gql([input_package])}) {{
          value {{
            address
            dataGroup
            success
          }}
          errors {{
            code: __typename
            ... on Error {{ message }}
            ... on RestRequestError {{ message }}
          }}
        }}
      }}
    }}
    """
    return self._trading(q)["updateCurveBidsPackage"]

VolueSdaApiError

Bases: RuntimeError

Raised for HTTP/GraphQL failures when calling the Volue SDA-EMS API.

Source code in physical_operations_utils/ems_utils/SdaApi.py
25
26
class VolueSdaApiError(RuntimeError):
    """Raised for HTTP/GraphQL failures when calling the Volue SDA-EMS API."""

format_curve_bids_for_sda(df, *, address, data_group, delivery_date, bid_type='PERIOD_MATRIX_DA')

Transform database bid rows into kwargs for :meth:SdaApi.update_curve_bids_package.

The DataFrame is expected to have the following columns (matching the database schema):

  • start_time_lb_utc: Period start time (datetime-like).
  • price: Bid price level.
  • volume_mw: Volume for this period/price combination.
  • currency: Currency code (e.g. "EUR").
  • resolution_seconds: Period length in seconds (e.g. 900 for 15 min).

Rows are grouped by price, sorted by start_time_lb_utc, and the volumes are collected into periodVolumes lists.

Parameters:

Name Type Description Default
df DataFrame

DataFrame with bid rows from the database.

required
address str

SDA bid address for the mutation.

required
data_group str

SDA data group for the mutation.

required
delivery_date str

Delivery date string (ISO-8601 date, e.g. "2026-05-21").

required
bid_type str

Bid type enum name (default "PERIOD_MATRIX_DA").

'PERIOD_MATRIX_DA'

Returns:

Type Description
Dict[str, Any]

Dict ready to unpack into

Dict[str, Any]

meth:SdaApi.update_curve_bids_package as **kwargs.

Raises:

Type Description
ValueError

If the DataFrame is empty or has unsupported resolution.

Example
    import pandas as pd
    from physical_operations_utils.ems_utils.SdaApi import (
        SdaApi, format_curve_bids_for_sda,
    )

    df = pd.read_sql(query, connection)

    kwargs = format_curve_bids_for_sda(
        df,
        address="DA_MLK_TEST_MATRIX",
        data_group="MLK_DA_MATRIX",
        delivery_date="2026-05-21",
    )

    api = SdaApi()
    result = api.update_curve_bids_package(**kwargs)
Source code in physical_operations_utils/ems_utils/SdaApi.py
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
def format_curve_bids_for_sda(
    df: pd.DataFrame,
    *,
    address: str,
    data_group: str,
    delivery_date: str,
    bid_type: str = "PERIOD_MATRIX_DA",
) -> Dict[str, Any]:
    """
    Transform database bid rows into kwargs for
    :meth:`SdaApi.update_curve_bids_package`.

    The DataFrame is expected to have the following columns
    (matching the database schema):

    - ``start_time_lb_utc``: Period start time (datetime-like).
    - ``price``: Bid price level.
    - ``volume_mw``: Volume for this period/price combination.
    - ``currency``: Currency code (e.g. ``"EUR"``).
    - ``resolution_seconds``: Period length in seconds (e.g. 900 for 15 min).

    Rows are grouped by ``price``, sorted by ``start_time_lb_utc``, and the
    volumes are collected into ``periodVolumes`` lists.

    Args:
        df: DataFrame with bid rows from the database.
        address: SDA bid address for the mutation.
        data_group: SDA data group for the mutation.
        delivery_date: Delivery date string (ISO-8601 date, e.g.
            ``"2026-05-21"``).
        bid_type: Bid type enum name (default ``"PERIOD_MATRIX_DA"``).

    Returns:
        Dict ready to unpack into
        :meth:`SdaApi.update_curve_bids_package` as ``**kwargs``.

    Raises:
        ValueError: If the DataFrame is empty or has unsupported resolution.

    Example:
        ```python
            import pandas as pd
            from physical_operations_utils.ems_utils.SdaApi import (
                SdaApi, format_curve_bids_for_sda,
            )

            df = pd.read_sql(query, connection)

            kwargs = format_curve_bids_for_sda(
                df,
                address="DA_MLK_TEST_MATRIX",
                data_group="MLK_DA_MATRIX",
                delivery_date="2026-05-21",
            )

            api = SdaApi()
            result = api.update_curve_bids_package(**kwargs)
        ```
    """
    if df.empty:
        raise ValueError("DataFrame is empty, cannot format bids.")

    resolution_seconds = int(df["resolution_seconds"].iloc[0])
    period_length_enum = PERIOD_LENGTH_MAP.get(resolution_seconds)
    if period_length_enum is None:
        raise ValueError(
            f"Unsupported resolution_seconds={resolution_seconds}. "
            f"Supported: {list(PERIOD_LENGTH_MAP.keys())}"
        )

    currency = str(df["currency"].iloc[0])

    delivery_start_utc = pd.Timestamp(df["start_time_lb_utc"].min()).strftime(
        "%Y-%m-%dT%H:%M:%S.000Z"
    )

    bids = []
    for price, group in df.groupby("price", sort=True):
        group_sorted = group.sort_values("start_time_lb_utc")
        period_volumes = group_sorted["volume_mw"].tolist()
        bids.append(
            {
                "price": float(price),
                "deliveryStartTimeUtc": delivery_start_utc,
                "periodVolumes": period_volumes,
            }
        )

    return {
        "address": address,
        "data_group": data_group,
        "delivery_date": delivery_date,
        "period_length": period_length_enum.value,
        "currency": currency,
        "bid_type": bid_type,
        "bids": bids,
    }