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

packages = \
['csv_permissions']

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

install_requires = \
['Django>=2.2']

setup_kwargs = {
    'name': 'django-csvpermissions',
    'version': '0.3.0.dev0',
    'description': 'CSV Based Permissions Module for Django',
    'long_description': '# CSV Permissions Module for Django\n\nCSV-based permissions for Django.\n\nRead [Motivation / Rationale](doc/background.md) for why this project exists.\n\n\n## System Requirements\n\n* Tested with django 2.2 and 3.2\n  * Pull requests accepted for other versions, but at minimum we test against current LTS versions\n* Python >=3.6 (no python 3.5 support)\n\n## CSV Permissions\n\nThe `csv_permissions` model works as follows:\n\n* Every user has a "user type" (the equivalent of `django.contrib.auth`\'s `Group`)\n\n* A CSV file that defines a permission matrix is used to define what permissions each `user_type` has\n\n### Quick Start\n\nIn your django settings:\n\n* Add `csv_permissions.permissions.CSVPermissionsBackend` to `AUTHENTICATION_BACKENDS` \n  \n* set `CSV_PERMISSIONS_PATHS` which is an array/tuple of `str`/`pathlib.Path`\n    pointing to the CSV files you want to use to define your permissions.\n    Multiple files will be merged.\n    The CSV files order does not matter: an error will be raised if the files are\n    inconsistent.\n    If a permission or user type is missing from one CSV file then this is not considered\n    inconsistent, but a blank cell vs a filled cell is inconsistent.\n  \n* Set `CSV_PERMISSIONS_RESOLVE_EVALUATORS` to `"csv_permissions.evaluators.default_resolve_evaluators"`\n\n#### Autoreload\n\n`csv_permissions` caches the data read from the CSV permissions file on server start.\nDuring development this means you have to manually restart the dev server when you make changes.\nYou can hook into django\'s autoreloader to automatically reload when the CSV file is changed:\n\nIn one of your [django app configs](https://docs.djangoproject.com/en/dev/ref/applications/#for-application-authors):\n\n```python\ndef add_csv_permissions_watcher(sender: django.utils.autoreload.StatReloader, **kwargs):\n    """In dev we want to reload if the csv permission file changes"""\n    sender.extra_files.add(settings.CSV_PERMISSIONS_PATHS)\n\nclass MySiteAppConfig(AppConfig):\n    name = "my_site"\n    verbose_name = "My Site"\n\n    def ready(self):\n        if settings.DEBUG:\n            from django.utils.autoreload import autoreload_started\n\n            autoreload_started.connect(add_csv_permissions_watcher)\n```\n\nIf you\'re using [`runserver_plus`](https://django-extensions.readthedocs.io/en/latest/runserver_plus.html)\nfrom `django-extensions` you can add your CSV files to\n[`settings.RUNSERVER_PLUS_EXTRA_FILES`](https://django-extensions.readthedocs.io/en/latest/runserver_plus.html#configuration).   \n\n  \n### The CSV File\n\nAn example permission file:\n\n```csv\nModel,     App,           Action,            Is Global, admin, assistant, customer\n\n# Comment lines and blank lines will be ignored\n\n# The horizontal column alignment is just for readability:\n#  leading/trailing spaces will be stripped from each cell\n\nPublisher, library,       add,               yes,       yes,   no,        no\nPublisher, library,       view,              no,        all,   no,        no\nPublisher, library,       change,            no,        all,   no,        no\nPublisher, library,       delete,            no,        all,   no,        no\n\nBook,      library,       add,               yes,       yes,   yes,       no\nBook,      library,       view,              no,        all,   all,       no\nBook,      library,       change,            no,        all,   all,       no\nBook,      library,       delete,            no,        all,   all,       no\n\nLoan,      library,       add,               yes,       yes,   yes,       yes\nLoan,      library,       view,              no,        all,   all,       no\nLoan,      library,       change,            no,        all,   all,       no\nLoan,      library,       delete,            no,        all,   all,       no\n\n# The model column can be blank. Note that the customer column here is also\n# empty; see below for the difference between this and "no"\n\n,          library,       report_outstanding,yes,      yes,   yes,\n,          library,       report_popularity, yes,      yes,   yes,\n```\n\nThe first 4 columns define the permission details.\nThese will be used to resolve the permission code name (see [Permission Names](#permission-names)). \n\n**Model** is used to resolve the permission name but is otherwise not used. There is no checks that objects passed to the `has_perm()` actually match the correct type.\n\n**App** is used to resolve the permission name and model.\n\n**Action** is an arbitrary identifier that is used to resolve the permission name.\n\n**Is Global** whether the permission is global or per-object (see "Global Permission" section below).\n    Right now you must provide a model if `Is Global` is false however this restriction may be\n    relaxed in future. \n\n**Evaluators**\n\nThe next columns define permission "evaluators" for each [user type](#user-type)\n\nBuilt-in evaluators are:\n\n* `all` - user has permission for all objects. Will raise an error if an object is not passed to `has_perm()`\n* `yes` - user has permission globally. Will raise an error if an object is passed to `has_perm()`.\n* `no` -- user does not have permission (global or per-object)\n* (empty cell) -- user permission is not defined.\n    If another CSV file defines this user/permission pair then that will be used.\n    If no CSV file defines this user/permission pair then the evaluator will be\n    treated as `""` and by default the `resolve_empty_evaluator` will treat this as no permission granted. \n\nThe distinction between `all` and `yes` is that `all` is a per-object\npermission and `yes` is a [global permission](#global-permissions). \n\n### Global Permissions\n\nUnlike vanilla django permissions, by default `cvs_permissions` imposes a hard\ndistinction between global and per-object permissions.\n\n* If you pass an object in a permission check against a permission with\n    `Is Global==yes` in the CSV file then a `ValueError` will be raised.\n* If you *don\'t* pass an object to a permission check against a permission with\n  `Is Global==no` in the CSV file then a `ValueError` will be raised.\n\nThe `CSVPermissionsBackend` provides an `is_global_perm()` method to query\nwhether a permission is global or per-object:\n\n```python\n# example of querying whether a permission is global \nprint(\n    "foo-bar is a global permission"\n    if CSVPermissionBackend().is_global("foo-bar")\n    else "foo-bar is a per-object permission"\n)\n```\n\n### Custom Evaluators\n\nBy default putting anything other than a built-in evaluator in a CSV permissions file\nwill raise an error.\n\nYou add your own permission evaluators by defining "evaluator resolver"\nfunctions which ingest a CSV cell value and returns a permission evaluator.\nIf the resolver does not recognise something it should return `None` and the\nnext resolver in the list will be called.\n\n```python\n# in settings.py\nCSV_PERMISSIONS_RESOLVE_EVALUATORS = (\n    # sanity check that non-global permissions have a model\n    \'csv_permissions.evaluators.resolve_validate_is_global_model\',\n    # custom validators (examples below)\n    \'my_app.evaluators.resolve_evaluators\',\n    # \'all\'/\'yes\'/\'no\' \n    \'csv_permissions.evaluators.resolve_all_evaluator\',\n    \'csv_permissions.evaluators.resolve_yes_evaluator\',\n    \'csv_permissions.evaluators.resolve_no_evaluator\',\n    # If you remove the empty evaluator then "" will fall through to the\n    # remaining evaluator(s). This can be used in combination with\n    # CSV_PERMISSIONS_STRICT to ensure that there are no blank cells in a CSV\n    # file. Note that cells not present in any file due to different headers\n    # still won\'t be processed.     \n    \'csv_permissions.evaluators.resolve_empty_evaluator\',\n    # normally if nothing matches an exception will be thrown however it \n    # can be more convenient (especially in early phases of development )\n    # to issue a warning during CSV parsing, and then throw a\n    # NotImplementedError() when the permission is evaluated\n    \'csv_permissions.evaluators.resolve_fallback_not_implemented_evaluator\',\n)\n\n# if you don\'t have any customisations you can point to a list/tuple\n# that is defined elsewhere; if you don\'t set it then this is the default setting:\n#CSV_PERMISSIONS_RESOLVE_EVALUATORS = "csv_permissions.evaluators.default_resolve_evaluators"\n\n# for compatibility with csv_permissions 0.1.0\n#CSV_PERMISSIONS_RESOLVE_EVALUATORS = "csv_permissions.legacy.resolve_evaluators"\n\n```\n\nThe following code will define some custom evaluators: \n- `\'if_monday\'` grants all access on mondays.\n- `\'all_caps\'` grants access to all objects that have a `name` field containing\n    all uppercase.\n\nIn `my_app.evaluators`:\n```python\nimport datetime\nfrom typing import Optional\n\nfrom csv_permissions.types import Evaluator\nfrom csv_permissions.types import UnresolvedEvaluator\n\n\ndef evaluate_if_monday(user, obj=None):\n    return datetime.datetime.today().weekday() == 0\n\ndef evaluate_all_caps(user, obj=None):\n    if obj is None:\n        raise ValueError("\'all_caps\' cannot be used as a global permission.")\n    \n    try:\n        return obj.name.isupper()\n    except AttributeError:\n        return False\n     \ndef resolve_evaluators(details: UnresolvedEvaluator) -> Optional[Evaluator]:\n    if details.evaluator_name == "if_monday":\n        return evaluate_if_monday\n\n    if details.evaluator_name == "all_caps":\n        if details.is_global != False:\n            raise ValueError("\'all_caps\' cannot be used as a global permission.")\n        return evaluate_all_caps\n\n    return None\n```\n\n* Note that evaluator names do not have to be static strings: you could implement\n    something that understood `\'all_caps:True\'` and `\'all_caps:False\'` for example\n\n### User Type\n\nUser Types are the `csv_permissions` equivalent of django\'s user Group.\nA user has a single user type, and from that is granted the set of permissions\nthat this user type has in the CSV file.\n\nThe default user type is obtained from the user\'s `user_type` attribute.\n\nIf this doesn\'t exist or you need custom logic then you can set\n`settings.CSV_PERMISSIONS_GET_USER_TYPE` to a function that takes a\nuser and returns the user type. If the function returns `None` or an empty\nstring then the user will have no permissions.\n\nCustom example where the user type field is stored on a user Profile record\ninstead of the User record:\n\nIn `settings.py`:\n\n```python\nCSV_PERMISSIONS_GET_USER_TYPE = \'my_site.auth.get_user_type\'\n```\n\nIn `my_site/auth.py`:\n\n```python\nfrom typing import Optional\n\nfrom django.contrib.auth import get_user_model\n\nUser = get_user_model()\n\ndef default_get_user_type(user: User) -> Optional[str]:\n    try:\n        return user.get_attached_profile().user_type\n    except AttributeError:\n        # user might be an AnonymousUser\n        # user_type==None will be treated as a user with no permissions\n        # if you do want to grant AnonymousUser selective permissions then return a placeholder string\n        return None\n```\n\n\n### Unrecognised Permissions\n\nIf `settings.CSV_PERMISSIONS_STRICT` is true then querying a permission\n(or `user_type`) that is not in the CSV will raise a `LookupError`.\n\nThis is not set by default as it prevents the ability to use multiple\nauthentication backends for permission checks. If you are using `csv_permissions`\nexclusively for permission checks then it can be helpful to catch typos.\n\n### Permission Names\n\nBy default `csv_permissions` will use the same permission name format as django: `<app label>.<action>_<model>`\n\nYou can optionally set `settings.CSV_PERMISSIONS_RESOLVE_PERM_NAME` to the fully qualified name of a function to\nresolve permission names to whatever pattern you want.\n\nIn `settings.py`:\n```python\nCSV_PERMISSIONS_RESOLVE_PERM_NAME = \'my_site.auth.resolve_perm_name\'\n```\n\nIn `my_site/auth.py`:\n```python\nfrom typing import Optional\nfrom typing import Type\n\nfrom django.apps import AppConfig\nfrom django.db.models import Model\n\ndef resolve_perm_name(app_config: AppConfig, model: Optional[Type[Model]], action: str, is_global: bool) -> str:\n    # here\'s an implementation that is almost the same as django, but\n    # uses - as a separator instead of _ and .\n    if model is None:\n        return f"{app_config.label}-{action}"\n    else:\n        return f"{app_config.label}-{action}-{model._meta.model_name}"\n\n```\n\nNote the handling of the case where a permission has no model.\nExamples of this can be seen in the `report_outstanding` and `report_popularity`\npermissions in the [sample CSV file](#the-csv-file).\n\n\n### Full Settings Reference\n\n**`CSV_PERMISSIONS_GET_USER_TYPE`**\n\nOptional. Function to get the user type from a user. Defaults to returning `user.user_type`.\n\n[Details](#user-type)\n\n**`CSV_PERMISSIONS_PATHS`** \n\nRequired. List/tuple of CSV file names to use for permissions.\n\nAlternately as a shorthand if you only have one CSV file you can instead set\n`CSV_PERMISSIONS_PATH` to that single file.  \n\n**`CSV_PERMISSIONS_RESOLVE_EVALUATORS`**\n\nRequired. A list/tuple of functions to resolve evaluators, or a string\nthat will be resolved to a module attribute that is expected to contain a\nlist/tuple of functions to resolve evaluators.\n\n[Details](#custom-evaluators)\n\n**`CSV_PERMISSIONS_RESOLVE_PERM_NAME`**\n\nOptional. string or function. Defaults to `\'csv_permissions.permissions.default_resolve_perm_name\'`.\n\n[Details](#permission-names)\n\n**`CSV_PERMISSIONS_STRICT`**\n\nOptional. boolean. Defaults to `False`.\n\nWill cause the CSVPermissionsBackend to throw an error if you try to query an\nunrecognised permission or user type.\n\n[Details](#unrecognised-permissions)\n\n## Changelog\n\nSee [CHANGELOG.md](CHANGELOG.md)\n\n## Development\n\nNote that due to pip/poetry/distutils issues you can\'t easily install dev versions directly from github with pip (works fine with poetry though)\n* https://github.com/python-poetry/poetry/issues/761#issuecomment-521124268\n    * proposed solution appears to work but actually installs the package as "UNKNOWN"\n* https://github.com/python-poetry/poetry/issues/3153#issuecomment-727196619\n    * dephell might have worked but there\'s no homebrew package & it\'s no longer maintained\n\n### Release Process\n\n#### Poetry Config\n* Add test repository\n    * `poetry config repositories.testpypi https://test.pypi.org/legacy/`\n    * Generate an account API token at https://test.pypi.org/manage/account/token/\n    * `poetry config pypi-token.testpypi ${TOKEN}`\n        * On macs this will be stored in the `login` keychain at `poetry-repository-testpypi`\n* Main pypi repository\n    * Generate an account API token at https://pypi.org/manage/account/token/\n    * `poetry config pypi-token.pypi ${TOKEN}`\n        * On macs this will be stored in the `login` keychain at `poetry-repository-pypi`\n\n#### Publishing a New Release\n* Update CHANGELOG.md with details of changes and new version\n* Run `bin/build.py`. This will extract version from CHANGELOG.md, bump version in `pyproject.toml` and generate a build for publishing\n* Tag with new version and update the version branch:\n    * `ver=$( poetry version --short ) && echo "Version: $ver"`\n    * `git tag v/$ver`\n    * `git push --tags`\n* To publish to test.pypi.org\n    * `poetry publish --repository testpypi`\n* To publish to pypi.org\n    * `poetry publish`\n\n\n',
    'author': 'Alliance Software',
    'author_email': 'support@alliancesoftware.com.au',
    'maintainer': None,
    'maintainer_email': None,
    'url': None,
    'packages': packages,
    'package_data': package_data,
    'install_requires': install_requires,
    'python_requires': '>=3.6,<4.0',
}


setup(**setup_kwargs)
