detquantlib 5.3.0__tar.gz → 5.4.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.3.0 → detquantlib-5.4.0}/PKG-INFO +4 -1
- detquantlib-5.4.0/detquantlib/data/__init__.py +15 -0
- detquantlib-5.4.0/detquantlib/data/databases/snowflake_client.py +381 -0
- {detquantlib-5.3.0 → detquantlib-5.4.0}/detquantlib/utils/utils.py +4 -3
- {detquantlib-5.3.0 → detquantlib-5.4.0}/pyproject.toml +3 -1
- detquantlib-5.3.0/detquantlib/data/__init__.py +0 -6
- {detquantlib-5.3.0 → detquantlib-5.4.0}/LICENSE.txt +0 -0
- {detquantlib-5.3.0 → detquantlib-5.4.0}/README.md +0 -0
- {detquantlib-5.3.0 → detquantlib-5.4.0}/detquantlib/__init__.py +0 -0
- {detquantlib-5.3.0 → detquantlib-5.4.0}/detquantlib/assets/__init__.py +0 -0
- {detquantlib-5.3.0 → detquantlib-5.4.0}/detquantlib/assets/battery.py +0 -0
- {detquantlib-5.3.0 → detquantlib-5.4.0}/detquantlib/assets/wind_turbine.py +0 -0
- {detquantlib-5.3.0 → detquantlib-5.4.0}/detquantlib/converters/__init__.py +0 -0
- {detquantlib-5.3.0 → detquantlib-5.4.0}/detquantlib/converters/definitions.py +0 -0
- {detquantlib-5.3.0 → detquantlib-5.4.0}/detquantlib/converters/energy.py +0 -0
- {detquantlib-5.3.0 → detquantlib-5.4.0}/detquantlib/converters/helpers.py +0 -0
- {detquantlib-5.3.0 → detquantlib-5.4.0}/detquantlib/converters/price.py +0 -0
- {detquantlib-5.3.0 → detquantlib-5.4.0}/detquantlib/data/databases/detdatabase.py +0 -0
- {detquantlib-5.3.0 → detquantlib-5.4.0}/detquantlib/data/databases/helpers.py +0 -0
- {detquantlib-5.3.0 → detquantlib-5.4.0}/detquantlib/data/entsoe/entsoe.py +0 -0
- {detquantlib-5.3.0 → detquantlib-5.4.0}/detquantlib/data/sftp/sftp.py +0 -0
- {detquantlib-5.3.0 → detquantlib-5.4.0}/detquantlib/dates/__init__.py +0 -0
- {detquantlib-5.3.0 → detquantlib-5.4.0}/detquantlib/dates/dates.py +0 -0
- {detquantlib-5.3.0 → detquantlib-5.4.0}/detquantlib/figures/__init__.py +0 -0
- {detquantlib-5.3.0 → detquantlib-5.4.0}/detquantlib/figures/plotly_figures.py +0 -0
- {detquantlib-5.3.0 → detquantlib-5.4.0}/detquantlib/forecasting/__init__.py +0 -0
- {detquantlib-5.3.0 → detquantlib-5.4.0}/detquantlib/forecasting/forecasting.py +0 -0
- {detquantlib-5.3.0 → detquantlib-5.4.0}/detquantlib/outputs/__init__.py +0 -0
- {detquantlib-5.3.0 → detquantlib-5.4.0}/detquantlib/outputs/outputs_interface.py +0 -0
- {detquantlib-5.3.0 → detquantlib-5.4.0}/detquantlib/stats/__init__.py +0 -0
- {detquantlib-5.3.0 → detquantlib-5.4.0}/detquantlib/stats/data_analysis.py +0 -0
- {detquantlib-5.3.0 → detquantlib-5.4.0}/detquantlib/tradable_products/__init__.py +0 -0
- {detquantlib-5.3.0 → detquantlib-5.4.0}/detquantlib/tradable_products/tradable_products.py +0 -0
- {detquantlib-5.3.0 → detquantlib-5.4.0}/detquantlib/utils/__init__.py +0 -0
- {detquantlib-5.3.0 → detquantlib-5.4.0}/detquantlib/utils/logging.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: detquantlib
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.4.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
|
|
@@ -11,7 +11,9 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.12
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.13
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
14
15
|
Requires-Dist: colorlog (>=6.10.1,<7.0.0)
|
|
16
|
+
Requires-Dist: cryptography (>=50.0.2,<51.0.0)
|
|
15
17
|
Requires-Dist: numpy (>=2.2.6,<3.0.0)
|
|
16
18
|
Requires-Dist: pandas (>=2.3.3,<3.0.0)
|
|
17
19
|
Requires-Dist: paramiko (>=4.0.0,<5.0.0)
|
|
@@ -21,6 +23,7 @@ Requires-Dist: pyodbc (>=5.2.0,<6.0.0)
|
|
|
21
23
|
Requires-Dist: python-dotenv (>=1.1.0,<2.0.0)
|
|
22
24
|
Requires-Dist: redis (>=7.4.0,<8.0.0)
|
|
23
25
|
Requires-Dist: scipy (>=1.15.2,<2.0.0)
|
|
26
|
+
Requires-Dist: snowflake-connector-python[pandas] (>=4.8.0,<5.0.0)
|
|
24
27
|
Requires-Dist: sqlalchemy (>=2.0.43,<3.0.0)
|
|
25
28
|
Description-Content-Type: text/markdown
|
|
26
29
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .databases.detdatabase import DetDatabase
|
|
2
|
+
from .databases.helpers import *
|
|
3
|
+
from .databases.snowflake_client import SnowflakeClient, SnowflakeConfig
|
|
4
|
+
from .entsoe.entsoe import Entsoe
|
|
5
|
+
from .sftp.sftp import Sftp
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"DetDatabase",
|
|
9
|
+
"Entsoe",
|
|
10
|
+
"Sftp",
|
|
11
|
+
"list_to_sql_set",
|
|
12
|
+
"list_to_sql_columns",
|
|
13
|
+
"SnowflakeClient",
|
|
14
|
+
"SnowflakeConfig",
|
|
15
|
+
]
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Client for working with a Snowflake database from Python.
|
|
3
|
+
|
|
4
|
+
This module provides a configuration dataclass and a client class that wrap
|
|
5
|
+
``snowflake-connector-python`` with a small, safe API for interacting with Snowflake. Data is
|
|
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
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from typing import Any, Iterator, Optional, Sequence, Union
|
|
20
|
+
|
|
21
|
+
import pandas as pd
|
|
22
|
+
import snowflake.connector
|
|
23
|
+
from snowflake.connector import SnowflakeConnection
|
|
24
|
+
from snowflake.connector.errors import Error as SnowflakeError
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
_IDENTIFIER_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_$]*$")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class SnowflakeConfig:
|
|
33
|
+
"""
|
|
34
|
+
Connection settings for a Snowflake account.
|
|
35
|
+
|
|
36
|
+
Exactly one authentication method is used, chosen in this order of precedence:
|
|
37
|
+
- ``private_key_path`` (i.e. key-pair)
|
|
38
|
+
- ``authenticator`` (e.g. SSO)
|
|
39
|
+
- ``password``
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
account: Snowflake account identifier, e.g. ``"xy12345.eu-west-1"``.
|
|
43
|
+
user: Login name of the Snowflake user.
|
|
44
|
+
password: Password for password authentication.
|
|
45
|
+
warehouse: Default virtual warehouse for the session.
|
|
46
|
+
database: Default database for the session.
|
|
47
|
+
schema: Default schema for the session.
|
|
48
|
+
role: Role to assume for the session.
|
|
49
|
+
authenticator: Alternative authenticator, e.g. ``"externalbrowser"`` for browser-based SSO.
|
|
50
|
+
private_key_path: Path to a PEM-encoded private key for key-pair authentication.
|
|
51
|
+
private_key_passphrase: Passphrase for an encrypted private key.
|
|
52
|
+
extra: Additional keyword arguments passed verbatim to ``snowflake.connector.connect``.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
account: Optional[str] = None
|
|
56
|
+
user: Optional[str] = None
|
|
57
|
+
password: Optional[str] = None
|
|
58
|
+
warehouse: Optional[str] = None
|
|
59
|
+
database: Optional[str] = None
|
|
60
|
+
schema: Optional[str] = None
|
|
61
|
+
role: Optional[str] = None
|
|
62
|
+
authenticator: Optional[str] = None
|
|
63
|
+
private_key_path: Optional[str] = None
|
|
64
|
+
private_key_passphrase: Optional[str] = None
|
|
65
|
+
extra: dict[str, Any] = field(default_factory=dict)
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def from_env(cls, prefix: str = "SNOWFLAKE_") -> "SnowflakeConfig":
|
|
69
|
+
"""
|
|
70
|
+
Builds a config from environment variables.
|
|
71
|
+
|
|
72
|
+
Reads variables named ``<prefix><FIELD>``, e.g. ``SNOWFLAKE_ACCOUNT``, ``SNOWFLAKE_USER``,
|
|
73
|
+
``SNOWFLAKE_PASSWORD``, ``SNOWFLAKE_WAREHOUSE``, ``SNOWFLAKE_DATABASE``,
|
|
74
|
+
``SNOWFLAKE_SCHEMA``, ``SNOWFLAKE_ROLE``, ``SNOWFLAKE_AUTHENTICATOR``,
|
|
75
|
+
``SNOWFLAKE_PRIVATE_KEY_PATH`` and ``SNOWFLAKE_PRIVATE_KEY_PASSPHRASE``. Unset variables
|
|
76
|
+
become ``None``.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
prefix: Prefix prepended to each variable name.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
A ``SnowflakeConfig`` populated from the environment.
|
|
83
|
+
"""
|
|
84
|
+
g = lambda name: os.getenv(f"{prefix}{name}")
|
|
85
|
+
return cls(
|
|
86
|
+
account=g("ACCOUNT"),
|
|
87
|
+
user=g("USER"),
|
|
88
|
+
password=g("PASSWORD"),
|
|
89
|
+
warehouse=g("WAREHOUSE"),
|
|
90
|
+
database=g("DATABASE"),
|
|
91
|
+
schema=g("SCHEMA"),
|
|
92
|
+
role=g("ROLE"),
|
|
93
|
+
authenticator=g("AUTHENTICATOR"),
|
|
94
|
+
private_key_path=g("PRIVATE_KEY_PATH"),
|
|
95
|
+
private_key_passphrase=g("PRIVATE_KEY_PASSPHRASE"),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class SnowflakeClient:
|
|
100
|
+
"""
|
|
101
|
+
Client for interacting with a Snowflake database.
|
|
102
|
+
|
|
103
|
+
Wraps a single Snowflake connection and exposes methods for running SQL and exchanging data as
|
|
104
|
+
pandas DataFrames. The connection is opened lazily on first use and reused for subsequent
|
|
105
|
+
operations. The class can be used as a context manager so the connection is closed
|
|
106
|
+
automatically.
|
|
107
|
+
|
|
108
|
+
Attributes:
|
|
109
|
+
config: The connection settings used by this client.
|
|
110
|
+
|
|
111
|
+
Example:
|
|
112
|
+
>>> with SnowflakeClient(SnowflakeConfig.from_env()) as client:
|
|
113
|
+
... df = client.query("SELECT * FROM orders WHERE status = %s", ("SHIPPED",))
|
|
114
|
+
... customers = client.load_table("CUSTOMERS", columns=["ID", "NAME"], limit=100)
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def __init__(self, config: Optional[SnowflakeConfig] = None) -> None:
|
|
118
|
+
"""
|
|
119
|
+
Initializes the client without opening a connection.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
config: Connection settings. If ``None``, settings are read from environment variables
|
|
123
|
+
via ``SnowflakeConfig.from_env()``.
|
|
124
|
+
"""
|
|
125
|
+
self.config = config or SnowflakeConfig.from_env()
|
|
126
|
+
self._conn: Optional[SnowflakeConnection] = None
|
|
127
|
+
|
|
128
|
+
# ---------------------------------------------------------------------------------------------
|
|
129
|
+
# Connection management
|
|
130
|
+
# ---------------------------------------------------------------------------------------------
|
|
131
|
+
def _connection_params(self) -> dict[str, Any]:
|
|
132
|
+
"""
|
|
133
|
+
Builds the keyword arguments for ``snowflake.connector.connect``.
|
|
134
|
+
|
|
135
|
+
Selects a single authentication method (key-pair, authenticator or password, in that order)
|
|
136
|
+
and drops any parameters that are ``None``.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
A dict of connection parameters.
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
ValueError: If ``account`` or ``user`` is missing, or if no credentials are configured.
|
|
143
|
+
"""
|
|
144
|
+
c = self.config
|
|
145
|
+
if not c.account or not c.user:
|
|
146
|
+
raise ValueError("Snowflake 'account' and 'user' are required.")
|
|
147
|
+
|
|
148
|
+
params: dict[str, Any] = {
|
|
149
|
+
"account": c.account,
|
|
150
|
+
"user": c.user,
|
|
151
|
+
"warehouse": c.warehouse,
|
|
152
|
+
"database": c.database,
|
|
153
|
+
"schema": c.schema,
|
|
154
|
+
"role": c.role,
|
|
155
|
+
**c.extra,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if c.private_key_path:
|
|
159
|
+
params["private_key"] = self._load_private_key(
|
|
160
|
+
c.private_key_path, c.private_key_passphrase
|
|
161
|
+
)
|
|
162
|
+
elif c.authenticator:
|
|
163
|
+
params["authenticator"] = c.authenticator
|
|
164
|
+
elif c.password:
|
|
165
|
+
params["password"] = c.password
|
|
166
|
+
else:
|
|
167
|
+
raise ValueError(
|
|
168
|
+
"No credentials provided: set password, private_key_path, or authenticator."
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
return {k: v for k, v in params.items() if v is not None}
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def _load_private_key(path: str, passphrase: Optional[str]) -> bytes:
|
|
175
|
+
"""
|
|
176
|
+
Loads a PEM private key and converts it to unencrypted DER bytes.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
path: Path to the PEM-encoded private key file.
|
|
180
|
+
passphrase: Passphrase for an encrypted key, or ``None`` if the key is not encrypted.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
The private key as PKCS#8 DER bytes, as expected by the Snowflake connector.
|
|
184
|
+
"""
|
|
185
|
+
from cryptography.hazmat.primitives import serialization
|
|
186
|
+
|
|
187
|
+
with open(path, "rb") as f:
|
|
188
|
+
key = serialization.load_pem_private_key(
|
|
189
|
+
f.read(),
|
|
190
|
+
password=passphrase.encode() if passphrase else None,
|
|
191
|
+
)
|
|
192
|
+
return key.private_bytes(
|
|
193
|
+
encoding=serialization.Encoding.DER,
|
|
194
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
195
|
+
encryption_algorithm=serialization.NoEncryption(),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
def connect(self) -> SnowflakeConnection:
|
|
199
|
+
"""
|
|
200
|
+
Opens the connection if it is not already open.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
The active Snowflake connection.
|
|
204
|
+
"""
|
|
205
|
+
if self._conn is None or self._conn.is_closed():
|
|
206
|
+
logger.info("Connecting to Snowflake account %s", self.config.account)
|
|
207
|
+
self._conn = snowflake.connector.connect(**self._connection_params())
|
|
208
|
+
return self._conn
|
|
209
|
+
|
|
210
|
+
def close(self) -> None:
|
|
211
|
+
"""
|
|
212
|
+
Closes the connection if it is open.
|
|
213
|
+
|
|
214
|
+
Safe to call multiple times, or when no connection was ever opened.
|
|
215
|
+
"""
|
|
216
|
+
if self._conn is not None and not self._conn.is_closed():
|
|
217
|
+
self._conn.close()
|
|
218
|
+
logger.info("Snowflake connection closed")
|
|
219
|
+
self._conn = None
|
|
220
|
+
|
|
221
|
+
def __enter__(self) -> "SnowflakeClient":
|
|
222
|
+
"""
|
|
223
|
+
Opens the connection when entering a ``with`` block.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
This client instance.
|
|
227
|
+
"""
|
|
228
|
+
self.connect()
|
|
229
|
+
return self
|
|
230
|
+
|
|
231
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
232
|
+
"""
|
|
233
|
+
Closes the connection when leaving a ``with`` block.
|
|
234
|
+
|
|
235
|
+
Exceptions raised inside the block are not suppressed.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
exc_type: Type of the exception raised in the block, if any.
|
|
239
|
+
exc: The exception instance raised in the block, if any.
|
|
240
|
+
tb: The traceback of the exception, if any.
|
|
241
|
+
"""
|
|
242
|
+
self.close()
|
|
243
|
+
|
|
244
|
+
# ---------------------------------------------------------------------------------------------
|
|
245
|
+
# Reading data
|
|
246
|
+
# ---------------------------------------------------------------------------------------------
|
|
247
|
+
def query(
|
|
248
|
+
self,
|
|
249
|
+
sql: str,
|
|
250
|
+
params: Optional[Union[Sequence[Any], dict[str, Any]]] = None,
|
|
251
|
+
lowercase_columns: bool = False,
|
|
252
|
+
) -> pd.DataFrame:
|
|
253
|
+
"""
|
|
254
|
+
Runs a SQL query and returns the full result as a DataFrame.
|
|
255
|
+
|
|
256
|
+
Always pass user-supplied values through ``params`` (``%s`` or ``%(name)s`` placeholders)
|
|
257
|
+
rather than formatting them into the SQL string, to avoid SQL injection.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
sql: The SQL statement to execute.
|
|
261
|
+
params: Values bound to the placeholders in ``sql``: a sequence for positional ``%s``
|
|
262
|
+
placeholders, or a dict for named ``%(name)s`` placeholders.
|
|
263
|
+
lowercase_columns: If ``True``, column names in the result are converted to lowercase
|
|
264
|
+
(Snowflake returns them uppercase by default).
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
A DataFrame containing every row returned by the query.
|
|
268
|
+
|
|
269
|
+
Raises:
|
|
270
|
+
SnowflakeError: If the query fails. The error is logged before being re-raised.
|
|
271
|
+
"""
|
|
272
|
+
conn = self.connect()
|
|
273
|
+
try:
|
|
274
|
+
with conn.cursor() as cur:
|
|
275
|
+
cur.execute(sql, params)
|
|
276
|
+
df = cur.fetch_pandas_all()
|
|
277
|
+
except SnowflakeError as e:
|
|
278
|
+
logger.error("Query failed: %s", e)
|
|
279
|
+
raise
|
|
280
|
+
if lowercase_columns:
|
|
281
|
+
df.columns = [c.lower() for c in df.columns]
|
|
282
|
+
logger.info("Loaded %d rows", len(df))
|
|
283
|
+
return df
|
|
284
|
+
|
|
285
|
+
def query_batches(
|
|
286
|
+
self,
|
|
287
|
+
sql: str,
|
|
288
|
+
params: Optional[Union[Sequence[Any], dict[str, Any]]] = None,
|
|
289
|
+
) -> Iterator[pd.DataFrame]:
|
|
290
|
+
"""
|
|
291
|
+
Streams a large result set as a sequence of DataFrames.
|
|
292
|
+
|
|
293
|
+
Useful for results too large to hold in memory at once. Batch sizes are determined by the
|
|
294
|
+
Snowflake connector.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
sql: The SQL statement to execute.
|
|
298
|
+
params: Values bound to the placeholders in ``sql``, as in ``query()``.
|
|
299
|
+
|
|
300
|
+
Yields:
|
|
301
|
+
DataFrames, each holding one batch of rows.
|
|
302
|
+
"""
|
|
303
|
+
conn = self.connect()
|
|
304
|
+
with conn.cursor() as cur:
|
|
305
|
+
cur.execute(sql, params)
|
|
306
|
+
for batch in cur.fetch_pandas_batches():
|
|
307
|
+
yield batch
|
|
308
|
+
|
|
309
|
+
def load_table(
|
|
310
|
+
self,
|
|
311
|
+
table: str,
|
|
312
|
+
columns: Optional[Sequence[str]] = None,
|
|
313
|
+
where: Optional[str] = None,
|
|
314
|
+
params: Optional[Union[Sequence[Any], dict[str, Any]]] = None,
|
|
315
|
+
limit: Optional[int] = None,
|
|
316
|
+
) -> pd.DataFrame:
|
|
317
|
+
"""
|
|
318
|
+
Loads a table, optionally filtered, into a DataFrame.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
table: Table name, optionally fully qualified, e.g. ``"DB.SCHEMA.TABLE"``. Each part is
|
|
322
|
+
validated as an identifier.
|
|
323
|
+
columns: Columns to select. If ``None``, all columns are selected.
|
|
324
|
+
where: Optional SQL condition, without the ``WHERE`` keyword. Use placeholders and pass
|
|
325
|
+
the values via ``params``; this string is inserted into the query as-is.
|
|
326
|
+
params: Values bound to the placeholders in ``where``.
|
|
327
|
+
limit: Maximum number of rows to return.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
A DataFrame containing the selected rows.
|
|
331
|
+
"""
|
|
332
|
+
table_sql = ".".join(self._check_identifier(p) for p in table.split("."))
|
|
333
|
+
cols_sql = ", ".join(self._check_identifier(c) for c in columns) if columns else "*"
|
|
334
|
+
sql = f"SELECT {cols_sql} FROM {table_sql}"
|
|
335
|
+
if where:
|
|
336
|
+
sql += f" WHERE {where}"
|
|
337
|
+
if limit is not None:
|
|
338
|
+
sql += f" LIMIT {int(limit)}"
|
|
339
|
+
return self.query(sql, params)
|
|
340
|
+
|
|
341
|
+
def list_tables(self, schema: Optional[str] = None) -> pd.DataFrame:
|
|
342
|
+
"""
|
|
343
|
+
Lists the tables visible in a schema.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
schema: Schema to inspect, optionally qualified with a database, e.g. ``"DB.SCHEMA"``.
|
|
347
|
+
If ``None``, the session's current schema is used.
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
A DataFrame with one row per table, using the columns returned by Snowflake's
|
|
351
|
+
``SHOW TABLES`` command.
|
|
352
|
+
"""
|
|
353
|
+
sql = "SHOW TABLES"
|
|
354
|
+
if schema:
|
|
355
|
+
sql += " IN SCHEMA " + ".".join(self._check_identifier(p) for p in schema.split("."))
|
|
356
|
+
conn = self.connect()
|
|
357
|
+
with conn.cursor() as cur:
|
|
358
|
+
cur.execute(sql)
|
|
359
|
+
cols = [d[0] for d in cur.description]
|
|
360
|
+
return pd.DataFrame(cur.fetchall(), columns=cols)
|
|
361
|
+
|
|
362
|
+
@staticmethod
|
|
363
|
+
def _check_identifier(name: str) -> str:
|
|
364
|
+
"""
|
|
365
|
+
Validates an unquoted Snowflake identifier.
|
|
366
|
+
|
|
367
|
+
Guards against SQL injection in table, schema and column names, which cannot be passed as
|
|
368
|
+
bind parameters.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
name: The identifier to validate.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
The identifier, unchanged.
|
|
375
|
+
|
|
376
|
+
Raises:
|
|
377
|
+
ValueError: If ``name`` is not a valid unquoted identifier.
|
|
378
|
+
"""
|
|
379
|
+
if not _IDENTIFIER_RE.match(name):
|
|
380
|
+
raise ValueError(f"Invalid Snowflake identifier: {name!r}")
|
|
381
|
+
return name
|
|
@@ -40,7 +40,8 @@ def divide_with_nan(
|
|
|
40
40
|
Returns:
|
|
41
41
|
Quotient array
|
|
42
42
|
"""
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
44
|
+
quotient = np.nan_to_num(
|
|
45
|
+
x=np.divide(numerator, denominator), nan=nan, posinf=posinf, neginf=neginf
|
|
46
|
+
)
|
|
46
47
|
return quotient
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "detquantlib"
|
|
3
|
-
version = "5.
|
|
3
|
+
version = "5.4.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"
|
|
@@ -21,6 +21,8 @@ pyarrow = "^21.0.0"
|
|
|
21
21
|
scipy = "^1.15.2"
|
|
22
22
|
colorlog = "^6.10.1"
|
|
23
23
|
redis = "^7.4.0"
|
|
24
|
+
snowflake-connector-python = {extras = ["pandas"], version = "^4.8.0"}
|
|
25
|
+
cryptography = "^50.0.2"
|
|
24
26
|
|
|
25
27
|
[tool.poetry.group.dev.dependencies]
|
|
26
28
|
toml = "^0.10.2"
|
|
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
|