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

package_dir = \
{'': 'src'}

packages = \
['patito']

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

install_requires = \
['polars>=0.14.0', 'pydantic>=1.7.0', 'typing-extensions']

extras_require = \
{':python_version < "3.8"': ['importlib-metadata'],
 'duckdb:python_version >= "3.8" and python_version < "4.0"': ['duckdb>=0.3.2,!=0.4.0',
                                                               'pyarrow>=5.0.0'],
 'pandas:python_version >= "3.8" and python_version < "4.0"': ['pandas']}

setup_kwargs = {
    'name': 'patito',
    'version': '0.3.2',
    'description': 'A dataframe modelling library built on top of polars and pydantic.',
    'long_description': '# 🐥 Patito\n\n<p align="center">\n    <em>\n        Patito combines <a href="https://github.com/samuelcolvin/pydantic">pydantic</a> and <a href="https://github.com/pola-rs/polars">polars</a> in order to write modern, type-annotated data frame logic.\n    </em>\n    <br>\n    <a href="https://github.com/kolonialno/patito/actions?workflow=CI">\n        <img src="https://github.com/kolonialno/patito/actions/workflows/ci.yml/badge.svg" alt="CI status">\n    </a>\n    <a href="https://codecov.io/gh/kolonialno/patito">\n        <img src="https://codecov.io/gh/kolonialno/patito/branch/main/graph/badge.svg?token=720LBDYH25"/>\n    </a>\n    <a href="https://pypi.python.org/pypi/patito">\n        <img src="https://img.shields.io/pypi/v/patito.svg">\n    </a>\n    <img src="https://img.shields.io/pypi/pyversions/patito">\n    <a href="https://github.com/kolonialno/patito/blob/master/LICENSE">\n        <img src="https://img.shields.io/github/license/kolonialno/patito.svg">\n    </a>\n</p>\n\nPatito offers a simple way to declare pydantic data models which double as schema for your polars data frames.\nThese schema can be used for:\n\n👮 Simple and performant data frame validation.\\\n🧪 Easy generation of valid mock data frames for tests.\\\n🐍 Retrieve and represent singular rows in an object-oriented manner.\\\n🧠 Provide a single source of truth for the core data models in your code base.\n\nPatito has first-class support for [polars]("https://github.com/pola-rs/polars"), a _"blazingly fast DataFrames library written in Rust"_.\n\n## Installation\n\n```sh\npip install patito\n```\n\n## 👮 Data validation\n\nPatito allows you to specify the type of each column in your dataframe by creating a type-annotated subclass of `patito.Model`:\n\n```py\n# models.py\nfrom typing import Literal, Optional\n\nimport patito as pt\n\n\nclass Product(pt.Model):\n    product_id: int = pt.Field(unique=True)\n    temperature_zone: Literal["dry", "cold", "frozen"]\n    is_for_sale: bool\n```\n\nThe **class** `Product` represents the **schema** of the data frame, while **instances** of `Product` represent single **rows** of the dataframe.\nPatito can efficiently validate the content of arbitrary data frames and provide human-readable error messages:\n\n```py\nimport polars as pl\n\ndf = pl.DataFrame(\n    {\n        "product_id": [1, 1, 3],\n        "temperature_zone": ["dry", "dry", "oven"],\n    }\n)\ntry:\n    Product.validate(df)\nexcept pt.ValidationError as exc:\n    print(exc)\n# 3 validation errors for Product\n# is_for_sale\n#   Missing column (type=type_error.missingcolumns)\n# product_id\n#   2 rows with duplicated values. (type=value_error.rowvalue)\n# temperature_zone\n#   Rows with invalid values: {\'oven\'}. (type=value_error.rowvalue)\n```\n\n<details>\n<summary><b>Click to see a summary of dataframe-compatible type annotations.</b></summary>\n\n* Regular python data types such as `int`, `float`, `bool`, `str`, `date`, which are validated against compatible polars data types.\n* Wrapping your type with `typing.Optional` indicates that the given column accepts missing values.\n* Model fields annotated with `typing.Literal[...]` check if only a restricted set of values are taken, either as the native dtype (e.g. `pl.Utf8`) or `pl.Categorical`.\n\nAdditonally, you can assign `patito.Field` to your class variables in order to specify additional checks:\n\n* `Field(dtype=...)` ensures that a specific dtype is used in those cases where several data types are compliant with the annotated python type, for example `product_id: int = Field(dtype=pl.UInt32)`.\n* `Field(unique=True)` checks if every row has a unique value.\n* `Field(gt=..., ge=..., le=..., lt=...)` allows you to specify bound checks for any combination of `> gt`, `>= ge`, `<= le` `< lt`, respectively.\n* `Field(multiple_of=divisor)` in order to check if a given column only contains values as multiples of the given value.\n* `Field(default=default_value, const=True)` indicates that the given column is required and _must_ take the given default value.\n* String fields annotated with `Field(regex=r"<regex-pattern>")`, `Field(max_length=bound)`, and/or `Field(min_length)` will be validated with [polars\' efficient string processing capabilities](https://pola-rs.github.io/polars-book/user-guide/howcani/data/strings.html).\n* Custom constraints can be specified with with `Field(constraints=...)`, either as a single polars expression or a list of expressions. All the rows of the dataframe must satisfy the given constraint(s) in order to be considered valid. Example: `even_field: int = pt.Field(constraints=pl.col("even_field") % 2 == 0)`.\n\nAlthough Patito supports [pandas](https://github.com/pandas-dev/pandas), it is highly recommemended to be used in combination with [polars]("https://github.com/pola-rs/polars").\nFor a much more feature-complete, pandas-first library, take a look at [pandera](https://pandera.readthedocs.io/).\n</details>\n\n## 🧪 Synthesize valid test data\n\nPatito encourages you to strictly validate dataframe inputs, thus ensuring correctness at runtime.\nBut with forced correctness comes friction, especially during testing.\nTake the following function as an example:\n\n```py\nimport polars as pl\n\ndef num_products_for_sale(products: pl.DataFrame) -> int:\n    Product.validate(products)\n    return products.filter(pl.col("is_for_sale")).height\n```\n\nThe following test would fail with a `patito.ValidationError`:\n\n```py\ndef test_num_products_for_sale():\n    products = pl.DataFrame({"is_for_sale": [True, True, False]})\n    assert num_products_for_sale(products) == 2\n```\n\nIn order to make the test pass we would have to add valid dummy data for the `temperature_zone` and `product_id` columns.\nThis will quickly introduce a lot of boilerplate to all tests involving data frames, obscuring what is actually being tested in each test.\nFor this reason Patito provides the `examples` constructor for generating test data that is fully compliant with the given model schema.\n\n```py\nProduct.examples({"is_for_sale": [True, True, False]})\n# shape: (3, 3)\n# ┌─────────────┬──────────────────┬────────────┐\n# │ is_for_sale ┆ temperature_zone ┆ product_id │\n# │ ---         ┆ ---              ┆ ---        │\n# │ bool        ┆ str              ┆ i64        │\n# ╞═════════════╪══════════════════╪════════════╡\n# │ true        ┆ dry              ┆ 0          │\n# ├╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┤\n# │ true        ┆ dry              ┆ 1          │\n# ├╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┤\n# │ false       ┆ dry              ┆ 2          │\n# └─────────────┴──────────────────┴────────────┘\n```\n\nThe `examples()` method accepts the same arguments as a regular data frame constructor, the main difference being that it fills in valid dummy data for any unspecified columns.\nThe test can therefore be rewritten as:\n\n```py\ndef test_num_products_for_sale():\n    products = Product.examples({"is_for_sale": [True, True, False]})\n    assert num_products_for_sale(products) == 2\n```\n\n## 🖼️ A model-aware data frame class\nPatito offers `patito.DataFrame`, a class that extends `polars.DataFrame` in order to provide utility methods related to `patito.Model`.\nThe schema of a data frame can be specified at runtime by invoking `patito.DataFrame.set_model(model)`, after which a set of contextualized methods become available:\n\n* `DataFrame.validate()` - Validate the given data frame and return itself.\n* `DataFrame.drop()` - Drop all superfluous columns _not_ specified as fields in the model.\n* `DataFrame.cast()` - Cast any columns which are not compatible with the given type annotations. When `Field(dtype=...)` is specified, the given dtype will always be forced, even in compatible cases.\n* `DataFrame.get(predicate)` - Retrieve a single row from the data frame as an instance of the model. An exception is raised if not exactly one row is yielded from the filter predicate.\n* `DataFrame.fill_null(strategy="defaults")` - Fill inn missing values according to the default values set on the model schema.\n* `DataFrame.derive()` - A model field annotated with `Field(derived_from=...)` indicates that a column should be defined by some arbitrary polars expression. If `derived_from` is specified as a string, then the given value will be interpreted as a column name with `polars.col()`. These columns are created and populated with data according to the `derived_from` expressions when you invoke `DataFrame.derive()`.\n\nThese methods are best illustrated with an example:\n\n```py\nfrom typing import Literal\n\nimport patito as pt\nimport polars as pl\n\n\nclass Product(pt.Model):\n    product_id: int = pt.Field(unique=True)\n    # Specify a specific dtype to be used\n    popularity_rank: int = pt.Field(dtype=pl.UInt16)\n    # Field with default value "for-sale"\n    status: Literal["draft", "for-sale", "discontinued"] = "for-sale"\n    # The eurocent cost is extracted from the Euro cost string "€X.Y EUR"\n    eurocent_cost: int = pt.Field(\n        derived_from=100 * pl.col("cost").str.extract(r"€(\\d+\\.+\\d+)").cast(float).round(2)\n    )\n\n\nproducts = pt.DataFrame(\n    {\n        "product_id": [1, 2],\n        "popularity_rank": [2, 1],\n        "status": [None, "discontinued"],\n        "cost": ["€2.30 EUR", "€1.19 EUR"],\n    }\n)\nproduct = (\n    products\n    # Specify the schema of the given data frame\n    .set_model(Product)\n    # Derive the `eurocent_cost` int column from the `cost` string column using regex\n    .derive()\n    # Drop the `cost` column as it is not part of the model\n    .drop()\n    # Cast the popularity rank column to an unsigned 16-bit integer and cents to an integer\n    .cast()\n    # Fill missing values with the default values specified in the schema\n    .fill_null(strategy="defaults")\n    # Assert that the data frame now complies with the schema\n    .validate()\n    # Retrieve a single row and cast it to the model class\n    .get(pl.col("product_id") == 1)\n)\nprint(repr(product))\n# Product(product_id=1, popularity_rank=2, status=\'for-sale\', eurocent_cost=230)\n```\n\nEvery Patito model automatically gets a `.DataFrame` attribute, a custom data frame subclass where `.set_model()` is invoked at instantiation. With other words, `pt.DataFrame(...).set_model(Product)` is equivalent to `Product.DataFrame(...)`.\n\n## 🐍 Representing rows as classes\n\nData frames are tailor-made for performing vectorized operations over a _set_ of objects.\nBut when the time comes to retrieving a _single_ row and operate upon it, the data frame construct naturally falls short.\nPatito allows you to embed row-level logic in methods defined on the model.\n\n\n```py\n# models.py\nimport patito as pt\n\nclass Product(pt.Model):\n    product_id: int = pt.Field(unique=True)\n    name: str\n\n    @property\n    def url(self) -> str:\n        return (\n            "https://oda.com/no/products/"\n            f"{self.product_id}-"\n            f"{self.name.lower().replace(\' \', \'-\')}"\n        )\n```\n\nThe class can be instantiated from a single row of a data frame by using the `from_row()` method:\n\n```py\nproducts = pl.DataFrame(\n    {\n        "product_id": [1, 2],\n        "name": ["Skimmed milk", "Eggs"],\n    }\n)\nmilk_row = products.filter(pl.col("product_id" == 1))\nmilk = Product.from_row(milk_row)\nprint(milk.url)\n# https://oda.com/no/products/1-skimmed-milk\n```\n\nIf you "connect" the `Product` model with the `DataFrame` by the use of `patito.DataFrame.set_model()`, or alternatively by using `Product.DataFrame` directly, you can use the `.get()` method in order to filter the data frame down to a single row _and_ cast it to the respective model class:\n\n```py\n\nproducts = Product.DataFrame(\n    {\n        "product_id": [1, 2],\n        "name": ["Skimmed milk", "Eggs"],\n    }\n)\nmilk = products.get(pl.col("product_id") == 1)\nprint(milk.url)\n# https://oda.com/no/products/1-skimmed-milk\n```\n',
    'author': 'Jakob Gerhard Martinussen',
    'author_email': 'jakobgm@gmail.com',
    'maintainer': None,
    'maintainer_email': None,
    'url': 'https://github.com/kolonialno/patito',
    'package_dir': package_dir,
    'packages': packages,
    'package_data': package_data,
    'install_requires': install_requires,
    'extras_require': extras_require,
    'python_requires': '>=3.7,<4.0',
}


setup(**setup_kwargs)
