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