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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: flit 3.11.0
3
+ Root-Is-Purelib: true
4
+ Tag: py2-none-any
5
+ Tag: py3-none-any
@@ -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