soda-duckdb 4.0.5__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.
- soda_duckdb-4.0.5/PKG-INFO +7 -0
- soda_duckdb-4.0.5/setup.cfg +4 -0
- soda_duckdb-4.0.5/setup.py +21 -0
- soda_duckdb-4.0.5/src/soda_duckdb/__init__.py +7 -0
- soda_duckdb-4.0.5/src/soda_duckdb/common/data_sources/duckdb_data_source.py +285 -0
- soda_duckdb-4.0.5/src/soda_duckdb/common/data_sources/duckdb_data_source_connection.py +48 -0
- soda_duckdb-4.0.5/src/soda_duckdb/test_helpers/duckdb_data_source_test_helper.py +35 -0
- soda_duckdb-4.0.5/src/soda_duckdb.egg-info/PKG-INFO +7 -0
- soda_duckdb-4.0.5/src/soda_duckdb.egg-info/SOURCES.txt +11 -0
- soda_duckdb-4.0.5/src/soda_duckdb.egg-info/dependency_links.txt +1 -0
- soda_duckdb-4.0.5/src/soda_duckdb.egg-info/entry_points.txt +2 -0
- soda_duckdb-4.0.5/src/soda_duckdb.egg-info/requires.txt +3 -0
- soda_duckdb-4.0.5/src/soda_duckdb.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
from setuptools import setup
|
|
4
|
+
|
|
5
|
+
package_name = "soda-duckdb"
|
|
6
|
+
package_version = "4.0.5"
|
|
7
|
+
description = "Soda DuckDB V4"
|
|
8
|
+
|
|
9
|
+
requires = [f"soda-core=={package_version}", "duckdb>=1.2.0", "pytz"]
|
|
10
|
+
|
|
11
|
+
setup(
|
|
12
|
+
name=package_name,
|
|
13
|
+
version=package_version,
|
|
14
|
+
install_requires=requires,
|
|
15
|
+
package_dir={"": "src"},
|
|
16
|
+
entry_points={
|
|
17
|
+
"soda.plugins.data_source.duckdb": [
|
|
18
|
+
"DuckDBDataSourceImpl = soda_duckdb.common.data_sources.duckdb_data_source:DuckDBDataSourceImpl",
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
)
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
from collections import namedtuple
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from duckdb import DuckDBPyConnection
|
|
5
|
+
from soda_core.common.data_source_connection import DataSourceConnection
|
|
6
|
+
from soda_core.common.data_source_impl import DataSourceImpl
|
|
7
|
+
from soda_core.common.exceptions import DataSourceConnectionException
|
|
8
|
+
from soda_core.common.metadata_types import DataSourceNamespace, SodaDataTypeName
|
|
9
|
+
from soda_core.common.sql_ast import *
|
|
10
|
+
from soda_core.common.sql_dialect import SqlDialect
|
|
11
|
+
from soda_duckdb.common.data_sources.duckdb_data_source_connection import (
|
|
12
|
+
DuckDBConnectionProperties,
|
|
13
|
+
)
|
|
14
|
+
from soda_duckdb.common.data_sources.duckdb_data_source_connection import (
|
|
15
|
+
DuckDBDataSource as DuckDBDataSourceModel,
|
|
16
|
+
)
|
|
17
|
+
from soda_duckdb.common.data_sources.duckdb_data_source_connection import (
|
|
18
|
+
DuckDBExistingConnectionProperties,
|
|
19
|
+
DuckDBStandardConnectionProperties,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
DuckDBColumn = namedtuple(
|
|
23
|
+
"DuckDBColumn", ["name", "type_code", "display_size", "internal_size", "precision", "scale", "null_ok"]
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
_in_memory_connection = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DuckDBCursor:
|
|
30
|
+
def __init__(self, connection):
|
|
31
|
+
self._connection = connection
|
|
32
|
+
|
|
33
|
+
def __getattr__(self, attr):
|
|
34
|
+
if attr in self.__dict__:
|
|
35
|
+
return getattr(self, attr)
|
|
36
|
+
return getattr(self._connection, attr)
|
|
37
|
+
|
|
38
|
+
def close(self):
|
|
39
|
+
# because a duckdb cursor is actually the current connection,
|
|
40
|
+
# we don't want to close it
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def description(self):
|
|
45
|
+
"""
|
|
46
|
+
Makes the cursor description available as a list of DuckDBColumn namedtuples.
|
|
47
|
+
This is to be compatible with the expected interface of a DBAPI cursor.
|
|
48
|
+
"""
|
|
49
|
+
return [DuckDBColumn(*col) for col in self._connection.description]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class DuckDBDataSourceConnectionWrapper:
|
|
53
|
+
def __init__(self, delegate):
|
|
54
|
+
self._delegate = delegate
|
|
55
|
+
|
|
56
|
+
def __getattr__(self, attr):
|
|
57
|
+
if attr in self.__dict__:
|
|
58
|
+
return getattr(self, attr)
|
|
59
|
+
return getattr(self._delegate, attr)
|
|
60
|
+
|
|
61
|
+
def cursor(self):
|
|
62
|
+
return DuckDBCursor(self._delegate)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class DuckDBSqlDialect(SqlDialect):
|
|
66
|
+
SODA_DATA_TYPE_SYNONYMS = (
|
|
67
|
+
(SodaDataTypeName.TEXT, SodaDataTypeName.VARCHAR, SodaDataTypeName.CHAR),
|
|
68
|
+
(SodaDataTypeName.NUMERIC, SodaDataTypeName.DECIMAL),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def get_database_prefix_index(self) -> int | None:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
def get_schema_prefix_index(self) -> int | None:
|
|
75
|
+
return 0
|
|
76
|
+
|
|
77
|
+
def information_schema_namespace_elements(self, prefixes: list[str]) -> list[str]:
|
|
78
|
+
"""
|
|
79
|
+
The prefixes / namespace of the information schema for a given dataset prefix / namespace
|
|
80
|
+
"""
|
|
81
|
+
return [self.schema_information_schema()]
|
|
82
|
+
|
|
83
|
+
def supports_data_type_character_maximum_length(self):
|
|
84
|
+
"""
|
|
85
|
+
From docs: "Variable-length character string. The maximum length n has no effect and is only provided for compatibility"
|
|
86
|
+
"""
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
def supports_data_type_numeric_precision(self) -> bool:
|
|
90
|
+
return True
|
|
91
|
+
|
|
92
|
+
def supports_data_type_numeric_scale(self) -> bool:
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
def supports_data_type_datetime_precision(self) -> bool:
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
def _build_regex_like_sql(self, matches: REGEX_LIKE) -> str:
|
|
99
|
+
expression: str = self.build_expression_sql(matches.expression)
|
|
100
|
+
return f"REGEXP_MATCHES({expression}, '{matches.regex_pattern}')"
|
|
101
|
+
|
|
102
|
+
def create_schema_if_not_exists_sql(self, prefixes: list[str], add_semicolon: bool = True) -> str:
|
|
103
|
+
schema_name: str = prefixes[0]
|
|
104
|
+
quoted_schema_name: str = self.quote_default(schema_name)
|
|
105
|
+
return f"CREATE SCHEMA IF NOT EXISTS {quoted_schema_name}" + (";" if add_semicolon else "")
|
|
106
|
+
|
|
107
|
+
def format_metadata_data_type(self, data_type: str) -> str:
|
|
108
|
+
paranthesis_index = data_type.find("(")
|
|
109
|
+
if paranthesis_index != -1:
|
|
110
|
+
return data_type[:paranthesis_index]
|
|
111
|
+
return data_type
|
|
112
|
+
|
|
113
|
+
def default_numeric_precision(self) -> Optional[int]:
|
|
114
|
+
return 18
|
|
115
|
+
|
|
116
|
+
def default_numeric_scale(self) -> Optional[int]:
|
|
117
|
+
return 3
|
|
118
|
+
|
|
119
|
+
def format_metadata_data_type(self, data_type: str) -> str:
|
|
120
|
+
"""DuckDB sometimes modifies data types to include precision (e.g. TIMESTAMP as TIMESTAMP(3)) in column metadata
|
|
121
|
+
|
|
122
|
+
We don't want data type comparisons to fail, so strip this extra information.
|
|
123
|
+
"""
|
|
124
|
+
paranthesis_index = data_type.find("(")
|
|
125
|
+
if paranthesis_index != -1:
|
|
126
|
+
return data_type[:paranthesis_index]
|
|
127
|
+
return data_type
|
|
128
|
+
|
|
129
|
+
def _get_data_type_name_synonyms(self) -> list[list[str]]:
|
|
130
|
+
# Implements data type synonyms
|
|
131
|
+
# Each list should represent a list of synonyms
|
|
132
|
+
return [
|
|
133
|
+
["varchar", "text", "string"],
|
|
134
|
+
["number", "decimal", "numeric", "int", "integer", "bigint", "smallint", "tinyint", "byteint"],
|
|
135
|
+
["float", "float4", "float8", "double", "double precision", "real"],
|
|
136
|
+
["timestamp", "datetime", "timestamp_ntz", "timestamp without time zone"],
|
|
137
|
+
["timestamp_ltz", "timestamp with local time zone"],
|
|
138
|
+
["timestamp_tz", "timestamp with time zone"],
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
def get_data_source_data_type_name_by_soda_data_type_names(self) -> dict:
|
|
142
|
+
"""
|
|
143
|
+
Maps DBDataType names to data source type names.
|
|
144
|
+
"""
|
|
145
|
+
return {
|
|
146
|
+
SodaDataTypeName.CHAR: "varchar", # DuckDB doesn't have fixed CHAR, maps to VARCHAR
|
|
147
|
+
SodaDataTypeName.VARCHAR: "varchar",
|
|
148
|
+
SodaDataTypeName.TEXT: "text", # TEXT is an alias for VARCHAR in DuckDB. So using text will result in a VARCHAR being created.
|
|
149
|
+
SodaDataTypeName.SMALLINT: "smallint",
|
|
150
|
+
SodaDataTypeName.INTEGER: "integer",
|
|
151
|
+
SodaDataTypeName.BIGINT: "bigint",
|
|
152
|
+
SodaDataTypeName.DECIMAL: "decimal", # DuckDB supports DECIMAL(p,s)
|
|
153
|
+
SodaDataTypeName.NUMERIC: "decimal", # NUMERIC is an alias for DECIMAL
|
|
154
|
+
SodaDataTypeName.FLOAT: "real", # single-precision float
|
|
155
|
+
SodaDataTypeName.DOUBLE: "double", # double-precision float
|
|
156
|
+
SodaDataTypeName.TIMESTAMP: "timestamp", # default = without timezone
|
|
157
|
+
SodaDataTypeName.TIMESTAMP_TZ: "timestamptz",
|
|
158
|
+
SodaDataTypeName.DATE: "date",
|
|
159
|
+
SodaDataTypeName.TIME: "time",
|
|
160
|
+
SodaDataTypeName.BOOLEAN: "boolean",
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
def get_soda_data_type_name_by_data_source_data_type_names(self) -> dict[str, SodaDataTypeName]:
|
|
164
|
+
return {
|
|
165
|
+
# Character / String types
|
|
166
|
+
"char": SodaDataTypeName.CHAR,
|
|
167
|
+
"varchar": SodaDataTypeName.VARCHAR,
|
|
168
|
+
"string": SodaDataTypeName.VARCHAR,
|
|
169
|
+
"text": SodaDataTypeName.TEXT,
|
|
170
|
+
# Integer types
|
|
171
|
+
"tinyint": SodaDataTypeName.SMALLINT, # closest match (no explicit TINYINT in Soda)
|
|
172
|
+
"smallint": SodaDataTypeName.SMALLINT,
|
|
173
|
+
"int2": SodaDataTypeName.SMALLINT,
|
|
174
|
+
"integer": SodaDataTypeName.INTEGER,
|
|
175
|
+
"int": SodaDataTypeName.INTEGER,
|
|
176
|
+
"int4": SodaDataTypeName.INTEGER,
|
|
177
|
+
"bigint": SodaDataTypeName.BIGINT,
|
|
178
|
+
"int8": SodaDataTypeName.BIGINT,
|
|
179
|
+
"utinyint": SodaDataTypeName.INTEGER, # Soda doesn’t have unsigned types
|
|
180
|
+
"usmallint": SodaDataTypeName.INTEGER,
|
|
181
|
+
"uinteger": SodaDataTypeName.INTEGER,
|
|
182
|
+
"uint": SodaDataTypeName.INTEGER,
|
|
183
|
+
"ubigint": SodaDataTypeName.BIGINT,
|
|
184
|
+
"hugeint": SodaDataTypeName.BIGINT, # Soda has no HUGEINT, map to BIGINT
|
|
185
|
+
# Decimal / Numeric
|
|
186
|
+
"decimal": SodaDataTypeName.DECIMAL,
|
|
187
|
+
"numeric": SodaDataTypeName.NUMERIC,
|
|
188
|
+
# Floating point
|
|
189
|
+
"real": SodaDataTypeName.FLOAT,
|
|
190
|
+
"float4": SodaDataTypeName.FLOAT,
|
|
191
|
+
"float": SodaDataTypeName.FLOAT,
|
|
192
|
+
"float8": SodaDataTypeName.DOUBLE,
|
|
193
|
+
"double": SodaDataTypeName.DOUBLE,
|
|
194
|
+
# Boolean
|
|
195
|
+
"boolean": SodaDataTypeName.BOOLEAN,
|
|
196
|
+
"bool": SodaDataTypeName.BOOLEAN,
|
|
197
|
+
# Date & Time
|
|
198
|
+
"date": SodaDataTypeName.DATE,
|
|
199
|
+
"time": SodaDataTypeName.TIME,
|
|
200
|
+
"timestamp": SodaDataTypeName.TIMESTAMP,
|
|
201
|
+
"datetime": SodaDataTypeName.TIMESTAMP,
|
|
202
|
+
"timestamp without time zone": SodaDataTypeName.TIMESTAMP,
|
|
203
|
+
"timestamptz": SodaDataTypeName.TIMESTAMP_TZ,
|
|
204
|
+
"timestamp with time zone": SodaDataTypeName.TIMESTAMP_TZ,
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
def build_columns_metadata_query_str(self, table_namespace: DataSourceNamespace, table_name: str) -> str:
|
|
208
|
+
return super().build_columns_metadata_query_str(table_namespace, table_name)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class DuckDBDataSourceConnection(DataSourceConnection):
|
|
212
|
+
REGISTERED_FORMAT_MAP = {
|
|
213
|
+
".csv": "read_csv_auto",
|
|
214
|
+
".parquet": "read_parquet",
|
|
215
|
+
".json": "read_json_auto",
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
def _create_connection(
|
|
219
|
+
self,
|
|
220
|
+
config: DuckDBConnectionProperties,
|
|
221
|
+
):
|
|
222
|
+
import duckdb
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
if isinstance(config, DuckDBExistingConnectionProperties):
|
|
226
|
+
return DuckDBDataSourceConnectionWrapper(config.duckdb_connection)
|
|
227
|
+
elif isinstance(config, DuckDBStandardConnectionProperties):
|
|
228
|
+
if (read_function := self.REGISTERED_FORMAT_MAP.get(self.extract_format(config))) is not None:
|
|
229
|
+
connection = DuckDBDataSourceConnectionWrapper(duckdb.connect(":default:"))
|
|
230
|
+
connection.sql(
|
|
231
|
+
f"CREATE TABLE {self.extract_dataset_name(config)} AS SELECT * FROM {read_function}('{config.database}')"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
return connection
|
|
235
|
+
else:
|
|
236
|
+
if config.database == ":memory:":
|
|
237
|
+
# Re-use existing in-memory connection if it exists
|
|
238
|
+
global _in_memory_connection
|
|
239
|
+
if _in_memory_connection is not None:
|
|
240
|
+
return DuckDBDataSourceConnectionWrapper(_in_memory_connection)
|
|
241
|
+
_in_memory_connection = duckdb.connect(
|
|
242
|
+
database=":memory:", read_only=config.read_only, config=config.configuration
|
|
243
|
+
)
|
|
244
|
+
return DuckDBDataSourceConnectionWrapper(_in_memory_connection)
|
|
245
|
+
return DuckDBDataSourceConnectionWrapper(
|
|
246
|
+
duckdb.connect(
|
|
247
|
+
database=config.database,
|
|
248
|
+
read_only=config.read_only,
|
|
249
|
+
config=config.configuration,
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
except Exception as e:
|
|
254
|
+
raise DataSourceConnectionException(e)
|
|
255
|
+
|
|
256
|
+
def extract_format(self, config: DuckDBStandardConnectionProperties) -> str:
|
|
257
|
+
return Path(config.database).suffix
|
|
258
|
+
|
|
259
|
+
def extract_dataset_name(self, config: DuckDBStandardConnectionProperties) -> str:
|
|
260
|
+
return Path(config.database).stem
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class DuckDBDataSourceImpl(DataSourceImpl, model_class=DuckDBDataSourceModel):
|
|
264
|
+
def _create_sql_dialect(self) -> SqlDialect:
|
|
265
|
+
return DuckDBSqlDialect(data_source_impl=self)
|
|
266
|
+
|
|
267
|
+
def _create_data_source_connection(self) -> DataSourceConnection:
|
|
268
|
+
return DuckDBDataSourceConnection(
|
|
269
|
+
name=self.data_source_model.name, connection_properties=self.data_source_model.connection_properties
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
@classmethod
|
|
273
|
+
def from_existing_cursor(cls, cursor: DuckDBPyConnection, name: str) -> DataSourceImpl:
|
|
274
|
+
ds_model = DuckDBDataSourceModel(
|
|
275
|
+
name=name,
|
|
276
|
+
connection_properties=DuckDBExistingConnectionProperties(
|
|
277
|
+
duckdb_connection=cursor,
|
|
278
|
+
),
|
|
279
|
+
)
|
|
280
|
+
soda_connection = DuckDBDataSourceConnection(
|
|
281
|
+
name=name,
|
|
282
|
+
connection_properties={},
|
|
283
|
+
connection=DuckDBDataSourceConnectionWrapper(cursor),
|
|
284
|
+
)
|
|
285
|
+
return cls(data_source_model=ds_model, connection=soda_connection)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from typing import Dict, Literal, Optional
|
|
3
|
+
|
|
4
|
+
from duckdb import DuckDBPyConnection
|
|
5
|
+
from pydantic import Field, field_validator
|
|
6
|
+
from soda_core.model.data_source.data_source import DataSourceBase
|
|
7
|
+
from soda_core.model.data_source.data_source_connection_properties import (
|
|
8
|
+
DataSourceConnectionProperties,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DuckDBConnectionProperties(DataSourceConnectionProperties, ABC):
|
|
13
|
+
schema_: Optional[str] = Field(
|
|
14
|
+
"main", description="Optional schema name to use for the DuckDB connection", alias="schema"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DuckDBStandardConnectionProperties(DuckDBConnectionProperties):
|
|
19
|
+
database: str = Field(
|
|
20
|
+
":memory:", description="Path to the DuckDB database file or ':memory:' for in-memory database"
|
|
21
|
+
)
|
|
22
|
+
read_only: bool = Field(False, description="If True, the database is opened in read-only mode")
|
|
23
|
+
configuration: Dict[str, str] = Field({}, description="Optional configuration dictionary for DuckDB")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DuckDBExistingConnectionProperties(DuckDBConnectionProperties, arbitrary_types_allowed=True):
|
|
27
|
+
duckdb_connection: Optional[DuckDBPyConnection] = Field(
|
|
28
|
+
None, description="Optional existing DuckDB connection to use instead of creating a new one"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DuckDBDataSource(DataSourceBase, ABC):
|
|
33
|
+
type: Literal["duckdb"] = Field("duckdb")
|
|
34
|
+
connection_properties: DuckDBConnectionProperties = Field(
|
|
35
|
+
..., alias="connection", description="DuckDB connection configuration"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
@field_validator("connection_properties", mode="before")
|
|
39
|
+
@classmethod
|
|
40
|
+
def infer_connection_type(cls, value):
|
|
41
|
+
if isinstance(value, DuckDBConnectionProperties):
|
|
42
|
+
return value
|
|
43
|
+
|
|
44
|
+
if "database" in value:
|
|
45
|
+
return DuckDBStandardConnectionProperties(**value)
|
|
46
|
+
elif "duckdb_connection" in value:
|
|
47
|
+
return DuckDBExistingConnectionProperties(**value)
|
|
48
|
+
raise ValueError("Could not infer DuckDB connection type from input")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from helpers.data_source_test_helper import DataSourceTestHelper
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DuckdbDataSourceTestHelper(DataSourceTestHelper):
|
|
9
|
+
def _create_data_source_yaml_str(self) -> str:
|
|
10
|
+
"""
|
|
11
|
+
Called in _create_data_source_impl to initialized self.data_source_impl
|
|
12
|
+
self.database_name and self.schema_name are available if appropriate for the data source type
|
|
13
|
+
"""
|
|
14
|
+
return f"""
|
|
15
|
+
type: duckdb
|
|
16
|
+
name: {self.name}
|
|
17
|
+
connection:
|
|
18
|
+
database: "{self.name}.db"
|
|
19
|
+
schema: main
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def _create_database_name(self) -> Optional[str]:
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
def _create_schema_name(self) -> Optional[str]:
|
|
26
|
+
return "main"
|
|
27
|
+
|
|
28
|
+
def _create_dataset_prefix(self) -> list[str]:
|
|
29
|
+
schema_name: str = self._create_schema_name()
|
|
30
|
+
return [schema_name]
|
|
31
|
+
|
|
32
|
+
def drop_test_schema_if_exists(self) -> None:
|
|
33
|
+
"""
|
|
34
|
+
In-memory DuckDB does not support schemas, so this method is a no-op.
|
|
35
|
+
"""
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
setup.py
|
|
2
|
+
src/soda_duckdb/__init__.py
|
|
3
|
+
src/soda_duckdb.egg-info/PKG-INFO
|
|
4
|
+
src/soda_duckdb.egg-info/SOURCES.txt
|
|
5
|
+
src/soda_duckdb.egg-info/dependency_links.txt
|
|
6
|
+
src/soda_duckdb.egg-info/entry_points.txt
|
|
7
|
+
src/soda_duckdb.egg-info/requires.txt
|
|
8
|
+
src/soda_duckdb.egg-info/top_level.txt
|
|
9
|
+
src/soda_duckdb/common/data_sources/duckdb_data_source.py
|
|
10
|
+
src/soda_duckdb/common/data_sources/duckdb_data_source_connection.py
|
|
11
|
+
src/soda_duckdb/test_helpers/duckdb_data_source_test_helper.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
soda_duckdb
|