PK!q22aiosql/__init__.pyfrom .aiosql import from_path, from_str, register_query_loader from .loaders.base import QueryLoader from .exceptions import SQLLoadException, SQLParseException __all__ = [ "from_path", "from_str", "register_query_loader", "QueryLoader", "SQLLoadException", "SQLParseException", ] PK!ɅBCCaiosql/aiosql.pyimport re from pathlib import Path from .loaders.aiosqlite import AioSQLiteQueryLoader from .loaders.asyncpg import AsyncPGQueryLoader from .loaders.psycopg2 import PsycoPG2QueryLoader from .loaders.sqlite3 import SQLite3QueryLoader from .exceptions import SQLLoadException namedef_pattern = re.compile(r"--\s*name\s*:\s*") empty_pattern = re.compile(r"^\s*$") _LOADERS = { "aiosqlite": lambda: AioSQLiteQueryLoader(), "asyncpg": lambda: AsyncPGQueryLoader(), "psycopg2": lambda: PsycoPG2QueryLoader(), "sqlite3": lambda: SQLite3QueryLoader(), } def register_query_loader(db_driver, loader): """Registers custom QueryLoader classes to extend ``aiosql`` to to handle new driver types. For details on how to create new ``QueryLoader`` see the :py:class:`aiosql.loaders.base.QueryLoader` documentation. Args: db_driver (str): The driver type name. loader: (aiosql.QueryLoader|function) Either an instance of a QueryLoader or a function which builds an instance. Returns: None Example: To register a new loader:: import aiosql class MyDbLoader(aiosql.QueryLoader): # ... overrides process_sql and create_fn pass aiosql.register_query_loader('mydb', MyDbLoader()) """ _LOADERS[db_driver] = loader def get_query_loader(db_driver): """Get the QueryLoader instance for registered by the ``db_driver`` name. Args: db_driver (str): The driver type name. Returns: aiosql.QueryLoader An instance of QueryLoader for the given db_driver. """ try: loader = _LOADERS[db_driver] except KeyError: raise ValueError(f"Encountered unregistered db_driver: {db_driver}") return loader() if callable(loader) else loader class Queries: """Container object containing methods built from SQL queries. The ``-- name: foobar`` definition comments in the SQL content determine what the dynamic methods of this class will be named. @DynamicAttrs """ def __init__(self, queries=None): """Queries constructor. Args: queries (list(tuple)): """ if queries is None: queries = [] self._available_queries = set() for name, fn in queries: self.add_query(name, fn) @property def available_queries(self): """Returns listing of all the available query methods loaded in this class. Returns: list(str): List of dot-separated method accessor names. """ return sorted(self._available_queries) def __repr__(self): return "Queries(" + self.available_queries.__repr__() + ")" def add_query(self, name, fn): """Adds a new dynamic method to this class. Args: name (str): The method name as found in the SQL content. fn (function): The loaded query function built by a QueryLoader class. Returns: """ setattr(self, name, fn) self._available_queries.add(name) def add_child_queries(self, name, child_queries): """Adds a Queries object as a property. Args: name (str): The property name to group the child queries under. child_queries (Queries): Queries instance to add as sub-queries. Returns: None """ setattr(self, name, child_queries) for child_name in child_queries.available_queries: self._available_queries.add(f"{name}.{child_name}") def load_queries_from_sql(sql, query_loader): queries = [] for query_text in namedef_pattern.split(sql): if not empty_pattern.match(query_text): queries.append(query_loader.load(query_text)) return queries def load_queries_from_file(file_path, query_loader): with file_path.open() as fp: return load_queries_from_sql(fp.read(), query_loader) def load_queries_from_dir_path(dir_path, query_loader): if not dir_path.is_dir(): raise ValueError(f"The path {dir_path} must be a directory") def _recurse_load_queries(path): queries = Queries() for p in path.iterdir(): if p.is_file() and p.suffix != ".sql": continue elif p.is_file() and p.suffix == ".sql": for name, fn in load_queries_from_file(p, query_loader): queries.add_query(name, fn) elif p.is_dir(): child_name = p.relative_to(dir_path).name child_queries = _recurse_load_queries(p) queries.add_child_queries(child_name, child_queries) else: raise RuntimeError(p) return queries return _recurse_load_queries(dir_path) def from_str(sql, db_driver): """Load queries from a SQL string. Args: sql (str) A string containing SQL statements and aiosql name: db_driver (str): The database driver to use to load and execute queries. Returns: Queries Example: Loading queries from a SQL string:: import sqlite3 import aiosql sql_text = \""" -- name: get-all-greetings -- Get all the greetings in the database select * from greetings; -- name: $get-users-by-username -- Get all the users from the database, -- and return it as a dict select * from users where username =:username; \""" queries = aiosql.from_str(sql_text, db_driver="sqlite3") # Example usage after loading: # queries.get_all_greetings(conn) # queries.get_users_by_username(conn, username="willvaughn") """ query_loader = get_query_loader(db_driver) return Queries(load_queries_from_sql(sql, query_loader)) def from_path(sql_path, db_driver): """Load queries from a sql file, or a directory of sql files. Args: sql_path (str|Path): Path to a ``.sql`` file or directory containing ``.sql`` files. db_driver (str): The database driver to use to load and execute queries. Returns: Queries Example: Loading queries paths:: import sqlite3 import aiosql queries = aiosql.from_path("./greetings.sql", db_driver="sqlite3") queries2 = aiosql.from_path("./sql_dir", db_driver="sqlite3") """ path = Path(sql_path) if not path.exists(): raise SQLLoadException(f"File does not exist: {path}") query_loader = get_query_loader(db_driver) if path.is_file(): return Queries(load_queries_from_file(path, query_loader)) elif path.is_dir(): return load_queries_from_dir_path(path, query_loader) else: raise SQLLoadException(f"The sql_path must be a directory or file, got {sql_path}") PK!A{[[aiosql/exceptions.pyclass SQLLoadException(Exception): pass class SQLParseException(Exception): pass PK!aiosql/loaders/__init__.pyPK!˜XXaiosql/loaders/aiosqlite.pyfrom .base import QueryLoader class AioSQLiteQueryLoader(QueryLoader): def process_sql(self, _name, _op_type, sql): """Pass through function because the ``aiosqlite`` driver can already handle the ``:var_name`` format used by aiosql and so doesn't need any additional processing. Args: _name (str): The name of the sql query. _op_type (SQLOperationType): The type of SQL operation performed by the query. sql (str): The sql as written before processing. Returns: str: Original SQL text unchanged. """ return sql def create_fn(self, _name, op_type, sql, return_as_dict): """Creates async coroutine function which can determine how to execute a SQLite query. Leverages the ``self.op_types`` enum to determine which operation type needs to be performed and expects to be passed a ``aiosqlite`` connection object to work with. Args: _name (str): The name of the sql query. _op_type (SQLOperationType): The type of SQL operation performed by the query. sql (str): The processed SQL to be executed. return_as_dict (bool): Whether or not to return rows as dictionaries using column names. Returns: callable: Asynchronous coroutine which executes SQLite query and return results. """ async def fn(conn, *args, **kwargs): results = None cur = await conn.execute(sql, kwargs if len(kwargs) > 0 else args) if op_type == self.op_types.SELECT: if return_as_dict: cols = [col[0] for col in cur.description] results = [] async for row in cur: results.append(dict(zip(cols, row))) else: results = await cur.fetchall() elif op_type == self.op_types.RETURNING: results = cur.lastrowid await cur.close() return results return fn PK!G- - aiosql/loaders/asyncpg.pyfrom collections import defaultdict from .base import QueryLoader class AsyncPGQueryLoader(QueryLoader): def __init__(self): self.var_replacements = defaultdict(dict) async def _execute_query(self, conn, op_type, sql, sql_args, return_as_dict): if op_type == self.op_types.SELECT: records = await conn.fetch(sql, *sql_args) if return_as_dict: return [dict(record) for record in records] else: return [tuple(record) for record in records] elif op_type == self.op_types.RETURNING: record = await conn.fetchrow(sql, *sql_args) return record[0] elif op_type == self.op_types.INSERT_UPDATE_DELETE: await conn.execute(sql, *sql_args) def process_sql(self, name, _op_type, sql): count = 0 adj = 0 for match in self.var_pattern.finditer(sql): gd = match.groupdict() if gd["dblquote"] is not None or gd["quote"] is not None: continue var_name = gd["var_name"] if var_name in self.var_replacements[name]: replacement = f"${self.var_replacements[name][var_name]}" else: count += 1 replacement = f"${count}" self.var_replacements[name][var_name] = count start = match.start() + len(gd["lead"]) + adj end = match.end() - len(gd["trail"]) + adj sql = sql[:start] + replacement + sql[end:] replacement_len = len(replacement) var_len = len(var_name) + 1 # the lead : is the +1 if replacement_len < var_len: adj = adj + replacement_len - var_len else: adj = adj + var_len - replacement_len return sql def create_fn(self, name, op_type, sql, return_as_dict): async def fn(conn, *args, **kwargs): if len(kwargs) > 0: sql_args = sorted( [(self.var_replacements[name][k], v) for k, v in kwargs.items()], key=lambda x: x[0], ) sql_args = [a[1] for a in sql_args] else: sql_args = args if "acquire" in dir(conn): # conn is a pool async with conn.acquire() as con: return await self._execute_query(con, op_type, sql, sql_args, return_as_dict) else: return await self._execute_query(conn, op_type, sql, sql_args, return_as_dict) return fn PK!DŽkkaiosql/loaders/base.pyimport re from abc import ABC, abstractmethod from enum import Enum from ..exceptions import SQLParseException class SQLOperationType(Enum): SELECT = 0 INSERT_UPDATE_DELETE = 1 RETURNING = 2 class QueryLoader(ABC): """Abstract Base Class for defining custom aiosql QueryLoader classes. Example: Defining a custom MyDb loader:: import anosql class MyDbQueryLoader(anosql.QueryLoader): def process_sql(self, name, op_type, sql): # ... Provides a hook to make any custom preparations to the sql text. return sql def create_fn(self, name, op_type, sql, return_as_dict): # This hook lets you define logic for how to build your query methods. # They take your driver connection and do the work of talking to your database. # The class helps parse your SQL text, and has class level variables such as self.op_type to help you decide # which operation a sql statement intends to perform. def fn(conn, *args, **kwargs): # ... pass return fn # To register your query loader as a valid anosql db_type do: anosql.register_query_loader("mydb", MyDbQueryLoader()) # To use make a connection to your db, and pass "mydb" as the db_type: import mydbdriver conn = mydbriver.connect("...") anosql.load_queries("mydb", "path/to/sql/") users = anosql.get_users(conn) conn.close() For concrete examples view the implementation of :py:mod:`aiosql.loaders.psycopg2` and :py:mod:`aiosql.loaders.sqlite3` modules. """ op_types = SQLOperationType name_pattern = re.compile(r"\w+") """ Pattern: Enforces names are valid python variable names. """ doc_pattern = re.compile(r"\s*--\s*(.*)$") """ Pattern: Identifies SQL comments. """ var_pattern = re.compile( r'(?P"[^"]+")|' r"(?P\'[^\']+\')|" r"(?P[^:]):(?P[\w-]+)(?P[^:])" ) """ Pattern: Identifies variable definitions in SQL code. """ def load(self, query_text): """Builds name and function pair from SQL query text to be attached as a dynamic method on a :py:class:`aiosql.aiosql.Queries` instance. Args: query_text (str): SQL Query, comments and name definition. Returns: tuple(str, callable): Name and function pair. """ lines = query_text.strip().splitlines() name = lines[0].replace("-", "_") if name.endswith(" 0 else args) if op_type == self.op_types.INSERT_UPDATE_DELETE: return None elif op_type == self.op_types.SELECT: if return_as_dict: cols = [col[0] for col in cur.description] return [dict(zip(cols, row)) for row in cur.fetchall()] else: return cur.fetchall() elif op_type == self.op_types.RETURNING: pool = cur.fetchone() return pool[0] if pool else None else: raise RuntimeError(f"Unknown SQLOperationType: {op_type}") return fn PK!Qr aiosql/loaders/sqlite3.pyfrom .base import QueryLoader, SQLOperationType class SQLite3QueryLoader(QueryLoader): def process_sql(self, _name, _op_type, sql): """Pass through function because the ``sqlite3`` driver can already handle the ``:var_name`` format used by aiosql and so doesn't need any additional processing. Args: _name (str): The name of the sql query. _op_type (SQLOperationType): The type of SQL operation performed by the query. sql (str): The sql as written before processing. Returns: str: Original SQL text unchanged. """ return sql def create_fn(self, name, op_type, sql, return_as_dict): """Creates function which can determine how to execute a SQLite query. Leverages the ``self.op_types`` enum to determine which operation type needs to be performed and expects to be passed a ``sqlite3`` connection object to work with. Args: name (str): The name of the sql query. op_type (SQLOperationType): The type of SQL operation performed by the query. sql (str): The processed SQL to be executed. return_as_dict (bool): Whether or not to return rows as dictionaries using column names. Returns: callable: Function which executes SQLite query and return results. """ def fn(conn, *args, **kwargs): results = None cur = conn.cursor() cur.execute(sql, kwargs if len(kwargs) > 0 else args) if op_type == self.op_types.SELECT: if return_as_dict: cols = [col[0] for col in cur.description] results = [dict(zip(cols, row)) for row in cur.fetchall()] else: results = cur.fetchall() elif op_type == self.op_types.RETURNING: results = cur.lastrowid cur.close() return results return fn PK! #>aiosql-1.0.0.dist-info/LICENSECopyright (c) 2014-2017, Honza Pokorny Copyright (c) 2018, William Vaughn All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. The views and conclusions contained in the software and documentation are those of the authors and should not be interpreted as representing official policies, either expressed or implied, of the aiosql Project. PK!H=jTTaiosql-1.0.0.dist-info/WHEEL 1 0 нRN&":WhAqo;VSoBtM4[`Z} Mz7M*{/ܒ?֎XPK!Hmaiosql-1.0.0.dist-info/METADATAT]o6|";)Em4yq|0 ܞ'GqEݕt @`Z.g26qb+xR<6Sh9}vqxd8Vpeޝp1}a[RUE}y}|ٚ|~:7i/}Fng\Rd3q~E6d%L>ߏA52}õͿL jnh#:\[Jf8Dؼ/s~n^A!ih]2 6AlDwЏ }ȳtIQE=I .r}!8[f-c` ljБ D-wwu3-zfųM=fg 1*r*pQA{RTtΉ;0O_W{uB֎"9R| G^t^^ZaN}rn8YmݙD'<786O\I6 <)4CBbO,+nh5*<m> MRu/3oDv!-LKDnC 1>e1ؘfc 2p\-nB?Mu`q{] k*,':6?0׃.Ez9è)HK͔nJʉX)&zSٔ EcEɔA'h;6?ܭ* A|PK!q22aiosql/__init__.pyPK!ɅBCCbaiosql/aiosql.pyPK!A{[[aiosql/exceptions.pyPK!`aiosql/loaders/__init__.pyPK!˜XXaiosql/loaders/aiosqlite.pyPK!G- - )&aiosql/loaders/asyncpg.pyPK!DŽkk0aiosql/loaders/base.pyPK!,Eaiosql/loaders/psycopg2.pyPK!Qr TNaiosql/loaders/sqlite3.pyPK! #>YVaiosql-1.0.0.dist-info/LICENSEPK!H=jTT\aiosql-1.0.0.dist-info/WHEELPK!Hm3]aiosql-1.0.0.dist-info/METADATAPK!HR, fp`aiosql-1.0.0.dist-info/RECORDPK c