detquantlib 5.10.1__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.1 → detquantlib-5.10.3}/PKG-INFO +1 -1
  2. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/data/databases/azure_client.py +68 -8
  3. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/data/databases/snowflake_client.py +1 -2
  4. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/utils/logging.py +2 -1
  5. {detquantlib-5.10.1 → detquantlib-5.10.3}/pyproject.toml +1 -1
  6. {detquantlib-5.10.1 → detquantlib-5.10.3}/LICENSE.txt +0 -0
  7. {detquantlib-5.10.1 → detquantlib-5.10.3}/README.md +0 -0
  8. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/__init__.py +0 -0
  9. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/assets/__init__.py +0 -0
  10. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/assets/battery.py +0 -0
  11. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/assets/wind_turbine.py +0 -0
  12. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/converters/__init__.py +0 -0
  13. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/converters/definitions.py +0 -0
  14. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/converters/energy.py +0 -0
  15. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/converters/helpers.py +0 -0
  16. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/converters/price.py +0 -0
  17. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/data/__init__.py +0 -0
  18. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/data/databases/detdatabase.py +0 -0
  19. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/data/databases/helpers.py +0 -0
  20. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/data/entsoe/entsoe.py +0 -0
  21. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/data/sftp/sftp.py +0 -0
  22. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/data/tennet/tennet_client.py +0 -0
  23. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/dates/__init__.py +0 -0
  24. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/dates/dates.py +0 -0
  25. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/figures/__init__.py +0 -0
  26. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/figures/plotly_figures.py +0 -0
  27. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/forecasting/__init__.py +0 -0
  28. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/forecasting/forecasting.py +0 -0
  29. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/outputs/__init__.py +0 -0
  30. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/outputs/outputs_interface.py +0 -0
  31. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/snapshot_testing/README.md +0 -0
  32. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/snapshot_testing/__init__.py +0 -0
  33. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/snapshot_testing/cases.py +0 -0
  34. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/snapshot_testing/comparison.py +0 -0
  35. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/snapshot_testing/config.py +0 -0
  36. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/snapshot_testing/errors.py +0 -0
  37. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/snapshot_testing/isolation.py +0 -0
  38. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/snapshot_testing/pytest_plugin.py +0 -0
  39. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/snapshot_testing/recording.py +0 -0
  40. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/snapshot_testing/tables.py +0 -0
  41. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/stats/__init__.py +0 -0
  42. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/stats/data_analysis.py +0 -0
  43. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/tradable_products/__init__.py +0 -0
  44. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/tradable_products/tradable_products.py +0 -0
  45. {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/utils/__init__.py +0 -0
  46. {detquantlib-5.10.1 → 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.1
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
 
@@ -254,7 +266,6 @@ class AzureClient:
254
266
  database = self._resolve_database(database)
255
267
  if database not in self._engines:
256
268
  conn_str = self._to_connection_string(self._connection_params(database))
257
- logger.info("Connecting to Azure SQL database %s on %s", database, self.config.server)
258
269
  self._engines[database] = create_engine(
259
270
  "mssql+pyodbc://",
260
271
  creator=lambda: pyodbc.connect(conn_str),
@@ -268,9 +279,8 @@ class AzureClient:
268
279
 
269
280
  Safe to call multiple times, or when no connection was ever opened.
270
281
  """
271
- for database, engine in self._engines.items():
282
+ for engine in self._engines.values():
272
283
  engine.dispose()
273
- logger.info("Azure SQL connection to database %s closed", database)
274
284
  self._engines = {}
275
285
 
276
286
  def __enter__(self) -> "AzureClient":
@@ -320,7 +330,9 @@ class AzureClient:
320
330
 
321
331
  Args:
322
332
  sql: The SQL statement to execute.
323
- 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()``.
324
336
  database: Database to run the query in. If ``None``, ``config.database`` is used.
325
337
 
326
338
  Returns:
@@ -333,7 +345,7 @@ class AzureClient:
333
345
  engine = self.connect(database)
334
346
  try:
335
347
  with engine.connect() as conn:
336
- result = conn.execute(text(sql), params or {})
348
+ result = conn.execute(text(sql), self._prepare_params(params))
337
349
  cols = list(result.keys())
338
350
  df = pd.DataFrame.from_records(result.fetchall(), columns=cols, coerce_float=True)
339
351
  except SQLAlchemyError as e:
@@ -366,7 +378,7 @@ class AzureClient:
366
378
  """
367
379
  engine = self.connect(database)
368
380
  with engine.connect() as conn:
369
- result = conn.execute(text(sql), params or {})
381
+ result = conn.execute(text(sql), self._prepare_params(params))
370
382
  cols = list(result.keys())
371
383
  while rows := result.fetchmany(batch_size):
372
384
  yield pd.DataFrame.from_records(rows, columns=cols, coerce_float=True)
@@ -459,9 +471,14 @@ class AzureClient:
459
471
  is much faster than row-by-row inserts. The whole write runs in a single transaction: if
460
472
  any part fails, the table is left unchanged.
461
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
+
462
479
  Args:
463
480
  df: The data to write. The index is not written. Unless the table is created by this
464
- call, column names must match the table's columns.
481
+ call, column names must match the table's columns. ``df`` itself is not modified.
465
482
  table: Table name, optionally qualified, e.g. ``"SCHEMA.TABLE"`` or
466
483
  ``"DB.SCHEMA.TABLE"``. A database part selects the database to write to. Parts may
467
484
  be wrapped in square brackets. If no schema is given, the user's default schema
@@ -496,6 +513,14 @@ class AzureClient:
496
513
  schema, table_name = ([None] + parts)[-2:]
497
514
  table_sql = ".".join(self._quote_identifier(p) for p in parts)
498
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
+
499
524
  engine = self.connect(database)
500
525
  # pandas' to_sql always creates missing tables, so the table must be checked beforehand
501
526
  if not auto_create_table and not inspect(engine).has_table(table_name, schema=schema):
@@ -513,6 +538,7 @@ class AzureClient:
513
538
  if_exists="replace" if overwrite and auto_create_table else "append",
514
539
  index=False,
515
540
  chunksize=chunk_size,
541
+ dtype={c: DATETIMEOFFSET() for c in tz_cols} or None,
516
542
  )
517
543
  except SQLAlchemyError as e:
518
544
  logger.error("Write to %s failed: %s", table, e)
@@ -549,7 +575,7 @@ class AzureClient:
549
575
  engine = self.connect(database)
550
576
  try:
551
577
  with engine.begin() as conn:
552
- rowcount = conn.execute(text(sql), params or {}).rowcount
578
+ rowcount = conn.execute(text(sql), self._prepare_params(params)).rowcount
553
579
  except SQLAlchemyError as e:
554
580
  logger.error("Statement failed: %s", e)
555
581
  raise
@@ -558,6 +584,40 @@ class AzureClient:
558
584
  logger.info("Statement affected %s rows", rowcount)
559
585
  return rowcount
560
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
+
561
621
  # ---------------------------------------------------------------------------------------------
562
622
  # Identifier handling
563
623
  # ---------------------------------------------------------------------------------------------
@@ -22,6 +22,7 @@ from snowflake.connector.errors import Error as SnowflakeError
22
22
  from snowflake.connector.pandas_tools import write_pandas
23
23
 
24
24
  logger = logging.getLogger(__name__)
25
+ logging.getLogger("snowflake.connector").setLevel(logging.WARNING)
25
26
 
26
27
  _IDENTIFIER_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_$]*$")
27
28
 
@@ -225,7 +226,6 @@ class SnowflakeClient:
225
226
  The active Snowflake connection.
226
227
  """
227
228
  if self._conn is None or self._conn.is_closed():
228
- logger.info("Connecting to Snowflake account %s", self.config.account)
229
229
  self._conn = snowflake.connector.connect(**self._connection_params())
230
230
  return self._conn
231
231
 
@@ -237,7 +237,6 @@ class SnowflakeClient:
237
237
  """
238
238
  if self._conn is not None and not self._conn.is_closed():
239
239
  self._conn.close()
240
- logger.info("Snowflake connection closed")
241
240
  self._conn = None
242
241
 
243
242
  def __enter__(self) -> "SnowflakeClient":
@@ -29,6 +29,7 @@ def setup_logger(log_dir: Path):
29
29
  fmt=(
30
30
  "%(log_color)s%(asctime)-8s%(reset)s "
31
31
  "%(log_color)s[%(levelname)s]%(reset)s "
32
+ "%(log_color)s[%(name)s]%(reset)s "
32
33
  "%(log_color)s%(message)s"
33
34
  ),
34
35
  datefmt="%Y-%m-%d %H:%M:%S",
@@ -40,7 +41,7 @@ def setup_logger(log_dir: Path):
40
41
 
41
42
  # File handler (plain, no colors)
42
43
  file_formatter = logging.Formatter(
43
- fmt="%(asctime)s [%(levelname)s] %(message)s",
44
+ fmt="%(asctime)s [%(levelname)s] [%(name)s] %(message)s",
44
45
  datefmt="%Y-%m-%d %H:%M:%S",
45
46
  )
46
47
  file_handler = logging.FileHandler(log_dir, mode="a", encoding="utf-8")
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "detquantlib"
3
- version = "5.10.1"
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