detquantlib 5.10.2__tar.gz → 5.10.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. {detquantlib-5.10.2 → detquantlib-5.10.3}/PKG-INFO +1 -1
  2. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/data/databases/azure_client.py +67 -5
  3. {detquantlib-5.10.2 → detquantlib-5.10.3}/pyproject.toml +1 -1
  4. {detquantlib-5.10.2 → detquantlib-5.10.3}/LICENSE.txt +0 -0
  5. {detquantlib-5.10.2 → detquantlib-5.10.3}/README.md +0 -0
  6. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/__init__.py +0 -0
  7. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/assets/__init__.py +0 -0
  8. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/assets/battery.py +0 -0
  9. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/assets/wind_turbine.py +0 -0
  10. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/converters/__init__.py +0 -0
  11. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/converters/definitions.py +0 -0
  12. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/converters/energy.py +0 -0
  13. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/converters/helpers.py +0 -0
  14. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/converters/price.py +0 -0
  15. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/data/__init__.py +0 -0
  16. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/data/databases/detdatabase.py +0 -0
  17. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/data/databases/helpers.py +0 -0
  18. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/data/databases/snowflake_client.py +0 -0
  19. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/data/entsoe/entsoe.py +0 -0
  20. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/data/sftp/sftp.py +0 -0
  21. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/data/tennet/tennet_client.py +0 -0
  22. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/dates/__init__.py +0 -0
  23. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/dates/dates.py +0 -0
  24. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/figures/__init__.py +0 -0
  25. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/figures/plotly_figures.py +0 -0
  26. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/forecasting/__init__.py +0 -0
  27. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/forecasting/forecasting.py +0 -0
  28. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/outputs/__init__.py +0 -0
  29. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/outputs/outputs_interface.py +0 -0
  30. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/snapshot_testing/README.md +0 -0
  31. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/snapshot_testing/__init__.py +0 -0
  32. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/snapshot_testing/cases.py +0 -0
  33. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/snapshot_testing/comparison.py +0 -0
  34. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/snapshot_testing/config.py +0 -0
  35. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/snapshot_testing/errors.py +0 -0
  36. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/snapshot_testing/isolation.py +0 -0
  37. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/snapshot_testing/pytest_plugin.py +0 -0
  38. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/snapshot_testing/recording.py +0 -0
  39. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/snapshot_testing/tables.py +0 -0
  40. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/stats/__init__.py +0 -0
  41. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/stats/data_analysis.py +0 -0
  42. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/tradable_products/__init__.py +0 -0
  43. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/tradable_products/tradable_products.py +0 -0
  44. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/utils/__init__.py +0 -0
  45. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/utils/logging.py +0 -0
  46. {detquantlib-5.10.2 → detquantlib-5.10.3}/detquantlib/utils/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detquantlib
3
- Version: 5.10.2
3
+ Version: 5.10.3
4
4
  Summary: An internal library containing functions and classes that can be used across Quant models.
5
5
  License-File: LICENSE.txt
6
6
  Author: Dynamic Energy Trading
@@ -11,6 +11,7 @@ import logging
11
11
  import os
12
12
  import re
13
13
  from dataclasses import dataclass, field
14
+ from datetime import datetime, timezone
14
15
  from types import TracebackType
15
16
  from typing import Any, Iterator, Optional, Sequence
16
17
 
@@ -18,6 +19,7 @@ from typing import Any, Iterator, Optional, Sequence
18
19
  import pandas as pd
19
20
  import pyodbc
20
21
  from sqlalchemy import Engine, create_engine, inspect, text
22
+ from sqlalchemy.dialects.mssql import DATETIMEOFFSET
21
23
  from sqlalchemy.exc import SQLAlchemyError
22
24
 
23
25
  logger = logging.getLogger(__name__)
@@ -107,6 +109,16 @@ class AzureClient:
107
109
 
108
110
  Query parameters use SQLAlchemy's named placeholder style, e.g. ``:status``.
109
111
 
112
+ Timezone-aware datetimes (in parameters and in DataFrames written with ``write_table()``) are
113
+ converted to UTC before being sent, because the ODBC driver would otherwise silently drop
114
+ their timezone. As a result, ``DATETIMEOFFSET`` columns are compared with, and store, the
115
+ correct point in time.
116
+ WARNING: ``DATETIME``/``DATETIME2`` database columns are assumed to hold UTC times. If they
117
+ instead hold times in a local timezone, first set timezone-aware datetimes (in parameters and
118
+ in DataFrames) to the same local timezone and then convert them to timezone-naive datetimes,
119
+ before passing them to the AzureClient. This ensures that the AzureClient will not convert
120
+ them to UTC.
121
+
110
122
  Attributes:
111
123
  config: The connection settings used by this client.
112
124
 
@@ -318,7 +330,9 @@ class AzureClient:
318
330
 
319
331
  Args:
320
332
  sql: The SQL statement to execute.
321
- params: Values bound to the named placeholders in ``sql``.
333
+ params: Values bound to the named placeholders in ``sql``. Timezone-aware datetimes
334
+ (including pandas Timestamps) are converted to UTC, and ``NaT`` to ``None``; see
335
+ ``_prepare_params()``.
322
336
  database: Database to run the query in. If ``None``, ``config.database`` is used.
323
337
 
324
338
  Returns:
@@ -331,7 +345,7 @@ class AzureClient:
331
345
  engine = self.connect(database)
332
346
  try:
333
347
  with engine.connect() as conn:
334
- result = conn.execute(text(sql), params or {})
348
+ result = conn.execute(text(sql), self._prepare_params(params))
335
349
  cols = list(result.keys())
336
350
  df = pd.DataFrame.from_records(result.fetchall(), columns=cols, coerce_float=True)
337
351
  except SQLAlchemyError as e:
@@ -364,7 +378,7 @@ class AzureClient:
364
378
  """
365
379
  engine = self.connect(database)
366
380
  with engine.connect() as conn:
367
- result = conn.execute(text(sql), params or {})
381
+ result = conn.execute(text(sql), self._prepare_params(params))
368
382
  cols = list(result.keys())
369
383
  while rows := result.fetchmany(batch_size):
370
384
  yield pd.DataFrame.from_records(rows, columns=cols, coerce_float=True)
@@ -457,9 +471,14 @@ class AzureClient:
457
471
  is much faster than row-by-row inserts. The whole write runs in a single transaction: if
458
472
  any part fails, the table is left unchanged.
459
473
 
474
+ Timezone-aware datetime columns are written as UTC times. A ``DATETIMEOFFSET`` column
475
+ stores them with a ``+00:00`` offset (i.e. the same point in time), and a ``DATETIME2``
476
+ column stores the UTC time. Tables created by this method use ``DATETIMEOFFSET`` for
477
+ these columns.
478
+
460
479
  Args:
461
480
  df: The data to write. The index is not written. Unless the table is created by this
462
- call, column names must match the table's columns.
481
+ call, column names must match the table's columns. ``df`` itself is not modified.
463
482
  table: Table name, optionally qualified, e.g. ``"SCHEMA.TABLE"`` or
464
483
  ``"DB.SCHEMA.TABLE"``. A database part selects the database to write to. Parts may
465
484
  be wrapped in square brackets. If no schema is given, the user's default schema
@@ -494,6 +513,14 @@ class AzureClient:
494
513
  schema, table_name = ([None] + parts)[-2:]
495
514
  table_sql = ".".join(self._quote_identifier(p) for p in parts)
496
515
 
516
+ # The ODBC driver drops timezones, so send UTC times. Without an explicit type, pandas
517
+ # would create these columns as TIMESTAMP, which is a row version in SQL Server.
518
+ tz_cols = [c for c in df.columns if isinstance(df[c].dtype, pd.DatetimeTZDtype)]
519
+ if tz_cols:
520
+ df = df.copy()
521
+ for c in tz_cols:
522
+ df[c] = df[c].dt.tz_convert("UTC").dt.tz_localize(None)
523
+
497
524
  engine = self.connect(database)
498
525
  # pandas' to_sql always creates missing tables, so the table must be checked beforehand
499
526
  if not auto_create_table and not inspect(engine).has_table(table_name, schema=schema):
@@ -511,6 +538,7 @@ class AzureClient:
511
538
  if_exists="replace" if overwrite and auto_create_table else "append",
512
539
  index=False,
513
540
  chunksize=chunk_size,
541
+ dtype={c: DATETIMEOFFSET() for c in tz_cols} or None,
514
542
  )
515
543
  except SQLAlchemyError as e:
516
544
  logger.error("Write to %s failed: %s", table, e)
@@ -547,7 +575,7 @@ class AzureClient:
547
575
  engine = self.connect(database)
548
576
  try:
549
577
  with engine.begin() as conn:
550
- rowcount = conn.execute(text(sql), params or {}).rowcount
578
+ rowcount = conn.execute(text(sql), self._prepare_params(params)).rowcount
551
579
  except SQLAlchemyError as e:
552
580
  logger.error("Statement failed: %s", e)
553
581
  raise
@@ -556,6 +584,40 @@ class AzureClient:
556
584
  logger.info("Statement affected %s rows", rowcount)
557
585
  return rowcount
558
586
 
587
+ # ---------------------------------------------------------------------------------------------
588
+ # Parameter handling
589
+ # ---------------------------------------------------------------------------------------------
590
+ @staticmethod
591
+ def _prepare_params(params: Optional[dict[str, Any]]) -> dict[str, Any]:
592
+ """
593
+ Converts parameter values so the ODBC driver binds them correctly.
594
+
595
+ Timezone-aware datetimes, including pandas Timestamps, are converted to naive UTC
596
+ datetimes, because the driver would otherwise drop their timezone and send the local
597
+ clock time. SQL Server interprets naive values as UTC (``+00:00``) when comparing them
598
+ with ``DATETIMEOFFSET`` columns, so the correct point in time is used. ``NaT`` becomes
599
+ ``None`` and pandas Timestamps become Python datetimes; Python datetimes only have
600
+ microsecond precision, so nanoseconds are dropped (pandas warns when this happens). Other
601
+ values are passed through unchanged.
602
+
603
+ Args:
604
+ params: Values bound to the placeholders of a query, as in ``query()``.
605
+
606
+ Returns:
607
+ The converted values, as a new dict.
608
+ """
609
+ converted = {}
610
+ for key, value in (params or {}).items():
611
+ if value is pd.NaT:
612
+ value = None
613
+ elif isinstance(value, datetime):
614
+ if value.tzinfo is not None:
615
+ value = value.astimezone(timezone.utc).replace(tzinfo=None)
616
+ if isinstance(value, pd.Timestamp):
617
+ value = value.to_pydatetime()
618
+ converted[key] = value
619
+ return converted
620
+
559
621
  # ---------------------------------------------------------------------------------------------
560
622
  # Identifier handling
561
623
  # ---------------------------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "detquantlib"
3
- version = "5.10.2"
3
+ version = "5.10.3"
4
4
  description = "An internal library containing functions and classes that can be used across Quant models."
5
5
  authors = ["Dynamic Energy Trading"]
6
6
  readme = "README.md"
File without changes
File without changes