# -*- coding: utf-8 -*-
from setuptools import setup

packages = \
['twinsqla']

package_data = \
{'': ['*']}

install_requires = \
['lark-parser>=0.11.1,<0.12.0', 'sqlalchemy>=1.3,<2.0']

setup_kwargs = {
    'name': 'twinsqla',
    'version': '0.0.2',
    'description': 'TWinSQLA is a light framework for mapping SQL statements to python functions and methods.',
    'long_description': '# TWinSQLA\n\nTWinSQLA is a light framework for mapping SQL statements to python functions and methods.\n\n## Features\n- Available in Python 3.6+\n    - We recommends Python 3.7+ since available to use `@dataclasses.dataclass` decorator in entity classes.\n- This framework concept is avoid ORM features!\n    Coding with almost-raw SQL query (with prepared parameters) simply.\n    - If you can use SQL query with coding simply, it make you to skipping the times of converting python coding\n        with ORM features and checking result.\n    - TWinSQLA support you to checking only SQL query without coding with ORM features.\n- Support "two-way SQL" template.\n    - "Two-way SQL" templates can be executed SQL statements with dynamic parameter written by python expression.\n    - In "two-way SQL", dynamic parameters and conditional expressions are surrounded by \'/\\*\' and \'\\*/\'.\n        So, "two-way SQL" templates are available to execute in SQL tools as they are.\n    - TWinSQLa is inspired by [Doma](https://github.com/domaframework/doma),\n        which is Java framework for accessing databases.\n- Since [SQLAlchemy](https://github.com/sqlalchemy/sqlalchemy) core is used for accessing databases,\n    SQLAlchemy core features can be utilized. (such as connection pool)\n\n\n## How to install\nYou can install from PyPI by the follow command.\n```bash\npip install twinsqla\n```\n\n## Usage\n\n### First step (In case that TWinSQLA object is available in global scope)\n\n```python\nfrom typing import Optional\nfrom collections import OrderedDict\nimport sqlalchemy\nfrom twinsqla import TWinSQLA\n\nengine: sqlalchemy.engine.base.Engine = sqlalchemy.create_engine(...)\nsqla: TWinSQLA = TWinSQLA(engine)\n\nclass StaffDao():\n    @sqla.select("SELECT * FROM staff WHERE staff_id = /* :staff_id */1")\n    def find_by_id(self, staff_id: int) -> Tuple[OrderDict, ...]:\n        pass\n```\n\nAt first, create instance of sqlalchemy engine, and create TWinSQLA instance with sqlalchemy engine.\n```python\nengine: sqlalchemy.engine.base.Engine = sqlalchemy.create_engine(...)\nsqla: TWinSQLA = TWinSQLA(engine)\n```\n\nTo execute select query, use `sqla.select` decorator. In this case, `sqla` is TWinSQLA instance.\n```python\n@sqla.select("SELECT * FROM staff WHERE staff_id = /* :staff_id */1")\ndef find_by_id(self, staff_id: int) -> Tuple[OrderDict, ...]:\n    pass\n```\n\nThe above example, select query in decorator\'s argument is written as "two-way SQL."\nWhen called `dao.find_by_id(staff_id=10)`, then the like following code will be executed.\n```python\n> query = sqlalchemy.sql.text("SELECT * FROM staff WHERE staff_id = :staff_id")\n> engine.execute(query, {staff_id:10})\n```\nThe execution results will be converted to sequence of OrderedDict, and returned from the above method.\n\nThe `sqla.select` decorator can return object for your custom class, or return the results iterable.  For more details, see the other section.\n\n### In production usage\n\nFor about production usage, you may separate source codes as dao classes, entity classes, and handling transaction classes.\n\n```python\nfrom dataclasses import dataclass\nimport twinsqla\nfrom twinsqla import TWinSQLA, table\n\n# Entity class\n@dataclass(frozen=True)\nclass Staff:\n    staff_id: int\n    staff_name: str\n    age: int\n\n\n# Entity class (Only used in insert query)\n@dataclass(frozen=True)\n@table("staff")\nclass NewStaff:\n    staff_name: str\n    age: int\n\n\n# Dao class\nclass StaffDao:\n    def __init__(self, sqla: TWinSQLA):\n        self.sqla: TWinSQLA = sqla\n\n    @twinsqla.select(\n        "SELECT * FROM staff WHERE staff_id >= /* :more_than_id */2",\n        result_type=List[Staff]\n    )\n    def fetch(self, more_than_id: int) -> List[Staff]:\n        pass\n\n    @twinsqla.insert()\n    def insert(self, staff: NewStaff):\n        pass\n\n\n# Service class (Handling database transaction)\nclass StaffService:\n    def __init__(self, sqla: TWinSQLA):\n        self.sqla: TWinSQLA = sqla\n        self.staff_dao: StaffDao = StaffDao(sqla)\n\n    def find_staff(self, more_than_id: int) -> List[Staff]:\n        return self.staff_dao.fetch(more_than_id)\n\n    def register(self, staff_name: str, age: int):\n        new_staff: NewStaff = NewStaff(staff_name=staff_name, age=age)\n\n        # DB transaction scope\n        with self.sqla.transaction():\n            self.staff_dao.insert(new_staff)\n```\n\n#### Dao class\n\n##### Initializing\n\nIn this cases, the TWinSQLA object is not existed in global scope but only in dao instance scope. So, you cannot use TWinSQLA instance decorators (for example : `@sqla.select()`) at the dao methods.\nInstead of using instance decorators, you can use TWinSQLA function decorators. (for example : `@twinsqla.select()`)\n\nWhen executing, the TWinSQLA function decorators search TWinSQLA object. By this search, TWinSQLA instance can be found specified by one of the follow ways.\n\n- By configured with instance parameter with named \'sqla\'. (above the sample code)\n    ```python\n    def __init__(self, sqla: TWinSQLA):\n        self.sqla: TWinSQLA = sqla\n    ```\n\n- Or, the other way, by specified with method arguments with named \'sqla\'.\n    ```python\n    @twinsqla.select(...)\n    def fetch(self, sqla: TWinSQLA, more_than_id: int) -> List[Staff]:\n        pass\n    ```\n\n##### Select\n\nTo executing select query, you need to use `twinsqla.select()` function decorator instead of `sqla.select()` instance decorator.\n```python\n@twinsqla.select(\n    "SELECT * FROM staff WHERE staff_id >= /* :more_than_id */2", ...\n)\ndef fetch(self, more_than_id: int) -> ... :\n    pass\n```\n\nYou can customise the results of select query by the decorator\'s argument `result_type`.\nThe argument `result_type` needs to be specified a class or sequence of a class.\nIn the case that results of select query is more than one object, you need specify the `result_type` as sequence of a class. (for example, `List[...]`)\n```python\n@twinsqla.select(\n    ... , result_type=List[Staff]\n)\ndef fetch(...) -> List[Staff]:\n    pass\n```\nIn the above code, each one result of select query is convert to Staff instance, and `fetch()` method returns list of Staff.\n\n##### Insert\n\nOther examples, to insert a record, you can use `twinsqla.insert()` function decorator.\n```python\n@twinsqla.insert()\ndef insert(self, staff: NewStaff):\n    pass\n```\nThe `insert()` decorator automatically build insert query with `NewStaff` instance which class decorated by `@table()` with table_name.\n```python\n> query = sqlalchemy.sql.text("INSERT INTO staff(staff_name, age) VALUES (:staff_name, :age)")\n> engine.execute(query, {staff_name: staff.staff_name, age: staff.age})\n```\n\nBy other way, you can build insert query by your hand as following.\n```python\n@twinsqla.insert("INSERT INTO staff(staff_name, age) VALUES (:staff_name, :age)")\ndef insert(self, staff_name: str, age: int):\n    pass\n```\n\n#### Entity class\n\n##### Result of select\n\nEntity class of select query needs to have the constructor with arguments of listed column names\n```python\nclass Staff:\n    def __init__(self, staff_id: int, staff_name: str, age: int):\n        self.staff_id: int = staff_id\n        self.staff_name: str = staff_name\n        self.age: int = age\n```\n\nThe above code can be replaced to the following with decorated by `@dataclass()`.\n```python\nfrom dataclasses import dataclass\n\n@dataclass(frozen=True)\nclass Staff:\n    staff_id: int\n    staff_name: str\n    age: int\n```\n\n##### Insert\n\nEntity class of insert query with automatically building needs to be decorated by `@table()` with argument of the table name and have attributes for inserting.\n\n```python\n@dataclass(frozen=True)\n@table("staff")\nclass NewStaff:\n    staff_name: str\n    age: int\n```\n\nIn the above code, use the `NewStaff` instance can insert into \'staff\' table with columns \'staff_name\' and \'age\'.\n\n### Transaction\nIn using TWinSQLA, `TWinSQLA.transaction()` can handle database transaction by context manager via sqlalchemy api.\n```python\nwith sqla.transaction():\n    # execute query\n```\nWhen any exceptions are not occured in context block, then database transaction are commited. Otherwise, if any exceptions are occured, database transaction will be rollbacked and sqlalchemy exception are raised over context bock.\n\n### Exceptions\nIn using TWinSQLA, two type base exceptions may be occured.\n- `twinsqla.exceptions.TWinSQLAException`\n- `sqlalchemy.exc.SQLAlchemyError`\n\n`TWinSQLAException` is occured when your queries or implementation are invalid.\nThe other hand, `SQLAlchemyError` is raised by sqlalchemy.\n\nIn implementation, you need to consider about handling [sqlalchemy.exc.DBAPIError](https://docs.sqlalchemy.org/en/13/core/exceptions.html#sqlalchemy.exc.DBAPIError), which raised in database operation failed.\n\n## API Reference\n\n### `twinsqla.TWinSQLA`\n```python\n    def __init__(self, engine: sqlalchemy.engine.base.Engine, *,\n                 available_dynamic_query: bool = True,\n                 sql_file_root: Optional[Union[Path, str]] = None,\n                 cache_size: Optional[int] = 128):\n        ...\n    """\n    Args:\n        engine (sqlalchemy.engine.base.Engine): SQLAlchemy engine instance.\n        available_dynamic_query (bool, optional):\n            If True, then two-ways SQL is available.\n            If False, sql statements are not converted in executing\n            but executed as it is specified. Defaults to True.\n        sql_file_root (Optional[Union[Path, str]], optional):\n            Specify the root directory of sql files. Defaults to None.\n        cache_size (Optional[int], optional):\n            Cache size of loaded query function. Defaults to 128.\n    """\n```\n\n### `TWinSQLA.transaction()`\n\n### `twinsqla.select()`, `TWinSQLA.select()`\n```python\ndef select(query: Optional[str] = None, *, sql_path: Optional[str] = None,\n           result_type: Type[Any] = Tuple[OrderedDict, ...],\n           iteratable: bool = False):\n    """\n    Function decorator of select operation.\n    Only one argument `query` or `sql_path` must be specified.\n\n    In called decorated method, the processing implemented by the method\n    is not executed, but arguments of method are used for bind parameters.\n\n    Args:\n        query (Optional[str], optional):\n            select query (available TwoWay SQL). Defaults to None.\n        sql_path (Optional[str], optional):\n            file path with sql (available TwoWay SQL). Defaults to None.\n        result_type (Type[Any], optional):\n            return type. Defaults to Tuple[OrderedDict, ...].\n        iteratable (bool, optional):\n            When you want to fetching iterataly result, then True specified\n            and returned ResultIterator object. Defaults to False.\n\n    Returns:\n        Callable: Function decorator\n    """\n```\n\n### `twinsqla.insert()`, `TWinSQLA.insert()`\n```python\ndef insert(query: Optional[str] = None, *, sql_path: Optional[str] = None,\n           table_name: Optional[str] = None, result_type: Type[Any] = None,\n           iteratable: bool = False):\n    """\n    Function decorator of insert operation.\n    In constructing insert query by yourself, you need to specify either\n    one of the arguments `query` or `sql_path`.\n\n    In neither `query` nor `sql_path` are specified, this decorator creates\n    insert query with arguments of decorated method.\n    In this case, you need to specify inserted table name by decorator\n    argument \'table_name\' or decorating \'@twinsqla.table\' to entity class.\n\n    Args:\n        query (Optional[str], optional):\n            insert query (available TwoWay SQL). Defaults to None.\n        sql_path (Optional[str], optional):\n            file path with sql (available TwoWay SQL). Defaults to None.\n        table_name (Optional[str], optional):\n            table name for inserting. Defaults to None.\n        result_type (Type[Any], optional):\n            When constructing "INSERT RETURNING" query, it is useful to\n            specify return type. Defaults to None.\n        iteratable (bool, optional):\n            In almost cases, this argument need not to specified.\n            The only useful case is in using "INSERT RETURNING" query.\n            Defaults to False.\n\n    Returns:\n        Callable: Function decorator for insert query\n    """\n```\n\n### `twinsqla.update()`, `TWinSQLA.update()`\n```python\ndef update(query: Optional[str] = None, *, sql_path: Optional[str] = None,\n           table_name: Optional[str] = None,\n           condition_columns: Union[str, Tuple[str, ...]] = (),\n           result_type: Type[Any] = None, iteratable: bool = False):\n    """\n    Function decorator of update operation.\n    In constructing update query by yourself, you need to specify either\n    one of the arguments `query` or `sql_path`.\n\n    In neither `query` nor `sql_path` are specified, this decorator creates\n    update query with arguments of decorated method.\n    In this case, you need follows.\n        1. To specify updated table name by decorator argument \'table_name\'\n            or by decorating \'@twinsqla.table\' to entity class.\n        2. To specifry the column names for using WHERE conditions\n            by decorator argument \'condition_columns\'\n\n    Args:\n        query (Optional[str], optional):\n            update query (available TwoWay SQL). Defaults to None.\n        sql_path (Optional[str], optional):\n            file path with sql (available TwoWay SQL). Defaults to None.\n        table_name (Optional[str], optional):\n            table name for updating. Defaults to None.\n        condition_columns (Union[str, Tuple[str, ...]], optional):\n            column names in WHERE condition. In almost cases, you are\n            recommended to specify primary key names of the table.\n            Defaults to ().\n        result_type (Type[Any], optional):\n            When constructing "UPDATE RETURNING" query, it is useful to\n            specify return type. Defaults to None.\n        iteratable (bool, optional):\n            In almost cases, this argument need not to specified.\n            The only useful case is in using "UPDATE RETURNING" query.\n            Defaults to False.\n\n    Returns:\n        Callable: Function decorator for update query\n    """\n```\n\n### `twinsqla.delete()`, `TWinSQLA.delete()`\n```python\ndef delete(query: Optional[str] = None, *, sql_path: Optional[str] = None,\n           table_name: Optional[str] = None,\n           condition_columns: Union[str, Tuple[str, ...]] = (),\n           result_type: Type[Any] = None, iteratable: bool = False):\n    """\n    Function decorator of delete operation.\n    In constructing delete query by yourself, you need to specify either\n    one of the arguments `query` or `sql_path`.\n\n    In neither `query` nor `sql_path` are specified, this decorator creates\n    delete query with arguments of decorated method.\n    In this case, you need follows.\n        1. To specify deleted table name by decorator argument \'table_name\'\n            or by decorating \'@twinsqla.table\' to entity class.\n        2. To specifry the column names for using WHERE conditions\n            by decorator argument \'condition_columns\'\n\n    Args:\n        query (Optional[str], optional):\n            delete query (available TwoWay SQL). Defaults to None.\n        sql_path (Optional[str], optional):\n            file path with sql (available TwoWay SQL). Defaults to None.\n        table_name (Optional[str], optional):\n            table name for deleting. Defaults to None.\n        condition_columns (Union[str, Tuple[str, ...]], optional):\n            column names in WHERE condition. In almost cases, you are\n            recommended to specify primary key names of the table.\n            Defaults to ().\n        result_type (Type[Any], optional):\n            When constructing "DELETE RETURNING" query, it is useful to\n            specify return type. Defaults to None.\n        iteratable (bool, optional):\n            In almost cases, this argument need not to specified.\n            The only useful case is in using "DELETE RETURNING" query.\n            Defaults to False.\n\n    Returns:\n        Callable: Function decorator for delete query\n    """\n```\n\n### `twinsqla.execute()`, `TWinSQLA.execute()`\n```python\ndef execute(query: Optional[str] = None, *, sql_path: Optional[str] = None,\n            result_type: Type[Any] = Tuple[OrderedDict, ...],\n            iteratable: bool = False):\n    """\n    Function decorator of any operation.\n    Only one argument `query` or `sql_path` must be specified.\n\n    In called decorated method, the processing implemented by the method\n    is not executed, but arguments of method are used for bind parameters.\n\n    Args:\n        query (Optional[str], optional):\n            any query (available TwoWay SQL). Defaults to None.\n        sql_path (Optional[str], optional):\n            file path with sql (available TwoWay SQL). Defaults to None.\n        result_type (Type[Any], optional):\n            return type. Defaults to Tuple[OrderedDict, ...].\n        iteratable (bool, optional):\n            When you want to fetching iterataly result, then True specified\n            and returned ResultIterator object. Defaults to False.\n\n    Returns:\n        Callable: Function decorator\n    """\n```\n\n## SQL Template\n### Bind variable\n\nTWinSQLA\'s two-way SQL can handle the bind parameter named *some_parameter* as follow.\n```sql\n/* :some_parameter */_dummy_value_\n```\nWhere, *_dummy_value_* is ignored in TWinSQLA dynamic query.\n\nImplementation.\n```python\n@twinsqla.select(\n    "SELECT * FROM table_name WHERE key = /* :some_value */300"\n)\ndef fetch_by_key(self, some_value: int) -> dict:\n    pass\n```\n\nCalling methods.\n```python\ndao.fetch_by_key(10)\n```\n\nIn this case, the follow statement and codes are executed.\n```python\nquery = sqlalchemy.sql.text("SELECT * FROM table_name WHERE key = :some_value")\nsqlalchemy_engin.execute(query, {"some_value": 10})\n```\n\n\n### ~~Bind variable with iterator~~ (Not yet implemented)\n\n(Not yet implemented handling iterable binding parameter)\n```sql\nSELECT * FROM table_name\nWHERE keys IN /* :some_values */(300, 305, 317)\n```\n\n\n### Python expression variable\n\nTWinSQLA\'s two-way SQL can embed a python expressions in sql statements as follow.\n```sql\n/* python_expression */_dummy_value_\n```\nWhere, *_dummy_value_* is ignored in TWinSQLA query.\n\nImplementation.\n```python\n@twinsqla.select(\n    "SELECT * FROM table_name WHERE key = /* some_value * 100 */300"\n)\ndef fetch_by_key(self, some_value: int) -> dict:\n    pass\n```\nIn this case, `some_value * 100` is the python expression, and `some_value` must be specified in this method\'s arguments.\n\nCall methods.\n```python\ndao.fetch_by_key(10)\n```\n\nThen the follow statement and codes are executed.\n```python\nquery = sqlalchemy.sql.text("SELECT * FROM table_name WHERE key = :dynamic_param")\nsqlalchemy_engin.execute(query, {"dynamic_param": 10 * 100})\n```\nThis bind parameter `:dynamic_param` is automatically generated by TWinSQLA to assign the python expression `some_value * 100` to this bind parameter.\n\n\n### IF block (Basic usage)\n\nDefinition of dynamic if-block\n```sql\n/*%if _python_expression_ */ sql_expression\n[ /*%elif _python_expression_ */ dummy_op sql_expression [...] ]\n[ /*%else*/ dummy_op sql_expression ]\n/*%end*/\n\ndummy_op := "AND" | "OR"\n```\n\nImplementation\n```python\n@twinsqla.select(r"""\n    SELECT * FROM table_name\n    WHERE\n        /*%if some_value == \'first\' */\n        some_column1 > 0\n        /*%elif some_value == \'second\' */\n        OR some_column2 > 0\n        /*%else*/\n        OR some_column1 = 0 AND some_column2 = 0\n        /*%end*/\n""")\ndef find(self, some_value: str) -> List[dict]:\n    pass\n```\n\nCall sample1.\n```python\ndao.find("first")\n```\nThen query1 is:\n```sql\n    SELECT * FROM table_name\n    WHERE\n        some_column1 > 0\n```\nBy the first if-condition is satisfied, then the others expressions are ignored.\n\nCall sample2.\n```python\ndao.find("second")\n```\nThen query2 is:\n```sql\n    SELECT * FROM table_name\n    WHERE\n        some_column2 > 0\n```\nBy the first if-condition is not satisfied and second is, then the excepts for second expression are ignored. And, noticed that `OR` operation ahead of expression `some_column2 > 0` is ignored.\n\nCall sample3.\n```python\ndao.find("other")\n```\nThen query3 is:\n```sql\n    SELECT * FROM table_name\n    WHERE\n        some_column1 = 0 AND some_column2 = 0\n```\n\n\n### IF block (Advanced usage)\n\n- Nested IF block\n\n    IF block can be nested.\n\n    Example.\n    ```python\n    @twinsqla.select(r"""\n        SELECT * FROM table_name\n        WHERE\n            /*%if some_value1 == \'first\' */\n            some_column1 > 0\n            /*%elif some_value1 == \'second\' */\n            OR some_column2 > 0 AND\n                /*%if some_value2 > 0 */\n                some_column3 = some_column4\n                /*%else*/\n                OR some_column3 IS NULL\n                /*%end*/\n            /*%else*/\n            OR some_column1 = 0 AND some_column2 = 0\n            /*%end*/\n    """)\n    def find(self, some_value1: str, some_value2: int) -> List[dict]:\n        pass\n    ```\n\n- About python expression nested in if-blocks evaluation\n\n    Python expression variables nested in if-blocks are evaluated only when if-condition is satisfied.\n    Consider the follow example with if-block and python expression variable.\n\n    ```python\n    @twinsqla.select(r"""\n        SELECT * FROM table_name\n        WHERE\n            /*%if some_value1 != 0 */\n            some_column1 > /* some_value2 / some_value1 */10\n            /*%else*/\n            OR some_column1 > 0\n            /*%end*/\n    """)\n    def find(self, some_value1: int, some_value2: int) -> List[dict]:\n        pass\n    ```\n\n    In the first case, the follow calling has no problem.\n    ```python\n    dao.find(10, 50)\n    ```\n    The above calling is the almost same the following execution.\n    ```python\n    query = sqlalchemy.sql.text("""\n        SELECT * FROM table_name\n        WHERE\n            some_column1 > :dynamic_param\n    """)\n    sqlalchemy_engin.execute(query, {"dynamic_param": (50 / 10)})\n    ```\n\n    In the next case, the follow calling.\n    ```python\n    dao.find(0, 7)\n    ```\n    In this case.\n    ```python\n    query = sqlalchemy.sql.text("""\n        SELECT * FROM table_name\n        WHERE\n            some_column1 > 0\n    """)\n    sqlalchemy_engin.execute(query, {"dynamic_param": None})\n    ```\n\n    Because for the first if-condition `some_value1 != 0` is not satisfied, the first python expression variable is not evaluated. (In detail, evaluated as `None` without evaluating dividing by zero `5 / 0`.)\n\n\n### ~~FOR block~~ (Not yet implemented)\n\nsample\n```sql\nSELECT * FROM table_name\nWHERE\n    /*%for item in iterator */\n    some_column = /* item */\'dummy\'\n    /*%or*/\n    /*%end*/\n```\n',
    'author': 'Yoshiki Kajihara',
    'author_email': None,
    'maintainer': None,
    'maintainer_email': None,
    'url': 'https://github.com/kajitiluna/twinsqla',
    'packages': packages,
    'package_data': package_data,
    'install_requires': install_requires,
    'python_requires': '>=3.6,<4.0',
}


setup(**setup_kwargs)
