%global _empty_manifest_terminate_build 0 Name: python-flask-taxonomies Version: 7.0.3 Release: 1 Summary: please add a summary manually as the author left a blank one License: Creative Commons Attribution-Noncommercial-Share Alike license URL: https://pypi.org/project/flask-taxonomies/ Source0: https://mirrors.nju.edu.cn/pypi/web/packages/90/97/7f40fc23f026daed1afe657e2ea092dd87e868a25b0dcd722cdf2e4fb820/flask-taxonomies-7.0.3.tar.gz BuildArch: noarch Requires: python3-flask Requires: python3-flask-sqlalchemy Requires: python3-blinker Requires: python3-sqlalchemy-utils Requires: python3-slugify Requires: python3-jsonpatch Requires: python3-webargs Requires: python3-jsonpointer Requires: python3-LinkHeader Requires: python3-flask-principal Requires: python3-luqum Requires: python3-flask-migrate Requires: python3-psycopg2 Requires: python3-pytest Requires: python3-pytest-flask-sqlalchemy Requires: python3-md-toc Requires: python3-isort Requires: python3-check-manifest Requires: python3-pytest-coverage Requires: python3-pytest-pep8 %description # Flask Taxonomies [![](https://img.shields.io/github/license/oarepo/flask-taxonomies.svg)](https://github.com/oarepo/flask-taxonomies/blob/master/LICENSE) [![](https://img.shields.io/travis/oarepo/flask-taxonomies.svg)](https://travis-ci.org/oarepo/flask-taxonomies) [![](https://img.shields.io/coveralls/oarepo/flask-taxonomies.svg)](https://coveralls.io/r/oarepo/flask-taxonomies) [![](https://img.shields.io/pypi/v/flask-taxonomies.svg)](https://pypi.org/pypi/flask-taxonomies) - [Flask Taxonomies](#flask-taxonomies) - [Installation](#installation) - [Terminology](#terminology) - [REST API](#rest-api) - [Retrieving resources](#retrieving-resources) - [``Prefer`` HTTP header](#prefer-http-header) - [Returned representation](#returned-representation) - [Includes and excludes](#includes-and-excludes) - [Including extra data](#including-extra-data) - [Excluding data](#excluding-data) - [Query parameters](#query-parameters) - [Selecting subset of term data](#selecting-subset-of-term-data) - [Pagination](#pagination) - [Taxonomy](#taxonomy) - [Creating](#creating) - [Updating](#updating) - [Replacing via HTTP PUT](#replacing-via-http-put) - [Patching with HTTP POST](#patching-with-http-post) - [Deleting](#deleting) - [Taxonomy Term](#taxonomy-term) - [Creating](#creating-1) - [Updating](#updating-1) - [Deleting](#deleting-1) - [Un-Deleting](#un-deleting) - [Moving](#moving) - [Renaming](#renaming) - [Configuration](#configuration) - [Configuration Variables](#configuration-variables) - [Security](#security) - [Recommended initial settings](#recommended-initial-settings) - [Python API](#python-api) - [Signals](#signals) ## Installation ```bash pip install flask-taxonomies ``` ```python from flask_taxonomies.ext import FlaskTaxonomies from flask_taxonomies.views import blueprint from flask import Flask from flask_principal import Principal app = Flask('__test__') FlaskTaxonomies(app) Principal(app) app.register_blueprint(blueprint, url_prefix=app.config['FLASK_TAXONOMIES_URL_PREFIX']) db = ... from flask_taxonomies.models import Base Base.metadata.create_all(db.engine) ``` ## Terminology **Taxonomy** is a tree of taxonomy terms. It is represented as a database object identified by *code*. A taxonomy may contain its original url (in case the taxonomy is defined elsewhere) and additional metadata as a json object (containing, for example, taxonomy title). It may also contain a default set of selectors for filtering metadata. **TaxonomyTerm** represents a single node in a taxonomy. It is identified by its *slug* and may contain additional metadata as json object. A term can contain children to represent hierarchy of taxonomy terms. Term does not define ordering within children, it is up to application logic to define any ordering. ## REST API The rest API sits on the ``app.config['FLASK_TAXONOMIES_URL_PREFIX']`` url, implicitly ``/api/2.0/taxonomies/``. It follows the REST API principles with pagination inspired by GitHub API. ### Retrieving resources #### ``Prefer`` HTTP header Implicitly, the API returns rather minimal representation. The amount of the returned metadata can be changed via HTTP ``prefer`` header or alternatively by query parameters. ##### Returned representation The ``prefer`` header is a standard way of telling what you expect to get as a response to the request. It is defined in [rfc7240](https://tools.ietf.org/html/rfc7240). If the header is not present, ``return=representation`` is assumed. One can specify ``return=minimal`` to obtain minimal dataset, or other return types (even your own) defined in ``FLASK_TAXONOMIES_REPRESENTATION`` config. **return=minimal** returns the minimal representation. Mostly not usable directly as it does not return any metadata, just the code/slug. *Listing:* ```console $ curl -i -H "Prefer: return=minimal" http://127.0.0.1:5000/api/2.0/taxonomies/ HTTP/1.0 200 OK Link: ; rel=self [ { "code": "country" } ] ``` *Get taxonomy:* ```console $ curl -i -H "Prefer: return=minimal" http://127.0.0.1:5000/api/2.0/taxonomies/country HTTP/1.0 200 OK Content-Type: application/json { "code": "country" } ``` *Get term:* ```console $ curl -i -H "Prefer: return=minimal" http://127.0.0.1:5000/api/2.0/taxonomies/country/europe HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "slug": "europe" } ``` **return=representation** this is the default return type. Returns all user data declared on taxonomy/term together with ancestor and urls. For example: *Listing:* ```console $ curl -i http://127.0.0.1:5000/api/2.0/taxonomies/ HTTP/1.0 200 OK Link: ; rel=self [ { "code": "country", "links": { "custom": "https://www.kaggle.com/nikitagrec/world-capitals-gps/data", "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/" }, "title": "List of countries" } ] ``` *Get term:* ```console $ curl -i http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "CapitalLatitude": "50.083333333333336", "CapitalLongitude": "14.466667", "CapitalName": "Prague", "ContinentName": "Europe", "CountryCode": "CZ", "CountryName": "Czech Republic", "ancestors": [ { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe" } } ], "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz" } } ``` ``Europe`` has no user data, so it contains only the ``links`` section. ##### Includes and excludes The returned representation can be modified by specifying which metadata should be included/excluded. Currently supported includes/excludes are: ```python INCLUDE_URL = 'url' INCLUDE_DESCENDANTS_URL = 'drl' INCLUDE_DESCENDANTS_COUNT = 'dcn' INCLUDE_ANCESTORS_HIERARCHY = 'anh' INCLUDE_ANCESTORS = 'anc' INCLUDE_ANCESTOR_LIST = 'anl' INCLUDE_DATA = 'data' INCLUDE_ID = 'id' INCLUDE_DESCENDANTS = 'dsc' INCLUDE_ENVELOPE='env' INCLUDE_DELETED = 'del' INCLUDE_SLUG = 'slug' INCLUDE_LEVEL = 'lvl' INCLUDE_STATUS = 'sta' ``` ###### Including extra data Examples: **Include record url in response** ```console $ curl -i -H "Prefer: return=minimal; include=url" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe" }, "slug": "europe" } ``` Adds a ``links`` section to payload with record url (``"self":``) **Include descendants url in response** ```console $ curl -i -H "Prefer: return=minimal; include=url drl" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe", "tree": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe?representation:include=dsc" }, "slug": "europe" } ``` Adds a ``links`` section to payload with recoord url with descendants (``"tree":``) **Include descendants count in response** ```console $ curl -i -H "Prefer: return=minimal; include=dcn" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "slug": "europe", "descendants_count": 58 } ``` Adds a ``descendant_count`` with a number of descendant terms under the term. The value is 0 if the term is a leaf term. On taxonomy, returns the total number of terms in taxonomy: ```console $ curl -i -H "Prefer: return=minimal; include=dcn" \ http://127.0.0.1:5000/api/2.0/taxonomies/country HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "code": "country", "descendants_count": 253 } ``` **Include ancestors with hierarchy in response** ```console $ curl -i -H "Prefer: return=minimal; include=anh url" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz { "children": [ { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz" }, "slug": "europe/cz" } ], "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe" }, "slug": "europe", "ancestor": true } ``` If the term has ancestors, they are serialized and the term is included as their child. This is useful for example when showing the taxonomy in tree form - the rendering mechanism for the tree will stay the same. All ancestor terms are marked with ``ancestor=true`` flag to help with ui rendering (for example to gray ancestors). Adding url as well is recommended to get urls of ancestors. **Include ancestors without hierarchy in response** ```console $ curl -i -H "Prefer: return=minimal; include=anc url" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "ancestors": [ { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe" }, "slug": "europe" } ], "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz" }, "slug": "europe/cz" } ``` The ancestors are rendered inside the ``ancestors`` element. Adding url as well is recommended to get urls of ancestors. **Include ancestor list in response** ```console $ curl -i -H "Prefer: return=representation; include=anl ant par" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree [ { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe" }, "is_ancestor": true }, { "CapitalLatitude": "50.083333333333336", "CapitalLongitude": "14.466667", "CapitalName": "Prague", "ContinentName": "Europe", "CountryCode": "CZ", "CountryName": "Czech Republic", "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz", "parent": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe" }, "is_ancestor": false } ] ``` The ancestors are rendered on the same level as the term. This rendering might be used for example when serializing the taxonomy term to elasticsearch - this way all the ancestors are serialized within one array and indexed into one object in ES. Note also the: * ``ant`` - adds ``ancestor`` field (``false`` if this is the term that has been queried), ``true`` for ancestor term * ``parent`` - adds link to the parent within ``links`` section Even one-term result is rendered as array: ```console $ curl -i -H "Prefer: return=representation; include=anl ant par" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree [ { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe" }, "is_ancestor": false } ] ``` **Include data in response** This is the default setting unless ``minimal`` representation is selected. In this case, pass ``include=data`` to have data included. ```console $ curl -i -H "Prefer: return=minimal; include=data" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "CapitalLatitude": "50.083333333333336", "CapitalLongitude": "14.466667", "CapitalName": "Prague", "ContinentName": "Europe", "CountryCode": "CZ", "CountryName": "Czech Republic", "slug": "europe/cz" } ``` **Include id in response** Use ``include=id`` to get the internal id included. This is rarely needed as API does not accept this id at all. **Include descendant terms in response** To serialize descendants into the response, use ``include=dsc``: ```console $ curl -i -H "Prefer: return=minimal; include=dsc" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "children": [ { "slug": "europe/ad" }, { "slug": "europe/al" }, ... { "slug": "europe/va" } ], "slug": "europe" } ``` **Include slug in response** Adds ``slug`` to response. In the ``minimal`` mode the slug is added automatically, use this tag to add it in ``return=representation``: ```console $ curl -i -H "Prefer: return=representation; include=slug" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "CapitalLatitude": "50.083333333333336", "CapitalLongitude": "14.466667", "CapitalName": "Prague", "ContinentName": "Europe", "CountryCode": "CZ", "CountryName": "Czech Republic", "ancestors": [ { "links":{"self":"http://127.0.0.1:5000/api/2.0/taxonomies/country/europe"}, "slug": "europe" } ], "links":{"self":"http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz"}, "slug": "europe/cz" } ``` **Include hierarchy level in response** Adds hierarchy level to taxonomy term. Top-level terms have ``level=1``, taxonomy ``0``. ```console $ curl -i -H "Prefer: return=minimal; include=lvl" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "level": 2, "slug": "europe/cz" } ``` **Include deleted terms in response** Let's delete a country from Europe: ```console $ curl -X DELETE -i http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/gb HTTP/1.0 200 OK Content-Type: application/json { "CapitalLatitude": "51.5", "CapitalLongitude": "-0.083333", "CapitalName": "London", "ContinentName": "Europe", "CountryCode": "GB", "CountryName": "United Kingdom", "links":{"self":"http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/gb"} } ``` United Kingdom has indeed been removed from Europe: ```console $ curl -i http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/gb HTTP/1.0 410 GONE { "message": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/gb was not found on the server", "reason": "deleted" } ``` Now run the GET again with removed terms included: ```console $ curl -i -H "Prefer: return=minimal; include=del sta" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/gb HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "slug": "europe/gb", "status": "deleted", "busy_count":0, "descendants_busy_count":0 } ``` **Include term status** Including the status will add the following metadata: * ``status`` of the term (alive, delete_pending, deleted, moved) * ``busy_count`` - an integer saying how "busy" the term is. Being busy means that a potentially destructive operation (such as deleting, moving or renaming slug) is in progress. * ``descendants_busy_count`` - a number of descendants that are busy ```console $ curl -i -H "Prefer: return=minimal; include=del sta dsc" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "busy_count":0, "children": [ { "busy_count":0, "descendants_busy_count":0, "slug": "europe/ad", "status": "alive" }, ... { "busy_count":0, "descendants_busy_count":0, "slug": "europe/gb", "status": "deleted" }, ... { "busy_count":0, "descendants_busy_count":0, "slug": "europe/va", "status": "alive" } ], "descendants_busy_count":0, "slug": "europe", "status": "alive" } ``` ###### Excluding data To exclude data from the representation, use ``exclude=...`` in prefer header. #### Query parameters Values from the ``prefer`` header can be used as query parameters: ``` curl -i -H "Prefer: return=minimal;" \ "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe? representation:include=sta,url&representation:exclude=slug" HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe" }, "status": "alive" } ``` #### Selecting subset of term data Use ``select= ...`` in ``prefer`` header or ``representation:select=`` query parameter to select just part of user data: ```console $ curl -i -H "Prefer: return=representation;select=/CapitalName /CountryCode" \ "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz" HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "CapitalName": "Prague", "CountryCode": "CZ", "ancestors": [ { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe" } } ], "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz" } } ``` #### Maximum levels When descendants are selected, a maximum level of descendants can be specified via ``levels=`` part of ``prefer`` header (or representation:levels=n in the query). Example: ```console $ curl -i -H "Prefer: return=representation; levels=1" \ "http://127.0.0.1:5000/api/2.0/taxonomies/country" HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { 'children': [ {'slug': 'africa'}, {'slug': 'antarctica'}, {'slug': 'asia'}, {'slug': 'australia'}, {'slug': 'central-america'}, {'slug': 'europe'}, {'slug': 'north-america'}, {'slug': 'south-america'} ], 'code': 'country', 'title': 'List of countries' } ``` #### Pagination If descendants are requested without further arguments the whole tree is returned (well, in fact at most FLASK_TAXONOMIES_MAX_RESULTS_RETURNED terms to prevent server crash). This leads to high amount of data transferred and possibly a client crash. To prevent this, pagination should be used on larger taxonomies. Specify ``size`` argument to have at most this number of taxonomy terms returned. For example: ```console $ curl -i -H "Prefer: return=minimal;" \ "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe?representation:include=dsc&size=5" HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree X-Page: 1 X-PageSize: 5 X-Total: 58 { "children": [ { "slug": "europe/ad" }, { "slug": "europe/al" }, { "slug": "europe/am" }, { "slug": "europe/at" } ], "slug": "europe" } ``` This returns 5 terms - europe and 4 children. To return the next page, add ``page=2`` argument: ```console $ curl -i -H "Prefer: return=minimal;" \ "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe?representation:include=dsc&size=5&page=4" HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree X-Page: 4 X-PageSize: 5 X-Total: 58 [ { "slug": "europe/dk" }, { "slug": "europe/ee" }, { "slug": "europe/es" }, { "slug": "europe/fi" }, { "slug": "europe/fo" } ] ``` Note that the first page contains the root element and the second does not. To fix it, either use ``include=anh`` to always get hierarchical representation: ```console $ curl -i -H "Prefer: return=minimal;" \ "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe?representation:include=dsc,anh&size=5&page=2" HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree X-Page: 2 X-PageSize: 5 X-Total: 58 { "ancestor": true, "children": [ { "slug": "europe/ax" }, { "slug": "europe/az" }, { "slug": "europe/ba" }, { "slug": "europe/be" } ], "slug": "europe" } ``` or, if interested only in descendants and not the node itself, ``exclude=self`` ```console $ curl -i -H "Prefer: return=minimal;" \ "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe? representation:include=dsc&representation:exclude=self&size=5&page=1" HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree X-Page: 1 X-PageSize: 5 X-Total: 58 [ { "slug": "europe/ad" }, { "slug": "europe/al" }, { "slug": "europe/am" }, { "slug": "europe/at" }, { "slug": "europe/ax" } ] ``` #### Searching Use ``q=`` parameter to search within terms. Returns all the resources whose ``metadata`` contain the expression in q. ##### Simple query If ``q`` is a simple string not containing ':' or string in quotes, it is interpreted as a string that must be present in any of values inside the json. The current implementation is dependent on the database backend and might perform sub-optimal ``ilike %x%`` query on textified json. ```console $ curl -i -H "Prefer: return=minimal; include=data dsc; exclude=self" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe?q=Prague HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree [ { "CapitalLatitude": "50.083333333333336", "CapitalLongitude": "14.466667", "CapitalName": "Prague", "ContinentName": "Europe", "CountryCode": "CZ", "CountryName": "Czech Republic", "slug": "europe/cz" } ] ``` ##### Lucene-like query If the ``q`` contains a ':' character not enclosed in quotes, it is parsed as a query in lucene syntax, with the following allowed constructs: * path:value for matching value at the given path * a.b.c:value for representing nested paths * AND, OR, NOT, brackets The query will be executed if the database or search backend support it. If not supported, HTTP 501 will be returned. ### Taxonomy #### Creating To create a taxonomy, either use HTTP PUT: ```console $ curl -i -X PUT 'http://127.0.0.1:5000/api/2.0/taxonomies/test' \ -H 'Content-Type: application/json' \ --data-raw '{ "title": "Test taxonomy" }' HTTP/1.0 201 CREATED Link: ; rel=self Link: ; rel=tree Location: http://127.0.0.1:5000/api/2.0/taxonomies/test/ { "code": "test", "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test/" }, "title": "Test taxonomy" } ``` Or HTTP POST with ``code`` in the payload ```console $ curl -i -X POST 'http://127.0.0.1:5000/api/2.0/taxonomies/' \ -H 'Content-Type: application/json' --data-raw '{ "title": "Test taxonomy 1", "code": "test1" }' HTTP/1.0 201 CREATED Link: ; rel=self Link: ; rel=tree Location: http://127.0.0.1:5000/api/2.0/taxonomies/test1/ { "code": "test1", "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test1/" }, "title": "Test taxonomy 1" } ``` #### Updating ##### Replacing via HTTP PUT ```console $ curl -i -X PUT 'http://127.0.0.1:5000/api/2.0/taxonomies/test' \ --header 'Content-Type: application/json' --data-raw '{ "title": "Test taxonomy updated" }' HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "code": "test", "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test/" }, "title": "Test taxonomy updated" } ``` Note that terms are not updated nor removed when taxonomy metadata are updated. ##### Patching with HTTP PATCH ```console $ curl -i -X PATCH 'http://127.0.0.1:5000/api/2.0/taxonomies/test' \ --header 'Content-Type: application/json' --data-raw '[{ "op": "replace", "path": "/title", "value": "Test taxonomy updated via patch" }]' HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "code": "test", "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test/" }, "title": "Test taxonomy updated via patch" } ``` #### Deleting ```console $ curl -i -X DELETE 'http://127.0.0.1:5000/api/2.0/taxonomies/test1' HTTP/1.0 204 NO CONTENT Content-Type: text/html; charset=utf-8 ``` ### Taxonomy Term #### Creating As in creating taxonomy, term can be created either via HTTP PUT: ```console $ curl -i -X PUT 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term' \ --header 'Content-Type: application/json' --data-raw '{ "title": "Test Term" }' HTTP/1.0 201 CREATED Link: ; rel=self Link: ; rel=tree Location: http://127.0.0.1:5000/api/2.0/taxonomies/test/term { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test/term" }, "title": "Test Term" } ``` or POST: ```console $ curl -i -X POST 'http://127.0.0.1:5000/api/2.0/taxonomies/test' \ --header 'Content-Type: application/json' --data-raw '{ "title": "Test term 1", "slug": "term1" }' HTTP/1.0 201 CREATED Link: ; rel=self Link: ; rel=tree Location: http://127.0.0.1:5000/api/2.0/taxonomies/test/term1 { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test/term1" }, "title": "Test term 1" } ``` As PUT/POST operation also mean updating term if slug exists, to be sure that you are creating a new one use ``If-None-Match: '*'`` header: ```console $ curl -i -X PUT 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term' \ --header 'Content-Type: application/json' \ --header 'If-None-Match: '*'' \ --data-raw '{ "title": "Test Term" }' HTTP/1.0 412 Precondition Failed { "message": "The taxonomy already contains a term on this slug. As If-None-Match: '*' has been requested, not modifying the term", "reason": "term-exists" } ``` Terms can be created within terms via HTTP PUT: ```console $ curl -i -X PUT 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term/nested' \ --header 'Content-Type: application/json' --data-raw '{ "title": "Nested Term" }' HTTP/1.0 201 CREATED Link: ; rel=self Link: ; rel=tree Location: http://127.0.0.1:5000/api/2.0/taxonomies/test/term/nested { "ancestors":[ { "links":{ "self":"http://127.0.0.1:5000/api/2.0/taxonomies/test/term" }, "title":"Test Term" } ], "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test/term/nested" }, "title": "Nested Term" } ``` or POST: ```console $ curl -i -X POST 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term1' \ --header 'Content-Type: application/json' --data-raw '{ "title": "Test nested term 1", "slug": "nested1" }' HTTP/1.0 201 CREATED Link: ; rel=self Link: ; rel=tree Location: http://127.0.0.1:5000/api/2.0/taxonomies/test/term1/nested1 { "ancestors":[ { "links":{ "self":"http://127.0.0.1:5000/api/2.0/taxonomies/test/term1" }, "title":"Test term 1" } ], "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test/term1/nested1" }, "title": "Test nested term 1" } ``` #### Updating As for taxonomy, use HTTP ``PUT``: ```console $ curl -i -X PUT 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term' \ --header 'Content-Type: application/json' --data-raw '{ "title": "Test Term updated" }' HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test/term" }, "title": "Test Term updated" } ``` or ``PATCH``: ```console $ curl -i -X PATCH 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term' \ --header 'Content-Type: application/json' --data-raw '[{ "op": "replace", "path": "/title", "value": "Test taxonomy term updated via patch" }]' HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test/term" }, "title": "Test taxonomy term updated via patch" } ``` As PUT operation also means creating term if slug does not exist, to be sure that you are just updating use ``If-Match: '*'`` header: ```console $ curl -i -X PUT 'http://127.0.0.1:5000/api/2.0/taxonomies/test/unknown' \ --header 'Content-Type: application/json' \ --header 'If-Match: '*'' \ --data-raw '{ "title": "Test Term" }' HTTP/1.0 412 Precondition Failed { "message": "The taxonomy does not contain a term on this slug. As If-Match: '*' has been requested, not creating a new term", "reason": "term-does-not-exist" } ``` #### Deleting Use HTTP delete to remove a term. The removed term will be returned in the response: ```console $ curl -i -X DELETE 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term1' HTTP/1.0 200 OK Content-Type: application/json { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test/term1" }, "title": "Test term 1" } ``` Subsequent GET returns 410: ```console $ curl -i 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term1' HTTP/1.0 410 GONE { "message": "http://127.0.0.1:5000/api/2.0/taxonomies/test/term1 was not found on the server", "reason": "deleted" } ``` But the term stays on the server: ```console $ curl -i 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term?representation:include=del' HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test/term" }, "title": "Test taxonomy term updated via patch" } ``` #### Un-Deleting To salvage a deleted term, update it via PATCH, with an empty set of operations to keep it unmodified: ```console $ curl -i -X PATCH -H "Prefer: return=minimal; include=del" \ 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term' \ --header 'Content-Type: application/json' --data-raw '[]' HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "slug": "term" } ``` #### Moving Use HTTP post with content type ``application/vnd.move`` and ``Destination`` header: ```console $ curl -i -X POST \ -H 'Content-Type: application/vnd.move' \ -H "Destination: /" \ 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term/nested' HTTP/1.0 200 OK { "links":{ "self":"http://127.0.0.1:5000/api/2.0/taxonomies/test/nested" }, "title":"Nested Term" } ``` The original url returns 301: ```console $ curl -i 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term/nested' HTTP/1.0 301 MOVED PERMANENTLY Location: http://127.0.0.1:5000/api/2.0/taxonomies/test/nested Link: ; rel=self Link: ; rel=obsoleted_by { "links": { "self": "http://localhost/api/2.0/taxonomies/test/term/nested", "obsoleted_by": "http://localhost/api/2.0/taxonomies/test/nested" }, "status": "moved" } ``` #### Renaming Use HTTP post with content type ``application/vnd.move`` and ``Rename`` header: ```console $ curl -i -X POST \ -H 'Content-Type: application/vnd.move' \ -H "Rename: renamed-nested" \ 'http://127.0.0.1:5000/api/2.0/taxonomies/test/nested' HTTP/1.0 200 OK { "links":{ "self":"http://127.0.0.1:5000/api/2.0/taxonomies/test/renamed-nested" }, "title":"Nested Term" } ``` The original url returns 301: ```console $ curl -i 'http://127.0.0.1:5000/api/2.0/taxonomies/test/nested' HTTP/1.0 301 MOVED PERMANENTLY Location: http://127.0.0.1:5000/api/2.0/taxonomies/test/renamed-nested Link: ; rel=self Link: ; rel=obsoleted_by { "links": { "self": "http://localhost/api/2.0/taxonomies/test/nested", "obsoleted_by": "http://localhost/api/2.0/taxonomies/test/renamed-nested" }, "status": "moved" } ``` ## Configuration ### Configuration Variables ``FLASK_TAXONOMIES_SERVER_NAME`` Server name hosting the taxonomies. If not set, SERVER_NAME is used. ``FLASK_TAXONOMIES_SERVER_SCHEME`` Scheme to use in generated urls, defaults to ``https`` ``FLASK_TAXONOMIES_URL_PREFIX`` A prefix on which taxonomies are served, defaults to ``/api/2.0/taxonomies/`` ``FLASK_TAXONOMIES_REPRESENTATION`` Values for ``Prefer: return=`` header. ``minimal`` and ``representation`` are obligatory, you are free to add other return representations. ```python from flask_taxonomies.constants import * FLASK_TAXONOMIES_REPRESENTATION = { 'minimal': { 'include': [INCLUDE_SLUG, INCLUDE_SELF], 'exclude': [], 'select': None, 'options': {} }, 'representation': { 'include': [INCLUDE_DATA, INCLUDE_ANCESTORS, INCLUDE_URL, INCLUDE_SELF], 'exclude': [], 'select': None, 'options': {} }, 'full': { 'include': [INCLUDE_DATA, INCLUDE_ANCESTORS, INCLUDE_URL, INCLUDE_DESCENDANTS_URL, INCLUDE_SELF], 'exclude': [], 'select': None, 'options': {} } } ``` ``FLASK_TAXONOMIES_MAX_RESULTS_RETURNED`` Specifies max results returned when pagination is not used. Defaults to ``10000``. ### Security Flask taxonomies uses ``flask-principal`` to handle security. The default permissions are that everyone is allowed to read/create/update/delete/move all taxonomies and terms. To restrict the access, specify permission factories (a function that returns a list of permission) for each operation. ``FLASK_TAXONOMIES_PERMISSION_FACTORIES`` A dictionary of operation to a list of permissions. ``` FLASK_TAXONOMIES_PERMISSION_FACTORIES = { 'taxonomy_list': request -> List[Permission] 'taxonomy_read': request, taxonomy -> List[Permission] 'taxonomy_create': request, code -> List[Permission] 'taxonomy_update': request, taxonomy -> List[Permission] 'taxonomy_delete': request, taxonomy -> List[Permission], 'taxonomy_term_read': request, taxonomy, slug -> List[Permission] 'taxonomy_term_create': request, taxonomy, slug -> List[Permission] 'taxonomy_term_update': request, taxonomy, term -> List[Permission] 'taxonomy_term_delete': request, taxonomy, term -> List[Permission], 'taxonomy_term_move': request, taxonomy, term, destination, rename -> List[Permission], } ``` The right-hand side can be either a list/tuple of permissions, function with the above-mentioned signatures or a string pointing to the implementation. The string form is resolved on the first request. If ``.can`` on any of the permissions returns True or the list is empty, access is granted. #### Recommended initial settings The recommended initial settings are read-only for everyone except admin role: ```python from flask_principal import RoleNeed FLASK_TAXONOMIES_PERMISSION_FACTORIES = { 'taxonomy_create': [RoleNeed('admin')], 'taxonomy_update': [RoleNeed('admin')], 'taxonomy_delete': [RoleNeed('admin')], 'taxonomy_term_create': [RoleNeed('admin')], 'taxonomy_term_update': [RoleNeed('admin')], 'taxonomy_term_delete': [RoleNeed('admin')], 'taxonomy_term_move': [RoleNeed('admin')] } ``` ## Python API The calls below use ``session`` as an optional parameter. If not supplied, session from current_app is used. ``TermIdentification`` is a class to identify taxonomy term, binding taxonomy (or its code), slug or a term instance. See [flask_taxonomies/term_identification.py](./flask_taxonomies/term_identification.py) for details. ```python from flask_taxonomies.proxies import current_flask_taxonomies # returns a taxonomy list current_flask_taxonomies.list_taxonomies(session=None) # returns a taxonomy with the given code. Fails by default if not found current_flask_taxonomies.get_taxonomy(code, fail=True, session=None) # creates a new taxonomy current_flask_taxonomies.create_taxonomy(code, extra_data=None, url=None, select=None, session=None) # updates a taxonomy current_flask_taxonomies.update_taxonomy( taxonomy: [Taxonomy, str], extra_data, url=MISSING, select=MISSING, session=None) # deletes a taxonomy current_flask_taxonomies.delete_taxonomy(taxonomy: Taxonomy, session=None) # lists terms within the taxonomy. current_flask_taxonomies.list_taxonomy(taxonomy: [Taxonomy, str], levels=None, status_cond=TaxonomyTerm.status == TermStatusEnum.alive, order=True, session=None) # creates a new term inside a taxonomy current_flask_taxonomies.create_term(ti: TermIdentification, extra_data=None, session=None) # updates a term, setting or patching extra_data current_flask_taxonomies.update_term(ti: [TaxonomyTerm, TermIdentification], status_cond=TaxonomyTerm.status == TermStatusEnum.alive, extra_data=None, patch=False, status=MISSING, session=None) # returns all descendants of a term current_flask_taxonomies.descendants(ti: TermIdentification, levels=None, status_cond=TaxonomyTerm.status == TermStatusEnum.alive, order=True, session=None) # returns a term and its descendants current_flask_taxonomies.descendants_or_self(ti: TermIdentification, levels=None, status_cond=TaxonomyTerm.status == TermStatusEnum.alive, order=True, session=None) # returns all ancestors of a term current_flask_taxonomies.ancestors(ti: TermIdentification, status_cond=TaxonomyTerm.status == TermStatusEnum.alive, session=None) # returns term and its ancestors current_flask_taxonomies.ancestors_or_self(ti: TermIdentification, status_cond=TaxonomyTerm.status == TermStatusEnum.alive, session=None) # removes a term current_flask_taxonomies.delete_term(ti: TermIdentification, remove_after_delete=True, session=None) # renames term's slug current_flask_taxonomies.rename_term(ti: TermIdentification, new_slug=None, remove_after_delete=True, session=None) # moves term into a new parent within the same taxonomy current_flask_taxonomies.move_term(ti: TermIdentification, new_parent=None, remove_after_delete=True, session=None) ``` ### Signals See [flask_taxonomies/signals.py](flask_taxonomies/signals.py) for details %package -n python3-flask-taxonomies Summary: please add a summary manually as the author left a blank one Provides: python-flask-taxonomies BuildRequires: python3-devel BuildRequires: python3-setuptools BuildRequires: python3-pip %description -n python3-flask-taxonomies # Flask Taxonomies [![](https://img.shields.io/github/license/oarepo/flask-taxonomies.svg)](https://github.com/oarepo/flask-taxonomies/blob/master/LICENSE) [![](https://img.shields.io/travis/oarepo/flask-taxonomies.svg)](https://travis-ci.org/oarepo/flask-taxonomies) [![](https://img.shields.io/coveralls/oarepo/flask-taxonomies.svg)](https://coveralls.io/r/oarepo/flask-taxonomies) [![](https://img.shields.io/pypi/v/flask-taxonomies.svg)](https://pypi.org/pypi/flask-taxonomies) - [Flask Taxonomies](#flask-taxonomies) - [Installation](#installation) - [Terminology](#terminology) - [REST API](#rest-api) - [Retrieving resources](#retrieving-resources) - [``Prefer`` HTTP header](#prefer-http-header) - [Returned representation](#returned-representation) - [Includes and excludes](#includes-and-excludes) - [Including extra data](#including-extra-data) - [Excluding data](#excluding-data) - [Query parameters](#query-parameters) - [Selecting subset of term data](#selecting-subset-of-term-data) - [Pagination](#pagination) - [Taxonomy](#taxonomy) - [Creating](#creating) - [Updating](#updating) - [Replacing via HTTP PUT](#replacing-via-http-put) - [Patching with HTTP POST](#patching-with-http-post) - [Deleting](#deleting) - [Taxonomy Term](#taxonomy-term) - [Creating](#creating-1) - [Updating](#updating-1) - [Deleting](#deleting-1) - [Un-Deleting](#un-deleting) - [Moving](#moving) - [Renaming](#renaming) - [Configuration](#configuration) - [Configuration Variables](#configuration-variables) - [Security](#security) - [Recommended initial settings](#recommended-initial-settings) - [Python API](#python-api) - [Signals](#signals) ## Installation ```bash pip install flask-taxonomies ``` ```python from flask_taxonomies.ext import FlaskTaxonomies from flask_taxonomies.views import blueprint from flask import Flask from flask_principal import Principal app = Flask('__test__') FlaskTaxonomies(app) Principal(app) app.register_blueprint(blueprint, url_prefix=app.config['FLASK_TAXONOMIES_URL_PREFIX']) db = ... from flask_taxonomies.models import Base Base.metadata.create_all(db.engine) ``` ## Terminology **Taxonomy** is a tree of taxonomy terms. It is represented as a database object identified by *code*. A taxonomy may contain its original url (in case the taxonomy is defined elsewhere) and additional metadata as a json object (containing, for example, taxonomy title). It may also contain a default set of selectors for filtering metadata. **TaxonomyTerm** represents a single node in a taxonomy. It is identified by its *slug* and may contain additional metadata as json object. A term can contain children to represent hierarchy of taxonomy terms. Term does not define ordering within children, it is up to application logic to define any ordering. ## REST API The rest API sits on the ``app.config['FLASK_TAXONOMIES_URL_PREFIX']`` url, implicitly ``/api/2.0/taxonomies/``. It follows the REST API principles with pagination inspired by GitHub API. ### Retrieving resources #### ``Prefer`` HTTP header Implicitly, the API returns rather minimal representation. The amount of the returned metadata can be changed via HTTP ``prefer`` header or alternatively by query parameters. ##### Returned representation The ``prefer`` header is a standard way of telling what you expect to get as a response to the request. It is defined in [rfc7240](https://tools.ietf.org/html/rfc7240). If the header is not present, ``return=representation`` is assumed. One can specify ``return=minimal`` to obtain minimal dataset, or other return types (even your own) defined in ``FLASK_TAXONOMIES_REPRESENTATION`` config. **return=minimal** returns the minimal representation. Mostly not usable directly as it does not return any metadata, just the code/slug. *Listing:* ```console $ curl -i -H "Prefer: return=minimal" http://127.0.0.1:5000/api/2.0/taxonomies/ HTTP/1.0 200 OK Link: ; rel=self [ { "code": "country" } ] ``` *Get taxonomy:* ```console $ curl -i -H "Prefer: return=minimal" http://127.0.0.1:5000/api/2.0/taxonomies/country HTTP/1.0 200 OK Content-Type: application/json { "code": "country" } ``` *Get term:* ```console $ curl -i -H "Prefer: return=minimal" http://127.0.0.1:5000/api/2.0/taxonomies/country/europe HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "slug": "europe" } ``` **return=representation** this is the default return type. Returns all user data declared on taxonomy/term together with ancestor and urls. For example: *Listing:* ```console $ curl -i http://127.0.0.1:5000/api/2.0/taxonomies/ HTTP/1.0 200 OK Link: ; rel=self [ { "code": "country", "links": { "custom": "https://www.kaggle.com/nikitagrec/world-capitals-gps/data", "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/" }, "title": "List of countries" } ] ``` *Get term:* ```console $ curl -i http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "CapitalLatitude": "50.083333333333336", "CapitalLongitude": "14.466667", "CapitalName": "Prague", "ContinentName": "Europe", "CountryCode": "CZ", "CountryName": "Czech Republic", "ancestors": [ { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe" } } ], "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz" } } ``` ``Europe`` has no user data, so it contains only the ``links`` section. ##### Includes and excludes The returned representation can be modified by specifying which metadata should be included/excluded. Currently supported includes/excludes are: ```python INCLUDE_URL = 'url' INCLUDE_DESCENDANTS_URL = 'drl' INCLUDE_DESCENDANTS_COUNT = 'dcn' INCLUDE_ANCESTORS_HIERARCHY = 'anh' INCLUDE_ANCESTORS = 'anc' INCLUDE_ANCESTOR_LIST = 'anl' INCLUDE_DATA = 'data' INCLUDE_ID = 'id' INCLUDE_DESCENDANTS = 'dsc' INCLUDE_ENVELOPE='env' INCLUDE_DELETED = 'del' INCLUDE_SLUG = 'slug' INCLUDE_LEVEL = 'lvl' INCLUDE_STATUS = 'sta' ``` ###### Including extra data Examples: **Include record url in response** ```console $ curl -i -H "Prefer: return=minimal; include=url" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe" }, "slug": "europe" } ``` Adds a ``links`` section to payload with record url (``"self":``) **Include descendants url in response** ```console $ curl -i -H "Prefer: return=minimal; include=url drl" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe", "tree": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe?representation:include=dsc" }, "slug": "europe" } ``` Adds a ``links`` section to payload with recoord url with descendants (``"tree":``) **Include descendants count in response** ```console $ curl -i -H "Prefer: return=minimal; include=dcn" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "slug": "europe", "descendants_count": 58 } ``` Adds a ``descendant_count`` with a number of descendant terms under the term. The value is 0 if the term is a leaf term. On taxonomy, returns the total number of terms in taxonomy: ```console $ curl -i -H "Prefer: return=minimal; include=dcn" \ http://127.0.0.1:5000/api/2.0/taxonomies/country HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "code": "country", "descendants_count": 253 } ``` **Include ancestors with hierarchy in response** ```console $ curl -i -H "Prefer: return=minimal; include=anh url" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz { "children": [ { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz" }, "slug": "europe/cz" } ], "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe" }, "slug": "europe", "ancestor": true } ``` If the term has ancestors, they are serialized and the term is included as their child. This is useful for example when showing the taxonomy in tree form - the rendering mechanism for the tree will stay the same. All ancestor terms are marked with ``ancestor=true`` flag to help with ui rendering (for example to gray ancestors). Adding url as well is recommended to get urls of ancestors. **Include ancestors without hierarchy in response** ```console $ curl -i -H "Prefer: return=minimal; include=anc url" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "ancestors": [ { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe" }, "slug": "europe" } ], "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz" }, "slug": "europe/cz" } ``` The ancestors are rendered inside the ``ancestors`` element. Adding url as well is recommended to get urls of ancestors. **Include ancestor list in response** ```console $ curl -i -H "Prefer: return=representation; include=anl ant par" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree [ { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe" }, "is_ancestor": true }, { "CapitalLatitude": "50.083333333333336", "CapitalLongitude": "14.466667", "CapitalName": "Prague", "ContinentName": "Europe", "CountryCode": "CZ", "CountryName": "Czech Republic", "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz", "parent": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe" }, "is_ancestor": false } ] ``` The ancestors are rendered on the same level as the term. This rendering might be used for example when serializing the taxonomy term to elasticsearch - this way all the ancestors are serialized within one array and indexed into one object in ES. Note also the: * ``ant`` - adds ``ancestor`` field (``false`` if this is the term that has been queried), ``true`` for ancestor term * ``parent`` - adds link to the parent within ``links`` section Even one-term result is rendered as array: ```console $ curl -i -H "Prefer: return=representation; include=anl ant par" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree [ { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe" }, "is_ancestor": false } ] ``` **Include data in response** This is the default setting unless ``minimal`` representation is selected. In this case, pass ``include=data`` to have data included. ```console $ curl -i -H "Prefer: return=minimal; include=data" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "CapitalLatitude": "50.083333333333336", "CapitalLongitude": "14.466667", "CapitalName": "Prague", "ContinentName": "Europe", "CountryCode": "CZ", "CountryName": "Czech Republic", "slug": "europe/cz" } ``` **Include id in response** Use ``include=id`` to get the internal id included. This is rarely needed as API does not accept this id at all. **Include descendant terms in response** To serialize descendants into the response, use ``include=dsc``: ```console $ curl -i -H "Prefer: return=minimal; include=dsc" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "children": [ { "slug": "europe/ad" }, { "slug": "europe/al" }, ... { "slug": "europe/va" } ], "slug": "europe" } ``` **Include slug in response** Adds ``slug`` to response. In the ``minimal`` mode the slug is added automatically, use this tag to add it in ``return=representation``: ```console $ curl -i -H "Prefer: return=representation; include=slug" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "CapitalLatitude": "50.083333333333336", "CapitalLongitude": "14.466667", "CapitalName": "Prague", "ContinentName": "Europe", "CountryCode": "CZ", "CountryName": "Czech Republic", "ancestors": [ { "links":{"self":"http://127.0.0.1:5000/api/2.0/taxonomies/country/europe"}, "slug": "europe" } ], "links":{"self":"http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz"}, "slug": "europe/cz" } ``` **Include hierarchy level in response** Adds hierarchy level to taxonomy term. Top-level terms have ``level=1``, taxonomy ``0``. ```console $ curl -i -H "Prefer: return=minimal; include=lvl" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "level": 2, "slug": "europe/cz" } ``` **Include deleted terms in response** Let's delete a country from Europe: ```console $ curl -X DELETE -i http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/gb HTTP/1.0 200 OK Content-Type: application/json { "CapitalLatitude": "51.5", "CapitalLongitude": "-0.083333", "CapitalName": "London", "ContinentName": "Europe", "CountryCode": "GB", "CountryName": "United Kingdom", "links":{"self":"http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/gb"} } ``` United Kingdom has indeed been removed from Europe: ```console $ curl -i http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/gb HTTP/1.0 410 GONE { "message": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/gb was not found on the server", "reason": "deleted" } ``` Now run the GET again with removed terms included: ```console $ curl -i -H "Prefer: return=minimal; include=del sta" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/gb HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "slug": "europe/gb", "status": "deleted", "busy_count":0, "descendants_busy_count":0 } ``` **Include term status** Including the status will add the following metadata: * ``status`` of the term (alive, delete_pending, deleted, moved) * ``busy_count`` - an integer saying how "busy" the term is. Being busy means that a potentially destructive operation (such as deleting, moving or renaming slug) is in progress. * ``descendants_busy_count`` - a number of descendants that are busy ```console $ curl -i -H "Prefer: return=minimal; include=del sta dsc" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "busy_count":0, "children": [ { "busy_count":0, "descendants_busy_count":0, "slug": "europe/ad", "status": "alive" }, ... { "busy_count":0, "descendants_busy_count":0, "slug": "europe/gb", "status": "deleted" }, ... { "busy_count":0, "descendants_busy_count":0, "slug": "europe/va", "status": "alive" } ], "descendants_busy_count":0, "slug": "europe", "status": "alive" } ``` ###### Excluding data To exclude data from the representation, use ``exclude=...`` in prefer header. #### Query parameters Values from the ``prefer`` header can be used as query parameters: ``` curl -i -H "Prefer: return=minimal;" \ "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe? representation:include=sta,url&representation:exclude=slug" HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe" }, "status": "alive" } ``` #### Selecting subset of term data Use ``select= ...`` in ``prefer`` header or ``representation:select=`` query parameter to select just part of user data: ```console $ curl -i -H "Prefer: return=representation;select=/CapitalName /CountryCode" \ "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz" HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "CapitalName": "Prague", "CountryCode": "CZ", "ancestors": [ { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe" } } ], "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz" } } ``` #### Maximum levels When descendants are selected, a maximum level of descendants can be specified via ``levels=`` part of ``prefer`` header (or representation:levels=n in the query). Example: ```console $ curl -i -H "Prefer: return=representation; levels=1" \ "http://127.0.0.1:5000/api/2.0/taxonomies/country" HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { 'children': [ {'slug': 'africa'}, {'slug': 'antarctica'}, {'slug': 'asia'}, {'slug': 'australia'}, {'slug': 'central-america'}, {'slug': 'europe'}, {'slug': 'north-america'}, {'slug': 'south-america'} ], 'code': 'country', 'title': 'List of countries' } ``` #### Pagination If descendants are requested without further arguments the whole tree is returned (well, in fact at most FLASK_TAXONOMIES_MAX_RESULTS_RETURNED terms to prevent server crash). This leads to high amount of data transferred and possibly a client crash. To prevent this, pagination should be used on larger taxonomies. Specify ``size`` argument to have at most this number of taxonomy terms returned. For example: ```console $ curl -i -H "Prefer: return=minimal;" \ "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe?representation:include=dsc&size=5" HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree X-Page: 1 X-PageSize: 5 X-Total: 58 { "children": [ { "slug": "europe/ad" }, { "slug": "europe/al" }, { "slug": "europe/am" }, { "slug": "europe/at" } ], "slug": "europe" } ``` This returns 5 terms - europe and 4 children. To return the next page, add ``page=2`` argument: ```console $ curl -i -H "Prefer: return=minimal;" \ "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe?representation:include=dsc&size=5&page=4" HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree X-Page: 4 X-PageSize: 5 X-Total: 58 [ { "slug": "europe/dk" }, { "slug": "europe/ee" }, { "slug": "europe/es" }, { "slug": "europe/fi" }, { "slug": "europe/fo" } ] ``` Note that the first page contains the root element and the second does not. To fix it, either use ``include=anh`` to always get hierarchical representation: ```console $ curl -i -H "Prefer: return=minimal;" \ "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe?representation:include=dsc,anh&size=5&page=2" HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree X-Page: 2 X-PageSize: 5 X-Total: 58 { "ancestor": true, "children": [ { "slug": "europe/ax" }, { "slug": "europe/az" }, { "slug": "europe/ba" }, { "slug": "europe/be" } ], "slug": "europe" } ``` or, if interested only in descendants and not the node itself, ``exclude=self`` ```console $ curl -i -H "Prefer: return=minimal;" \ "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe? representation:include=dsc&representation:exclude=self&size=5&page=1" HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree X-Page: 1 X-PageSize: 5 X-Total: 58 [ { "slug": "europe/ad" }, { "slug": "europe/al" }, { "slug": "europe/am" }, { "slug": "europe/at" }, { "slug": "europe/ax" } ] ``` #### Searching Use ``q=`` parameter to search within terms. Returns all the resources whose ``metadata`` contain the expression in q. ##### Simple query If ``q`` is a simple string not containing ':' or string in quotes, it is interpreted as a string that must be present in any of values inside the json. The current implementation is dependent on the database backend and might perform sub-optimal ``ilike %x%`` query on textified json. ```console $ curl -i -H "Prefer: return=minimal; include=data dsc; exclude=self" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe?q=Prague HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree [ { "CapitalLatitude": "50.083333333333336", "CapitalLongitude": "14.466667", "CapitalName": "Prague", "ContinentName": "Europe", "CountryCode": "CZ", "CountryName": "Czech Republic", "slug": "europe/cz" } ] ``` ##### Lucene-like query If the ``q`` contains a ':' character not enclosed in quotes, it is parsed as a query in lucene syntax, with the following allowed constructs: * path:value for matching value at the given path * a.b.c:value for representing nested paths * AND, OR, NOT, brackets The query will be executed if the database or search backend support it. If not supported, HTTP 501 will be returned. ### Taxonomy #### Creating To create a taxonomy, either use HTTP PUT: ```console $ curl -i -X PUT 'http://127.0.0.1:5000/api/2.0/taxonomies/test' \ -H 'Content-Type: application/json' \ --data-raw '{ "title": "Test taxonomy" }' HTTP/1.0 201 CREATED Link: ; rel=self Link: ; rel=tree Location: http://127.0.0.1:5000/api/2.0/taxonomies/test/ { "code": "test", "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test/" }, "title": "Test taxonomy" } ``` Or HTTP POST with ``code`` in the payload ```console $ curl -i -X POST 'http://127.0.0.1:5000/api/2.0/taxonomies/' \ -H 'Content-Type: application/json' --data-raw '{ "title": "Test taxonomy 1", "code": "test1" }' HTTP/1.0 201 CREATED Link: ; rel=self Link: ; rel=tree Location: http://127.0.0.1:5000/api/2.0/taxonomies/test1/ { "code": "test1", "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test1/" }, "title": "Test taxonomy 1" } ``` #### Updating ##### Replacing via HTTP PUT ```console $ curl -i -X PUT 'http://127.0.0.1:5000/api/2.0/taxonomies/test' \ --header 'Content-Type: application/json' --data-raw '{ "title": "Test taxonomy updated" }' HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "code": "test", "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test/" }, "title": "Test taxonomy updated" } ``` Note that terms are not updated nor removed when taxonomy metadata are updated. ##### Patching with HTTP PATCH ```console $ curl -i -X PATCH 'http://127.0.0.1:5000/api/2.0/taxonomies/test' \ --header 'Content-Type: application/json' --data-raw '[{ "op": "replace", "path": "/title", "value": "Test taxonomy updated via patch" }]' HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "code": "test", "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test/" }, "title": "Test taxonomy updated via patch" } ``` #### Deleting ```console $ curl -i -X DELETE 'http://127.0.0.1:5000/api/2.0/taxonomies/test1' HTTP/1.0 204 NO CONTENT Content-Type: text/html; charset=utf-8 ``` ### Taxonomy Term #### Creating As in creating taxonomy, term can be created either via HTTP PUT: ```console $ curl -i -X PUT 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term' \ --header 'Content-Type: application/json' --data-raw '{ "title": "Test Term" }' HTTP/1.0 201 CREATED Link: ; rel=self Link: ; rel=tree Location: http://127.0.0.1:5000/api/2.0/taxonomies/test/term { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test/term" }, "title": "Test Term" } ``` or POST: ```console $ curl -i -X POST 'http://127.0.0.1:5000/api/2.0/taxonomies/test' \ --header 'Content-Type: application/json' --data-raw '{ "title": "Test term 1", "slug": "term1" }' HTTP/1.0 201 CREATED Link: ; rel=self Link: ; rel=tree Location: http://127.0.0.1:5000/api/2.0/taxonomies/test/term1 { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test/term1" }, "title": "Test term 1" } ``` As PUT/POST operation also mean updating term if slug exists, to be sure that you are creating a new one use ``If-None-Match: '*'`` header: ```console $ curl -i -X PUT 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term' \ --header 'Content-Type: application/json' \ --header 'If-None-Match: '*'' \ --data-raw '{ "title": "Test Term" }' HTTP/1.0 412 Precondition Failed { "message": "The taxonomy already contains a term on this slug. As If-None-Match: '*' has been requested, not modifying the term", "reason": "term-exists" } ``` Terms can be created within terms via HTTP PUT: ```console $ curl -i -X PUT 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term/nested' \ --header 'Content-Type: application/json' --data-raw '{ "title": "Nested Term" }' HTTP/1.0 201 CREATED Link: ; rel=self Link: ; rel=tree Location: http://127.0.0.1:5000/api/2.0/taxonomies/test/term/nested { "ancestors":[ { "links":{ "self":"http://127.0.0.1:5000/api/2.0/taxonomies/test/term" }, "title":"Test Term" } ], "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test/term/nested" }, "title": "Nested Term" } ``` or POST: ```console $ curl -i -X POST 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term1' \ --header 'Content-Type: application/json' --data-raw '{ "title": "Test nested term 1", "slug": "nested1" }' HTTP/1.0 201 CREATED Link: ; rel=self Link: ; rel=tree Location: http://127.0.0.1:5000/api/2.0/taxonomies/test/term1/nested1 { "ancestors":[ { "links":{ "self":"http://127.0.0.1:5000/api/2.0/taxonomies/test/term1" }, "title":"Test term 1" } ], "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test/term1/nested1" }, "title": "Test nested term 1" } ``` #### Updating As for taxonomy, use HTTP ``PUT``: ```console $ curl -i -X PUT 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term' \ --header 'Content-Type: application/json' --data-raw '{ "title": "Test Term updated" }' HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test/term" }, "title": "Test Term updated" } ``` or ``PATCH``: ```console $ curl -i -X PATCH 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term' \ --header 'Content-Type: application/json' --data-raw '[{ "op": "replace", "path": "/title", "value": "Test taxonomy term updated via patch" }]' HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test/term" }, "title": "Test taxonomy term updated via patch" } ``` As PUT operation also means creating term if slug does not exist, to be sure that you are just updating use ``If-Match: '*'`` header: ```console $ curl -i -X PUT 'http://127.0.0.1:5000/api/2.0/taxonomies/test/unknown' \ --header 'Content-Type: application/json' \ --header 'If-Match: '*'' \ --data-raw '{ "title": "Test Term" }' HTTP/1.0 412 Precondition Failed { "message": "The taxonomy does not contain a term on this slug. As If-Match: '*' has been requested, not creating a new term", "reason": "term-does-not-exist" } ``` #### Deleting Use HTTP delete to remove a term. The removed term will be returned in the response: ```console $ curl -i -X DELETE 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term1' HTTP/1.0 200 OK Content-Type: application/json { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test/term1" }, "title": "Test term 1" } ``` Subsequent GET returns 410: ```console $ curl -i 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term1' HTTP/1.0 410 GONE { "message": "http://127.0.0.1:5000/api/2.0/taxonomies/test/term1 was not found on the server", "reason": "deleted" } ``` But the term stays on the server: ```console $ curl -i 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term?representation:include=del' HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test/term" }, "title": "Test taxonomy term updated via patch" } ``` #### Un-Deleting To salvage a deleted term, update it via PATCH, with an empty set of operations to keep it unmodified: ```console $ curl -i -X PATCH -H "Prefer: return=minimal; include=del" \ 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term' \ --header 'Content-Type: application/json' --data-raw '[]' HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "slug": "term" } ``` #### Moving Use HTTP post with content type ``application/vnd.move`` and ``Destination`` header: ```console $ curl -i -X POST \ -H 'Content-Type: application/vnd.move' \ -H "Destination: /" \ 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term/nested' HTTP/1.0 200 OK { "links":{ "self":"http://127.0.0.1:5000/api/2.0/taxonomies/test/nested" }, "title":"Nested Term" } ``` The original url returns 301: ```console $ curl -i 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term/nested' HTTP/1.0 301 MOVED PERMANENTLY Location: http://127.0.0.1:5000/api/2.0/taxonomies/test/nested Link: ; rel=self Link: ; rel=obsoleted_by { "links": { "self": "http://localhost/api/2.0/taxonomies/test/term/nested", "obsoleted_by": "http://localhost/api/2.0/taxonomies/test/nested" }, "status": "moved" } ``` #### Renaming Use HTTP post with content type ``application/vnd.move`` and ``Rename`` header: ```console $ curl -i -X POST \ -H 'Content-Type: application/vnd.move' \ -H "Rename: renamed-nested" \ 'http://127.0.0.1:5000/api/2.0/taxonomies/test/nested' HTTP/1.0 200 OK { "links":{ "self":"http://127.0.0.1:5000/api/2.0/taxonomies/test/renamed-nested" }, "title":"Nested Term" } ``` The original url returns 301: ```console $ curl -i 'http://127.0.0.1:5000/api/2.0/taxonomies/test/nested' HTTP/1.0 301 MOVED PERMANENTLY Location: http://127.0.0.1:5000/api/2.0/taxonomies/test/renamed-nested Link: ; rel=self Link: ; rel=obsoleted_by { "links": { "self": "http://localhost/api/2.0/taxonomies/test/nested", "obsoleted_by": "http://localhost/api/2.0/taxonomies/test/renamed-nested" }, "status": "moved" } ``` ## Configuration ### Configuration Variables ``FLASK_TAXONOMIES_SERVER_NAME`` Server name hosting the taxonomies. If not set, SERVER_NAME is used. ``FLASK_TAXONOMIES_SERVER_SCHEME`` Scheme to use in generated urls, defaults to ``https`` ``FLASK_TAXONOMIES_URL_PREFIX`` A prefix on which taxonomies are served, defaults to ``/api/2.0/taxonomies/`` ``FLASK_TAXONOMIES_REPRESENTATION`` Values for ``Prefer: return=`` header. ``minimal`` and ``representation`` are obligatory, you are free to add other return representations. ```python from flask_taxonomies.constants import * FLASK_TAXONOMIES_REPRESENTATION = { 'minimal': { 'include': [INCLUDE_SLUG, INCLUDE_SELF], 'exclude': [], 'select': None, 'options': {} }, 'representation': { 'include': [INCLUDE_DATA, INCLUDE_ANCESTORS, INCLUDE_URL, INCLUDE_SELF], 'exclude': [], 'select': None, 'options': {} }, 'full': { 'include': [INCLUDE_DATA, INCLUDE_ANCESTORS, INCLUDE_URL, INCLUDE_DESCENDANTS_URL, INCLUDE_SELF], 'exclude': [], 'select': None, 'options': {} } } ``` ``FLASK_TAXONOMIES_MAX_RESULTS_RETURNED`` Specifies max results returned when pagination is not used. Defaults to ``10000``. ### Security Flask taxonomies uses ``flask-principal`` to handle security. The default permissions are that everyone is allowed to read/create/update/delete/move all taxonomies and terms. To restrict the access, specify permission factories (a function that returns a list of permission) for each operation. ``FLASK_TAXONOMIES_PERMISSION_FACTORIES`` A dictionary of operation to a list of permissions. ``` FLASK_TAXONOMIES_PERMISSION_FACTORIES = { 'taxonomy_list': request -> List[Permission] 'taxonomy_read': request, taxonomy -> List[Permission] 'taxonomy_create': request, code -> List[Permission] 'taxonomy_update': request, taxonomy -> List[Permission] 'taxonomy_delete': request, taxonomy -> List[Permission], 'taxonomy_term_read': request, taxonomy, slug -> List[Permission] 'taxonomy_term_create': request, taxonomy, slug -> List[Permission] 'taxonomy_term_update': request, taxonomy, term -> List[Permission] 'taxonomy_term_delete': request, taxonomy, term -> List[Permission], 'taxonomy_term_move': request, taxonomy, term, destination, rename -> List[Permission], } ``` The right-hand side can be either a list/tuple of permissions, function with the above-mentioned signatures or a string pointing to the implementation. The string form is resolved on the first request. If ``.can`` on any of the permissions returns True or the list is empty, access is granted. #### Recommended initial settings The recommended initial settings are read-only for everyone except admin role: ```python from flask_principal import RoleNeed FLASK_TAXONOMIES_PERMISSION_FACTORIES = { 'taxonomy_create': [RoleNeed('admin')], 'taxonomy_update': [RoleNeed('admin')], 'taxonomy_delete': [RoleNeed('admin')], 'taxonomy_term_create': [RoleNeed('admin')], 'taxonomy_term_update': [RoleNeed('admin')], 'taxonomy_term_delete': [RoleNeed('admin')], 'taxonomy_term_move': [RoleNeed('admin')] } ``` ## Python API The calls below use ``session`` as an optional parameter. If not supplied, session from current_app is used. ``TermIdentification`` is a class to identify taxonomy term, binding taxonomy (or its code), slug or a term instance. See [flask_taxonomies/term_identification.py](./flask_taxonomies/term_identification.py) for details. ```python from flask_taxonomies.proxies import current_flask_taxonomies # returns a taxonomy list current_flask_taxonomies.list_taxonomies(session=None) # returns a taxonomy with the given code. Fails by default if not found current_flask_taxonomies.get_taxonomy(code, fail=True, session=None) # creates a new taxonomy current_flask_taxonomies.create_taxonomy(code, extra_data=None, url=None, select=None, session=None) # updates a taxonomy current_flask_taxonomies.update_taxonomy( taxonomy: [Taxonomy, str], extra_data, url=MISSING, select=MISSING, session=None) # deletes a taxonomy current_flask_taxonomies.delete_taxonomy(taxonomy: Taxonomy, session=None) # lists terms within the taxonomy. current_flask_taxonomies.list_taxonomy(taxonomy: [Taxonomy, str], levels=None, status_cond=TaxonomyTerm.status == TermStatusEnum.alive, order=True, session=None) # creates a new term inside a taxonomy current_flask_taxonomies.create_term(ti: TermIdentification, extra_data=None, session=None) # updates a term, setting or patching extra_data current_flask_taxonomies.update_term(ti: [TaxonomyTerm, TermIdentification], status_cond=TaxonomyTerm.status == TermStatusEnum.alive, extra_data=None, patch=False, status=MISSING, session=None) # returns all descendants of a term current_flask_taxonomies.descendants(ti: TermIdentification, levels=None, status_cond=TaxonomyTerm.status == TermStatusEnum.alive, order=True, session=None) # returns a term and its descendants current_flask_taxonomies.descendants_or_self(ti: TermIdentification, levels=None, status_cond=TaxonomyTerm.status == TermStatusEnum.alive, order=True, session=None) # returns all ancestors of a term current_flask_taxonomies.ancestors(ti: TermIdentification, status_cond=TaxonomyTerm.status == TermStatusEnum.alive, session=None) # returns term and its ancestors current_flask_taxonomies.ancestors_or_self(ti: TermIdentification, status_cond=TaxonomyTerm.status == TermStatusEnum.alive, session=None) # removes a term current_flask_taxonomies.delete_term(ti: TermIdentification, remove_after_delete=True, session=None) # renames term's slug current_flask_taxonomies.rename_term(ti: TermIdentification, new_slug=None, remove_after_delete=True, session=None) # moves term into a new parent within the same taxonomy current_flask_taxonomies.move_term(ti: TermIdentification, new_parent=None, remove_after_delete=True, session=None) ``` ### Signals See [flask_taxonomies/signals.py](flask_taxonomies/signals.py) for details %package help Summary: Development documents and examples for flask-taxonomies Provides: python3-flask-taxonomies-doc %description help # Flask Taxonomies [![](https://img.shields.io/github/license/oarepo/flask-taxonomies.svg)](https://github.com/oarepo/flask-taxonomies/blob/master/LICENSE) [![](https://img.shields.io/travis/oarepo/flask-taxonomies.svg)](https://travis-ci.org/oarepo/flask-taxonomies) [![](https://img.shields.io/coveralls/oarepo/flask-taxonomies.svg)](https://coveralls.io/r/oarepo/flask-taxonomies) [![](https://img.shields.io/pypi/v/flask-taxonomies.svg)](https://pypi.org/pypi/flask-taxonomies) - [Flask Taxonomies](#flask-taxonomies) - [Installation](#installation) - [Terminology](#terminology) - [REST API](#rest-api) - [Retrieving resources](#retrieving-resources) - [``Prefer`` HTTP header](#prefer-http-header) - [Returned representation](#returned-representation) - [Includes and excludes](#includes-and-excludes) - [Including extra data](#including-extra-data) - [Excluding data](#excluding-data) - [Query parameters](#query-parameters) - [Selecting subset of term data](#selecting-subset-of-term-data) - [Pagination](#pagination) - [Taxonomy](#taxonomy) - [Creating](#creating) - [Updating](#updating) - [Replacing via HTTP PUT](#replacing-via-http-put) - [Patching with HTTP POST](#patching-with-http-post) - [Deleting](#deleting) - [Taxonomy Term](#taxonomy-term) - [Creating](#creating-1) - [Updating](#updating-1) - [Deleting](#deleting-1) - [Un-Deleting](#un-deleting) - [Moving](#moving) - [Renaming](#renaming) - [Configuration](#configuration) - [Configuration Variables](#configuration-variables) - [Security](#security) - [Recommended initial settings](#recommended-initial-settings) - [Python API](#python-api) - [Signals](#signals) ## Installation ```bash pip install flask-taxonomies ``` ```python from flask_taxonomies.ext import FlaskTaxonomies from flask_taxonomies.views import blueprint from flask import Flask from flask_principal import Principal app = Flask('__test__') FlaskTaxonomies(app) Principal(app) app.register_blueprint(blueprint, url_prefix=app.config['FLASK_TAXONOMIES_URL_PREFIX']) db = ... from flask_taxonomies.models import Base Base.metadata.create_all(db.engine) ``` ## Terminology **Taxonomy** is a tree of taxonomy terms. It is represented as a database object identified by *code*. A taxonomy may contain its original url (in case the taxonomy is defined elsewhere) and additional metadata as a json object (containing, for example, taxonomy title). It may also contain a default set of selectors for filtering metadata. **TaxonomyTerm** represents a single node in a taxonomy. It is identified by its *slug* and may contain additional metadata as json object. A term can contain children to represent hierarchy of taxonomy terms. Term does not define ordering within children, it is up to application logic to define any ordering. ## REST API The rest API sits on the ``app.config['FLASK_TAXONOMIES_URL_PREFIX']`` url, implicitly ``/api/2.0/taxonomies/``. It follows the REST API principles with pagination inspired by GitHub API. ### Retrieving resources #### ``Prefer`` HTTP header Implicitly, the API returns rather minimal representation. The amount of the returned metadata can be changed via HTTP ``prefer`` header or alternatively by query parameters. ##### Returned representation The ``prefer`` header is a standard way of telling what you expect to get as a response to the request. It is defined in [rfc7240](https://tools.ietf.org/html/rfc7240). If the header is not present, ``return=representation`` is assumed. One can specify ``return=minimal`` to obtain minimal dataset, or other return types (even your own) defined in ``FLASK_TAXONOMIES_REPRESENTATION`` config. **return=minimal** returns the minimal representation. Mostly not usable directly as it does not return any metadata, just the code/slug. *Listing:* ```console $ curl -i -H "Prefer: return=minimal" http://127.0.0.1:5000/api/2.0/taxonomies/ HTTP/1.0 200 OK Link: ; rel=self [ { "code": "country" } ] ``` *Get taxonomy:* ```console $ curl -i -H "Prefer: return=minimal" http://127.0.0.1:5000/api/2.0/taxonomies/country HTTP/1.0 200 OK Content-Type: application/json { "code": "country" } ``` *Get term:* ```console $ curl -i -H "Prefer: return=minimal" http://127.0.0.1:5000/api/2.0/taxonomies/country/europe HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "slug": "europe" } ``` **return=representation** this is the default return type. Returns all user data declared on taxonomy/term together with ancestor and urls. For example: *Listing:* ```console $ curl -i http://127.0.0.1:5000/api/2.0/taxonomies/ HTTP/1.0 200 OK Link: ; rel=self [ { "code": "country", "links": { "custom": "https://www.kaggle.com/nikitagrec/world-capitals-gps/data", "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/" }, "title": "List of countries" } ] ``` *Get term:* ```console $ curl -i http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "CapitalLatitude": "50.083333333333336", "CapitalLongitude": "14.466667", "CapitalName": "Prague", "ContinentName": "Europe", "CountryCode": "CZ", "CountryName": "Czech Republic", "ancestors": [ { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe" } } ], "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz" } } ``` ``Europe`` has no user data, so it contains only the ``links`` section. ##### Includes and excludes The returned representation can be modified by specifying which metadata should be included/excluded. Currently supported includes/excludes are: ```python INCLUDE_URL = 'url' INCLUDE_DESCENDANTS_URL = 'drl' INCLUDE_DESCENDANTS_COUNT = 'dcn' INCLUDE_ANCESTORS_HIERARCHY = 'anh' INCLUDE_ANCESTORS = 'anc' INCLUDE_ANCESTOR_LIST = 'anl' INCLUDE_DATA = 'data' INCLUDE_ID = 'id' INCLUDE_DESCENDANTS = 'dsc' INCLUDE_ENVELOPE='env' INCLUDE_DELETED = 'del' INCLUDE_SLUG = 'slug' INCLUDE_LEVEL = 'lvl' INCLUDE_STATUS = 'sta' ``` ###### Including extra data Examples: **Include record url in response** ```console $ curl -i -H "Prefer: return=minimal; include=url" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe" }, "slug": "europe" } ``` Adds a ``links`` section to payload with record url (``"self":``) **Include descendants url in response** ```console $ curl -i -H "Prefer: return=minimal; include=url drl" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe", "tree": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe?representation:include=dsc" }, "slug": "europe" } ``` Adds a ``links`` section to payload with recoord url with descendants (``"tree":``) **Include descendants count in response** ```console $ curl -i -H "Prefer: return=minimal; include=dcn" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "slug": "europe", "descendants_count": 58 } ``` Adds a ``descendant_count`` with a number of descendant terms under the term. The value is 0 if the term is a leaf term. On taxonomy, returns the total number of terms in taxonomy: ```console $ curl -i -H "Prefer: return=minimal; include=dcn" \ http://127.0.0.1:5000/api/2.0/taxonomies/country HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "code": "country", "descendants_count": 253 } ``` **Include ancestors with hierarchy in response** ```console $ curl -i -H "Prefer: return=minimal; include=anh url" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz { "children": [ { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz" }, "slug": "europe/cz" } ], "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe" }, "slug": "europe", "ancestor": true } ``` If the term has ancestors, they are serialized and the term is included as their child. This is useful for example when showing the taxonomy in tree form - the rendering mechanism for the tree will stay the same. All ancestor terms are marked with ``ancestor=true`` flag to help with ui rendering (for example to gray ancestors). Adding url as well is recommended to get urls of ancestors. **Include ancestors without hierarchy in response** ```console $ curl -i -H "Prefer: return=minimal; include=anc url" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "ancestors": [ { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe" }, "slug": "europe" } ], "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz" }, "slug": "europe/cz" } ``` The ancestors are rendered inside the ``ancestors`` element. Adding url as well is recommended to get urls of ancestors. **Include ancestor list in response** ```console $ curl -i -H "Prefer: return=representation; include=anl ant par" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree [ { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe" }, "is_ancestor": true }, { "CapitalLatitude": "50.083333333333336", "CapitalLongitude": "14.466667", "CapitalName": "Prague", "ContinentName": "Europe", "CountryCode": "CZ", "CountryName": "Czech Republic", "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz", "parent": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe" }, "is_ancestor": false } ] ``` The ancestors are rendered on the same level as the term. This rendering might be used for example when serializing the taxonomy term to elasticsearch - this way all the ancestors are serialized within one array and indexed into one object in ES. Note also the: * ``ant`` - adds ``ancestor`` field (``false`` if this is the term that has been queried), ``true`` for ancestor term * ``parent`` - adds link to the parent within ``links`` section Even one-term result is rendered as array: ```console $ curl -i -H "Prefer: return=representation; include=anl ant par" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree [ { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe" }, "is_ancestor": false } ] ``` **Include data in response** This is the default setting unless ``minimal`` representation is selected. In this case, pass ``include=data`` to have data included. ```console $ curl -i -H "Prefer: return=minimal; include=data" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "CapitalLatitude": "50.083333333333336", "CapitalLongitude": "14.466667", "CapitalName": "Prague", "ContinentName": "Europe", "CountryCode": "CZ", "CountryName": "Czech Republic", "slug": "europe/cz" } ``` **Include id in response** Use ``include=id`` to get the internal id included. This is rarely needed as API does not accept this id at all. **Include descendant terms in response** To serialize descendants into the response, use ``include=dsc``: ```console $ curl -i -H "Prefer: return=minimal; include=dsc" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "children": [ { "slug": "europe/ad" }, { "slug": "europe/al" }, ... { "slug": "europe/va" } ], "slug": "europe" } ``` **Include slug in response** Adds ``slug`` to response. In the ``minimal`` mode the slug is added automatically, use this tag to add it in ``return=representation``: ```console $ curl -i -H "Prefer: return=representation; include=slug" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "CapitalLatitude": "50.083333333333336", "CapitalLongitude": "14.466667", "CapitalName": "Prague", "ContinentName": "Europe", "CountryCode": "CZ", "CountryName": "Czech Republic", "ancestors": [ { "links":{"self":"http://127.0.0.1:5000/api/2.0/taxonomies/country/europe"}, "slug": "europe" } ], "links":{"self":"http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz"}, "slug": "europe/cz" } ``` **Include hierarchy level in response** Adds hierarchy level to taxonomy term. Top-level terms have ``level=1``, taxonomy ``0``. ```console $ curl -i -H "Prefer: return=minimal; include=lvl" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "level": 2, "slug": "europe/cz" } ``` **Include deleted terms in response** Let's delete a country from Europe: ```console $ curl -X DELETE -i http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/gb HTTP/1.0 200 OK Content-Type: application/json { "CapitalLatitude": "51.5", "CapitalLongitude": "-0.083333", "CapitalName": "London", "ContinentName": "Europe", "CountryCode": "GB", "CountryName": "United Kingdom", "links":{"self":"http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/gb"} } ``` United Kingdom has indeed been removed from Europe: ```console $ curl -i http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/gb HTTP/1.0 410 GONE { "message": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/gb was not found on the server", "reason": "deleted" } ``` Now run the GET again with removed terms included: ```console $ curl -i -H "Prefer: return=minimal; include=del sta" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/gb HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "slug": "europe/gb", "status": "deleted", "busy_count":0, "descendants_busy_count":0 } ``` **Include term status** Including the status will add the following metadata: * ``status`` of the term (alive, delete_pending, deleted, moved) * ``busy_count`` - an integer saying how "busy" the term is. Being busy means that a potentially destructive operation (such as deleting, moving or renaming slug) is in progress. * ``descendants_busy_count`` - a number of descendants that are busy ```console $ curl -i -H "Prefer: return=minimal; include=del sta dsc" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "busy_count":0, "children": [ { "busy_count":0, "descendants_busy_count":0, "slug": "europe/ad", "status": "alive" }, ... { "busy_count":0, "descendants_busy_count":0, "slug": "europe/gb", "status": "deleted" }, ... { "busy_count":0, "descendants_busy_count":0, "slug": "europe/va", "status": "alive" } ], "descendants_busy_count":0, "slug": "europe", "status": "alive" } ``` ###### Excluding data To exclude data from the representation, use ``exclude=...`` in prefer header. #### Query parameters Values from the ``prefer`` header can be used as query parameters: ``` curl -i -H "Prefer: return=minimal;" \ "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe? representation:include=sta,url&representation:exclude=slug" HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe" }, "status": "alive" } ``` #### Selecting subset of term data Use ``select= ...`` in ``prefer`` header or ``representation:select=`` query parameter to select just part of user data: ```console $ curl -i -H "Prefer: return=representation;select=/CapitalName /CountryCode" \ "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz" HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "CapitalName": "Prague", "CountryCode": "CZ", "ancestors": [ { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe" } } ], "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe/cz" } } ``` #### Maximum levels When descendants are selected, a maximum level of descendants can be specified via ``levels=`` part of ``prefer`` header (or representation:levels=n in the query). Example: ```console $ curl -i -H "Prefer: return=representation; levels=1" \ "http://127.0.0.1:5000/api/2.0/taxonomies/country" HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { 'children': [ {'slug': 'africa'}, {'slug': 'antarctica'}, {'slug': 'asia'}, {'slug': 'australia'}, {'slug': 'central-america'}, {'slug': 'europe'}, {'slug': 'north-america'}, {'slug': 'south-america'} ], 'code': 'country', 'title': 'List of countries' } ``` #### Pagination If descendants are requested without further arguments the whole tree is returned (well, in fact at most FLASK_TAXONOMIES_MAX_RESULTS_RETURNED terms to prevent server crash). This leads to high amount of data transferred and possibly a client crash. To prevent this, pagination should be used on larger taxonomies. Specify ``size`` argument to have at most this number of taxonomy terms returned. For example: ```console $ curl -i -H "Prefer: return=minimal;" \ "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe?representation:include=dsc&size=5" HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree X-Page: 1 X-PageSize: 5 X-Total: 58 { "children": [ { "slug": "europe/ad" }, { "slug": "europe/al" }, { "slug": "europe/am" }, { "slug": "europe/at" } ], "slug": "europe" } ``` This returns 5 terms - europe and 4 children. To return the next page, add ``page=2`` argument: ```console $ curl -i -H "Prefer: return=minimal;" \ "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe?representation:include=dsc&size=5&page=4" HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree X-Page: 4 X-PageSize: 5 X-Total: 58 [ { "slug": "europe/dk" }, { "slug": "europe/ee" }, { "slug": "europe/es" }, { "slug": "europe/fi" }, { "slug": "europe/fo" } ] ``` Note that the first page contains the root element and the second does not. To fix it, either use ``include=anh`` to always get hierarchical representation: ```console $ curl -i -H "Prefer: return=minimal;" \ "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe?representation:include=dsc,anh&size=5&page=2" HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree X-Page: 2 X-PageSize: 5 X-Total: 58 { "ancestor": true, "children": [ { "slug": "europe/ax" }, { "slug": "europe/az" }, { "slug": "europe/ba" }, { "slug": "europe/be" } ], "slug": "europe" } ``` or, if interested only in descendants and not the node itself, ``exclude=self`` ```console $ curl -i -H "Prefer: return=minimal;" \ "http://127.0.0.1:5000/api/2.0/taxonomies/country/europe? representation:include=dsc&representation:exclude=self&size=5&page=1" HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree X-Page: 1 X-PageSize: 5 X-Total: 58 [ { "slug": "europe/ad" }, { "slug": "europe/al" }, { "slug": "europe/am" }, { "slug": "europe/at" }, { "slug": "europe/ax" } ] ``` #### Searching Use ``q=`` parameter to search within terms. Returns all the resources whose ``metadata`` contain the expression in q. ##### Simple query If ``q`` is a simple string not containing ':' or string in quotes, it is interpreted as a string that must be present in any of values inside the json. The current implementation is dependent on the database backend and might perform sub-optimal ``ilike %x%`` query on textified json. ```console $ curl -i -H "Prefer: return=minimal; include=data dsc; exclude=self" \ http://127.0.0.1:5000/api/2.0/taxonomies/country/europe?q=Prague HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree [ { "CapitalLatitude": "50.083333333333336", "CapitalLongitude": "14.466667", "CapitalName": "Prague", "ContinentName": "Europe", "CountryCode": "CZ", "CountryName": "Czech Republic", "slug": "europe/cz" } ] ``` ##### Lucene-like query If the ``q`` contains a ':' character not enclosed in quotes, it is parsed as a query in lucene syntax, with the following allowed constructs: * path:value for matching value at the given path * a.b.c:value for representing nested paths * AND, OR, NOT, brackets The query will be executed if the database or search backend support it. If not supported, HTTP 501 will be returned. ### Taxonomy #### Creating To create a taxonomy, either use HTTP PUT: ```console $ curl -i -X PUT 'http://127.0.0.1:5000/api/2.0/taxonomies/test' \ -H 'Content-Type: application/json' \ --data-raw '{ "title": "Test taxonomy" }' HTTP/1.0 201 CREATED Link: ; rel=self Link: ; rel=tree Location: http://127.0.0.1:5000/api/2.0/taxonomies/test/ { "code": "test", "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test/" }, "title": "Test taxonomy" } ``` Or HTTP POST with ``code`` in the payload ```console $ curl -i -X POST 'http://127.0.0.1:5000/api/2.0/taxonomies/' \ -H 'Content-Type: application/json' --data-raw '{ "title": "Test taxonomy 1", "code": "test1" }' HTTP/1.0 201 CREATED Link: ; rel=self Link: ; rel=tree Location: http://127.0.0.1:5000/api/2.0/taxonomies/test1/ { "code": "test1", "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test1/" }, "title": "Test taxonomy 1" } ``` #### Updating ##### Replacing via HTTP PUT ```console $ curl -i -X PUT 'http://127.0.0.1:5000/api/2.0/taxonomies/test' \ --header 'Content-Type: application/json' --data-raw '{ "title": "Test taxonomy updated" }' HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "code": "test", "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test/" }, "title": "Test taxonomy updated" } ``` Note that terms are not updated nor removed when taxonomy metadata are updated. ##### Patching with HTTP PATCH ```console $ curl -i -X PATCH 'http://127.0.0.1:5000/api/2.0/taxonomies/test' \ --header 'Content-Type: application/json' --data-raw '[{ "op": "replace", "path": "/title", "value": "Test taxonomy updated via patch" }]' HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "code": "test", "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test/" }, "title": "Test taxonomy updated via patch" } ``` #### Deleting ```console $ curl -i -X DELETE 'http://127.0.0.1:5000/api/2.0/taxonomies/test1' HTTP/1.0 204 NO CONTENT Content-Type: text/html; charset=utf-8 ``` ### Taxonomy Term #### Creating As in creating taxonomy, term can be created either via HTTP PUT: ```console $ curl -i -X PUT 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term' \ --header 'Content-Type: application/json' --data-raw '{ "title": "Test Term" }' HTTP/1.0 201 CREATED Link: ; rel=self Link: ; rel=tree Location: http://127.0.0.1:5000/api/2.0/taxonomies/test/term { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test/term" }, "title": "Test Term" } ``` or POST: ```console $ curl -i -X POST 'http://127.0.0.1:5000/api/2.0/taxonomies/test' \ --header 'Content-Type: application/json' --data-raw '{ "title": "Test term 1", "slug": "term1" }' HTTP/1.0 201 CREATED Link: ; rel=self Link: ; rel=tree Location: http://127.0.0.1:5000/api/2.0/taxonomies/test/term1 { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test/term1" }, "title": "Test term 1" } ``` As PUT/POST operation also mean updating term if slug exists, to be sure that you are creating a new one use ``If-None-Match: '*'`` header: ```console $ curl -i -X PUT 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term' \ --header 'Content-Type: application/json' \ --header 'If-None-Match: '*'' \ --data-raw '{ "title": "Test Term" }' HTTP/1.0 412 Precondition Failed { "message": "The taxonomy already contains a term on this slug. As If-None-Match: '*' has been requested, not modifying the term", "reason": "term-exists" } ``` Terms can be created within terms via HTTP PUT: ```console $ curl -i -X PUT 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term/nested' \ --header 'Content-Type: application/json' --data-raw '{ "title": "Nested Term" }' HTTP/1.0 201 CREATED Link: ; rel=self Link: ; rel=tree Location: http://127.0.0.1:5000/api/2.0/taxonomies/test/term/nested { "ancestors":[ { "links":{ "self":"http://127.0.0.1:5000/api/2.0/taxonomies/test/term" }, "title":"Test Term" } ], "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test/term/nested" }, "title": "Nested Term" } ``` or POST: ```console $ curl -i -X POST 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term1' \ --header 'Content-Type: application/json' --data-raw '{ "title": "Test nested term 1", "slug": "nested1" }' HTTP/1.0 201 CREATED Link: ; rel=self Link: ; rel=tree Location: http://127.0.0.1:5000/api/2.0/taxonomies/test/term1/nested1 { "ancestors":[ { "links":{ "self":"http://127.0.0.1:5000/api/2.0/taxonomies/test/term1" }, "title":"Test term 1" } ], "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test/term1/nested1" }, "title": "Test nested term 1" } ``` #### Updating As for taxonomy, use HTTP ``PUT``: ```console $ curl -i -X PUT 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term' \ --header 'Content-Type: application/json' --data-raw '{ "title": "Test Term updated" }' HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test/term" }, "title": "Test Term updated" } ``` or ``PATCH``: ```console $ curl -i -X PATCH 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term' \ --header 'Content-Type: application/json' --data-raw '[{ "op": "replace", "path": "/title", "value": "Test taxonomy term updated via patch" }]' HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test/term" }, "title": "Test taxonomy term updated via patch" } ``` As PUT operation also means creating term if slug does not exist, to be sure that you are just updating use ``If-Match: '*'`` header: ```console $ curl -i -X PUT 'http://127.0.0.1:5000/api/2.0/taxonomies/test/unknown' \ --header 'Content-Type: application/json' \ --header 'If-Match: '*'' \ --data-raw '{ "title": "Test Term" }' HTTP/1.0 412 Precondition Failed { "message": "The taxonomy does not contain a term on this slug. As If-Match: '*' has been requested, not creating a new term", "reason": "term-does-not-exist" } ``` #### Deleting Use HTTP delete to remove a term. The removed term will be returned in the response: ```console $ curl -i -X DELETE 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term1' HTTP/1.0 200 OK Content-Type: application/json { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test/term1" }, "title": "Test term 1" } ``` Subsequent GET returns 410: ```console $ curl -i 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term1' HTTP/1.0 410 GONE { "message": "http://127.0.0.1:5000/api/2.0/taxonomies/test/term1 was not found on the server", "reason": "deleted" } ``` But the term stays on the server: ```console $ curl -i 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term?representation:include=del' HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "links": { "self": "http://127.0.0.1:5000/api/2.0/taxonomies/test/term" }, "title": "Test taxonomy term updated via patch" } ``` #### Un-Deleting To salvage a deleted term, update it via PATCH, with an empty set of operations to keep it unmodified: ```console $ curl -i -X PATCH -H "Prefer: return=minimal; include=del" \ 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term' \ --header 'Content-Type: application/json' --data-raw '[]' HTTP/1.0 200 OK Link: ; rel=self Link: ; rel=tree { "slug": "term" } ``` #### Moving Use HTTP post with content type ``application/vnd.move`` and ``Destination`` header: ```console $ curl -i -X POST \ -H 'Content-Type: application/vnd.move' \ -H "Destination: /" \ 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term/nested' HTTP/1.0 200 OK { "links":{ "self":"http://127.0.0.1:5000/api/2.0/taxonomies/test/nested" }, "title":"Nested Term" } ``` The original url returns 301: ```console $ curl -i 'http://127.0.0.1:5000/api/2.0/taxonomies/test/term/nested' HTTP/1.0 301 MOVED PERMANENTLY Location: http://127.0.0.1:5000/api/2.0/taxonomies/test/nested Link: ; rel=self Link: ; rel=obsoleted_by { "links": { "self": "http://localhost/api/2.0/taxonomies/test/term/nested", "obsoleted_by": "http://localhost/api/2.0/taxonomies/test/nested" }, "status": "moved" } ``` #### Renaming Use HTTP post with content type ``application/vnd.move`` and ``Rename`` header: ```console $ curl -i -X POST \ -H 'Content-Type: application/vnd.move' \ -H "Rename: renamed-nested" \ 'http://127.0.0.1:5000/api/2.0/taxonomies/test/nested' HTTP/1.0 200 OK { "links":{ "self":"http://127.0.0.1:5000/api/2.0/taxonomies/test/renamed-nested" }, "title":"Nested Term" } ``` The original url returns 301: ```console $ curl -i 'http://127.0.0.1:5000/api/2.0/taxonomies/test/nested' HTTP/1.0 301 MOVED PERMANENTLY Location: http://127.0.0.1:5000/api/2.0/taxonomies/test/renamed-nested Link: ; rel=self Link: ; rel=obsoleted_by { "links": { "self": "http://localhost/api/2.0/taxonomies/test/nested", "obsoleted_by": "http://localhost/api/2.0/taxonomies/test/renamed-nested" }, "status": "moved" } ``` ## Configuration ### Configuration Variables ``FLASK_TAXONOMIES_SERVER_NAME`` Server name hosting the taxonomies. If not set, SERVER_NAME is used. ``FLASK_TAXONOMIES_SERVER_SCHEME`` Scheme to use in generated urls, defaults to ``https`` ``FLASK_TAXONOMIES_URL_PREFIX`` A prefix on which taxonomies are served, defaults to ``/api/2.0/taxonomies/`` ``FLASK_TAXONOMIES_REPRESENTATION`` Values for ``Prefer: return=`` header. ``minimal`` and ``representation`` are obligatory, you are free to add other return representations. ```python from flask_taxonomies.constants import * FLASK_TAXONOMIES_REPRESENTATION = { 'minimal': { 'include': [INCLUDE_SLUG, INCLUDE_SELF], 'exclude': [], 'select': None, 'options': {} }, 'representation': { 'include': [INCLUDE_DATA, INCLUDE_ANCESTORS, INCLUDE_URL, INCLUDE_SELF], 'exclude': [], 'select': None, 'options': {} }, 'full': { 'include': [INCLUDE_DATA, INCLUDE_ANCESTORS, INCLUDE_URL, INCLUDE_DESCENDANTS_URL, INCLUDE_SELF], 'exclude': [], 'select': None, 'options': {} } } ``` ``FLASK_TAXONOMIES_MAX_RESULTS_RETURNED`` Specifies max results returned when pagination is not used. Defaults to ``10000``. ### Security Flask taxonomies uses ``flask-principal`` to handle security. The default permissions are that everyone is allowed to read/create/update/delete/move all taxonomies and terms. To restrict the access, specify permission factories (a function that returns a list of permission) for each operation. ``FLASK_TAXONOMIES_PERMISSION_FACTORIES`` A dictionary of operation to a list of permissions. ``` FLASK_TAXONOMIES_PERMISSION_FACTORIES = { 'taxonomy_list': request -> List[Permission] 'taxonomy_read': request, taxonomy -> List[Permission] 'taxonomy_create': request, code -> List[Permission] 'taxonomy_update': request, taxonomy -> List[Permission] 'taxonomy_delete': request, taxonomy -> List[Permission], 'taxonomy_term_read': request, taxonomy, slug -> List[Permission] 'taxonomy_term_create': request, taxonomy, slug -> List[Permission] 'taxonomy_term_update': request, taxonomy, term -> List[Permission] 'taxonomy_term_delete': request, taxonomy, term -> List[Permission], 'taxonomy_term_move': request, taxonomy, term, destination, rename -> List[Permission], } ``` The right-hand side can be either a list/tuple of permissions, function with the above-mentioned signatures or a string pointing to the implementation. The string form is resolved on the first request. If ``.can`` on any of the permissions returns True or the list is empty, access is granted. #### Recommended initial settings The recommended initial settings are read-only for everyone except admin role: ```python from flask_principal import RoleNeed FLASK_TAXONOMIES_PERMISSION_FACTORIES = { 'taxonomy_create': [RoleNeed('admin')], 'taxonomy_update': [RoleNeed('admin')], 'taxonomy_delete': [RoleNeed('admin')], 'taxonomy_term_create': [RoleNeed('admin')], 'taxonomy_term_update': [RoleNeed('admin')], 'taxonomy_term_delete': [RoleNeed('admin')], 'taxonomy_term_move': [RoleNeed('admin')] } ``` ## Python API The calls below use ``session`` as an optional parameter. If not supplied, session from current_app is used. ``TermIdentification`` is a class to identify taxonomy term, binding taxonomy (or its code), slug or a term instance. See [flask_taxonomies/term_identification.py](./flask_taxonomies/term_identification.py) for details. ```python from flask_taxonomies.proxies import current_flask_taxonomies # returns a taxonomy list current_flask_taxonomies.list_taxonomies(session=None) # returns a taxonomy with the given code. Fails by default if not found current_flask_taxonomies.get_taxonomy(code, fail=True, session=None) # creates a new taxonomy current_flask_taxonomies.create_taxonomy(code, extra_data=None, url=None, select=None, session=None) # updates a taxonomy current_flask_taxonomies.update_taxonomy( taxonomy: [Taxonomy, str], extra_data, url=MISSING, select=MISSING, session=None) # deletes a taxonomy current_flask_taxonomies.delete_taxonomy(taxonomy: Taxonomy, session=None) # lists terms within the taxonomy. current_flask_taxonomies.list_taxonomy(taxonomy: [Taxonomy, str], levels=None, status_cond=TaxonomyTerm.status == TermStatusEnum.alive, order=True, session=None) # creates a new term inside a taxonomy current_flask_taxonomies.create_term(ti: TermIdentification, extra_data=None, session=None) # updates a term, setting or patching extra_data current_flask_taxonomies.update_term(ti: [TaxonomyTerm, TermIdentification], status_cond=TaxonomyTerm.status == TermStatusEnum.alive, extra_data=None, patch=False, status=MISSING, session=None) # returns all descendants of a term current_flask_taxonomies.descendants(ti: TermIdentification, levels=None, status_cond=TaxonomyTerm.status == TermStatusEnum.alive, order=True, session=None) # returns a term and its descendants current_flask_taxonomies.descendants_or_self(ti: TermIdentification, levels=None, status_cond=TaxonomyTerm.status == TermStatusEnum.alive, order=True, session=None) # returns all ancestors of a term current_flask_taxonomies.ancestors(ti: TermIdentification, status_cond=TaxonomyTerm.status == TermStatusEnum.alive, session=None) # returns term and its ancestors current_flask_taxonomies.ancestors_or_self(ti: TermIdentification, status_cond=TaxonomyTerm.status == TermStatusEnum.alive, session=None) # removes a term current_flask_taxonomies.delete_term(ti: TermIdentification, remove_after_delete=True, session=None) # renames term's slug current_flask_taxonomies.rename_term(ti: TermIdentification, new_slug=None, remove_after_delete=True, session=None) # moves term into a new parent within the same taxonomy current_flask_taxonomies.move_term(ti: TermIdentification, new_parent=None, remove_after_delete=True, session=None) ``` ### Signals See [flask_taxonomies/signals.py](flask_taxonomies/signals.py) for details %prep %autosetup -n flask-taxonomies-7.0.3 %build %py3_build %install %py3_install install -d -m755 %{buildroot}/%{_pkgdocdir} if [ -d doc ]; then cp -arf doc %{buildroot}/%{_pkgdocdir}; fi if [ -d docs ]; then cp -arf docs %{buildroot}/%{_pkgdocdir}; fi if [ -d example ]; then cp -arf example %{buildroot}/%{_pkgdocdir}; fi if [ -d examples ]; then cp -arf examples %{buildroot}/%{_pkgdocdir}; fi pushd %{buildroot} if [ -d usr/lib ]; then find usr/lib -type f -printf "/%h/%f\n" >> filelist.lst fi if [ -d usr/lib64 ]; then find usr/lib64 -type f -printf "/%h/%f\n" >> filelist.lst fi if [ -d usr/bin ]; then find usr/bin -type f -printf "/%h/%f\n" >> filelist.lst fi if [ -d usr/sbin ]; then find usr/sbin -type f -printf "/%h/%f\n" >> filelist.lst fi touch doclist.lst if [ -d usr/share/man ]; then find usr/share/man -type f -printf "/%h/%f.gz\n" >> doclist.lst fi popd mv %{buildroot}/filelist.lst . mv %{buildroot}/doclist.lst . %files -n python3-flask-taxonomies -f filelist.lst %dir %{python3_sitelib}/* %files help -f doclist.lst %{_docdir}/* %changelog * Wed May 31 2023 Python_Bot - 7.0.3-1 - Package Spec generated