pandas_query_sql 0.8.2__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.
- pandas_query_sql-0.8.2.dist-info/METADATA +110 -0
- pandas_query_sql-0.8.2.dist-info/RECORD +10 -0
- pandas_query_sql-0.8.2.dist-info/WHEEL +4 -0
- pandas_query_sql-0.8.2.dist-info/licenses/LICENSE.txt +7 -0
- pandasql/__init__.py +23 -0
- pandasql/data/births.csv +64 -0
- pandasql/data/births_by_month.csv +409 -0
- pandasql/data/meat.csv +828 -0
- pandasql/py.typed +0 -0
- pandasql/sqldf.py +241 -0
pandasql/py.typed
ADDED
|
File without changes
|
pandasql/sqldf.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""SQL DataFrame File."""
|
|
2
|
+
|
|
3
|
+
import importlib.metadata
|
|
4
|
+
import inspect
|
|
5
|
+
import re
|
|
6
|
+
import sqlite3
|
|
7
|
+
from collections.abc import Iterator
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from typing import Any
|
|
10
|
+
from warnings import catch_warnings, filterwarnings
|
|
11
|
+
|
|
12
|
+
import packaging.version
|
|
13
|
+
from pandas import DataFrame
|
|
14
|
+
from pandas.errors import DatabaseError as PandasDatabaseError
|
|
15
|
+
from pandas.io.sql import read_sql_query
|
|
16
|
+
from sqlalchemy import create_engine # pyright: ignore[reportUnknownVariableType]
|
|
17
|
+
from sqlalchemy.engine import Connection, Engine
|
|
18
|
+
from sqlalchemy.event import listen # pyright: ignore[reportUnknownVariableType]
|
|
19
|
+
from sqlalchemy.exc import DatabaseError as SQLAlchemyDatabaseError
|
|
20
|
+
from sqlalchemy.exc import ResourceClosedError
|
|
21
|
+
from sqlalchemy.pool import NullPool
|
|
22
|
+
from sqlalchemy.pool.base import (
|
|
23
|
+
_ConnectionRecord, # pyright: ignore[reportPrivateUsage]
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__ = ["PandaSQL", "PandaSQLException", "sqldf"]
|
|
27
|
+
|
|
28
|
+
SQLALCHEMY_MAJOR_VERSION = packaging.version.Version(
|
|
29
|
+
importlib.metadata.version("sqlalchemy"),
|
|
30
|
+
).major
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class PandaSQLException(Exception):
|
|
34
|
+
"""Exception Class for PandaSQL."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PandaSQL:
|
|
38
|
+
"""Class for using SQL database to query pandas DataFrames.
|
|
39
|
+
|
|
40
|
+
Attributes:
|
|
41
|
+
engine: sqlalchemy database engine
|
|
42
|
+
persist: boolean to determine if tables stay loaded between function calls
|
|
43
|
+
loaded_tables: set of currently loaded table names
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, db_uri: str | None, persist: bool = False) -> None:
|
|
47
|
+
"""Initialize with a specific database.
|
|
48
|
+
|
|
49
|
+
:param db_uri: SQLAlchemy-compatible database URI.
|
|
50
|
+
:param persist: keep tables in database between different calls on the same object of this class.
|
|
51
|
+
"""
|
|
52
|
+
if not db_uri:
|
|
53
|
+
db_uri = "sqlite:///:memory:"
|
|
54
|
+
|
|
55
|
+
self.engine: Engine = create_engine(url=db_uri, poolclass=NullPool)
|
|
56
|
+
|
|
57
|
+
if self.engine.name == "sqlite":
|
|
58
|
+
listen(target=self.engine, identifier="connect", fn=self._set_text_factory)
|
|
59
|
+
elif self.engine.name == "postgresql":
|
|
60
|
+
pass
|
|
61
|
+
else:
|
|
62
|
+
raise PandaSQLException(
|
|
63
|
+
"Currently only sqlite and postgresql are supported.",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
self.persist = persist
|
|
67
|
+
self.loaded_tables: set[str] = set()
|
|
68
|
+
if self.persist:
|
|
69
|
+
self._conn = self.engine.connect()
|
|
70
|
+
self._init_connection(self._conn)
|
|
71
|
+
|
|
72
|
+
def __call__(
|
|
73
|
+
self,
|
|
74
|
+
query: str,
|
|
75
|
+
env: dict[str, Any] | None = None,
|
|
76
|
+
) -> DataFrame | None:
|
|
77
|
+
"""Execute the SQL query.
|
|
78
|
+
|
|
79
|
+
Automatically creates tables mentioned in the query from dataframes before executing.
|
|
80
|
+
|
|
81
|
+
:param query: SQL query string, which can reference pandas dataframes as SQL tables.
|
|
82
|
+
:param env: Variables environment - a dict mapping table names to pandas dataframes.
|
|
83
|
+
If not specified use local and global variables of the caller.
|
|
84
|
+
:return: Pandas dataframe with the result of the SQL query.
|
|
85
|
+
"""
|
|
86
|
+
if env is None:
|
|
87
|
+
env = get_outer_frame_variables()
|
|
88
|
+
|
|
89
|
+
with self.conn as conn:
|
|
90
|
+
for table_name in extract_table_names(query):
|
|
91
|
+
if table_name not in env:
|
|
92
|
+
# don't raise error because the table may be already in the database
|
|
93
|
+
continue
|
|
94
|
+
if self.persist and table_name in self.loaded_tables:
|
|
95
|
+
# table was loaded before using the same instance, don't do it again
|
|
96
|
+
continue
|
|
97
|
+
self.loaded_tables.add(table_name)
|
|
98
|
+
write_table(df=env[table_name], tablename=table_name, conn=conn)
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
result = read_sql_query(sql=query, con=conn)
|
|
102
|
+
except ResourceClosedError:
|
|
103
|
+
# query returns nothing
|
|
104
|
+
result = None
|
|
105
|
+
except (SQLAlchemyDatabaseError, PandasDatabaseError) as ex:
|
|
106
|
+
raise PandaSQLException(ex) from ex
|
|
107
|
+
|
|
108
|
+
return result
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
@contextmanager
|
|
112
|
+
def conn(self) -> Iterator[Connection]:
|
|
113
|
+
"""Context Manager for database connection."""
|
|
114
|
+
if self.persist:
|
|
115
|
+
# the connection is created in __init__, so just return it
|
|
116
|
+
yield self._conn
|
|
117
|
+
# no cleanup needed
|
|
118
|
+
else:
|
|
119
|
+
# create the connection
|
|
120
|
+
conn = self.engine.connect()
|
|
121
|
+
self._init_connection(conn)
|
|
122
|
+
try:
|
|
123
|
+
yield conn
|
|
124
|
+
finally:
|
|
125
|
+
# cleanup - close connection on exit
|
|
126
|
+
conn.close()
|
|
127
|
+
|
|
128
|
+
def _init_connection(self, conn: Connection) -> None:
|
|
129
|
+
if self.engine.name == "postgresql":
|
|
130
|
+
if SQLALCHEMY_MAJOR_VERSION >= 2:
|
|
131
|
+
from sqlalchemy import text # noqa: PLC0415
|
|
132
|
+
|
|
133
|
+
conn.execute( # pyright: ignore[reportUnknownMemberType]
|
|
134
|
+
statement=text("set search_path to pg_temp"),
|
|
135
|
+
)
|
|
136
|
+
else:
|
|
137
|
+
conn.execute( # ty: ignore[no-matching-overload]
|
|
138
|
+
statement="set search_path to pg_temp",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def _set_text_factory(
|
|
142
|
+
self,
|
|
143
|
+
dbapi_conn: sqlite3.Connection,
|
|
144
|
+
_: _ConnectionRecord,
|
|
145
|
+
) -> None:
|
|
146
|
+
dbapi_conn.text_factory = str
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def get_outer_frame_variables() -> dict[str, Any]:
|
|
150
|
+
"""Get a dict of local and global variables of the first outer frame from another file."""
|
|
151
|
+
frame = inspect.currentframe()
|
|
152
|
+
variables: dict[str, Any] = {}
|
|
153
|
+
|
|
154
|
+
if not frame:
|
|
155
|
+
return variables
|
|
156
|
+
|
|
157
|
+
cur_filename = inspect.getframeinfo(frame).filename
|
|
158
|
+
outer_frame = next(f for f in inspect.getouterframes(inspect.currentframe()) if f.filename != cur_filename)
|
|
159
|
+
variables.update(outer_frame.frame.f_globals)
|
|
160
|
+
variables.update(outer_frame.frame.f_locals)
|
|
161
|
+
return variables
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def extract_table_names(query: str) -> set[str]:
|
|
165
|
+
"""Extract table names from an SQL query."""
|
|
166
|
+
# a good old fashioned regex. turns out this worked better than actually parsing the code
|
|
167
|
+
tables_blocks = re.findall(
|
|
168
|
+
r"(?:FROM|JOIN)\s+(\w+(?:\s*,\s*\w+)*)",
|
|
169
|
+
query,
|
|
170
|
+
re.IGNORECASE,
|
|
171
|
+
)
|
|
172
|
+
return {tbl for block in tables_blocks for tbl in re.findall(r"\w+", block)}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def write_table(
|
|
176
|
+
df: DataFrame,
|
|
177
|
+
tablename: str,
|
|
178
|
+
conn: Engine | Connection | sqlite3.Connection,
|
|
179
|
+
) -> None:
|
|
180
|
+
"""Write Pandas DataFrame to SQL Table.
|
|
181
|
+
|
|
182
|
+
Parameters:
|
|
183
|
+
df: pandas.DataFrame
|
|
184
|
+
dataframe to serialize
|
|
185
|
+
tablename: string
|
|
186
|
+
name of sql table to write
|
|
187
|
+
conn: sqlite or sqlalchemy connector
|
|
188
|
+
connection to sql database
|
|
189
|
+
"""
|
|
190
|
+
with catch_warnings():
|
|
191
|
+
filterwarnings(
|
|
192
|
+
"ignore",
|
|
193
|
+
message=f"The provided table name '{tablename}' is not found exactly as such in the database",
|
|
194
|
+
)
|
|
195
|
+
df.to_sql(
|
|
196
|
+
name=tablename,
|
|
197
|
+
con=conn,
|
|
198
|
+
index=not any(
|
|
199
|
+
name is None # pyright: ignore[reportUnnecessaryComparison]
|
|
200
|
+
for name in df.index.names # pyright: ignore[reportUnknownMemberType]
|
|
201
|
+
),
|
|
202
|
+
) # load index into db if all levels are named
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def sqldf(
|
|
206
|
+
query: str,
|
|
207
|
+
env: dict[str, Any] | None = None,
|
|
208
|
+
db_uri: str | None = None,
|
|
209
|
+
) -> DataFrame | None:
|
|
210
|
+
"""Query pandas data frames using sql syntax.
|
|
211
|
+
|
|
212
|
+
This function is meant for backward compatibility only. New users are encouraged to use the PandaSQL class.
|
|
213
|
+
|
|
214
|
+
Parameters:
|
|
215
|
+
----------
|
|
216
|
+
query: string
|
|
217
|
+
a sql query using DataFrames as tables
|
|
218
|
+
env: locals() or globals()
|
|
219
|
+
variable environment; locals() or globals() in your function
|
|
220
|
+
allows sqldf to access the variables in your python environment
|
|
221
|
+
db_uri: string
|
|
222
|
+
SQLAlchemy-compatible database URI
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
-------
|
|
226
|
+
result: DataFrame
|
|
227
|
+
returns a DataFrame with your query's result
|
|
228
|
+
|
|
229
|
+
Examples:
|
|
230
|
+
--------
|
|
231
|
+
>>> import pandas as pd
|
|
232
|
+
>>> df = pd.DataFrame({
|
|
233
|
+
"x": range(100),
|
|
234
|
+
"y": range(100)
|
|
235
|
+
})
|
|
236
|
+
>>> from pandasql import sqldf
|
|
237
|
+
>>> sqldf("select * from df;", globals())
|
|
238
|
+
>>> sqldf("select * from df;", locals())
|
|
239
|
+
>>> sqldf("select avg(x) from df;", locals())
|
|
240
|
+
"""
|
|
241
|
+
return PandaSQL(db_uri=db_uri)(query, env)
|