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.
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)