Package: aesqlapius
aeSQLAPIusSummarySo you don't want to use ORM, but want to organize your SQL queries
in a convenient way. Don't mix them with your python code, don't
writeexecuteandfetchrows by hand for each query. Withaesqlapius:Store your SQL queries separately from the code, in a dedicated
file or directory hierarchyAnnotate each query with python-like function definition specifying
input arguments and output types and patternsaesqlapiusbuilds a class out of this, where you can call your
queries as plain methods. It handles arguments (pass positional
or keyword arguments as you like, default values are also handled) and
output types and patterns (you may specify whether a method returns
iterator, list, dict of rows, or a single row, where row may
be represented as a tuple, list, dict, single value or a custom
type such as a dataclass).Examplequeries.sql:-- specify arguments to queries in python format, including-- type annotations and support for default values-- def add_city(name: str, population: int = None) -> None: ...INSERTINTOcitiesVALUES(%(name)s,%(population)s);-- specify return value format out of wide range of formats-- (iterator, list, dict, or single instance of tuples, dicts-- or simple values)-- def list_cities() -> List[Value]: ...SELECTnameFROMcitiesORDERBYname;-- def get_population(city: str) -> Single[Value]: ...SELECTpopulationFROMcitiesWHEREname=%(city)s;-- def get_populations() -> Dict[-'name', Value]: ...SELECTname,populationFROMcitiesWHEREpopulationISNOTNONEORDERBYname;-- def iter_cities()() -> Iterator[Tuple] ...SELECT*FROMcitiesORDERBYname;script.py:fromaesqlapiusimportgenerate_apidb=psycopg2.connect('...')api=generate_api('queries.sql','psycopg2',db)# pass arguments to queries in either positional and kw formapi.add_city('Moscow',12500000)api.add_city('Paris')api.add_city(population=3800000,name='Berlin')# get query results in the desired formatassertapi.list_cities()==['Berlin','Moscow','Paris']assertapi.get_population('Moscow')==12500000assertapi.get_populations()=={'Berlin':3800000,'Moscow':12500000}assertnext(api.iter_cities())==('Berlin',3800000)ReferencePython APIThe module has a single entry point in form of a function:defgenerate_api(path,driver,db=None,*,target=None,extension='.sql',namespace_mode='dirs',namespace_root='__init__')This loads SQL queries frompath(a file or directory) and returns an API class to use with specified databasedriver(psycopg2,sqlite3,mysql,aiopg,asyncpg).Ifdbis specified, all generated methods are bound to the given database connection object:db=psycopg2.connect('...')api=generate_api('queries.sql','psycopg2',db)api.my_method('arg1','arg2')otherwise caller is expected to pass database connection object to each call:db=psycopg2.connect('...')api=generate_api('queries.sql','psycopg2')api.my_method(db,'arg1','arg2')Iftargetis specified, methods are injected into the given object (which is also returned fromgenerate_api):db=psycopg2.connect('...')generate_api('queries.sql','psycopg2',db,target=db)db.my_method('arg1','arg2')extension(by default.sql) specifies which files are loaded from the queries directory.namespace_modecontrols how hierarchy of files is converted into hierarchy of objects when constructing the API class. There are 3 modes supported:dirs(the default), which maps each subdirectory to a nested method namespace ignoring file names:path under query dirfunction definitionresulting APIroot.sql-- def a(): ...api.a()subdir/foo.sql-- def b(): ...api.subdir.b()subdir/bar.sql-- def c(): ...api.subdir.c()fileswhich uses file names (after stripping the extension) as an extra nesting level:path under query dirfunctionresulting APIroot.sql-- def a(): ...api.root.a()subdir/foo.sql-- def b(): ...api.subdir.foo.b()subdir/bar.sql-- def c(): ...api.subdir.bar.c()In this mode,namespace_rootallows to specify a special file name which circumvents this behavior, allowing to mimic how Python handles module namespaces. For example, whennamespace_root="__init__"(the default):path under query dirfunctionresulting API__init__.sql-- def a(): ...api.a()foo.sql-- def b(): ...api.foo.b()subdir/__init__.sql-- def c(): ...api.subdir.c()subdir/bar.sql-- def d(): ...api.subdir.bar.d()flatmode which ignores hierarchy:path under query dirfunctionresulting APIroot.sql-- def a(): ...api.a()subdir/foo.sql-- def b(): ...api.b()subdir/bar.sql-- def c(): ...api.c()Query annotationsEach query managed byaesqlapiusmust be preceded with a--(SQL comment) followed by a Python-style function definition:-- def function_name(parameters, ...) -> return_type: ......someSQLcode...ParametersParameters allow optional literal default values and optional type annotations (which are currently ignored) and may be specified in both positional, keyword or mixed style in the resulting API:-- def myfunc(foo, bar: str, baz=123) -> None: ...`...someSQLcode...api.myfunc(1, bar="sometext")  # foo=1, bar="sometext", baz=123Return valueReturn value annotation is required and may either beNone(when query does not return anything) or a nested type annotation with specific structureRowsFormat[RowFormat].OuterRowsFormatspecifies how multiple rows returned by the query are handled. Allowed values are:Iterator[RowFormat]- return a row iterator.List[RowFormat]- return a list of rows.Single[RowFormat]- return a single row.Dict[KeyColumn, RowFormat]- return a dictionary of rows. The column to be used as a dictionary key is specified in the first argument, e.g.Dict[0, ...]uses first returned column as key and `Dict['colname', ...] uses column namedcolname. Precede column index or name with unary minus to make it removed from the row contents.InnerRowFormatspecifies how data for each row is presented:Tuple- return row as a tuple of values.Dict- return row as a dict, where keys are set to the column names returned by the query.Value- return single value from the row. If the query returns multiple fields, the first one is returned.Examples:-- def example1() -> List[Tuple]: ...SELECT1,'foo'UNIONSELECT2,'bar';-- def example2() -> Single[Value]: ...SELECT2*2;-- def example3() -> Iterator[Dict]: ...SELECT1ASn,'foo'ASsUNIONSELECT2ASn,'bar'ASs;-- def example4() -> Dict['key', Dict]: ...SELECT'foo'ASkey,1ASa,2ASb;-- def example5() -> Dict[-'key', Dict]: ...SELECT'foo'ASkey,1ASa,2ASb;>>> api.example1()
[(1, 'foo'), (2, 'bar')]
>>> api.example2()
4
>>> it = api.example3()
>>> next(it)
{'n': 1, 's': 'foo'}
>>> next(it)
{'n': 2, 's': 'bar'}
>>> api.example4()
{'foo': {'key': 'foo', 'a': 1, 'b': 2}}
>>> api.example5()
{'foo': {'a': 1, 'b': 2}}BodyFunction body of the annotationis required to contain a single ellipsis.Driverspsycopg2Use withpsycopg2connections:importaesqlapius,psycopg2dbconn=psycopg2.connect('dname=... user=... password=...')api=aesqlapius.generate_api('queries.sql','psycopg2',dbconn)api.some_method(arg1=1,arg2=2)sqlite3Use withsqlite3connections:importaesqlapius,sqlite3dbconn=sqlite3.connect('path_to_database.sqlite')api=aesqlapius.generate_api('queries.sql','sqlite3',dbconn)api.some_method(arg1=1,arg2=2)mysqlUse withmysql.connectorconnections:importaesqlapius,mysql.connectordbconn=mysql.connector.connect(database=...,user=...,password=...)api=aesqlapius.generate_api('queries.sql','mysql',dbconn)api.some_method(arg1=1,arg2=2)Notes:The driver usesbuffered=Trueparameter when creating cursor.aiopgUse withaiopgmodule. This driver generates asynchronous APIs, and accepts both connection and pool objects (in the latter case, connection is automatically acquired from the pool).importaesqlapius,aiopgasyncdefpool_example():asyncwithaiopg.create_pool('dname=... user=... password=...')aspool:api=aesqlapius.generate_api('queries.sql','aiopg',pool)awaitapi.some_method(arg1=1,arg2=2)asyncdefconnection_example():api=aesqlapius.generate_api('queries.sql','aiopg')asyncwithaiopg.create_pool('dname=... user=... password=...')aspool:asyncwithpool.acquire()asconn:awaitapi.some_method(conn,arg1=1,arg2=2)asyncpgUse withasyncpgmodule. This driver generates asynchronous APIs, and accepts both connection and pool objects (in the latter case, connection is automatically acquired from the pool).importaesqlapius,asyncpgasyncdefpool_example():asyncwithasyncpg.create_pool(database=...,user=...,password=...)aspool:api=aesqlapius.generate_api('queries.sql','asyncpg',pool)awaitapi.some_method(arg1=1,arg2=2)asyncdefconnection_example():conn=awaitasyncpg.connect(database=...,user=...,password=...)api=aesqlapius.generate_api('queries.sql','asyncpg',conn)awaitapi.some_method(arg1=1,arg2=2)asyncdefanother_connection_example():api=aesqlapius.generate_api('queries.sql','asyncpg')asyncwithasyncpg.create_pool('dname=... user=... password=...')aspool:asyncwithpool.acquire()asconn:awaitapi.some_method(conn,arg1=1,arg2=2)Notes:Methods withIteratorrows format use asyncpg cursors under the hood which are only available in transaction. The driver automatically wraps such methods in a transaction if they are called outside of one.LicenseMIT license, copyright (c) 2020 Dmitry Marakasovamdmi3@amdmi3.ru.