datablade 0.0.0__py3-none-any.whl → 0.0.5__py3-none-any.whl
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.
- datablade/__init__.py +41 -1
- datablade/blade.py +153 -0
- datablade/core/__init__.py +28 -7
- datablade/core/frames.py +23 -236
- datablade/core/json.py +5 -10
- datablade/core/lists.py +5 -10
- datablade/core/messages.py +23 -11
- datablade/core/strings.py +5 -43
- datablade/core/zip.py +5 -24
- datablade/dataframes/__init__.py +43 -0
- datablade/dataframes/frames.py +485 -0
- datablade/dataframes/readers.py +540 -0
- datablade/io/__init__.py +15 -0
- datablade/io/json.py +33 -0
- datablade/io/zip.py +73 -0
- datablade/sql/__init__.py +32 -0
- datablade/sql/bulk_load.py +405 -0
- datablade/sql/ddl.py +227 -0
- datablade/sql/ddl_pyarrow.py +287 -0
- datablade/sql/dialects.py +10 -0
- datablade/sql/quoting.py +42 -0
- datablade/utils/__init__.py +37 -0
- datablade/utils/lists.py +29 -0
- datablade/utils/logging.py +159 -0
- datablade/utils/messages.py +29 -0
- datablade/utils/strings.py +86 -0
- datablade-0.0.5.dist-info/METADATA +351 -0
- datablade-0.0.5.dist-info/RECORD +31 -0
- {datablade-0.0.0.dist-info → datablade-0.0.5.dist-info}/WHEEL +1 -1
- {datablade-0.0.0.dist-info → datablade-0.0.5.dist-info/licenses}/LICENSE +20 -20
- datablade-0.0.0.dist-info/METADATA +0 -13
- datablade-0.0.0.dist-info/RECORD +0 -13
- {datablade-0.0.0.dist-info → datablade-0.0.5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
from ..utils.messages import print_verbose
|
|
7
|
+
from .ddl import _qualify_name
|
|
8
|
+
from .dialects import Dialect
|
|
9
|
+
from .quoting import quote_identifier
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("datablade")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _require_pyarrow():
|
|
15
|
+
try:
|
|
16
|
+
import pyarrow as pa # type: ignore
|
|
17
|
+
import pyarrow.parquet as pq # type: ignore
|
|
18
|
+
except ImportError as exc: # pragma: no cover
|
|
19
|
+
raise ImportError(
|
|
20
|
+
"Parquet DDL generation requires 'pyarrow'. Install with: pip install pyarrow"
|
|
21
|
+
) from exc
|
|
22
|
+
|
|
23
|
+
return pa, pq
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _sql_type_from_arrow(data_type, dialect: Dialect) -> Optional[str]: # noqa: C901
|
|
27
|
+
"""Map a pyarrow.DataType to a SQL type string.
|
|
28
|
+
|
|
29
|
+
Returns None when there is no clean mapping and the caller should drop the column.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
pa, _ = _require_pyarrow()
|
|
33
|
+
|
|
34
|
+
# Dictionary-encoded columns behave like their value type for DDL purposes.
|
|
35
|
+
if pa.types.is_dictionary(data_type):
|
|
36
|
+
return _sql_type_from_arrow(data_type.value_type, dialect)
|
|
37
|
+
|
|
38
|
+
# Nested/complex types: no clean general mapping across dialects.
|
|
39
|
+
if (
|
|
40
|
+
pa.types.is_struct(data_type)
|
|
41
|
+
or pa.types.is_list(data_type)
|
|
42
|
+
or pa.types.is_large_list(data_type)
|
|
43
|
+
or pa.types.is_fixed_size_list(data_type)
|
|
44
|
+
or pa.types.is_map(data_type)
|
|
45
|
+
or pa.types.is_union(data_type)
|
|
46
|
+
):
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
if dialect == Dialect.SQLSERVER:
|
|
50
|
+
if pa.types.is_boolean(data_type):
|
|
51
|
+
return "bit"
|
|
52
|
+
if pa.types.is_int8(data_type) or pa.types.is_int16(data_type):
|
|
53
|
+
return "smallint"
|
|
54
|
+
if pa.types.is_int32(data_type):
|
|
55
|
+
return "int"
|
|
56
|
+
if pa.types.is_int64(data_type):
|
|
57
|
+
return "bigint"
|
|
58
|
+
if pa.types.is_uint8(data_type) or pa.types.is_uint16(data_type):
|
|
59
|
+
return "int"
|
|
60
|
+
if pa.types.is_uint32(data_type):
|
|
61
|
+
return "bigint"
|
|
62
|
+
if pa.types.is_uint64(data_type):
|
|
63
|
+
return "decimal(20, 0)"
|
|
64
|
+
if pa.types.is_float16(data_type) or pa.types.is_float32(data_type):
|
|
65
|
+
return "real"
|
|
66
|
+
if pa.types.is_float64(data_type):
|
|
67
|
+
return "float"
|
|
68
|
+
if pa.types.is_decimal(data_type):
|
|
69
|
+
precision = min(int(data_type.precision), 38)
|
|
70
|
+
scale = int(data_type.scale)
|
|
71
|
+
return f"decimal({precision}, {scale})"
|
|
72
|
+
if pa.types.is_date(data_type):
|
|
73
|
+
return "date"
|
|
74
|
+
if pa.types.is_time(data_type):
|
|
75
|
+
return "time"
|
|
76
|
+
if pa.types.is_timestamp(data_type):
|
|
77
|
+
# SQL Server has datetimeoffset for tz-aware values.
|
|
78
|
+
return "datetimeoffset" if data_type.tz is not None else "datetime2"
|
|
79
|
+
if pa.types.is_binary(data_type) or pa.types.is_large_binary(data_type):
|
|
80
|
+
return "varbinary(max)"
|
|
81
|
+
if pa.types.is_fixed_size_binary(data_type):
|
|
82
|
+
return (
|
|
83
|
+
f"varbinary({int(data_type.byte_width)})"
|
|
84
|
+
if int(data_type.byte_width) <= 8000
|
|
85
|
+
else "varbinary(max)"
|
|
86
|
+
)
|
|
87
|
+
if pa.types.is_string(data_type) or pa.types.is_large_string(data_type):
|
|
88
|
+
return "nvarchar(max)"
|
|
89
|
+
|
|
90
|
+
# Anything else (including null) is not reliably representable.
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
if dialect == Dialect.POSTGRES:
|
|
94
|
+
if pa.types.is_boolean(data_type):
|
|
95
|
+
return "boolean"
|
|
96
|
+
if pa.types.is_int8(data_type) or pa.types.is_int16(data_type):
|
|
97
|
+
return "smallint"
|
|
98
|
+
if pa.types.is_int32(data_type):
|
|
99
|
+
return "integer"
|
|
100
|
+
if pa.types.is_int64(data_type):
|
|
101
|
+
return "bigint"
|
|
102
|
+
if pa.types.is_unsigned_integer(data_type):
|
|
103
|
+
# Postgres has no unsigned ints; use a wider signed or numeric.
|
|
104
|
+
if pa.types.is_uint8(data_type) or pa.types.is_uint16(data_type):
|
|
105
|
+
return "integer"
|
|
106
|
+
if pa.types.is_uint32(data_type):
|
|
107
|
+
return "bigint"
|
|
108
|
+
if pa.types.is_uint64(data_type):
|
|
109
|
+
return "numeric(20, 0)"
|
|
110
|
+
if pa.types.is_float16(data_type) or pa.types.is_float32(data_type):
|
|
111
|
+
return "real"
|
|
112
|
+
if pa.types.is_float64(data_type):
|
|
113
|
+
return "double precision"
|
|
114
|
+
if pa.types.is_decimal(data_type):
|
|
115
|
+
precision = int(data_type.precision)
|
|
116
|
+
scale = int(data_type.scale)
|
|
117
|
+
return f"numeric({precision}, {scale})"
|
|
118
|
+
if pa.types.is_date(data_type):
|
|
119
|
+
return "date"
|
|
120
|
+
if pa.types.is_time(data_type):
|
|
121
|
+
return "time"
|
|
122
|
+
if pa.types.is_timestamp(data_type):
|
|
123
|
+
return "timestamptz" if data_type.tz is not None else "timestamp"
|
|
124
|
+
if pa.types.is_binary(data_type) or pa.types.is_large_binary(data_type):
|
|
125
|
+
return "bytea"
|
|
126
|
+
if pa.types.is_fixed_size_binary(data_type):
|
|
127
|
+
return "bytea"
|
|
128
|
+
if pa.types.is_string(data_type) or pa.types.is_large_string(data_type):
|
|
129
|
+
return "text"
|
|
130
|
+
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
if dialect == Dialect.MYSQL:
|
|
134
|
+
if pa.types.is_boolean(data_type):
|
|
135
|
+
return "TINYINT(1)"
|
|
136
|
+
if pa.types.is_int8(data_type) or pa.types.is_int16(data_type):
|
|
137
|
+
return "SMALLINT"
|
|
138
|
+
if pa.types.is_int32(data_type):
|
|
139
|
+
return "INT"
|
|
140
|
+
if pa.types.is_int64(data_type):
|
|
141
|
+
return "BIGINT"
|
|
142
|
+
if pa.types.is_unsigned_integer(data_type):
|
|
143
|
+
# MySQL supports UNSIGNED, but we keep mappings consistent with the existing
|
|
144
|
+
# pandas-based DDL generator (signed types).
|
|
145
|
+
if pa.types.is_uint8(data_type) or pa.types.is_uint16(data_type):
|
|
146
|
+
return "INT"
|
|
147
|
+
if pa.types.is_uint32(data_type):
|
|
148
|
+
return "BIGINT"
|
|
149
|
+
if pa.types.is_uint64(data_type):
|
|
150
|
+
return "DECIMAL(20, 0)"
|
|
151
|
+
if pa.types.is_float16(data_type) or pa.types.is_float32(data_type):
|
|
152
|
+
return "FLOAT"
|
|
153
|
+
if pa.types.is_float64(data_type):
|
|
154
|
+
return "DOUBLE"
|
|
155
|
+
if pa.types.is_decimal(data_type):
|
|
156
|
+
precision = int(data_type.precision)
|
|
157
|
+
scale = int(data_type.scale)
|
|
158
|
+
return f"DECIMAL({precision}, {scale})"
|
|
159
|
+
if pa.types.is_date(data_type):
|
|
160
|
+
return "DATE"
|
|
161
|
+
if pa.types.is_time(data_type):
|
|
162
|
+
return "TIME"
|
|
163
|
+
if pa.types.is_timestamp(data_type):
|
|
164
|
+
return "DATETIME"
|
|
165
|
+
if pa.types.is_binary(data_type) or pa.types.is_large_binary(data_type):
|
|
166
|
+
return "LONGBLOB"
|
|
167
|
+
if pa.types.is_fixed_size_binary(data_type):
|
|
168
|
+
width = int(data_type.byte_width)
|
|
169
|
+
return f"VARBINARY({width})" if width <= 65535 else "LONGBLOB"
|
|
170
|
+
if pa.types.is_string(data_type) or pa.types.is_large_string(data_type):
|
|
171
|
+
return "TEXT"
|
|
172
|
+
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
if dialect == Dialect.DUCKDB:
|
|
176
|
+
if pa.types.is_boolean(data_type):
|
|
177
|
+
return "BOOLEAN"
|
|
178
|
+
if pa.types.is_signed_integer(data_type):
|
|
179
|
+
return "BIGINT"
|
|
180
|
+
if pa.types.is_unsigned_integer(data_type):
|
|
181
|
+
return "UBIGINT"
|
|
182
|
+
if pa.types.is_floating(data_type):
|
|
183
|
+
return "DOUBLE"
|
|
184
|
+
if pa.types.is_decimal(data_type):
|
|
185
|
+
precision = int(data_type.precision)
|
|
186
|
+
scale = int(data_type.scale)
|
|
187
|
+
return f"DECIMAL({precision}, {scale})"
|
|
188
|
+
if pa.types.is_date(data_type):
|
|
189
|
+
return "DATE"
|
|
190
|
+
if pa.types.is_time(data_type):
|
|
191
|
+
return "TIME"
|
|
192
|
+
if pa.types.is_timestamp(data_type):
|
|
193
|
+
return "TIMESTAMPTZ" if data_type.tz is not None else "TIMESTAMP"
|
|
194
|
+
if pa.types.is_binary(data_type) or pa.types.is_large_binary(data_type):
|
|
195
|
+
return "BLOB"
|
|
196
|
+
if pa.types.is_fixed_size_binary(data_type):
|
|
197
|
+
return "BLOB"
|
|
198
|
+
if pa.types.is_string(data_type) or pa.types.is_large_string(data_type):
|
|
199
|
+
return "VARCHAR"
|
|
200
|
+
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
raise NotImplementedError(f"Dialect not supported: {dialect}")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def generate_create_table_from_parquet(
|
|
207
|
+
parquet_path: str,
|
|
208
|
+
catalog: Optional[str] = None,
|
|
209
|
+
schema: Optional[str] = None,
|
|
210
|
+
table: str = "table",
|
|
211
|
+
drop_existing: bool = True,
|
|
212
|
+
dialect: Dialect = Dialect.SQLSERVER,
|
|
213
|
+
verbose: bool = False,
|
|
214
|
+
) -> str:
|
|
215
|
+
"""Generate a CREATE TABLE statement from a Parquet file schema.
|
|
216
|
+
|
|
217
|
+
This reads the Parquet schema only (via PyArrow) and does not materialize data.
|
|
218
|
+
|
|
219
|
+
Columns whose Parquet types have no clean mapping for the chosen dialect are
|
|
220
|
+
dropped, and a warning is logged under logger name 'datablade'.
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
if (
|
|
224
|
+
parquet_path is None
|
|
225
|
+
or not isinstance(parquet_path, str)
|
|
226
|
+
or not parquet_path.strip()
|
|
227
|
+
):
|
|
228
|
+
raise ValueError("parquet_path must be a non-empty string")
|
|
229
|
+
if not isinstance(table, str) or not table.strip():
|
|
230
|
+
raise ValueError("table must be a non-empty string")
|
|
231
|
+
if catalog is not None and (not isinstance(catalog, str) or not catalog.strip()):
|
|
232
|
+
raise ValueError("catalog, if provided, must be a non-empty string")
|
|
233
|
+
if schema is not None and (not isinstance(schema, str) or not schema.strip()):
|
|
234
|
+
raise ValueError("schema, if provided, must be a non-empty string")
|
|
235
|
+
|
|
236
|
+
_, pq = _require_pyarrow()
|
|
237
|
+
|
|
238
|
+
arrow_schema = pq.ParquetFile(parquet_path).schema_arrow
|
|
239
|
+
|
|
240
|
+
qualified_name = _qualify_name(catalog, schema, table, dialect)
|
|
241
|
+
lines: List[str] = []
|
|
242
|
+
|
|
243
|
+
for field in arrow_schema:
|
|
244
|
+
sql_type = _sql_type_from_arrow(field.type, dialect)
|
|
245
|
+
if sql_type is None:
|
|
246
|
+
logger.warning(
|
|
247
|
+
"Dropping Parquet column %r (type=%s) for dialect=%s: unsupported type",
|
|
248
|
+
field.name,
|
|
249
|
+
str(field.type),
|
|
250
|
+
dialect.value,
|
|
251
|
+
)
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
null_str = "NULL" if field.nullable else "NOT NULL"
|
|
255
|
+
lines.append(
|
|
256
|
+
f" {quote_identifier(str(field.name), dialect)} {sql_type} {null_str}"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
if not lines:
|
|
260
|
+
raise ValueError(
|
|
261
|
+
"No supported columns found in Parquet schema for the selected dialect"
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
body = ",\n".join(lines)
|
|
265
|
+
|
|
266
|
+
drop_clause = ""
|
|
267
|
+
if drop_existing:
|
|
268
|
+
if dialect == Dialect.SQLSERVER:
|
|
269
|
+
if catalog:
|
|
270
|
+
drop_clause = (
|
|
271
|
+
f"USE {quote_identifier(catalog, dialect)};\n"
|
|
272
|
+
f"IF OBJECT_ID('{qualified_name}') IS NOT NULL "
|
|
273
|
+
f"DROP TABLE {qualified_name};\n"
|
|
274
|
+
)
|
|
275
|
+
else:
|
|
276
|
+
drop_clause = (
|
|
277
|
+
f"IF OBJECT_ID('{qualified_name}') IS NOT NULL "
|
|
278
|
+
f"DROP TABLE {qualified_name};\n"
|
|
279
|
+
)
|
|
280
|
+
else:
|
|
281
|
+
drop_clause = f"DROP TABLE IF EXISTS {qualified_name};\n"
|
|
282
|
+
|
|
283
|
+
statement = f"{drop_clause}CREATE TABLE {qualified_name} (\n{body}\n);"
|
|
284
|
+
print_verbose(
|
|
285
|
+
f"Generated CREATE TABLE from Parquet schema for {qualified_name}", verbose
|
|
286
|
+
)
|
|
287
|
+
return statement
|
datablade/sql/quoting.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from .dialects import Dialect
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def quote_identifier(name: Optional[str], dialect: Dialect = Dialect.SQLSERVER) -> str:
|
|
7
|
+
"""
|
|
8
|
+
Quote an identifier for the given SQL dialect.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
name: Identifier to quote; must be non-empty string.
|
|
12
|
+
dialect: Target SQL dialect.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
Quoted identifier string.
|
|
16
|
+
|
|
17
|
+
Raises:
|
|
18
|
+
ValueError: If name is missing/empty.
|
|
19
|
+
TypeError: If name is not a string.
|
|
20
|
+
NotImplementedError: If dialect is unsupported.
|
|
21
|
+
"""
|
|
22
|
+
if name is None:
|
|
23
|
+
raise ValueError("name must be provided")
|
|
24
|
+
if not isinstance(name, str):
|
|
25
|
+
raise TypeError("name must be a string")
|
|
26
|
+
cleaned = name.strip()
|
|
27
|
+
if not cleaned:
|
|
28
|
+
raise ValueError("name must be a non-empty string")
|
|
29
|
+
|
|
30
|
+
if dialect == Dialect.SQLSERVER:
|
|
31
|
+
return f"[{cleaned.replace('[', '').replace(']', '')}]"
|
|
32
|
+
if dialect == Dialect.POSTGRES:
|
|
33
|
+
escaped = cleaned.replace('"', '""')
|
|
34
|
+
return f'"{escaped}"'
|
|
35
|
+
if dialect == Dialect.MYSQL:
|
|
36
|
+
escaped = cleaned.replace("`", "``")
|
|
37
|
+
return f"`{escaped}`"
|
|
38
|
+
if dialect == Dialect.DUCKDB:
|
|
39
|
+
escaped = cleaned.replace('"', '""')
|
|
40
|
+
return f'"{escaped}"'
|
|
41
|
+
|
|
42
|
+
raise NotImplementedError(f"Dialect not supported: {dialect}")
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
General utility functions for common operations.
|
|
3
|
+
|
|
4
|
+
This module provides functions for:
|
|
5
|
+
- String manipulation and SQL name quoting
|
|
6
|
+
- List operations (flattening)
|
|
7
|
+
- Logging and messaging
|
|
8
|
+
- Path standardization
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .lists import flatten
|
|
12
|
+
from .logging import print_verbose # backward compatibility
|
|
13
|
+
from .logging import (
|
|
14
|
+
configure_logging,
|
|
15
|
+
get_logger,
|
|
16
|
+
log,
|
|
17
|
+
log_debug,
|
|
18
|
+
log_error,
|
|
19
|
+
log_info,
|
|
20
|
+
log_warning,
|
|
21
|
+
)
|
|
22
|
+
from .strings import pathing, sql_quotename
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"sql_quotename",
|
|
26
|
+
"pathing",
|
|
27
|
+
"flatten",
|
|
28
|
+
# Logging
|
|
29
|
+
"get_logger",
|
|
30
|
+
"configure_logging",
|
|
31
|
+
"log",
|
|
32
|
+
"log_debug",
|
|
33
|
+
"log_info",
|
|
34
|
+
"log_warning",
|
|
35
|
+
"log_error",
|
|
36
|
+
"print_verbose",
|
|
37
|
+
]
|
datablade/utils/lists.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from typing import Any, List
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def flatten(nest: List[Any]) -> List[Any]:
|
|
5
|
+
"""
|
|
6
|
+
Flatten a nested list recursively to a single-level list.
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
nest: A potentially nested list structure.
|
|
10
|
+
|
|
11
|
+
Returns:
|
|
12
|
+
A flat list containing all elements from the nested structure.
|
|
13
|
+
|
|
14
|
+
Examples:
|
|
15
|
+
>>> flatten([1, [2, 3], [[4], 5]])
|
|
16
|
+
[1, 2, 3, 4, 5]
|
|
17
|
+
>>> flatten([1, 2, 3])
|
|
18
|
+
[1, 2, 3]
|
|
19
|
+
"""
|
|
20
|
+
if not isinstance(nest, list):
|
|
21
|
+
raise TypeError("nest must be a list")
|
|
22
|
+
|
|
23
|
+
result = []
|
|
24
|
+
for item in nest:
|
|
25
|
+
if isinstance(item, list):
|
|
26
|
+
result.extend(flatten(item))
|
|
27
|
+
else:
|
|
28
|
+
result.append(item)
|
|
29
|
+
return result
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Logging utilities for datablade.
|
|
3
|
+
|
|
4
|
+
Provides a configurable logger that can be used across all modules.
|
|
5
|
+
By default, logs to console at INFO level. Users can configure
|
|
6
|
+
handlers, levels, and formatters as needed.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import pathlib
|
|
11
|
+
from typing import Any, Optional
|
|
12
|
+
|
|
13
|
+
# Create the datablade logger
|
|
14
|
+
_logger = logging.getLogger("datablade")
|
|
15
|
+
_logger.setLevel(logging.DEBUG) # Allow all levels; handlers control output
|
|
16
|
+
|
|
17
|
+
# Default console handler (can be replaced by user)
|
|
18
|
+
_default_handler: Optional[logging.Handler] = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _ensure_handler() -> None:
|
|
22
|
+
"""Ensure at least one handler is configured."""
|
|
23
|
+
global _default_handler
|
|
24
|
+
if not _logger.handlers and _default_handler is None:
|
|
25
|
+
_default_handler = logging.StreamHandler()
|
|
26
|
+
_default_handler.setLevel(logging.INFO)
|
|
27
|
+
formatter = logging.Formatter(
|
|
28
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
29
|
+
)
|
|
30
|
+
_default_handler.setFormatter(formatter)
|
|
31
|
+
_logger.addHandler(_default_handler)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_logger() -> logging.Logger:
|
|
35
|
+
"""
|
|
36
|
+
Get the datablade logger instance.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
The configured datablade logger.
|
|
40
|
+
"""
|
|
41
|
+
_ensure_handler()
|
|
42
|
+
return _logger
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def configure_logging(
|
|
46
|
+
level: int = logging.INFO,
|
|
47
|
+
handler: Optional[logging.Handler] = None,
|
|
48
|
+
format_string: Optional[str] = None,
|
|
49
|
+
*,
|
|
50
|
+
log_file: Optional[str | pathlib.Path] = None,
|
|
51
|
+
format: Optional[str] = None,
|
|
52
|
+
) -> logging.Logger:
|
|
53
|
+
"""
|
|
54
|
+
Configure the datablade logger.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
level: Logging level (e.g., logging.DEBUG, logging.INFO).
|
|
58
|
+
handler: Optional custom handler. If None, uses StreamHandler.
|
|
59
|
+
format_string: Optional format string for log messages.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
The configured logger instance.
|
|
63
|
+
"""
|
|
64
|
+
global _default_handler
|
|
65
|
+
|
|
66
|
+
if format is not None:
|
|
67
|
+
if format_string is not None:
|
|
68
|
+
raise ValueError("Provide only one of format_string or format")
|
|
69
|
+
format_string = format
|
|
70
|
+
|
|
71
|
+
# Remove existing handlers
|
|
72
|
+
for h in _logger.handlers[:]:
|
|
73
|
+
_logger.removeHandler(h)
|
|
74
|
+
_default_handler = None
|
|
75
|
+
|
|
76
|
+
# Add new handler
|
|
77
|
+
if handler is None:
|
|
78
|
+
if log_file is not None:
|
|
79
|
+
log_path = pathlib.Path(log_file)
|
|
80
|
+
if log_path.parent:
|
|
81
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
handler = logging.FileHandler(log_path, encoding="utf-8")
|
|
83
|
+
else:
|
|
84
|
+
handler = logging.StreamHandler()
|
|
85
|
+
|
|
86
|
+
handler.setLevel(level)
|
|
87
|
+
|
|
88
|
+
if format_string:
|
|
89
|
+
formatter = logging.Formatter(format_string)
|
|
90
|
+
else:
|
|
91
|
+
formatter = logging.Formatter(
|
|
92
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
93
|
+
)
|
|
94
|
+
handler.setFormatter(formatter)
|
|
95
|
+
|
|
96
|
+
_logger.addHandler(handler)
|
|
97
|
+
_default_handler = handler
|
|
98
|
+
|
|
99
|
+
return _logger
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def log(
|
|
103
|
+
message: Any,
|
|
104
|
+
level: int = logging.INFO,
|
|
105
|
+
verbose: bool = True,
|
|
106
|
+
) -> None:
|
|
107
|
+
"""
|
|
108
|
+
Log a message at the specified level if verbose is True.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
message: The message to log (converted to string).
|
|
112
|
+
level: Logging level (default: INFO).
|
|
113
|
+
verbose: If False, message is not logged.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
None
|
|
117
|
+
"""
|
|
118
|
+
if not verbose:
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
_ensure_handler()
|
|
122
|
+
_logger.log(level, str(message))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def log_debug(message: Any, verbose: bool = True) -> None:
|
|
126
|
+
"""Log a DEBUG level message."""
|
|
127
|
+
log(message, logging.DEBUG, verbose)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def log_info(message: Any, verbose: bool = True) -> None:
|
|
131
|
+
"""Log an INFO level message."""
|
|
132
|
+
log(message, logging.INFO, verbose)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def log_warning(message: Any, verbose: bool = True) -> None:
|
|
136
|
+
"""Log a WARNING level message."""
|
|
137
|
+
log(message, logging.WARNING, verbose)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def log_error(message: Any, verbose: bool = True) -> None:
|
|
141
|
+
"""Log an ERROR level message."""
|
|
142
|
+
log(message, logging.ERROR, verbose)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# Backward compatibility alias
|
|
146
|
+
def print_verbose(message: Any, verbose: bool = True) -> None:
|
|
147
|
+
"""
|
|
148
|
+
Print a message if verbose is True.
|
|
149
|
+
|
|
150
|
+
This is a backward-compatible alias for log_info.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
message: The message to print (converted to string).
|
|
154
|
+
verbose: If True, the message will be logged.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
None
|
|
158
|
+
"""
|
|
159
|
+
log_info(message, verbose)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Messaging utilities for datablade.
|
|
3
|
+
|
|
4
|
+
This module provides backward-compatible message functions.
|
|
5
|
+
For new code, prefer using datablade.utils.logging directly.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# Re-export from logging module for backward compatibility
|
|
9
|
+
from .logging import (
|
|
10
|
+
configure_logging,
|
|
11
|
+
get_logger,
|
|
12
|
+
log,
|
|
13
|
+
log_debug,
|
|
14
|
+
log_error,
|
|
15
|
+
log_info,
|
|
16
|
+
log_warning,
|
|
17
|
+
print_verbose,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"print_verbose",
|
|
22
|
+
"log",
|
|
23
|
+
"log_debug",
|
|
24
|
+
"log_info",
|
|
25
|
+
"log_warning",
|
|
26
|
+
"log_error",
|
|
27
|
+
"get_logger",
|
|
28
|
+
"configure_logging",
|
|
29
|
+
]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
from typing import Optional, Union
|
|
3
|
+
|
|
4
|
+
from .messages import print_verbose
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def sql_quotename(
|
|
8
|
+
name: Optional[str] = None,
|
|
9
|
+
brackets: bool = True,
|
|
10
|
+
ticks: bool = False,
|
|
11
|
+
verbose: bool = False,
|
|
12
|
+
) -> str:
|
|
13
|
+
"""
|
|
14
|
+
Quote a SQL Server name string with brackets or ticks.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
name: The name to quote. Must be a non-empty string.
|
|
18
|
+
brackets: If True, wraps the name in square brackets [name].
|
|
19
|
+
ticks: If True, wraps the name in single quotes 'name'.
|
|
20
|
+
Takes precedence over brackets if both are True.
|
|
21
|
+
verbose: If True, prints error messages.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
The quoted name string.
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
ValueError: If name is None or empty after stripping.
|
|
28
|
+
TypeError: If name is not a string.
|
|
29
|
+
|
|
30
|
+
Examples:
|
|
31
|
+
>>> sql_quotename('table_name')
|
|
32
|
+
'[table_name]'
|
|
33
|
+
>>> sql_quotename('table_name', brackets=False, ticks=True)
|
|
34
|
+
"'table_name'"
|
|
35
|
+
"""
|
|
36
|
+
if name is None:
|
|
37
|
+
print_verbose("No name provided; exiting sql_quotename.", verbose)
|
|
38
|
+
raise ValueError("name must be provided")
|
|
39
|
+
if not isinstance(name, str):
|
|
40
|
+
raise TypeError("name must be a string")
|
|
41
|
+
cleaned = name.strip()
|
|
42
|
+
if not cleaned:
|
|
43
|
+
raise ValueError("name must be a non-empty string")
|
|
44
|
+
|
|
45
|
+
return_value = cleaned.replace("[", "").replace("]", "")
|
|
46
|
+
if brackets:
|
|
47
|
+
return_value = f"[{return_value}]"
|
|
48
|
+
if ticks or not brackets:
|
|
49
|
+
return_value = f"'{return_value}'"
|
|
50
|
+
return return_value
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def pathing(
|
|
54
|
+
input: Optional[Union[str, pathlib.Path]], verbose: bool = False
|
|
55
|
+
) -> pathlib.Path:
|
|
56
|
+
"""
|
|
57
|
+
Standardize and validate a path string or Path object.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
input: The path to standardize (string or pathlib.Path). Must not be None.
|
|
61
|
+
verbose: If True, prints error messages.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
A pathlib.Path object if the path exists.
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
ValueError: If input is None or the path does not exist.
|
|
68
|
+
TypeError: If input is not a string or pathlib.Path.
|
|
69
|
+
"""
|
|
70
|
+
if input is None:
|
|
71
|
+
print_verbose("No path provided; exiting pathing.", verbose)
|
|
72
|
+
raise ValueError("path input must be provided")
|
|
73
|
+
|
|
74
|
+
if isinstance(input, str):
|
|
75
|
+
normalized = input.replace("\\", "/")
|
|
76
|
+
path_obj = pathlib.Path(normalized)
|
|
77
|
+
elif isinstance(input, pathlib.Path):
|
|
78
|
+
path_obj = input
|
|
79
|
+
else:
|
|
80
|
+
raise TypeError("input must be a string or pathlib.Path")
|
|
81
|
+
|
|
82
|
+
if path_obj.exists():
|
|
83
|
+
return path_obj
|
|
84
|
+
|
|
85
|
+
print_verbose(f"Path {path_obj} does not exist; exiting pathing.", verbose)
|
|
86
|
+
raise ValueError(f"Path does not exist: {path_obj}")
|