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.
- {detquantlib-5.10.1 → detquantlib-5.10.3}/PKG-INFO +1 -1
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/data/databases/azure_client.py +68 -8
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/data/databases/snowflake_client.py +1 -2
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/utils/logging.py +2 -1
- {detquantlib-5.10.1 → detquantlib-5.10.3}/pyproject.toml +1 -1
- {detquantlib-5.10.1 → detquantlib-5.10.3}/LICENSE.txt +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/README.md +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/__init__.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/assets/__init__.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/assets/battery.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/assets/wind_turbine.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/converters/__init__.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/converters/definitions.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/converters/energy.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/converters/helpers.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/converters/price.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/data/__init__.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/data/databases/detdatabase.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/data/databases/helpers.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/data/entsoe/entsoe.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/data/sftp/sftp.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/data/tennet/tennet_client.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/dates/__init__.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/dates/dates.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/figures/__init__.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/figures/plotly_figures.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/forecasting/__init__.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/forecasting/forecasting.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/outputs/__init__.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/outputs/outputs_interface.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/snapshot_testing/README.md +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/snapshot_testing/__init__.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/snapshot_testing/cases.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/snapshot_testing/comparison.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/snapshot_testing/config.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/snapshot_testing/errors.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/snapshot_testing/isolation.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/snapshot_testing/pytest_plugin.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/snapshot_testing/recording.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/snapshot_testing/tables.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/stats/__init__.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/stats/data_analysis.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/tradable_products/__init__.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/tradable_products/tradable_products.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/utils/__init__.py +0 -0
- {detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/utils/utils.py +0 -0
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{detquantlib-5.10.1 → detquantlib-5.10.3}/detquantlib/tradable_products/tradable_products.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|