{"id":20757815,"url":"https://github.com/amdmi3/aesqlapius","last_synced_at":"2025-04-29T23:30:08.226Z","repository":{"id":57408442,"uuid":"315152272","full_name":"AMDmi3/aesqlapius","owner":"AMDmi3","description":"Manage SQL queries as a Python API","archived":false,"fork":false,"pushed_at":"2022-11-07T11:21:39.000Z","size":156,"stargazers_count":3,"open_issues_count":10,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-29T22:37:46.539Z","etag":null,"topics":["python","sql"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/AMDmi3.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-11-22T23:21:01.000Z","updated_at":"2023-07-26T20:30:09.000Z","dependencies_parsed_at":"2023-01-21T06:00:45.130Z","dependency_job_id":null,"html_url":"https://github.com/AMDmi3/aesqlapius","commit_stats":null,"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AMDmi3%2Faesqlapius","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AMDmi3%2Faesqlapius/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AMDmi3%2Faesqlapius/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AMDmi3%2Faesqlapius/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/AMDmi3","download_url":"https://codeload.github.com/AMDmi3/aesqlapius/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251599628,"owners_count":21615558,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["python","sql"],"created_at":"2024-11-17T09:45:24.882Z","updated_at":"2025-04-29T23:30:08.205Z","avatar_url":"https://github.com/AMDmi3.png","language":"Python","readme":"# aeSQLAPIus\n\n\u003ca href=\"https://repology.org/project/python:aesqlapius/versions\"\u003e\n\t\u003cimg src=\"https://repology.org/badge/vertical-allrepos/python:aesqlapius.svg\" alt=\"Packaging status\" align=\"right\"\u003e\n\u003c/a\u003e\n\n![CI](https://github.com/AMDmi3/aesqlapius/workflows/CI/badge.svg)\n[![codecov](https://codecov.io/gh/AMDmi3/aesqlapius/branch/master/graph/badge.svg?token=87aZsxlja2)](https://codecov.io/gh/AMDmi3/aesqlapius)\n[![PyPI downloads](https://img.shields.io/pypi/dm/aesqlapius.svg)](https://pypi.org/project/aesqlapius/)\n[![Github commits (since latest release)](https://img.shields.io/github/commits-since/AMDmi3/aesqlapius/latest.svg)](https://github.com/AMDmi3/aesqlapius)\n\n## Summary\n\nSo you don't want to use ORM, but want to organize your SQL queries\nin a convenient way. Don't mix them with your python code, don't\nwrite `execute` and `fetchrow`s by hand for each query. With\n**aesqlapius**:\n\n- Store your SQL queries separately from the code, in a dedicated\n  file or directory hierarchy\n- Annotate each query with python-like function definition specifying\n  input arguments and output types and patterns\n\n**aesqlapius** builds a class out of this, where you can call your\nqueries as plain methods. It handles arguments (pass positional\nor keyword arguments as you like, default values are also handled) and\noutput types and patterns (you may specify whether a method returns\niterator, list, dict of rows, or a single row, where row may\nbe represented as a tuple, list, dict, single value or a custom\ntype such as a dataclass).\n\n## Example\n\nqueries.sql:\n```sql\n-- specify arguments to queries in python format, including\n-- type annotations and support for default values\n\n-- def add_city(name: str, population: int = None) -\u003e None: ...\nINSERT INTO cities VALUES (%(name)s, %(population)s);\n\n-- specify return value format out of wide range of formats\n-- (iterator, list, dict, or single instance of tuples, dicts\n-- or simple values)\n\n-- def list_cities() -\u003e List[Value]: ...\nSELECT name FROM cities ORDER BY name;\n\n-- def get_population(city: str) -\u003e Single[Value]: ...\nSELECT population FROM cities WHERE name = %(city)s;\n\n-- def get_populations() -\u003e Dict[-'name', Value]: ...\nSELECT name, population\nFROM cities\nWHERE population IS NOT NONE\nORDER BY name;\n\n-- def iter_cities()() -\u003e Iterator[Tuple] ...\nSELECT * FROM cities ORDER BY name;\n```\n\nscript.py:\n```python\nfrom aesqlapius import generate_api\n\ndb = psycopg2.connect('...')\napi = generate_api('queries.sql', 'psycopg2', db)\n\n# pass arguments to queries in either positional and kw form\napi.add_city('Moscow', 12500000)\napi.add_city('Paris')\napi.add_city(population=3800000, name='Berlin')\n\n# get query results in the desired format\nassert api.list_cities() == ['Berlin', 'Moscow', 'Paris']\nassert api.get_population('Moscow') == 12500000\nassert api.get_populations() == {'Berlin': 3800000, 'Moscow': 12500000}\nassert next(api.iter_cities()) == ('Berlin', 3800000)\n```\n\n## Reference\n\n### Python API\n\nThe module has a single entry point in form of a function:\n\n```python\ndef generate_api(path, driver, db=None, *, target=None, extension='.sql', namespace_mode='dirs', namespace_root='__init__')\n```\n\nThis loads SQL queries from *path* (a file or directory) and returns an API class to use with specified database *driver* (`psycopg2`, `sqlite3`, `mysql`, `aiopg`, `asyncpg`).\n\nIf *db* is specified, all generated methods are bound to the given database connection object:\n\n```python\ndb = psycopg2.connect('...')\napi = generate_api('queries.sql', 'psycopg2', db)\napi.my_method('arg1', 'arg2')\n```\notherwise caller is expected to pass database connection object to each call:\n```python\ndb = psycopg2.connect('...')\napi = generate_api('queries.sql', 'psycopg2')\napi.my_method(db, 'arg1', 'arg2')\n```\n\nIf *target* is specified, methods are injected into the given object (which is also returned from `generate_api`):\n```python\ndb = psycopg2.connect('...')\ngenerate_api('queries.sql', 'psycopg2', db, target=db)\ndb.my_method('arg1', 'arg2')\n```\n\n*extension* (by default `.sql`) specifies which files are loaded from the queries directory.\n\n*namespace_mode* controls how hierarchy of files is converted into hierarchy of objects when constructing the API class. There are 3 modes supported:\n\n* `dirs` (the default), which maps each subdirectory to a nested method namespace ignoring file names:\n\n| path under query dir | function definition | resulting API    |\n|----------------------|---------------------|------------------|\n| `root.sql`           | `-- def a(): ...`   | `api.a()`        |\n| `subdir/foo.sql`     | `-- def b(): ...`   | `api.subdir.b()` |\n| `subdir/bar.sql`     | `-- def c(): ...`   | `api.subdir.c()` |\n\n* `files` which uses file names (after stripping the extension) as an extra nesting level:\n\n| path under query dir | function          | resulting API        |\n|----------------------|-------------------|----------------------|\n| `root.sql`           | `-- def a(): ...` | `api.root.a()`       |\n| `subdir/foo.sql`     | `-- def b(): ...` | `api.subdir.foo.b()` |\n| `subdir/bar.sql`     | `-- def c(): ...` | `api.subdir.bar.c()` |\n\nIn this mode, *namespace_root* allows to specify a special file name which circumvents this behavior, allowing to mimic how Python handles module namespaces. For example, when *namespace_root* = `\"__init__\"` (the default):\n\n| path under query dir  | function          | resulting API        |\n|-----------------------|-------------------|----------------------|\n| `__init__.sql`        | `-- def a(): ...` | `api.a()`            |\n| `foo.sql`             | `-- def b(): ...` | `api.foo.b()`        |\n| `subdir/__init__.sql` | `-- def c(): ...` | `api.subdir.c()`     |\n| `subdir/bar.sql`      | `-- def d(): ...` | `api.subdir.bar.d()` |\n\n* `flat` mode which ignores hierarchy:\n\n| path under query dir | function          | resulting API |\n|----------------------|-------------------|---------------|\n| `root.sql`           | `-- def a(): ...` | `api.a()`     |\n| `subdir/foo.sql`     | `-- def b(): ...` | `api.b()`     |\n| `subdir/bar.sql`     | `-- def c(): ...` | `api.c()`     |\n\n### Query annotations\n\nEach query managed by **aesqlapius** must be preceded with a `-- ` (SQL comment) followed by a Python-style function definition:\n\n```sql\n-- def function_name(parameters, ...) -\u003e return_type: ...\n...some SQL code...\n```\n\n#### Parameters\n\nParameters 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:\n\n```sql\n-- def myfunc(foo, bar: str, baz=123) -\u003e None: ...`\n...some SQL code...\n```\n```pyton\napi.myfunc(1, bar=\"sometext\")  # foo=1, bar=\"sometext\", baz=123\n```\n\n#### Return value\n\nReturn value annotation is required and may either be `None` (when query does not return anything) or a nested type annotation with specific structure `RowsFormat[RowFormat]`.\n\nOuter `RowsFormat` specifies how multiple rows returned by the query are handled. Allowed values are:\n* `Iterator[RowFormat]` - return a row iterator.\n* `List[RowFormat]` - return a list of rows.\n* `Single[RowFormat]` - return a single row.\n* `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 named *colname*. Precede column index or name with unary minus to make it removed from the row contents.\n\nInner `RowFormat` specifies how data for each row is presented:\n* `Tuple` - return row as a tuple of values.\n* `Dict` - return row as a dict, where keys are set to the column names returned by the query.\n* `Value` - return single value from the row. If the query returns multiple fields, the first one is returned.\n\nExamples:\n```sql\n-- def example1() -\u003e List[Tuple]: ...\nSELECT 1, 'foo' UNION SELECT 2, 'bar';\n-- def example2() -\u003e Single[Value]: ...\nSELECT 2*2;\n-- def example3() -\u003e Iterator[Dict]: ...\nSELECT 1 AS n, 'foo' AS s UNION SELECT 2 AS n, 'bar' AS s;\n-- def example4() -\u003e Dict['key', Dict]: ...\nSELECT 'foo' AS key, 1 AS a, 2 AS b;\n-- def example5() -\u003e Dict[-'key', Dict]: ...\nSELECT 'foo' AS key, 1 AS a, 2 AS b;\n```\n```\n\u003e\u003e\u003e api.example1()\n[(1, 'foo'), (2, 'bar')]\n\u003e\u003e\u003e api.example2()\n4\n\u003e\u003e\u003e it = api.example3()\n\u003e\u003e\u003e next(it)\n{'n': 1, 's': 'foo'}\n\u003e\u003e\u003e next(it)\n{'n': 2, 's': 'bar'}\n\u003e\u003e\u003e api.example4()\n{'foo': {'key': 'foo', 'a': 1, 'b': 2}}\n\u003e\u003e\u003e api.example5()\n{'foo': {'a': 1, 'b': 2}}\n```\n\n#### Body\n\nFunction body of the annotationis required to contain a single ellipsis.\n\n## Drivers\n\n### psycopg2\n\nUse with [psycopg2](https://pypi.org/project/psycopg2/) connections:\n\n```python\nimport aesqlapius, psycopg2\ndbconn = psycopg2.connect('dname=... user=... password=...')\napi = aesqlapius.generate_api('queries.sql', 'psycopg2', dbconn)\napi.some_method(arg1=1, arg2=2)\n```\n\n### sqlite3\n\nUse with `sqlite3` connections:\n\n```python\nimport aesqlapius, sqlite3\ndbconn = sqlite3.connect('path_to_database.sqlite')\napi = aesqlapius.generate_api('queries.sql', 'sqlite3', dbconn)\napi.some_method(arg1=1, arg2=2)\n```\n\n### mysql\n\nUse with [mysql.connector](https://dev.mysql.com/doc/connector-python/en/) connections:\n\n```python\nimport aesqlapius, mysql.connector\ndbconn = mysql.connector.connect(database=..., user=..., password=...)\napi = aesqlapius.generate_api('queries.sql', 'mysql', dbconn)\napi.some_method(arg1=1, arg2=2)\n```\n\nNotes:\n- The driver uses `buffered=True` parameter when creating cursor.\n\n### aiopg\n\nUse with [aiopg](https://pypi.org/project/aiopg/) module. This driver generates asynchronous APIs, and accepts both connection and pool objects (in the latter case, connection is automatically acquired from the pool).\n\n```python\nimport aesqlapius, aiopg\n\nasync def pool_example():\n    async with aiopg.create_pool('dname=... user=... password=...') as pool:\n        api = aesqlapius.generate_api('queries.sql', 'aiopg', pool)\n        await api.some_method(arg1=1, arg2=2)\n\nasync def connection_example():\n    api = aesqlapius.generate_api('queries.sql', 'aiopg')\n    async with aiopg.create_pool('dname=... user=... password=...') as pool:\n        async with pool.acquire() as conn:\n\t    await api.some_method(conn, arg1=1, arg2=2)\n```\n\n### asyncpg\n\nUse with [asyncpg](https://pypi.org/project/asyncpg/) module. This driver generates asynchronous APIs, and accepts both connection and pool objects (in the latter case, connection is automatically acquired from the pool).\n\n```python\nimport aesqlapius, asyncpg\n\nasync def pool_example():\n    async with asyncpg.create_pool(database=..., user=..., password=...) as pool:\n        api = aesqlapius.generate_api('queries.sql', 'asyncpg', pool)\n        await api.some_method(arg1=1, arg2=2)\n\nasync def connection_example():\n    conn = await asyncpg.connect(database=..., user=..., password=...)\n    api = aesqlapius.generate_api('queries.sql', 'asyncpg', conn)\n    await api.some_method(arg1=1, arg2=2)\n\nasync def another_connection_example():\n    api = aesqlapius.generate_api('queries.sql', 'asyncpg')\n    async with asyncpg.create_pool('dname=... user=... password=...') as pool:\n        async with pool.acquire() as conn:\n\t    await api.some_method(conn, arg1=1, arg2=2)\n```\n\nNotes:\n- Methods with `Iterator` rows 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.\n\n## License\n\nMIT license, copyright (c) 2020 Dmitry Marakasov amdmi3@amdmi3.ru.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Famdmi3%2Faesqlapius","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Famdmi3%2Faesqlapius","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Famdmi3%2Faesqlapius/lists"}