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

packages = \
['pyrsona']

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

install_requires = \
['numpy>=1.22.2,<2.0.0',
 'parse>=1.19.0,<2.0.0',
 'psutil>=5.9.0,<6.0.0',
 'pydantic>=1.9.0,<2.0.0',
 'unsync>=1.4.0,<2.0.0']

setup_kwargs = {
    'name': 'pyrsona',
    'version': '0.5',
    'description': '',
    'long_description': '# pyrsona\n\nText data file validation and structure management using the [pydantic](https://pydantic-docs.helpmanual.io/) and [parse](https://github.com/r1chardj0n3s/parse) Python packages.\n\n\n## Installation\n\nInstall using `pip install pyrsona`.\n\n\n## A Simple Example\n\nFor the text file `example.txt`:\n\n```\noperator name: Jane Smith\ncountry: NZ\nyear: 2022\n\nID,Time,Duration (sec),Reading\n1,20:04:05,12.2,2098\n2,20:05:00,2.35,4328\n```\n\nThe following *pyrsona* file structure model can be defined:\n\n```python\nfrom pyrsona import BaseStructure\nfrom pydantic import BaseModel\nfrom datetime import time\n\n\nclass ExampleStructure(BaseStructure):\n\n    structure = (\n        "operator name: {operator_name}\\n"\n        "country: {country}\\n"\n        "year: {}\\n"\n        "\\n"\n        "ID,Time,Duration (sec),Reading\\n"\n    )\n\n    class meta_model(BaseModel):\n        operator_name: str\n        country: str\n\n    class row_model(BaseModel):\n        id: int\n        time: time\n        duration_sec: float\n        value: float\n```\n\nThe `read()` method can then be used to read the file, parse its contents and validate the meta data and table rows:\n\n```python\nmeta, table_rows, structure_id = ExampleStructure.read("example.txt")\n\nprint(meta)\n#> {\'operator_name\': \'Jane Smith\', \'country\': \'NZ\'}\n\nprint(table_rows)\n#> [{\'id\': 1, \'time\': datetime.time(20, 4, 5), \'value\': 2098.0}, {\'id\': 2,\n# \'time\': datetime.time(20, 5), \'value\': 4328.0}]\n\nprint(structure_id)\n#> ExampleStructure\n```\n\n**What\'s going on here:**\n\n- The `structure` class attribute contains a definition of the basic file structure. This definition includes the meta data lines and table header lines. Any variable text of interest is replaced with curly brackets and a field name, E.g. `\'{operator_name}\'`, while any variable text that should be ignored is replaced with empty curly brackets, E.g. `\'{}\'`. The `structure` definition must contain all spaces, tabs and new line characters in order for a file to successfully match it. The named fields in the `structure` definition will be passed to `meta_model`.\n\n- `meta_model` is simply a [pydantic model](https://pydantic-docs.helpmanual.io/usage/models/) with field names that match the named fields in the `structure` definition. All values sent to `meta_model` will be strings and these will be converted to the field types defined in `meta_model`. Custom [pydantic validators](https://pydantic-docs.helpmanual.io/usage/validators/) can be included in the `meta_model` definition as per standard pydantic models.\n\n- `row_model` is also a [pydantic model](https://pydantic-docs.helpmanual.io/usage/models/). This time the field names do not need to match the header line in the `structure` definition; however, the `row_model` fields do need to be provided in the **same order as the table columns**. This allows the table column names to be customised/standardised where the user does not control the file structure itself. Again, custom [pydantic validators](https://pydantic-docs.helpmanual.io/usage/validators/) can be included in the `row_model` definition if required.\n\n\n## Another Example\n\nShould the file structure change at some point in the future a new model can be created based on the original model. This is referred to as a *sub-model*, where the original model is the *parent* model.\n\nGiven the slightly modified file structure of `new_example.txt`:\n\n```\noperator name: Jane Smith\ncountry: NZ\ncity: Auckland\nyear: 2022\n\nID,Time,Duration (sec),Reading\n1,20:04:05,12.2,2098\n2,20:05:00,2.35,4328\n```\n\nAttempting to parse this file using the original `ExampleStructure` model will raise a `PyrsonaError` due to the addition of the `\'city: Auckland\'` line. In order to successfully parse the file and capture the new `\'city\'` field the following *sub-model* should be defined.\n\n```python\nfrom pyrsona import BaseStructure\nfrom pydantic import BaseModel\nfrom datetime import time\n\n\nclass NewExampleStructure(ExampleStructure):\n\n    structure = (\n        "operator name: {operator_name}\\n"\n        "country: {country}\\n"\n        "city: {city}\\n"\n        "year: {}\\n"\n        "\\n"\n        "ID,Time,Duration (sec),Reading\\n"\n    )\n\n    class meta_model(BaseModel):\n        operator_name: str\n        country: str\n        city: str\n```\n\n`ExampleStructure` is still used as the entry point; however, *pyrsona* will attempt to parse the file using any *sub-models* that exist (in this case `NewExampleStructure`) before using `ExampleStructure` itself.\n\n```python\nmeta, table_rows, structure_id = ExampleStructure.read("new_example.txt")\n\nprint(meta)\n#> {\'operator_name\': \'Jane Smith\', \'country\': \'NZ\', \'city\': \'Auckland\'}\n\nprint(table_rows)\n#> [{\'id\': 1, \'time\': datetime.time(20, 4, 5), \'value\': 2098.0}, {\'id\': 2,\n# \'time\': datetime.time(20, 5), \'value\': 4328.0}]\n\nprint(structure_id)\n#> NewExampleStructure\n```\n\n**What\'s going on here:**\n\n- A new *pyrsona* file structure model is defined based on the original `ExampleStructure` model. This means that `structure`, `meta_model` and `row_model` will be inherited from `ExampleStructure`. This also provides a single entry point (I.e. `ExampleStructure.read()`) when attempting to read the different file versions.\n\n- `structure` and `meta_model` are redefined to include the new `"city: Auckland"` meta data line. Alternatively, the original `meta_model` in `ExampleStructure` could have been updated to include an *optional* `city` field.\n\n\n## Post-processors\n\nIt is sometimes necessary to modify some of the data following parsing by the `meta_model` and `row_model`. Two post-processing methods are available for this purpose.\n\nUsing the `ExampleStructure` class above, `meta_postprocessor` and `table_postprocessor` static methods are defined for post-processing the meta data and table_rows, respectively:\n\n```python\nclass ExampleStructure(BaseStructure):\n\n    # Lines omitted for brevity\n\n    @staticmethod\n    def meta_postprocessor(meta):\n        meta["version"] = 3\n        return meta\n\n    @staticmethod\n    def table_postprocessor(table_rows, meta):\n        # Add a cumulative total and delete the "id" field:\n        total = 0\n        for ii, row in enumerate(table_rows):\n            total += row["value"]\n            row["total"] = total\n            del(row["id"])\n            table_rows[ii] = row\n        return table_rows\n```\n\nThe meta data and table_rows are now run through the post-processing stages before being returned, resulting in the following changes:\n\n - A new *version* field is added to the meta data.\n - The *id* field is deleted from the table_rows and a cumulative total field is added.\n\n```python\nmeta, table_rows, structure_id = ExampleStructure.read("example.txt")\n\nprint(meta)\n#> {\'operator_name\': \'Jane Smith\', \'country\': \'NZ\', \'version\': 3}\n\nprint(table_rows)\n#> [{\'time\': datetime.time(20, 4, 5), \'duration_sec\': 12.2, \'value\': 2098.0,\n# \'total\': 2098.0}, {\'time\': datetime.time(20, 5), \'duration_sec\': 2.35, \'value\': 4328.0,\n# \'total\': 6426.0}]\n\nprint(structure_id)\n#> NewExampleStructure\n```\n\n\n## Extra details\n\n\n### All meta lines MUST be included\n\nWhile the *parse* package allows a wildcard `\'{}\'` to be used to ignore several lines this can cause a named field to be unexpectedly included in the wildcard section. *pyrsona* therefore checks for the presence of a new line character `\'\\n\'` in the named field values and fails if one is found.\n\n\n### Sub-sub-models\n\nCalling the `read()` method will first build a list of *pyrsona* file structure models from the *parent* model down. \n\nAny *sub-models* of the *parent* model will themselves be checked for *sub-models*, meaning that every model in the tree below the *parent* model will be used when attempting to parse a file.\n\nEach branch of models will be ordered bottom-up so that the deepest nested model in a branch will be used first. The *parent* model will be the final model used if all others fail.\n\n### Model names\n\nThe `read()` method returns a `structure_id` variable that matches the model name. This `structure_id` can be useful when creating automated tests that sit alongside the *pyrsona* models as it provides a mechanism for confirming that a text file was parsed using the expected *pyrsona* model where multiple *sub-models* exist.\n\nAs the number of *sub-models* grows a naming convention becomes more important. One option is to set the names of any `sub-models` to a random hexadecimal value prefixed with a single underscore (in case the value begins with a number), E.g. `\'_a4c15356\'`. The initial underscore will be removed from model name when returning the `structure_id` value.\n\n\n### *parse* formats\n\nThe *parse* package allows format specifications to be included alongside the fields, E.g. `\'{year:d}\'`. While including these format types in the structure definition is valid, more complex format conversions can be made using `meta_model`. Keeping all format conversions in `meta_model` means that all conversions are defined in one place.\n',
    'author': 'John',
    'author_email': 'johnbullnz@gmail.com',
    'maintainer': None,
    'maintainer_email': None,
    'url': 'https://github.com/johnbullnz/pyrsona',
    'packages': packages,
    'package_data': package_data,
    'install_requires': install_requires,
    'python_requires': '>=3.9,<4.0',
}


setup(**setup_kwargs)
