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.
Files changed (34) hide show
  1. {detquantlib-5.4.0 → detquantlib-5.5.0}/PKG-INFO +1 -1
  2. {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/data/databases/snowflake_client.py +144 -10
  3. {detquantlib-5.4.0 → detquantlib-5.5.0}/pyproject.toml +1 -1
  4. {detquantlib-5.4.0 → detquantlib-5.5.0}/LICENSE.txt +0 -0
  5. {detquantlib-5.4.0 → detquantlib-5.5.0}/README.md +0 -0
  6. {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/__init__.py +0 -0
  7. {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/assets/__init__.py +0 -0
  8. {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/assets/battery.py +0 -0
  9. {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/assets/wind_turbine.py +0 -0
  10. {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/converters/__init__.py +0 -0
  11. {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/converters/definitions.py +0 -0
  12. {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/converters/energy.py +0 -0
  13. {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/converters/helpers.py +0 -0
  14. {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/converters/price.py +0 -0
  15. {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/data/__init__.py +0 -0
  16. {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/data/databases/detdatabase.py +0 -0
  17. {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/data/databases/helpers.py +0 -0
  18. {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/data/entsoe/entsoe.py +0 -0
  19. {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/data/sftp/sftp.py +0 -0
  20. {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/dates/__init__.py +0 -0
  21. {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/dates/dates.py +0 -0
  22. {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/figures/__init__.py +0 -0
  23. {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/figures/plotly_figures.py +0 -0
  24. {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/forecasting/__init__.py +0 -0
  25. {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/forecasting/forecasting.py +0 -0
  26. {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/outputs/__init__.py +0 -0
  27. {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/outputs/outputs_interface.py +0 -0
  28. {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/stats/__init__.py +0 -0
  29. {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/stats/data_analysis.py +0 -0
  30. {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/tradable_products/__init__.py +0 -0
  31. {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/tradable_products/tradable_products.py +0 -0
  32. {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/utils/__init__.py +0 -0
  33. {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/utils/logging.py +0 -0
  34. {detquantlib-5.4.0 → detquantlib-5.5.0}/detquantlib/utils/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: detquantlib
3
- Version: 5.4.0
3
+ Version: 5.5.0
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
@@ -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
- from __future__ import annotations
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
- Example:
112
- >>> with SnowflakeClient(SnowflakeConfig.from_env()) as client:
113
- ... df = client.query("SELECT * FROM orders WHERE status = %s", ("SHIPPED",))
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__(self, exc_type, exc, tb) -> None:
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
  """
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "detquantlib"
3
- version = "5.4.0"
3
+ version = "5.5.0"
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