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

packages = \
['pymssqlutils']

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

install_requires = \
['pymssql>=2.1.4,<3.0.0']

extras_require = \
{'all': ['orjson>=3.4.0,<4.0.0', 'pandas>=1.0.0,<2.0.0'],
 'json': ['orjson>=3.4.0,<4.0.0'],
 'pandas': ['pandas>=1.0.0,<2.0.0']}

setup_kwargs = {
    'name': 'pymssql-utils',
    'version': '0.2.0',
    'description': 'pymssql-utils is a small library that wraps pymssql to make your life easier.',
    'long_description': '# pymssql-utils\n_pymssql-utils_ is a small library that wraps\n[pymssql](https://github.com/pymssql/pymssql) to make your life easier.\nIt provides you with a higher-level API as well as various utility functions\nfor generating dynamic queries.\n\nThis module features:\n* Higher-level API that reduces the amount of boilerplate required.\n* Baked-in sensible defaults and usage patterns.\n* Provides optional execution batching, similar to\n  [_pyodbc\'s_](https://github.com/mkleehammer/pyodbc) `fast_executemany`.\n* Provides consistent parsing between SQL Types and native Python types over different platforms and drivers.\n* Makes it easy to serialize your data with\n  [_orjson_](https://github.com/ijl/orjson).\n* Provides you with simple and clear options for error handling.\n* Extra utility functions for building dynamic SQL queries.\n* Fixing various edge case bugs that arise when using _pymssql_.\n* Fully type hinted.\n\nThis module\'s enforced opinions (check these work for you):\n* Each execution opens and closes a connection using _pymssql_\'s\n  context management.\n* Automatically converts certain data types for ease of use, e.g. `Decimal` -> `float`, `UUID` -> `str`.\n  \nWhen you shouldn\'t use this module:\n* If you need fine-grained control over your cursors.\n  \nPlease raise any suggestions or issues via GitHub.\n\n## Status\n\nThis library is tested and stable. There will not be any breaking changes to the \npublic API without a major version release.  There is also scope for expanding the \nlibrary if new features are requested.\n\n## Changes\n\nSee the repository\'s [GitHub releases](https://github.com/invokermain/pymssql-utils/releases)\nor the `CHANGELOG.md`.\n\n## Usage\n### Installation\n\nThis library can be installed via pip: `pip install --upgrade pymssql-utils`.\nThis library requires `Python >= 3.7`.\n\nIf you want to serialize your results to JSON you can install the optional dependency `ORJSON`\nby running `pip install --upgrade pymssql-utils[json]`.\n\nIf you want to cast your results to DataFrame you can install the optional dependency `Pandas`\nby running `pip install --upgrade pymssql-utils[pandas]`.\n\n### Quickstart\n\nFor querying the database this library provides two high-level methods:\n * `query`: executes a SQL operation that fetches the result, and DOES NOT commit the transaction (by default).\n * `execute`: executes a SQL operation that does not fetch the result (by default), and DOES commit the transaction.\n\nThis separation of _pymssql\'s_ `execute` is to make your code more explicit and readable.\n\nHere is an example for running a simple query and accessing the returned data:\n```python\n>>> import pymssqlutils as sql\n>>> result = sql.query(\n      "SELECT SYSDATETIMEOFFSET() as now",\n      server="..."\n    )\n>>> result.data\n[{\'now\': datetime.datetime(2021, 1, 21, 23, 31, 11, 272299, tzinfo=datetime.timezone.utc)}]\n```\n\nAnd running a simple execution:\n```python\n>>> import pymssqlutils as sql\n>>> result = sql.execute(\n      "INSERT INTO mytable VALUES (1, \'test\')",\n      server="MySQLServer"\n    )\n```\n\n### Specifying Connection\nThere are two ways of specifying the connection parameters to the SQL Server:\n1. Passing the required parameters\n   ([see pymssql docs](https://pymssql.readthedocs.io/en/stable/ref/pymssql.html#pymssql.connect))\n   to `query` or `execute` like in the quickstart example above.\n   All extra `**kwargs` passed to these methods are passed on to the `pymssql.connection()`.\n2. Specify the connection parameters in the environment like the example below, this is the recommended way\n   (however, any parameters given explicitly to a method will take priority).\n   \n```python\nimport os\nimport pymssqlutils as sql\n\nos.environ["MSSQL_SERVER"] = "sqlserver.mycompany.com"\nos.environ["MSSQL_USER"] = "my_login"\nos.environ["MSSQL_PASSWORD"] = "my_password123"\n\nresult = sql.execute("INSERT INTO mytable VALUES (%s, %s)", (1, "test"))\n```\n\nThere is a helper method to set this in code, see `set_connection_details` below.\n\n### Executing SQL\n#### Query\n\nThe `query` method executes a SQL Operation which does not commit the transaction & returns the result.\n\n```python\nquery(\n    operation: str,\n    parameters: SQLParameters = None,\n    raise_errors: bool = True,\n    **kwargs,\n) -> DatabaseResult:\n```\n\nParameters:\n * `operation (str)`: the SQL operation to execute.\n * `parameters (SQLParameters)`: parameters to substitute into the operation,\n   these can be a single value, tuple or dictionary.\n * `raise_errors (bool)`: whether to raise exceptions or to return the error information with the result.\n * Any extra kwargs are passed to _pymssql\'s_ `connect` method.\n\nReturns a `DatabaseResult` class, see documentation below.\n\n#### Execute\n\nThe `execute` method executes a SQL Operation which commits the transaction\n& optionally returns the result (not by default).\n\n```python\nexecute(\n    operations: Union[str, List[str]],\n    parameters: Union[SQLParameters, List[SQLParameters]] = None,\n    batch_size: int = None,\n    fetch: bool = False,\n    raise_errors: bool = True,\n    **kwargs,\n) -> DatabaseResult:\n```\n\nParameters:\n * `operations (Union[str, List[str]])`: the SQL Operation/s to execute.\n * `parameters (Union[SQLParameters, List[SQLParameters]])`: parameters to substitute into the operation/s,\n   these can be a single value, tuple or dictionary OR this can be a list of any of the previous.\n * `batch_size (int)`: if specified concatenates the operations together according to the batch_size,\n   this can vastly increase performance if executing many statements.\n   Raises an error if set to True and both operations and parameters are singular.\n * `fetch (bool)`: if True returns the result from the LAST execution, by default false.  \n * `raise_errors (bool)`: whether to raise exceptions or to return the error information with the result.\n * Any extra kwargs are passed to _pymssql\'s_ `connect` method.\n\nReturns a `DatabaseResult` class, see documentation below.\n\nThere are two ways of using this function:\n\nPassing in a single operation (`str`) to operations:\n* If parameters is singular, this calls `pymssql.execute()` and executes a single operation\n* If parameters is plural, this calls `pymssql.execute_many()` and executes one execution per parameter set\n\nPassing in multiple operations (`List[str]`) to operations:\n* If parameters is None, this calls `pymssql.execute_many()` and executes one execution per operation\n* If parameters is the same length as operations, this calls `pymssql.execute()` multiple times\n  and executes one execution per operation.\n\nOptionally `batch_size` can be specified to use string concatenation to batch the operations, this can\nprovide significant performance gains if executing 100+ small operations. This is similar to `fast_executemany`\nfound in the `pyodbc` package. A value of 500-1000 is a good default.\n\n### DatabaseResult Class\n\nOne big difference between this library and _pymssql_ is that here\n`execute` and `query` return an instance of the `DatabaseResult` class.\n\nThis class holds the returned data, if there is any, and provides\nvarious useful attributes and methods to work with the result.\n\n#### Attributes & Properties\n * `ok`: True if the execution did not error, else False. Only useful if using `raise_errors = False`,\n   see below section on Error Handling.\n * `error`: Populated by the error raised during execution (if applicable). Only useful if using `raise_errors = False`.\n * `fetch`: True if results from the execution were fetched (e.g. if using `query`), else False.\n * `commit`: True if the execution was committed (i.e. if using `execute`), else False.\n * `columns`: A list of the column names in the dataset returned from the execution (if applicable)\n * `data`: The dataset returned from the execution (if applicable), this is a list of dictionaries.\n * `raw_data`: The dataset returned from the execution (if applicable), this is a list of tuples.\n\n#### Methods\n * `to_dataframe`: (requires Pandas to be installed), returns the dataset as a DataFrame object.\n   All args and kwargs are parsed to the DataFrame constructor.\n * `to_json`: returns the dataset as a json serialized string using the `orjson` library, make sure this \n   optional dependency is installed by running `pip install --upgrade pymssql-utils[json]`.\n   Note that this will fail if your data contains `bytes` type values. By default, this method returns a string, but\n   pass `as_bytes = True` to return a byte string.\n * `write_error_to_logger`: writes the error information to the library\'s logger, optionally pass a `name` parameter\n  to allow you to easier indentify the query in the logging output.\n * `raise_error`: raises a `pymssqlutils.DatabaseError` from the underlying `pymssql` error,\n   optionally pass a `name` parameter to allow you to easier indentify the query in the error output.\n\n\n### Error handling\n\nBoth `query` & `execute` take `raise_errors` as a parameter, which is by default `True`. This means that by default\n_pymssql-utils_ will allow _pymssql_ to raise errors as normal.\n\nPassing `raise_errors` as `False` will pass any errors onto the `DatabaseResult` class, which allows you\nto handle errors gracefully using the `DatabaseResult` class (see above), e.g.:\n\n```python\nimport pymssqlutils as db\n\nresult = db.query("Bad Operation", raise_errors=False)\n\nif not result.ok: # result.ok will be False due to error\n    \n    # write the error to logging output\n    result.write_error_to_logger(\'An optional query identifier to aid logging\')\n    \n    # the error is stored under the error attribute\n    error = result.error \n   \n    # can always re-raise the error\n    result.raise_error(\'Query Identifier\')\n```\n\nThis can be useful in situations where you do not want the error to propogate, e.g. if querying the database\nas part of an API response.\n\n### Utility Functions\n#### set_connection_details\n\nThe `set_connection_details` method is a helper function which will set the value of\nthe relevant environment variable for the connection kwargs given.\n\nWarning: this function has program wide side effects and will overwrite any\npreviously set connection details in the environment; therefore its usage is only recommended\nin single script projects/notebooks. All the connections details will also be visible in your code.\n\nThe preferred method in production scenarios is to set the environment variables directly.\n\n```python\ndef set_connection_details(\n    server: str = None,\n    database: str = None,\n    user: str = None,\n    password: str = None\n) -> None:\n```\n\nParameters:\n* `server (str)`: the network address of the SQL server to connect to, sets \'MSSQL_SERVER\' in the environment.\n* `database (str)`: the default database to use on the SQL server, sets \'MSSQL_DATABASE\' in the environment.\n* `user (str)`: the user to authenticate against the SQL server with, sets \'MSSQL_USER\' in the environment\n* `password (str)`: the password to authenticate against the SQL server with, sets \'MSSQL_PASSWORD\' in the environment\n\n#### substitute_parameters\n\nThe `substitute_parameters` method does the same parameter substitution as `query` and `execute`, but returns the\nsubstituted operation instead of executing it. This allows you to see the actual operation being run\nagainst the database and is useful for debugging and logging.\n\n```python\nsubstitute_parameters(\n    operation: str,\n    parameters: SQLParameters\n) -> str:\n```\n\nParameters:\n* `operation (str)`: The SQL operation requiring substitution.\n* `parameters (SQLParameters)`: The parameters to substitute in.\n\nReturns the parameter substituted SQL operation as a string.\n\nExample:\n\n```python3\n>>> substitute_parameters("SELECT %s Col1, %s Col2", ("Hello", 1.23))\n"SELECT N\'Hello\' Col1, 1.23 Col2"\n```\n\n#### to_sql_list\n\nThe `to_sql_list` method converts a Python iterable to a string form of the SQL equivalent list. This is useful\nwhen creating dynamic SQL operations using the \'IN\' operator.\n\n```python\nto_sql_list(\n    listlike: Iterable[SQLParameter]\n) -> str:\n```\n\nParameters:\n* `listlike (Iterable[SQLParameter])`: The iterable of SQLParameter to transform\n\nReturns the SQL equivalent list as a string\n\nExamples:\n\n```python3\n>>> to_sql_list([1, \'hello\', datetime.now()])\n"(1, N\'hello\', N\'2021-03-22T10:56:27.981173\')"\n```\n\n```python3\n>>> my_ids = [1, 10, 21]\n>>> f"SELECT * FROM MyTable WHERE Id IN {to_sql_list(my_ids)}"\n\'SELECT * FROM MyTable WHERE Id IN (1, 10, 21)\'\n```\n\n#### model_to_values\n\nThe `model_to_values` method converts a Python mapping (e.g. dictionary of Pydantic model) to the SQL equivalent\nvalues string. This is useful when creating dynamic SQL operations using the \'INSERT\' statement.\n\n```python3\nmodel_to_values(\n    model: Any,\n    prepend: List[Tuple[str, str]] = None,\n    append: List[Tuple[str, str]] = None,\n) -> str:\n```\n\nParameters:\n* `model (Any)`: A mapping to transform, i.e. a dictionary or an object that has the __dict__ method implemented,\n  with string keys and SQLParameter values.\n* `prepend (List[Tuple[str, str]])`: prepend a variable number of columns to the beginning of the values statement.\n* `append (List[Tuple[str, str]])`: append a variable number of columns to the end of the values statement.\n\nReturns a string of the form: `([attr1], [attr2], ...) VALUES (val1, val2, ...)`.\n\nWarning: prepended and appended columns are not parameter substituted,\nthis can leave your code open to SQL injection attacks.\n\nExample:\n\n```python3\n>>> my_data = {\'value\': 1.56, \'insertDate\': datetime.now()}\n\n>>> model_to_values(my_data, prepend=[(\'ForeignId\', \'@Id\')])\n"([ForeignId], [value], [insertDate]) VALUES (@Id, 1.56, N\'2021-03-22T13:58:33.758740\')"\n\n>>> f"INSERT IN MyTable {model_to_values(my_data, prepend=[(\'foreignId\', \'@Id\')])}"\n"INSERT IN MyTable ([foreignId], [value], [insertDate]) VALUES (@Id, 1.56, N\'2021-03-22T13:58:33.758740\')"\n```\n\n## Notes\n### Type Parsing\n\n_pymssql-utils_ parses SQL types to their native python types regardless of the environment.\nThis ensures consistent behaviour across various systems, see the table below for a comparison.\n\n|                   | Windows         |            | Ubuntu          |            |\n|-------------------|-----------------|------------|-----------------|------------|\n| *SQL DataType*    | *pymssql-utils* | *pymssql*  | *pymssql-utils* | *pymssql*  |\n| Date              | date            | date       | date            | str        |\n| Binary            | bytes           | bytes      | bytes           | bytes      |\n| Time1             | time            | time       | time            | str        |\n| Time2             | time            | time       | time            | str        |\n| Time3             | time            | time       | time            | str        |\n| Time4             | time            | time       | time            | str        |\n| Time5             | time            | time       | time            | str        |\n| Time6             | time            | time       | time            | str        |\n| Time7             | time            | time       | time            | str        |\n| Small DateTime    | datetime        | datetime   | datetime        | datetime   |\n| Datetime          | datetime        | datetime   | datetime        | datetime   |\n| Datetime2         | datetime        | datetime   | datetime        | str        |\n| DatetimeOffset0   | datetime        | bytes      | datetime        | str        |\n| DatetimeOffset1   | datetime        | bytes      | datetime        | str        |\n| DatetimeOffset2   | datetime        | bytes      | datetime        | str        |\n| UniqueIdentifier  | str             | bytes      | str             | ???        |\n\n## Testing\n\nInstall pytest to run non-integration tests via `pytest .`,\nthese tests mock the cursor results allowing the library to test locally.\n\nTo test against an MSSQL instance install `pytest-dotenv`.\nThen create a `.env` file with `"TEST_ON_DATABASE"` set as a truthy value, as well as any\nconnection environemt variables for the MSSQL server.\nThese tests will then be run (not-skipped), e.g. `pytest . --envfile .test.env`\n\n### Why _pymssql_ when Microsoft officially recommends _pyodbc_ (opinion)?\n\nThere are other minor reasons someone might prefer _pymssql_, e.g.:\n1. _pymssql_ supports for MSSQL specific data types such as `Datetimeoffset`.\n2. _pymssql\'s_ parameter subsitution is done client-side improving debug/logging ability.\n3. _pymssql\'s_ drivers are easily installed, meaning that your code \'just works\' in \n   more environments without extra steps, [e.g. this](https://docs.microsoft.com/en-us/sql/connect/odbc/linux-mac/installing-the-microsoft-odbc-driver-for-sql-server?view=sql-server-ver15)!',
    'author': 'Tim OSullivan',
    'author_email': 'tim@lanster.dev',
    'maintainer': None,
    'maintainer_email': None,
    'url': 'https://github.com/invokermain/pymssql-utils',
    'packages': packages,
    'package_data': package_data,
    'install_requires': install_requires,
    'extras_require': extras_require,
    'python_requires': '>=3.7,<4.0',
}


setup(**setup_kwargs)
