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