detquantlib 5.4.0__tar.gz → 5.5.0__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.4.0 → detquantlib-5.5.0}/PKG-INFO +1 -1
- {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/data/databases/snowflake_client.py +144 -10
- {detquantlib-5.4.0 → detquantlib-5.5.0}/pyproject.toml +1 -1
- {detquantlib-5.4.0 → detquantlib-5.5.0}/LICENSE.txt +0 -0
- {detquantlib-5.4.0 → detquantlib-5.5.0}/README.md +0 -0
- {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/__init__.py +0 -0
- {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/assets/__init__.py +0 -0
- {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/assets/battery.py +0 -0
- {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/assets/wind_turbine.py +0 -0
- {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/converters/__init__.py +0 -0
- {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/converters/definitions.py +0 -0
- {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/converters/energy.py +0 -0
- {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/converters/helpers.py +0 -0
- {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/converters/price.py +0 -0
- {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/data/__init__.py +0 -0
- {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/data/databases/detdatabase.py +0 -0
- {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/data/databases/helpers.py +0 -0
- {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/data/entsoe/entsoe.py +0 -0
- {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/data/sftp/sftp.py +0 -0
- {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/dates/__init__.py +0 -0
- {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/dates/dates.py +0 -0
- {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/figures/__init__.py +0 -0
- {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/figures/plotly_figures.py +0 -0
- {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/forecasting/__init__.py +0 -0
- {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/forecasting/forecasting.py +0 -0
- {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/outputs/__init__.py +0 -0
- {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/outputs/outputs_interface.py +0 -0
- {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/stats/__init__.py +0 -0
- {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/stats/data_analysis.py +0 -0
- {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/tradable_products/__init__.py +0 -0
- {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/tradable_products/tradable_products.py +0 -0
- {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/utils/__init__.py +0 -0
- {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/utils/logging.py +0 -0
- {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/utils/utils.py +0 -0
|
@@ -4,24 +4,22 @@ Client for working with a Snowflake database from Python.
|
|
|
4
4
|
This module provides a configuration dataclass and a client class that wrap
|
|
5
5
|
``snowflake-connector-python`` with a small, safe API for interacting with Snowflake. Data is
|
|
6
6
|
exchanged as pandas DataFrames.
|
|
7
|
-
|
|
8
|
-
Example:
|
|
9
|
-
>>> with SnowflakeClient(SnowflakeConfig.from_env()) as client:
|
|
10
|
-
... df = client.query("SELECT CURRENT_VERSION() AS version")
|
|
11
7
|
"""
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
# Python built-in packages
|
|
15
10
|
import logging
|
|
16
11
|
import os
|
|
17
12
|
import re
|
|
18
13
|
from dataclasses import dataclass, field
|
|
14
|
+
from types import TracebackType
|
|
19
15
|
from typing import Any, Iterator, Optional, Sequence, Union
|
|
20
16
|
|
|
17
|
+
# Third-party packages
|
|
21
18
|
import pandas as pd
|
|
22
19
|
import snowflake.connector
|
|
23
20
|
from snowflake.connector import SnowflakeConnection
|
|
24
21
|
from snowflake.connector.errors import Error as SnowflakeError
|
|
22
|
+
from snowflake.connector.pandas_tools import write_pandas
|
|
25
23
|
|
|
26
24
|
logger = logging.getLogger(__name__)
|
|
27
25
|
|
|
@@ -108,10 +106,27 @@ class SnowflakeClient:
|
|
|
108
106
|
Attributes:
|
|
109
107
|
config: The connection settings used by this client.
|
|
110
108
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
109
|
+
Examples:
|
|
110
|
+
Example 1: Loading a table
|
|
111
|
+
>>> with SnowflakeClient() as client:
|
|
114
112
|
... customers = client.load_table("CUSTOMERS", columns=["ID", "NAME"], limit=100)
|
|
113
|
+
|
|
114
|
+
Example 2: Standard query method
|
|
115
|
+
>>> with SnowflakeClient() as client:
|
|
116
|
+
... df = client.query("SELECT * FROM orders WHERE status = %s", ("SHIPPED",))
|
|
117
|
+
|
|
118
|
+
Example 3: Batch query method to stream a large table in chunks
|
|
119
|
+
>>> with SnowflakeClient(SnowflakeConfig.from_env()) as client:
|
|
120
|
+
... for chunk in client.query_batches("SELECT * FROM BIG_TABLE"):
|
|
121
|
+
... process(chunk)
|
|
122
|
+
|
|
123
|
+
Example 4: Writing a DataFrame to a table, creating the table if needed
|
|
124
|
+
>>> with SnowflakeClient() as client:
|
|
125
|
+
... client.write_table(df, "DB.SCHEMA.ORDERS", auto_create_table=True)
|
|
126
|
+
|
|
127
|
+
Example 5: Running a statement that does not return data
|
|
128
|
+
>>> with SnowflakeClient() as client:
|
|
129
|
+
... client.execute("DELETE FROM ORDERS WHERE STATUS = %s", ("CANCELLED",))
|
|
115
130
|
"""
|
|
116
131
|
|
|
117
132
|
def __init__(self, config: Optional[SnowflakeConfig] = None) -> None:
|
|
@@ -228,7 +243,12 @@ class SnowflakeClient:
|
|
|
228
243
|
self.connect()
|
|
229
244
|
return self
|
|
230
245
|
|
|
231
|
-
def __exit__(
|
|
246
|
+
def __exit__(
|
|
247
|
+
self,
|
|
248
|
+
exc_type: Optional[type[BaseException]],
|
|
249
|
+
exc: Optional[BaseException],
|
|
250
|
+
tb: Optional[TracebackType],
|
|
251
|
+
) -> None:
|
|
232
252
|
"""
|
|
233
253
|
Closes the connection when leaving a ``with`` block.
|
|
234
254
|
|
|
@@ -359,6 +379,120 @@ class SnowflakeClient:
|
|
|
359
379
|
cols = [d[0] for d in cur.description]
|
|
360
380
|
return pd.DataFrame(cur.fetchall(), columns=cols)
|
|
361
381
|
|
|
382
|
+
# ---------------------------------------------------------------------------------------------
|
|
383
|
+
# Writing data
|
|
384
|
+
# ---------------------------------------------------------------------------------------------
|
|
385
|
+
def write_table(
|
|
386
|
+
self,
|
|
387
|
+
df: pd.DataFrame,
|
|
388
|
+
table: str,
|
|
389
|
+
overwrite: bool = False,
|
|
390
|
+
auto_create_table: bool = False,
|
|
391
|
+
chunk_size: Optional[int] = None,
|
|
392
|
+
) -> int:
|
|
393
|
+
"""
|
|
394
|
+
Writes a DataFrame to a table.
|
|
395
|
+
|
|
396
|
+
Data is uploaded in bulk with the connector's ``write_pandas`` (staged Parquet files loaded
|
|
397
|
+
with ``COPY INTO``), which is much faster than row-by-row inserts. Table and column names
|
|
398
|
+
are treated as unquoted identifiers, so they are case-insensitive and stored in uppercase,
|
|
399
|
+
consistent with ``load_table()``.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
df: The data to write. Column names must be valid unquoted identifiers and, unless the
|
|
403
|
+
table is created by this call, must match the table's columns.
|
|
404
|
+
table: Table name, optionally qualified, e.g. ``"SCHEMA.TABLE"`` or
|
|
405
|
+
``"DB.SCHEMA.TABLE"``. Omitted parts default to the session's current database and
|
|
406
|
+
schema.
|
|
407
|
+
overwrite: If ``True``, the existing rows are replaced by ``df``. If ``False``, ``df``
|
|
408
|
+
is appended. Combined with ``auto_create_table``, the table itself is recreated
|
|
409
|
+
with column types inferred from ``df``.
|
|
410
|
+
auto_create_table: If ``True``, the table is created if it does not exist, with column
|
|
411
|
+
types inferred from ``df``.
|
|
412
|
+
chunk_size: Number of rows per uploaded file. If ``None``, the whole DataFrame is
|
|
413
|
+
uploaded at once.
|
|
414
|
+
|
|
415
|
+
Returns:
|
|
416
|
+
The number of rows written. If ``df`` is empty, nothing is written, the table is left
|
|
417
|
+
unchanged and ``0`` is returned.
|
|
418
|
+
|
|
419
|
+
Raises:
|
|
420
|
+
ValueError: If the table name has more than three parts, or if the table name or a
|
|
421
|
+
column name is not a valid identifier.
|
|
422
|
+
RuntimeError: If Snowflake reports that the upload did not complete.
|
|
423
|
+
SnowflakeError: If the write fails. The error is logged before being re-raised.
|
|
424
|
+
"""
|
|
425
|
+
parts = [self._check_identifier(p) for p in table.split(".")]
|
|
426
|
+
if len(parts) > 3:
|
|
427
|
+
raise ValueError(f"Invalid table name: {table!r}. Expected at most DB.SCHEMA.TABLE.")
|
|
428
|
+
for col in df.columns:
|
|
429
|
+
self._check_identifier(str(col))
|
|
430
|
+
|
|
431
|
+
if df.empty:
|
|
432
|
+
logger.warning("DataFrame is empty; nothing written to %s", table)
|
|
433
|
+
return 0
|
|
434
|
+
|
|
435
|
+
# Pad the qualifier on the left, so "T" -> (None, None), "S.T" -> (None, S), etc.
|
|
436
|
+
*qualifier, table_name = parts
|
|
437
|
+
database, schema = ([None, None] + qualifier)[-2:]
|
|
438
|
+
|
|
439
|
+
conn = self.connect()
|
|
440
|
+
try:
|
|
441
|
+
success, n_chunks, n_rows, _ = write_pandas(
|
|
442
|
+
conn,
|
|
443
|
+
df,
|
|
444
|
+
table_name,
|
|
445
|
+
database=database,
|
|
446
|
+
schema=schema,
|
|
447
|
+
chunk_size=chunk_size,
|
|
448
|
+
quote_identifiers=False,
|
|
449
|
+
auto_create_table=auto_create_table,
|
|
450
|
+
overwrite=overwrite,
|
|
451
|
+
use_logical_type=True,
|
|
452
|
+
)
|
|
453
|
+
except SnowflakeError as e:
|
|
454
|
+
logger.error("Write to %s failed: %s", table, e)
|
|
455
|
+
raise
|
|
456
|
+
if not success:
|
|
457
|
+
raise RuntimeError(f"Write to {table} did not complete successfully.")
|
|
458
|
+
logger.info("Wrote %d rows to %s in %d chunk(s)", n_rows, table, n_chunks)
|
|
459
|
+
return n_rows
|
|
460
|
+
|
|
461
|
+
def execute(
|
|
462
|
+
self,
|
|
463
|
+
sql: str,
|
|
464
|
+
params: Optional[Union[Sequence[Any], dict[str, Any]]] = None,
|
|
465
|
+
) -> Optional[int]:
|
|
466
|
+
"""
|
|
467
|
+
Runs a SQL statement that does not return data.
|
|
468
|
+
|
|
469
|
+
Intended for statements such as ``INSERT``, ``UPDATE``, ``DELETE``, ``MERGE``,
|
|
470
|
+
``CREATE TABLE`` or ``USE SCHEMA``. Always pass user-supplied values through ``params``, as
|
|
471
|
+
in ``query()``. Changes are committed automatically, as the connection runs in Snowflake's
|
|
472
|
+
default autocommit mode.
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
sql: The SQL statement to execute.
|
|
476
|
+
params: Values bound to the placeholders in ``sql``, as in ``query()``.
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
The number of rows affected, as reported by Snowflake, or ``None`` if unavailable. For
|
|
480
|
+
statements that do not modify rows (e.g. DDL), the value is not meaningful.
|
|
481
|
+
|
|
482
|
+
Raises:
|
|
483
|
+
SnowflakeError: If the statement fails. The error is logged before being re-raised.
|
|
484
|
+
"""
|
|
485
|
+
conn = self.connect()
|
|
486
|
+
try:
|
|
487
|
+
with conn.cursor() as cur:
|
|
488
|
+
cur.execute(sql, params)
|
|
489
|
+
rowcount = cur.rowcount
|
|
490
|
+
except SnowflakeError as e:
|
|
491
|
+
logger.error("Statement failed: %s", e)
|
|
492
|
+
raise
|
|
493
|
+
logger.info("Statement affected %s rows", rowcount)
|
|
494
|
+
return rowcount
|
|
495
|
+
|
|
362
496
|
@staticmethod
|
|
363
497
|
def _check_identifier(name: str) -> str:
|
|
364
498
|
"""
|
|
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
|