qsql 1.0.2.2__py2.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.
- qsql-1.0.2.2.dist-info/METADATA +81 -0
- qsql-1.0.2.2.dist-info/RECORD +8 -0
- qsql-1.0.2.2.dist-info/WHEEL +5 -0
- qsql-1.0.2.2.dist-info/licenses/LICENSE +21 -0
- quicksql/__init__.py +7 -0
- quicksql/__init__.pyi +62 -0
- quicksql/_quicksql.py +215 -0
- quicksql/py.typed +0 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qsql
|
|
3
|
+
Version: 1.0.2.2
|
|
4
|
+
Summary: Parses queries from an sql file, and turns them into callable f-strings. Also a file cache decorator for slow queries and multi-session permanence, supports async functions.
|
|
5
|
+
Author-email: Charles Marks <charlesmarksco@gmail.com>
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: orjson
|
|
10
|
+
Project-URL: Home, https://github.com/CircuitCM/pyquicksql
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# PyQuickSQL
|
|
15
|
+
For a more thorough explanation see [example.md](https://github.com/CircuitCM/pyquicksql/blob/main/example.md)
|
|
16
|
+
To install: `pip install qsql`
|
|
17
|
+
### How to use the query loader:
|
|
18
|
+
```python
|
|
19
|
+
import quicksql as qq
|
|
20
|
+
queries = qq.LoadSQL('path/to/queries.sql')
|
|
21
|
+
```
|
|
22
|
+
Printing the queries should give you something like this:
|
|
23
|
+
```text
|
|
24
|
+
LoadSQL('path/to/queries.sql')
|
|
25
|
+
Query Name: examplequery_1, Params: order_avg, num_orders
|
|
26
|
+
Query Name: examplequery_2, Params: sdate, edate, product_id
|
|
27
|
+
```
|
|
28
|
+
These are callable objects that return the string given the non-optional **params.
|
|
29
|
+
They are equivalent to an f-string + a lambda but loaded from an sql file and stored in the LoadSQL object.
|
|
30
|
+
`print(str(queries))` will give you the raw SQL string.
|
|
31
|
+
|
|
32
|
+
How to use them:
|
|
33
|
+
```python
|
|
34
|
+
print(queries.examplequery_2(
|
|
35
|
+
product_id=10,
|
|
36
|
+
sdate='1-10-2022',
|
|
37
|
+
edate=qq.NoStr("DATE'4-11-2023'"),
|
|
38
|
+
something_not_a_param='test'))
|
|
39
|
+
```
|
|
40
|
+
```text
|
|
41
|
+
Unused variables: something_not_a_param in query examplequery_2
|
|
42
|
+
```
|
|
43
|
+
Above will be printed as a warning with invalid inclusions, no non-verbose option yet.
|
|
44
|
+
```sql
|
|
45
|
+
SELECT
|
|
46
|
+
c.CustomerName,
|
|
47
|
+
o.OrderDate,
|
|
48
|
+
o.Status,
|
|
49
|
+
(SELECT SUM(od.Quantity * od.UnitPrice) FROM OrderDetails od WHERE od.OrderID = o.OrderID) AS TotalValue
|
|
50
|
+
FROM
|
|
51
|
+
Customers c
|
|
52
|
+
INNER JOIN Orders o ON c.CustomerID = o.CustomerID
|
|
53
|
+
WHERE
|
|
54
|
+
o.OrderDate BETWEEN '1-10-2022' AND DATE'4-11-2023'
|
|
55
|
+
AND EXISTS (SELECT 1 FROM OrderDetails od WHERE od.OrderID = o.OrderID AND od.ProductID = 10)
|
|
56
|
+
ORDER BY
|
|
57
|
+
TotalValue DESC;
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### How to use the file cache:
|
|
61
|
+
This is very similar to functool's `cache`, with the main difference being that `@qq.file_cache` caches the asset to memory
|
|
62
|
+
and to your system's default temporary directory. If the memory cache ever fails (eg a restarted kernel) it will load the asset from it's pickled file.
|
|
63
|
+
```python
|
|
64
|
+
from random import randint
|
|
65
|
+
import quicksql as qq
|
|
66
|
+
@qq.file_cache()
|
|
67
|
+
def test_mem(size:int):
|
|
68
|
+
return [randint(0,10) for _ in range(size)]
|
|
69
|
+
|
|
70
|
+
print(test_mem(8))
|
|
71
|
+
```
|
|
72
|
+
`[10, 3, 4, 9, 2, 2, 4, 2]`
|
|
73
|
+
```python
|
|
74
|
+
print(test_mem(8))
|
|
75
|
+
```
|
|
76
|
+
`[10, 3, 4, 9, 2, 2, 4, 2]`
|
|
77
|
+
|
|
78
|
+
To clear the cache: `qq.clear_cache()`
|
|
79
|
+
|
|
80
|
+
For more examples and how to configure see [example.ipynb](https://github.com/CircuitCM/pyquicksql/blob/main/example.ipynb)
|
|
81
|
+
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
quicksql/__init__.py,sha256=X_D71NOyiKNGcpBskCWvIDQlF8-gAGhs-n9_dIRL5bE,374
|
|
2
|
+
quicksql/__init__.pyi,sha256=aSP6YBmEPuqXttuqPz4bxUboPS1jzKnEItyo2SkKdCo,1356
|
|
3
|
+
quicksql/_quicksql.py,sha256=FdnfluTL2fAhZXcyYRsVpGNL62mwIJSpj2XlKWML764,7586
|
|
4
|
+
quicksql/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
qsql-1.0.2.2.dist-info/licenses/LICENSE,sha256=OeyQ5X2phWTZ63Z3P4zUTrh0z_bHbfsQdkWThs2juu4,1091
|
|
6
|
+
qsql-1.0.2.2.dist-info/WHEEL,sha256=BXjIu84EnBiZ4HkNUBN93Hamt5EPQMQ6VkF7-VZ_Pu0,100
|
|
7
|
+
qsql-1.0.2.2.dist-info/METADATA,sha256=h6lIokbjc_McPymhvh3vW_FYhslLVsGUN_6bXGlFMew,2732
|
|
8
|
+
qsql-1.0.2.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Charles Marks
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
quicksql/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Parses queries from an sql file, and turns them into callable f-strings. Also a file cache decorator for slow queries and multi-session permanence, supports async functions."""
|
|
2
|
+
|
|
3
|
+
__version__ = '1.0.2.2'
|
|
4
|
+
|
|
5
|
+
from ._quicksql import file_cache,test_cache,Query,LoadSQL,clear_cache,NoStr
|
|
6
|
+
|
|
7
|
+
__all__ = ["file_cache", "test_cache", "Query", "LoadSQL", "clear_cache", "NoStr"]
|
quicksql/__init__.pyi
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def file_cache(use_mem_cache=..., threadsafe=...): # -> Callable[..., Callable[..., CoroutineType[Any, Any, Any | _NA]] | Callable[..., Any]]:
|
|
5
|
+
|
|
6
|
+
...
|
|
7
|
+
|
|
8
|
+
def clear_cache(clr_mem=..., clr_file=...):# -> None:
|
|
9
|
+
...
|
|
10
|
+
|
|
11
|
+
@file_cache()
|
|
12
|
+
def test_cache(*args, **kwargs): # -> tuple[tuple[Any, ...], dict[str, Any]]:
|
|
13
|
+
...
|
|
14
|
+
|
|
15
|
+
class NoStr:
|
|
16
|
+
__slots__ = ...
|
|
17
|
+
def __init__(self, string: str) -> None:
|
|
18
|
+
...
|
|
19
|
+
|
|
20
|
+
def __str__(self) -> str:
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
def __repr__(self): # -> str:
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Query:
|
|
29
|
+
|
|
30
|
+
def __init__(self, name, query) -> None:
|
|
31
|
+
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
def __call__(self, **kwargs): # -> Any:
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
@cache
|
|
38
|
+
def __str__(self) -> str:
|
|
39
|
+
...
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class LoadSQL:
|
|
44
|
+
|
|
45
|
+
def __init__(self, path) -> None:
|
|
46
|
+
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
@cache
|
|
50
|
+
def __str__(self) -> str:
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
@cache
|
|
54
|
+
def __repr__(self): # -> str:
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
from ._quicksql import LoadSQL, NoStr, Query, clear_cache, file_cache, test_cache
|
|
59
|
+
|
|
60
|
+
"""Parses queries from an sql file, and turns them into callable f-strings. Also a file cache decorator for slow queries and multi-session permanence, supports async functions."""
|
|
61
|
+
__version__ = ...
|
|
62
|
+
__all__ = ["file_cache", "test_cache", "Query", "LoadSQL", "clear_cache", "NoStr"]
|
quicksql/_quicksql.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import uuid as id
|
|
2
|
+
import pickle as pkl
|
|
3
|
+
import os
|
|
4
|
+
from functools import cache
|
|
5
|
+
import re
|
|
6
|
+
import copy
|
|
7
|
+
import asyncio as aio
|
|
8
|
+
import hashlib as hl
|
|
9
|
+
import orjson as js
|
|
10
|
+
|
|
11
|
+
_mem_cache = {}
|
|
12
|
+
cache_dir = os.environ.get('QQ_CACHE_DIR', None)
|
|
13
|
+
if cache_dir is None:
|
|
14
|
+
import tempfile
|
|
15
|
+
cache_dir=os.path.join(tempfile.gettempdir(),'qqcache')
|
|
16
|
+
os.makedirs(cache_dir, exist_ok=True)
|
|
17
|
+
|
|
18
|
+
_FQS = re.compile(r"--\s*name\s*:\s*")
|
|
19
|
+
_FNAM = re.compile(r"\W")
|
|
20
|
+
_VIN = re.compile(r"(?<!:):\w+")
|
|
21
|
+
_alljopts=js.OPT_SERIALIZE_DATACLASS|js.OPT_SERIALIZE_NUMPY|js.OPT_SERIALIZE_UUID|js.OPT_NAIVE_UTC|js.OPT_NON_STR_KEYS
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _NA(object): #so None is still valid
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
def _lf(name):
|
|
29
|
+
try:
|
|
30
|
+
with open(name, 'rb') as file:
|
|
31
|
+
result = pkl.load(file)
|
|
32
|
+
return result
|
|
33
|
+
except FileNotFoundError:
|
|
34
|
+
return _NA()
|
|
35
|
+
|
|
36
|
+
def _sf(name,result):
|
|
37
|
+
with open(name, 'wb') as file:
|
|
38
|
+
pkl.dump(result, file)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _make_key(*args,**kwargs):
|
|
42
|
+
return hl.blake2b(js.dumps((args, kwargs),option=_alljopts),usedforsecurity=False).hexdigest()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def file_cache(use_mem_cache=True,threadsafe=True):
|
|
46
|
+
"""
|
|
47
|
+
:param use_mem_cache: If true, will cache in memory as well as file
|
|
48
|
+
:param threadsafe: If false, and your function is async, will deepcopy the result in a separate thread to not block, for small assets false is probably a bit slower.
|
|
49
|
+
:return: A decorator that will cache the result of a function in a file
|
|
50
|
+
"""
|
|
51
|
+
def wrapper1(func):
|
|
52
|
+
isco,iscofun= aio.iscoroutinefunction(func), aio.iscoroutine(func)
|
|
53
|
+
if isco or iscofun:
|
|
54
|
+
async def wrapper(*args, **kwargs):
|
|
55
|
+
nargs=args+(func.__name__,)
|
|
56
|
+
key=_make_key(*nargs,**kwargs)
|
|
57
|
+
file_name = os.path.join(cache_dir,f"{key}.pkl")
|
|
58
|
+
if use_mem_cache:
|
|
59
|
+
val = _mem_cache.get(key, _NA())
|
|
60
|
+
incache = type(val) is not _NA
|
|
61
|
+
if incache:
|
|
62
|
+
if threadsafe:
|
|
63
|
+
return copy.deepcopy(val)
|
|
64
|
+
else:
|
|
65
|
+
return await aio.get_running_loop().run_in_executor(None, copy.deepcopy, val)
|
|
66
|
+
|
|
67
|
+
result = await aio.get_running_loop().run_in_executor(None, _lf, file_name)
|
|
68
|
+
if type(result) is _NA:
|
|
69
|
+
if isco:
|
|
70
|
+
result = await func(*args, **kwargs)
|
|
71
|
+
else:
|
|
72
|
+
result = await func
|
|
73
|
+
#File cache is always a backup so restarting an interpreter is safe, Need to explicitly clear cache if the remote dataset changes
|
|
74
|
+
await aio.get_running_loop().run_in_executor(None,_sf, file_name, result)
|
|
75
|
+
if use_mem_cache:
|
|
76
|
+
_mem_cache[key] = result
|
|
77
|
+
return result
|
|
78
|
+
else:
|
|
79
|
+
def wrapper(*args, **kwargs):
|
|
80
|
+
nargs=args+(func.__name__,)
|
|
81
|
+
key = _make_key(*nargs, **kwargs)
|
|
82
|
+
file_name = os.path.join(cache_dir,f"{key}.pkl")
|
|
83
|
+
if use_mem_cache:
|
|
84
|
+
val = _mem_cache.get(key, _NA())
|
|
85
|
+
incache = type(val) is not _NA
|
|
86
|
+
if incache:
|
|
87
|
+
return copy.deepcopy(val)
|
|
88
|
+
try:
|
|
89
|
+
with open(file_name, 'rb') as file:
|
|
90
|
+
result = pkl.load(file)
|
|
91
|
+
except FileNotFoundError:
|
|
92
|
+
result = func(*args, **kwargs)
|
|
93
|
+
#File cache is always a backup so restarting an interpreter is safe, Need to explicitly clear cache if the remote dataset changes
|
|
94
|
+
with open(file_name, 'wb') as file:
|
|
95
|
+
pkl.dump(result, file)
|
|
96
|
+
if use_mem_cache:
|
|
97
|
+
_mem_cache[key] = result
|
|
98
|
+
return result
|
|
99
|
+
|
|
100
|
+
return wrapper
|
|
101
|
+
return wrapper1
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def clear_cache(clr_mem=True, clr_file=True):
|
|
105
|
+
"""
|
|
106
|
+
:param clr_mem: If true, will clear memory cache
|
|
107
|
+
:param clr_file: If true, will clear file cache
|
|
108
|
+
:return: None
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
if clr_mem:
|
|
112
|
+
_mem_cache.clear()
|
|
113
|
+
print("Memory cache cleared.")
|
|
114
|
+
if clr_file:
|
|
115
|
+
for file in os.listdir(cache_dir):
|
|
116
|
+
os.remove(os.path.join(cache_dir, file))
|
|
117
|
+
print("File cache cleared.")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
#need to add closed parentheses for it to work idk why, maybe an optional arg thing.
|
|
121
|
+
@file_cache()
|
|
122
|
+
def test_cache(*args, **kwargs):
|
|
123
|
+
return args, kwargs
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class NoStr:
|
|
127
|
+
|
|
128
|
+
__slots__=('string',)
|
|
129
|
+
|
|
130
|
+
def __init__(self,string:str):
|
|
131
|
+
self.string=string
|
|
132
|
+
|
|
133
|
+
def __str__(self):
|
|
134
|
+
return self.string
|
|
135
|
+
|
|
136
|
+
def __repr__(self):
|
|
137
|
+
return self.string
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class Query:
|
|
141
|
+
"""
|
|
142
|
+
This is a convenience wrapper to generate SQL queries with named variables. It is basically the same as a lambda **g: f-string
|
|
143
|
+
but it takes the SQL variable notation so it can parse a query from a .sql file.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
def __init__(self, name, query):
|
|
147
|
+
"""
|
|
148
|
+
:param name: Name of query
|
|
149
|
+
:param query: SQL query with :var_name variables
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
self.name = name
|
|
153
|
+
self.query = query
|
|
154
|
+
self._rvrs=list({vr for vr in _VIN.findall(query)}) #to remove duplicate keys
|
|
155
|
+
self.vars = [v[1:] for v in self._rvrs]
|
|
156
|
+
|
|
157
|
+
def __call__(self, **kwargs):
|
|
158
|
+
#Can't use python string methods here as it might conflict with SQL syntax
|
|
159
|
+
oq=self.query
|
|
160
|
+
for v in zip(self._rvrs, self.vars):
|
|
161
|
+
rp=kwargs.get(v[1],None)
|
|
162
|
+
if rp is None:
|
|
163
|
+
raise ValueError(f"Missing value for variable {v[1]}")
|
|
164
|
+
oq=oq.replace(v[0], str(rp) if type(rp) is not str else f"'{rp}'")
|
|
165
|
+
chk_nkg=kwargs.keys()-self.vars
|
|
166
|
+
if len(chk_nkg)>0:
|
|
167
|
+
print(f"Unused variables: {', '.join(chk_nkg)} in query {self.name}")
|
|
168
|
+
return oq
|
|
169
|
+
|
|
170
|
+
@cache
|
|
171
|
+
def __str__(self):
|
|
172
|
+
return f"-- Query:: {self.name}\n{self.query}"
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class LoadSQL:
|
|
177
|
+
"""
|
|
178
|
+
An object that holds all the named queries from a .sql file.
|
|
179
|
+
Names are specified with -- name: <name> and queries are separated by the next -- name: or -- :end.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
def __init__(self, path):
|
|
183
|
+
"""
|
|
184
|
+
:param path: Path to .sql file
|
|
185
|
+
"""
|
|
186
|
+
self.path = path
|
|
187
|
+
end_marker = '-- :end'
|
|
188
|
+
with open(path, 'r') as file:
|
|
189
|
+
qf = file.read()
|
|
190
|
+
|
|
191
|
+
qdefs = _FQS.split(qf)
|
|
192
|
+
for qdef in qdefs[1:]:
|
|
193
|
+
name, query = qdef.split('\n', 1)
|
|
194
|
+
name = name.strip()
|
|
195
|
+
|
|
196
|
+
# Validate that the name is a valid Python identifier
|
|
197
|
+
if not name.isidentifier():
|
|
198
|
+
raise ValueError(f"The name '{name}' is not a valid Python identifier.")
|
|
199
|
+
|
|
200
|
+
# Truncate the query if end marker is present
|
|
201
|
+
end_idx = query.rfind(end_marker)
|
|
202
|
+
if end_idx != -1:
|
|
203
|
+
query = query[:end_idx]
|
|
204
|
+
|
|
205
|
+
setattr(self, name, Query(name, query.rstrip()))
|
|
206
|
+
|
|
207
|
+
@cache
|
|
208
|
+
def __str__(self):
|
|
209
|
+
_s = '\n\n'
|
|
210
|
+
return f"Queries from:: {self.path}\n\n{_s.join([str(qr) for nm, qr in self.__dict__.items() if nm != 'path']) if len(self.__dict__) > 1 else 'No queries found.'}"
|
|
211
|
+
|
|
212
|
+
@cache
|
|
213
|
+
def __repr__(self):
|
|
214
|
+
_s = '\n'
|
|
215
|
+
return f"LoadSQL({self.path})\n{_s.join([f'Query Name: '+nm+', Params: '+', '.join(qr.vars) for nm, qr in self.__dict__.items() if nm != 'path']) if len(self.__dict__) > 1 else 'No queries found.'}"
|
quicksql/py.typed
ADDED
|
File without changes
|