diff --git a/README.rst b/README.rst
index b092c00..0dd7d72 100644
--- a/README.rst
+++ b/README.rst
@@ -1,7 +1,12 @@
.. image:: https://github.com/AdCombo/flask-combo-jsonapi/workflows/Python%20tests%20and%20coverage/badge.svg
- :target: https://github.com/AdCombo/flask-combo-jsonapi/actions
+ :alt: flask-combo-jsonapi actions
+ :target: https://github.com/AdCombo/flask-combo-jsonapi/actions
.. image:: https://coveralls.io/repos/github/AdCombo/flask-combo-jsonapi/badge.svg
- :target: https://coveralls.io/github/AdCombo/flask-combo-jsonapi
+ :alt: flask-combo-jsonapi coverage
+ :target: https://coveralls.io/github/AdCombo/flask-combo-jsonapi
+.. image:: https://img.shields.io/pypi/v/flask-combo-jsonapi.svg
+ :alt: PyPI
+ :target: http://pypi.org/p/flask-combo-jsonapi
Flask-COMBO-JSONAPI
@@ -9,7 +14,7 @@ Flask-COMBO-JSONAPI
Flask-COMBO-JSONAPI is a flask extension for building REST APIs. It combines the power of `Flask-Restless `_ and the flexibility of `Flask-RESTful `_ around a strong specification `JSONAPI 1.0 `_. This framework is designed to quickly build REST APIs and fit the complexity of real life projects with legacy data and multiple data storages.
-The main goal is to make it flexible using `plugin system `_
+The main goal is to make it flexible using `plugin system `_
Install
@@ -17,8 +22,6 @@ Install
pip install Flask-COMBO-JSONAPI
-Installation from pypi is not ready yet. Refer to the `installation manual `_
-
A minimal API
=============
@@ -28,23 +31,27 @@ A minimal API
from flask import Flask
from flask_combo_jsonapi import Api, ResourceDetail, ResourceList
from flask_sqlalchemy import SQLAlchemy
+ from marshmallow import pre_load
from marshmallow_jsonapi.flask import Schema
from marshmallow_jsonapi import fields
# Create the Flask application and the Flask-SQLAlchemy object.
app = Flask(__name__)
app.config['DEBUG'] = True
- app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
+ app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/api_minimal.db'
db = SQLAlchemy(app)
+
# Create model
class Person(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String)
+
# Create the database.
db.create_all()
+
# Create schema
class PersonSchema(Schema):
class Meta:
@@ -54,18 +61,37 @@ A minimal API
self_view_many = 'person_list'
id = fields.Integer(as_string=True, dump_only=True)
- name = fields.Str()
+ name = fields.String()
+
+ @pre_load
+ def remove_id_before_deserializing(self, data, **kwargs):
+ """
+ We don't want to allow editing ID on POST / PATCH
+
+ Related issues:
+ https://github.com/AdCombo/flask-combo-jsonapi/issues/34
+ https://github.com/miLibris/flask-rest-jsonapi/issues/193
+ """
+ if 'id' in data:
+ del data['id']
+ return data
# Create resource managers
class PersonList(ResourceList):
schema = PersonSchema
- data_layer = {'session': db.session,
- 'model': Person}
+ data_layer = {
+ 'session': db.session,
+ 'model': Person,
+ }
+
class PersonDetail(ResourceDetail):
schema = PersonSchema
- data_layer = {'session': db.session,
- 'model': Person}
+ data_layer = {
+ 'session': db.session,
+ 'model': Person,
+ }
+
# Create the API object
api = Api(app)
@@ -76,6 +102,7 @@ A minimal API
if __name__ == '__main__':
app.run()
+
This example provides the following API structure:
======================== ====== ============= ===========================
@@ -88,6 +115,10 @@ URL method endpoint Usage
/persons/ DELETE person_detail Delete a person
======================== ====== ============= ===========================
+
+`More detailed example in the docs `_
+
+
Flask-COMBO-JSONAPI vs `Flask-RESTful `_
==========================================================================================
@@ -107,7 +138,7 @@ Flask-COMBO-JSONAPI vs `Flask-Restless `_.
+| Flask-COMBO-JSONAPI has a full-featured data layer that uses the popular ORM `SQLAlchemy `_.
.. note::
- The default data layer used by a resource manager is the SQLAlchemy one. So if you want to use it, you don't have to specify the class of the data layer in the resource manager
+ The default data layer used by a resource manager is the SQLAlchemy one. So if that's what you want to use, you don't have to specify the class of the data layer in the resource manager
To configure the data layer you have to set its required parameters in the resource manager.
@@ -28,11 +28,13 @@ Example:
data_layer = {'session': db.session,
'model': Person}
-You can also plug additional methods to your data layer in the resource manager. There are two kinds of additional methods:
+You can also plug additional methods into your data layer in the resource manager. There are two kinds of additional methods:
-* query: the "query" additional method takes view_kwargs as parameter and return an alternative query to retrieve the collection of objects in the GET method of the ResourceList manager.
+* query: the "query" additional method takes view_kwargs as parameter and returns an alternative query to retrieve the collection of objects in the GET method of the ResourceList manager.
-* pre / post process methods: all CRUD and relationship(s) operations have a pre / post process methods. Thanks to it you can make additional work before and after each operations of the data layer. Parameters of each pre / post process methods are available in the `flask_combo_jsonapi.data_layers.base.Base `_ base class.
+* pre-/postprocess methods: all CRUD and relationship(s) operations have pre-/postprocess methods.
+Thanks to these you can do additional work before and after each operation of the data layer.
+Parameters of each pre-/postprocess method are available in the `flask_combo_jsonapi.data_layers.base.Base `_ base class.
Example:
@@ -68,7 +70,7 @@ Example:
.. note::
- You don't have to declare additional data layer methods in the resource manager. You can declare them in a dedicated module or in the declaration of the model.
+ You don't have to declare additional data layer methods in the resource manager. You can declare them in a dedicated module or in the model's declaration.
Example:
@@ -100,12 +102,14 @@ Optional parameters:
:id_field: the field used as identifier field instead of the primary key of the model
:url_field: the name of the parameter in the route to get value to filter with. Instead "id" is used.
-By default SQLAlchemy eagerload related data specified in include querystring parameter. If you want to disable this feature you must add eagerload_includes: False to data layer parameters.
+By default SQLAlchemy eagerly loads related data specified in the include query string parameter. If you want to disable this feature you must add eagerload_includes: False to the data layer parameters.
Custom data layer
-----------------
-Like I said previously you can create and use your own data layer. A custom data layer must inherit from `flask_combo_jsonapi.data_layers.base.Base `_. You can see the full scope of possibilities of a data layer in this base class.
+As previously mentioned, you can create and use your own data layer.
+A custom data layer must inherit from `flask_combo_jsonapi.data_layers.base.Base `_.
+You can see the full scope of possibilities of a data layer in this base class.
Usage example:
diff --git a/docs/errors.rst b/docs/errors.rst
index 16a1c86..c8a2363 100644
--- a/docs/errors.rst
+++ b/docs/errors.rst
@@ -5,7 +5,7 @@ Errors
.. currentmodule:: flask_combo_jsonapi
-JSONAPI 1.0 specification recommand to return errors like that:
+The JSON:API 1.0 specification recommends to return errors like this:
.. sourcecode:: http
@@ -28,9 +28,9 @@ JSONAPI 1.0 specification recommand to return errors like that:
}
}
-The "source" field gives information about the error if it is located in data provided or in querystring parameter.
+The "source" field gives information about the error if it is located in data provided or in a query string parameter.
-The previous example displays error located in data provided instead of this next example displays error located in querystring parameter "include":
+The previous example shows an error located in data provided. The following example shows error in the query string parameter "include":
.. sourcecode:: http
@@ -53,13 +53,13 @@ The previous example displays error located in data provided instead of this nex
}
}
-Flask-COMBO-JSONAPI provides two kind of helpers to achieve error displaying:
+Flask-COMBO-JSONAPI provides two kinds of helpers for displaying errors:
| * **the errors module**: you can import jsonapi_errors from the `errors module `_ to create the structure of a list of errors according to JSONAPI 1.0 specification
|
-| * **the exceptions module**: you can import lot of exceptions from this `module `_ that helps you to raise exceptions that will be well formatted according to JSONAPI 1.0 specification
+| * **the exceptions module**: you can import a lot of exceptions from this `module `_ that helps you to raise exceptions that will be well-formatted according to the JSON:API 1.0 specification
-When you create custom code for your api I recommand to use exceptions from exceptions module of Flask-COMBO-JSONAPI to raise errors because JsonApiException based exceptions are catched and rendered according to JSONAPI 1.0 specification.
+When you create custom code for your API I recommand using exceptions from the Flask-COMBO-JSONAPI's exceptions module to raise errors because JsonApiException-based exceptions are caught and rendered according to the JSON:API 1.0 specification.
Example:
diff --git a/docs/filtering.rst b/docs/filtering.rst
index a701985..6018bf4 100644
--- a/docs/filtering.rst
+++ b/docs/filtering.rst
@@ -5,7 +5,12 @@ Filtering
.. currentmodule:: flask_combo_jsonapi
-Flask-COMBO-JSONAPI as a very flexible filtering system. The filtering system is completely related to the data layer used by the ResourceList manager. I will explain the filtering interface for SQLAlchemy data layer but you can use the same interface to your filtering implementation of your custom data layer. The only requirement is that you have to use the "filter" querystring parameter to make filtering according to the JSONAPI 1.0 specification.
+Flask-COMBO-JSONAPI has a very flexible filtering system.
+The filtering system is directly attached to the data layer used by the ResourceList manager.
+These examples show the filtering interface for SQLAlchemy's data layer
+but you can use the same interface for your custom data layer's filtering implementation as well.
+The only requirement is that you have to use the "filter" query string parameter
+to filter according to the JSON:API 1.0 specification.
.. note::
@@ -14,7 +19,7 @@ Flask-COMBO-JSONAPI as a very flexible filtering system. The filtering system is
SQLAlchemy
----------
-The filtering system of SQLAlchemy data layer has exactly the same interface as the filtering system of `Flask-Restless `_.
+The filtering system of SQLAlchemy's data layer has exactly the same interface as the one used in `Flask-Restless `_.
So this is a first example:
.. sourcecode:: http
@@ -22,11 +27,11 @@ So this is a first example:
GET /persons?filter=[{"name":"name","op":"eq","val":"John"}] HTTP/1.1
Accept: application/vnd.api+json
-In this example we want to retrieve persons which name is John. So we can see that the filtering interface completely fit the filtering interface of SQLAlchemy: a list a filter information.
+In this example we want to retrieve person records for people named John. So we can see that the filtering interface completely fits that of SQLAlchemy: a list a filter information.
:name: the name of the field you want to filter on
- :op: the operation you want to use (all sqlalchemy operations are available)
- :val: the value that you want to compare. You can replace this by "field" if you want to compare against the value of an other field
+ :op: the operation you want to use (all SQLAlchemy operations are available)
+ :val: the value that you want to compare. You can replace this by "field" if you want to compare against the value of another field
Example with field:
@@ -35,7 +40,7 @@ Example with field:
GET /persons?filter=[{"name":"name","op":"eq","field":"birth_date"}] HTTP/1.1
Accept: application/vnd.api+json
-In this example, we want to retrieve persons that name is equal to his birth_date. I know, this example is absurd but it is just to explain the syntax of this kind of filter.
+In this example, we want to retrieve people whose name is equal to their birth_date. This example is absurd, it's just here to explain the syntax of this kind of filter.
If you want to filter through relationships you can do that:
@@ -56,9 +61,9 @@ If you want to filter through relationships you can do that:
.. note ::
- When you filter on relationships use "any" operator for "to many" relationships and "has" operator for "to one" relationships.
+ When you filter on relationships use the "any" operator for "to many" relationships and the "has" operator for "to one" relationships.
-There is a shortcut to achieve the same filter:
+There is a shortcut to achieve the same filtering:
.. sourcecode:: http
@@ -107,36 +112,36 @@ You can also use boolean combination of operations:
Common available operators:
-* any: used to filter on to many relationships
+* any: used to filter on "to many" relationships
* between: used to filter a field between two values
-* endswith: check if field ends with a string
-* eq: check if field is equal to something
-* ge: check if field is greater than or equal to something
-* gt: check if field is greater than to something
-* has: used to filter on to one relationships
-* ilike: check if field contains a string (case insensitive)
-* in\_: check if field is in a list of values
-* is\_: check if field is a value
-* isnot: check if field is not a value
-* like: check if field contains a string
-* le: check if field is less than or equal to something
-* lt: check if field is less than to something
-* match: check if field match against a string or pattern
-* ne: check if field is not equal to something
-* notilike: check if field does not contains a string (case insensitive)
-* notin\_: check if field is not in a list of values
-* notlike: check if field does not contains a string
-* startswith: check if field starts with a string
+* endswith: checks if field ends with a string
+* eq: checks if field is equal to something
+* ge: checks if field is greater than or equal to something
+* gt: checks if field is greater than something
+* has: used to filter on "to one" relationships
+* ilike: checks if field contains a string (case insensitive)
+* in\_: checks if field is in a list of values
+* is\_: checks if field is a value
+* isnot: checks if field is not a value
+* like: checks if field contains a string
+* le: checks if field is less than or equal to something
+* lt: checks if field is less than something
+* match: checks if field matches against a string or pattern
+* ne: checks if field is not equal to something
+* notilike: checks if field does not contain a string (case insensitive)
+* notin\_: checks if field is not in a list of values
+* notlike: checks if field does not contain a string
+* startswith: checks if field starts with a string
.. note::
- Availables operators depend on field type in your model
+ Available operators depend on the field type in your model
Simple filters
--------------
-Simple filter adds support for a simplified form of filters and supports only *eq* operator.
-Each simple filter transforms to original filter and appends to list of filters.
+Simple filters add support for a simplified form of filters and support only the *eq* operator.
+Each simple filter is transformed into a full filter and appended to the list of filters.
For example
@@ -145,7 +150,7 @@ For example
GET /persons?filter[name]=John HTTP/1.1
Accept: application/vnd.api+json
-equals to:
+equals:
.. sourcecode:: http
@@ -153,14 +158,14 @@ equals to:
Accept: application/vnd.api+json
-You can also use more than one simple filter in request:
+You can also use more than one simple filter in a request:
.. sourcecode:: http
GET /persons?filter[name]=John&filter[gender]=male HTTP/1.1
Accept: application/vnd.api+json
-which equals to:
+which is equal to:
.. sourcecode:: http
diff --git a/docs/http_snippets/README.md b/docs/http_snippets/README.md
new file mode 100644
index 0000000..01d9207
--- /dev/null
+++ b/docs/http_snippets/README.md
@@ -0,0 +1,103 @@
+# Generate HTTP code snippets
+
+
+## Install
+
+> Waiting for a PR to be merged: https://github.com/Kong/httpsnippet/pull/222
+>
+> Otherwise, extra options cannot be passed
+> (you'll need to delete Host and Content-Length manually, or use the forked version)
+
+```shell
+# to use in cli
+npm install --global httpsnippet
+```
+
+```shell
+# to use as a module
+npm install --save httpsnippet
+```
+
+## Spec
+
+Create HAR specs (.json files) in this directory.
+
+> **Don't edit any files in the `./snippets` directory manually!**
+
+## Generate
+
+### Create all snippets and run requests for them
+
+```shell
+# run and create for all minimal api requests
+./run_and_create.sh minimal_api
+```
+
+```shell
+# run and create for all relationship api requests
+./run_and_create.sh relationship_api
+```
+
+```shell
+# run and create for delete example relationship api requests
+./run_and_create.sh relationship_api__delete
+```
+
+### Or do it manually:
+
+```shell
+# example
+httpsnippet example.json --target node --client unirest --output ./snippets
+```
+
+```shell
+# minimal api to python3
+httpsnippet minimal_api__create_person.json --target python --client python3 --output ./snippets
+```
+
+```shell
+# minimal api to http
+httpsnippet minimal_api__create_person.json --target http --output ./snippets
+```
+
+
+```shell
+# minimal api to http. don't write Host, Content-Length
+httpsnippet minimal_api__create_person.json --target http --output ./snippets -x '{"autoHost": false, "autoContentLength": false}'
+```
+
+
+```shell
+# process multiple
+httpsnippet ./*.json --target http --output ./snippets
+```
+
+
+### Create requests and run them, write results
+
+```shell
+# create python-requests requests snippets
+httpsnippet ./*.json --target python --client requests --output ./snippets
+```
+
+```shell
+# Run requests for minimal api, save output
+python3 update_snippets_with_responses.py minimal_api
+```
+
+```shell
+# Run requests for relationship api, save output
+python3 update_snippets_with_responses.py relationship_api
+```
+
+#### Verbose logs (DEBUG level)
+
+```shell
+# Run requests for relationship api, save output
+python3 update_snippets_with_responses.py relationship_api --verbose
+```
+
+> **Pro tip:** run webserver for specs before running update_snippets_with_responses, otherwise it won't work 😉
+
+
+Copy-paste resulting help text (from between the "===" lines) to make includes.
diff --git a/docs/http_snippets/minimal_api__create_person.json b/docs/http_snippets/minimal_api__create_person.json
new file mode 100644
index 0000000..c72af59
--- /dev/null
+++ b/docs/http_snippets/minimal_api__create_person.json
@@ -0,0 +1,17 @@
+{
+ "method": "POST",
+ "url": "http://localhost:5000/persons",
+ "httpVersion": "HTTP/1.1",
+ "queryString": [
+ ],
+ "headers": [
+ {
+ "name": "content-type",
+ "value": "application/vnd.api+json"
+ }
+ ],
+ "postData": {
+ "mimeType": "application/json",
+ "text": "{\n \"data\": {\n \"type\": \"person\",\n \"attributes\": {\n \"name\": \"John\"\n }\n }\n}"
+ }
+}
diff --git a/docs/http_snippets/minimal_api__delete_person.json b/docs/http_snippets/minimal_api__delete_person.json
new file mode 100644
index 0000000..71d9e17
--- /dev/null
+++ b/docs/http_snippets/minimal_api__delete_person.json
@@ -0,0 +1,13 @@
+{
+ "method": "DELETE",
+ "url": "http://localhost:5000/persons/1",
+ "httpVersion": "HTTP/1.1",
+ "queryString": [
+ ],
+ "headers": [
+ {
+ "name": "content-type",
+ "value": "application/vnd.api+json"
+ }
+ ]
+}
diff --git a/docs/http_snippets/minimal_api__get_person.json b/docs/http_snippets/minimal_api__get_person.json
new file mode 100644
index 0000000..0306da1
--- /dev/null
+++ b/docs/http_snippets/minimal_api__get_person.json
@@ -0,0 +1,13 @@
+{
+ "method": "GET",
+ "url": "http://localhost:5000/persons/1",
+ "httpVersion": "HTTP/1.1",
+ "queryString": [
+ ],
+ "headers": [
+ {
+ "name": "content-type",
+ "value": "application/vnd.api+json"
+ }
+ ]
+}
diff --git a/docs/http_snippets/minimal_api__get_persons.json b/docs/http_snippets/minimal_api__get_persons.json
new file mode 100644
index 0000000..99e62c3
--- /dev/null
+++ b/docs/http_snippets/minimal_api__get_persons.json
@@ -0,0 +1,13 @@
+{
+ "method": "GET",
+ "url": "http://localhost:5000/persons",
+ "httpVersion": "HTTP/1.1",
+ "queryString": [
+ ],
+ "headers": [
+ {
+ "name": "content-type",
+ "value": "application/vnd.api+json"
+ }
+ ]
+}
diff --git a/docs/http_snippets/minimal_api__patch_person.json b/docs/http_snippets/minimal_api__patch_person.json
new file mode 100644
index 0000000..d340bf5
--- /dev/null
+++ b/docs/http_snippets/minimal_api__patch_person.json
@@ -0,0 +1,17 @@
+{
+ "method": "PATCH",
+ "url": "http://localhost:5000/persons/1",
+ "httpVersion": "HTTP/1.1",
+ "queryString": [
+ ],
+ "headers": [
+ {
+ "name": "content-type",
+ "value": "application/vnd.api+json"
+ }
+ ],
+ "postData": {
+ "mimeType": "application/json",
+ "text": "{\n \"data\": {\n \"id\": 1,\n \"type\": \"person\",\n \"attributes\": {\n \"name\": \"Sam\"\n }\n }\n}"
+ }
+}
diff --git a/docs/http_snippets/relationship_api__create_computer.json b/docs/http_snippets/relationship_api__create_computer.json
new file mode 100644
index 0000000..ecc4489
--- /dev/null
+++ b/docs/http_snippets/relationship_api__create_computer.json
@@ -0,0 +1,17 @@
+{
+ "method": "POST",
+ "url": "http://localhost:5000/computers",
+ "httpVersion": "HTTP/1.1",
+ "queryString": [
+ ],
+ "headers": [
+ {
+ "name": "content-type",
+ "value": "application/vnd.api+json"
+ }
+ ],
+ "postData": {
+ "mimeType": "application/json",
+ "text": "{\n \"data\": {\n \"type\": \"computer\",\n \"attributes\": {\n \"serial\": \"Amstrad\"\n }\n }\n}"
+ }
+}
diff --git a/docs/http_snippets/relationship_api__create_computer_relationship_for_person.json b/docs/http_snippets/relationship_api__create_computer_relationship_for_person.json
new file mode 100644
index 0000000..de09de4
--- /dev/null
+++ b/docs/http_snippets/relationship_api__create_computer_relationship_for_person.json
@@ -0,0 +1,17 @@
+{
+ "method": "POST",
+ "url": "http://localhost:5000/persons/1/relationships/computers",
+ "httpVersion": "HTTP/1.1",
+ "queryString": [
+ ],
+ "headers": [
+ {
+ "name": "content-type",
+ "value": "application/vnd.api+json"
+ }
+ ],
+ "postData": {
+ "mimeType": "application/json",
+ "text": "{\n \"data\": [\n {\n \"type\": \"computer\",\n \"id\": \"4\"\n }\n ]\n}"
+ }
+}
diff --git a/docs/http_snippets/relationship_api__create_person_with_computer_relationship.json b/docs/http_snippets/relationship_api__create_person_with_computer_relationship.json
new file mode 100644
index 0000000..182706d
--- /dev/null
+++ b/docs/http_snippets/relationship_api__create_person_with_computer_relationship.json
@@ -0,0 +1,17 @@
+{
+ "method": "POST",
+ "url": "http://localhost:5000/persons?include=computers",
+ "httpVersion": "HTTP/1.1",
+ "queryString": [
+ ],
+ "headers": [
+ {
+ "name": "content-type",
+ "value": "application/vnd.api+json"
+ }
+ ],
+ "postData": {
+ "mimeType": "application/json",
+ "text": "{\n \"data\": {\n \"type\": \"person\",\n \"attributes\": {\n \"name\": \"John\",\n \"email\": \"john@exmple.com\"\n },\n \"relationships\": {\n \"computers\": {\n \"data\": [\n {\n \"type\": \"computer\",\n \"id\": \"2\"\n }\n ]\n }\n }\n }\n}"
+ }
+}
diff --git a/docs/http_snippets/relationship_api__delete_computer.json b/docs/http_snippets/relationship_api__delete_computer.json
new file mode 100644
index 0000000..30a74e0
--- /dev/null
+++ b/docs/http_snippets/relationship_api__delete_computer.json
@@ -0,0 +1,13 @@
+{
+ "method": "DELETE",
+ "url": "http://localhost:5000/computers/1",
+ "httpVersion": "HTTP/1.1",
+ "queryString": [
+ ],
+ "headers": [
+ {
+ "name": "content-type",
+ "value": "application/vnd.api+json"
+ }
+ ]
+}
diff --git a/docs/http_snippets/relationship_api__delete_computer_relationship.json b/docs/http_snippets/relationship_api__delete_computer_relationship.json
new file mode 100644
index 0000000..d87ef91
--- /dev/null
+++ b/docs/http_snippets/relationship_api__delete_computer_relationship.json
@@ -0,0 +1,17 @@
+{
+ "method": "DELETE",
+ "url": "http://localhost:5000/persons/1/relationships/computers",
+ "httpVersion": "HTTP/1.1",
+ "queryString": [
+ ],
+ "headers": [
+ {
+ "name": "content-type",
+ "value": "application/vnd.api+json"
+ }
+ ],
+ "postData": {
+ "mimeType": "application/json",
+ "text": "{\n \"data\": [\n {\n \"type\": \"computer\",\n \"id\": \"3\"\n }\n ]\n}"
+ }
+}
diff --git a/docs/http_snippets/relationship_api__get_computers.json b/docs/http_snippets/relationship_api__get_computers.json
new file mode 100644
index 0000000..38cc024
--- /dev/null
+++ b/docs/http_snippets/relationship_api__get_computers.json
@@ -0,0 +1,13 @@
+{
+ "method": "GET",
+ "url": "http://localhost:5000/computers",
+ "httpVersion": "HTTP/1.1",
+ "queryString": [
+ ],
+ "headers": [
+ {
+ "name": "content-type",
+ "value": "application/vnd.api+json"
+ }
+ ]
+}
diff --git a/docs/http_snippets/relationship_api__get_person_related_computers.json b/docs/http_snippets/relationship_api__get_person_related_computers.json
new file mode 100644
index 0000000..b46a122
--- /dev/null
+++ b/docs/http_snippets/relationship_api__get_person_related_computers.json
@@ -0,0 +1,13 @@
+{
+ "method": "GET",
+ "url": "http://localhost:5000/persons/1/computers",
+ "httpVersion": "HTTP/1.1",
+ "queryString": [
+ ],
+ "headers": [
+ {
+ "name": "content-type",
+ "value": "application/vnd.api+json"
+ }
+ ]
+}
diff --git a/docs/http_snippets/relationship_api__get_person_with_computers.json b/docs/http_snippets/relationship_api__get_person_with_computers.json
new file mode 100644
index 0000000..66a29ff
--- /dev/null
+++ b/docs/http_snippets/relationship_api__get_person_with_computers.json
@@ -0,0 +1,13 @@
+{
+ "method": "GET",
+ "url": "http://localhost:5000/persons/1?include=computers",
+ "httpVersion": "HTTP/1.1",
+ "queryString": [
+ ],
+ "headers": [
+ {
+ "name": "content-type",
+ "value": "application/vnd.api+json"
+ }
+ ]
+}
diff --git a/docs/http_snippets/relationship_api__patch_computer.json b/docs/http_snippets/relationship_api__patch_computer.json
new file mode 100644
index 0000000..51e018f
--- /dev/null
+++ b/docs/http_snippets/relationship_api__patch_computer.json
@@ -0,0 +1,17 @@
+{
+ "method": "PATCH",
+ "url": "http://localhost:5000/computers/1",
+ "httpVersion": "HTTP/1.1",
+ "queryString": [
+ ],
+ "headers": [
+ {
+ "name": "content-type",
+ "value": "application/vnd.api+json"
+ }
+ ],
+ "postData": {
+ "mimeType": "application/json",
+ "text": "{\n \"data\": {\n \"type\": \"computer\",\n \"id\": \"1\",\n \"attributes\": {\n \"serial\": \"New Amstrad\"\n }\n }\n}"
+ }
+}
diff --git a/docs/http_snippets/relationship_api__update_person_with_computer_relationship.json b/docs/http_snippets/relationship_api__update_person_with_computer_relationship.json
new file mode 100644
index 0000000..9d20fa2
--- /dev/null
+++ b/docs/http_snippets/relationship_api__update_person_with_computer_relationship.json
@@ -0,0 +1,17 @@
+{
+ "method": "PATCH",
+ "url": "http://localhost:5000/persons/1?include=computers",
+ "httpVersion": "HTTP/1.1",
+ "queryString": [
+ ],
+ "headers": [
+ {
+ "name": "content-type",
+ "value": "application/vnd.api+json"
+ }
+ ],
+ "postData": {
+ "mimeType": "application/json",
+ "text": "{\n \"data\": {\n \"type\": \"person\",\n \"id\": \"1\",\n \"attributes\": {\n \"email\": \"john@example.com\"\n },\n \"relationships\": {\n \"computers\": {\n \"data\": [\n {\n \"type\": \"computer\",\n \"id\": \"3\"\n }\n ]\n }\n }\n }\n}"
+ }
+}
diff --git a/docs/http_snippets/run_and_create.sh b/docs/http_snippets/run_and_create.sh
new file mode 100755
index 0000000..1c0cabe
--- /dev/null
+++ b/docs/http_snippets/run_and_create.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+
+echo "Create HTTP and Python snippets"
+
+for filename in ./"$1"*.json; do
+ echo "process $filename"
+ httpsnippet "$filename" --target http --output ./snippets -x '{"autoHost": false, "autoContentLength": false}'
+ httpsnippet "$filename" --target python --client requests --output ./snippets
+done
+
+echo "Run requests"
+python3 update_snippets_with_responses.py "$1"
diff --git a/docs/http_snippets/snippets/.gitignore b/docs/http_snippets/snippets/.gitignore
new file mode 100644
index 0000000..f104652
--- /dev/null
+++ b/docs/http_snippets/snippets/.gitignore
@@ -0,0 +1 @@
+*.py
diff --git a/docs/http_snippets/snippets/minimal_api__create_person b/docs/http_snippets/snippets/minimal_api__create_person
new file mode 100644
index 0000000..6f37fe2
--- /dev/null
+++ b/docs/http_snippets/snippets/minimal_api__create_person
@@ -0,0 +1,11 @@
+POST /persons HTTP/1.1
+Content-Type: application/vnd.api+json
+
+{
+ "data": {
+ "type": "person",
+ "attributes": {
+ "name": "John"
+ }
+ }
+}
diff --git a/docs/http_snippets/snippets/minimal_api__create_person_result b/docs/http_snippets/snippets/minimal_api__create_person_result
new file mode 100644
index 0000000..0fb5eb3
--- /dev/null
+++ b/docs/http_snippets/snippets/minimal_api__create_person_result
@@ -0,0 +1,21 @@
+HTTP/1.1 201 Created
+Content-Type: application/vnd.api+json
+
+{
+ "data": {
+ "attributes": {
+ "name": "John"
+ },
+ "id": "1",
+ "links": {
+ "self": "/persons/1"
+ },
+ "type": "person"
+ },
+ "jsonapi": {
+ "version": "1.0"
+ },
+ "links": {
+ "self": "/persons/1"
+ }
+}
diff --git a/docs/http_snippets/snippets/minimal_api__delete_person b/docs/http_snippets/snippets/minimal_api__delete_person
new file mode 100644
index 0000000..77a58ba
--- /dev/null
+++ b/docs/http_snippets/snippets/minimal_api__delete_person
@@ -0,0 +1,4 @@
+DELETE /persons/1 HTTP/1.1
+Content-Type: application/vnd.api+json
+
+
diff --git a/docs/http_snippets/snippets/minimal_api__delete_person_result b/docs/http_snippets/snippets/minimal_api__delete_person_result
new file mode 100644
index 0000000..a4fe8d7
--- /dev/null
+++ b/docs/http_snippets/snippets/minimal_api__delete_person_result
@@ -0,0 +1,11 @@
+HTTP/1.1 200 OK
+Content-Type: application/vnd.api+json
+
+{
+ "jsonapi": {
+ "version": "1.0"
+ },
+ "meta": {
+ "message": "Object successfully deleted"
+ }
+}
diff --git a/docs/http_snippets/snippets/minimal_api__get_person b/docs/http_snippets/snippets/minimal_api__get_person
new file mode 100644
index 0000000..3171d4c
--- /dev/null
+++ b/docs/http_snippets/snippets/minimal_api__get_person
@@ -0,0 +1,4 @@
+GET /persons/1 HTTP/1.1
+Content-Type: application/vnd.api+json
+
+
diff --git a/docs/http_snippets/snippets/minimal_api__get_person_result b/docs/http_snippets/snippets/minimal_api__get_person_result
new file mode 100644
index 0000000..e5dd448
--- /dev/null
+++ b/docs/http_snippets/snippets/minimal_api__get_person_result
@@ -0,0 +1,21 @@
+HTTP/1.1 200 OK
+Content-Type: application/vnd.api+json
+
+{
+ "data": {
+ "attributes": {
+ "name": "John"
+ },
+ "id": "1",
+ "links": {
+ "self": "/persons/1"
+ },
+ "type": "person"
+ },
+ "jsonapi": {
+ "version": "1.0"
+ },
+ "links": {
+ "self": "/persons/1"
+ }
+}
diff --git a/docs/http_snippets/snippets/minimal_api__get_persons b/docs/http_snippets/snippets/minimal_api__get_persons
new file mode 100644
index 0000000..9d75f29
--- /dev/null
+++ b/docs/http_snippets/snippets/minimal_api__get_persons
@@ -0,0 +1,4 @@
+GET /persons HTTP/1.1
+Content-Type: application/vnd.api+json
+
+
diff --git a/docs/http_snippets/snippets/minimal_api__get_persons_result b/docs/http_snippets/snippets/minimal_api__get_persons_result
new file mode 100644
index 0000000..a2f3725
--- /dev/null
+++ b/docs/http_snippets/snippets/minimal_api__get_persons_result
@@ -0,0 +1,26 @@
+HTTP/1.1 200 OK
+Content-Type: application/vnd.api+json
+
+{
+ "data": [
+ {
+ "attributes": {
+ "name": "John"
+ },
+ "id": "1",
+ "links": {
+ "self": "/persons/1"
+ },
+ "type": "person"
+ }
+ ],
+ "jsonapi": {
+ "version": "1.0"
+ },
+ "links": {
+ "self": "http://localhost:5000/persons"
+ },
+ "meta": {
+ "count": 1
+ }
+}
diff --git a/docs/http_snippets/snippets/minimal_api__patch_person b/docs/http_snippets/snippets/minimal_api__patch_person
new file mode 100644
index 0000000..cde9046
--- /dev/null
+++ b/docs/http_snippets/snippets/minimal_api__patch_person
@@ -0,0 +1,12 @@
+PATCH /persons/1 HTTP/1.1
+Content-Type: application/vnd.api+json
+
+{
+ "data": {
+ "id": 1,
+ "type": "person",
+ "attributes": {
+ "name": "Sam"
+ }
+ }
+}
diff --git a/docs/http_snippets/snippets/minimal_api__patch_person_result b/docs/http_snippets/snippets/minimal_api__patch_person_result
new file mode 100644
index 0000000..5e777fe
--- /dev/null
+++ b/docs/http_snippets/snippets/minimal_api__patch_person_result
@@ -0,0 +1,21 @@
+HTTP/1.1 200 OK
+Content-Type: application/vnd.api+json
+
+{
+ "data": {
+ "attributes": {
+ "name": "Sam"
+ },
+ "id": "1",
+ "links": {
+ "self": "/persons/1"
+ },
+ "type": "person"
+ },
+ "jsonapi": {
+ "version": "1.0"
+ },
+ "links": {
+ "self": "/persons/1"
+ }
+}
diff --git a/docs/http_snippets/snippets/relationship_api__create_computer b/docs/http_snippets/snippets/relationship_api__create_computer
new file mode 100644
index 0000000..140ca67
--- /dev/null
+++ b/docs/http_snippets/snippets/relationship_api__create_computer
@@ -0,0 +1,11 @@
+POST /computers HTTP/1.1
+Content-Type: application/vnd.api+json
+
+{
+ "data": {
+ "type": "computer",
+ "attributes": {
+ "serial": "Amstrad"
+ }
+ }
+}
diff --git a/docs/http_snippets/snippets/relationship_api__create_computer_relationship_for_person b/docs/http_snippets/snippets/relationship_api__create_computer_relationship_for_person
new file mode 100644
index 0000000..203b578
--- /dev/null
+++ b/docs/http_snippets/snippets/relationship_api__create_computer_relationship_for_person
@@ -0,0 +1,11 @@
+POST /persons/1/relationships/computers HTTP/1.1
+Content-Type: application/vnd.api+json
+
+{
+ "data": [
+ {
+ "type": "computer",
+ "id": "4"
+ }
+ ]
+}
diff --git a/docs/http_snippets/snippets/relationship_api__create_computer_relationship_for_person_result b/docs/http_snippets/snippets/relationship_api__create_computer_relationship_for_person_result
new file mode 100644
index 0000000..2a117f7
--- /dev/null
+++ b/docs/http_snippets/snippets/relationship_api__create_computer_relationship_for_person_result
@@ -0,0 +1,11 @@
+HTTP/1.1 200 OK
+Content-Type: application/vnd.api+json
+
+{
+ "jsonapi": {
+ "version": "1.0"
+ },
+ "meta": {
+ "message": "Relationship successfully created"
+ }
+}
diff --git a/docs/http_snippets/snippets/relationship_api__create_computer_result b/docs/http_snippets/snippets/relationship_api__create_computer_result
new file mode 100644
index 0000000..e6fac98
--- /dev/null
+++ b/docs/http_snippets/snippets/relationship_api__create_computer_result
@@ -0,0 +1,29 @@
+HTTP/1.1 201 Created
+Content-Type: application/vnd.api+json
+
+{
+ "data": {
+ "attributes": {
+ "serial": "Amstrad"
+ },
+ "id": "1",
+ "links": {
+ "self": "/computers/1"
+ },
+ "relationships": {
+ "owner": {
+ "links": {
+ "related": "/computers/1/owner",
+ "self": "/computers/1/relationships/owner"
+ }
+ }
+ },
+ "type": "computer"
+ },
+ "jsonapi": {
+ "version": "1.0"
+ },
+ "links": {
+ "self": "/computers/1"
+ }
+}
diff --git a/docs/http_snippets/snippets/relationship_api__create_person_with_computer_relationship b/docs/http_snippets/snippets/relationship_api__create_person_with_computer_relationship
new file mode 100644
index 0000000..aa36a00
--- /dev/null
+++ b/docs/http_snippets/snippets/relationship_api__create_person_with_computer_relationship
@@ -0,0 +1,22 @@
+POST /persons?include=computers HTTP/1.1
+Content-Type: application/vnd.api+json
+
+{
+ "data": {
+ "type": "person",
+ "attributes": {
+ "name": "John",
+ "email": "john@exmple.com"
+ },
+ "relationships": {
+ "computers": {
+ "data": [
+ {
+ "type": "computer",
+ "id": "2"
+ }
+ ]
+ }
+ }
+ }
+}
diff --git a/docs/http_snippets/snippets/relationship_api__create_person_with_computer_relationship_result b/docs/http_snippets/snippets/relationship_api__create_person_with_computer_relationship_result
new file mode 100644
index 0000000..e765684
--- /dev/null
+++ b/docs/http_snippets/snippets/relationship_api__create_person_with_computer_relationship_result
@@ -0,0 +1,56 @@
+HTTP/1.1 201 Created
+Content-Type: application/vnd.api+json
+
+{
+ "data": {
+ "attributes": {
+ "display_name": "JOHN ",
+ "name": "John"
+ },
+ "id": "1",
+ "links": {
+ "self": "/persons/1"
+ },
+ "relationships": {
+ "computers": {
+ "data": [
+ {
+ "id": "2",
+ "type": "computer"
+ }
+ ],
+ "links": {
+ "related": "/persons/1/computers",
+ "self": "/persons/1/relationships/computers"
+ }
+ }
+ },
+ "type": "person"
+ },
+ "included": [
+ {
+ "attributes": {
+ "serial": "Halo"
+ },
+ "id": "2",
+ "links": {
+ "self": "/computers/2"
+ },
+ "relationships": {
+ "owner": {
+ "links": {
+ "related": "/computers/2/owner",
+ "self": "/computers/2/relationships/owner"
+ }
+ }
+ },
+ "type": "computer"
+ }
+ ],
+ "jsonapi": {
+ "version": "1.0"
+ },
+ "links": {
+ "self": "/persons/1"
+ }
+}
diff --git a/docs/http_snippets/snippets/relationship_api__delete_computer b/docs/http_snippets/snippets/relationship_api__delete_computer
new file mode 100644
index 0000000..a4a17bf
--- /dev/null
+++ b/docs/http_snippets/snippets/relationship_api__delete_computer
@@ -0,0 +1,4 @@
+DELETE /computers/1 HTTP/1.1
+Content-Type: application/vnd.api+json
+
+
diff --git a/docs/http_snippets/snippets/relationship_api__delete_computer_relationship b/docs/http_snippets/snippets/relationship_api__delete_computer_relationship
new file mode 100644
index 0000000..1b285c6
--- /dev/null
+++ b/docs/http_snippets/snippets/relationship_api__delete_computer_relationship
@@ -0,0 +1,11 @@
+DELETE /persons/1/relationships/computers HTTP/1.1
+Content-Type: application/vnd.api+json
+
+{
+ "data": [
+ {
+ "type": "computer",
+ "id": "3"
+ }
+ ]
+}
diff --git a/docs/http_snippets/snippets/relationship_api__delete_computer_relationship_result b/docs/http_snippets/snippets/relationship_api__delete_computer_relationship_result
new file mode 100644
index 0000000..d57ff11
--- /dev/null
+++ b/docs/http_snippets/snippets/relationship_api__delete_computer_relationship_result
@@ -0,0 +1,11 @@
+HTTP/1.1 200 OK
+Content-Type: application/vnd.api+json
+
+{
+ "jsonapi": {
+ "version": "1.0"
+ },
+ "meta": {
+ "message": "Relationship successfully updated"
+ }
+}
diff --git a/docs/http_snippets/snippets/relationship_api__delete_computer_result b/docs/http_snippets/snippets/relationship_api__delete_computer_result
new file mode 100644
index 0000000..a4fe8d7
--- /dev/null
+++ b/docs/http_snippets/snippets/relationship_api__delete_computer_result
@@ -0,0 +1,11 @@
+HTTP/1.1 200 OK
+Content-Type: application/vnd.api+json
+
+{
+ "jsonapi": {
+ "version": "1.0"
+ },
+ "meta": {
+ "message": "Object successfully deleted"
+ }
+}
diff --git a/docs/http_snippets/snippets/relationship_api__get_computers b/docs/http_snippets/snippets/relationship_api__get_computers
new file mode 100644
index 0000000..20f2449
--- /dev/null
+++ b/docs/http_snippets/snippets/relationship_api__get_computers
@@ -0,0 +1,4 @@
+GET /computers HTTP/1.1
+Content-Type: application/vnd.api+json
+
+
diff --git a/docs/http_snippets/snippets/relationship_api__get_computers_result b/docs/http_snippets/snippets/relationship_api__get_computers_result
new file mode 100644
index 0000000..b6bf118
--- /dev/null
+++ b/docs/http_snippets/snippets/relationship_api__get_computers_result
@@ -0,0 +1,34 @@
+HTTP/1.1 200 OK
+Content-Type: application/vnd.api+json
+
+{
+ "data": [
+ {
+ "attributes": {
+ "serial": "Amstrad"
+ },
+ "id": "1",
+ "links": {
+ "self": "/computers/1"
+ },
+ "relationships": {
+ "owner": {
+ "links": {
+ "related": "/computers/1/owner",
+ "self": "/computers/1/relationships/owner"
+ }
+ }
+ },
+ "type": "computer"
+ }
+ ],
+ "jsonapi": {
+ "version": "1.0"
+ },
+ "links": {
+ "self": "http://localhost:5000/computers"
+ },
+ "meta": {
+ "count": 1
+ }
+}
diff --git a/docs/http_snippets/snippets/relationship_api__get_person_related_computers b/docs/http_snippets/snippets/relationship_api__get_person_related_computers
new file mode 100644
index 0000000..254b5b1
--- /dev/null
+++ b/docs/http_snippets/snippets/relationship_api__get_person_related_computers
@@ -0,0 +1,4 @@
+GET /persons/1/computers HTTP/1.1
+Content-Type: application/vnd.api+json
+
+
diff --git a/docs/http_snippets/snippets/relationship_api__get_person_related_computers_result b/docs/http_snippets/snippets/relationship_api__get_person_related_computers_result
new file mode 100644
index 0000000..81e0814
--- /dev/null
+++ b/docs/http_snippets/snippets/relationship_api__get_person_related_computers_result
@@ -0,0 +1,55 @@
+HTTP/1.1 200 OK
+Content-Type: application/vnd.api+json
+
+{
+ "data": [
+ {
+ "attributes": {
+ "serial": "Nestor"
+ },
+ "id": "3",
+ "links": {
+ "self": "/computers/3"
+ },
+ "relationships": {
+ "owner": {
+ "links": {
+ "related": "/computers/3/owner",
+ "self": "/computers/3/relationships/owner"
+ }
+ }
+ },
+ "type": "computer"
+ },
+ {
+ "attributes": {
+ "serial": "Commodore"
+ },
+ "id": "4",
+ "links": {
+ "self": "/computers/4"
+ },
+ "relationships": {
+ "owner": {
+ "links": {
+ "related": "/computers/4/owner",
+ "self": "/computers/4/relationships/owner"
+ }
+ }
+ },
+ "type": "computer"
+ }
+ ],
+ "jsonapi": {
+ "version": "1.0"
+ },
+ "links": {
+ "first": "http://localhost:5000/computers",
+ "last": "http://localhost:5000/computers?page%5Bnumber%5D=2",
+ "next": "http://localhost:5000/computers?page%5Bnumber%5D=2",
+ "self": "http://localhost:5000/computers"
+ },
+ "meta": {
+ "count": 2
+ }
+}
diff --git a/docs/http_snippets/snippets/relationship_api__get_person_with_computers b/docs/http_snippets/snippets/relationship_api__get_person_with_computers
new file mode 100644
index 0000000..45db5ba
--- /dev/null
+++ b/docs/http_snippets/snippets/relationship_api__get_person_with_computers
@@ -0,0 +1,4 @@
+GET /persons/1?include=computers HTTP/1.1
+Content-Type: application/vnd.api+json
+
+
diff --git a/docs/http_snippets/snippets/relationship_api__get_person_with_computers_result b/docs/http_snippets/snippets/relationship_api__get_person_with_computers_result
new file mode 100644
index 0000000..6e8e1e5
--- /dev/null
+++ b/docs/http_snippets/snippets/relationship_api__get_person_with_computers_result
@@ -0,0 +1,78 @@
+HTTP/1.1 200 OK
+Content-Type: application/vnd.api+json
+
+{
+ "data": {
+ "attributes": {
+ "display_name": "JOHN ",
+ "name": "John"
+ },
+ "id": "1",
+ "links": {
+ "self": "/persons/1"
+ },
+ "relationships": {
+ "computers": {
+ "data": [
+ {
+ "id": "3",
+ "type": "computer"
+ },
+ {
+ "id": "4",
+ "type": "computer"
+ }
+ ],
+ "links": {
+ "related": "/persons/1/computers",
+ "self": "/persons/1/relationships/computers"
+ }
+ }
+ },
+ "type": "person"
+ },
+ "included": [
+ {
+ "attributes": {
+ "serial": "Nestor"
+ },
+ "id": "3",
+ "links": {
+ "self": "/computers/3"
+ },
+ "relationships": {
+ "owner": {
+ "links": {
+ "related": "/computers/3/owner",
+ "self": "/computers/3/relationships/owner"
+ }
+ }
+ },
+ "type": "computer"
+ },
+ {
+ "attributes": {
+ "serial": "Commodore"
+ },
+ "id": "4",
+ "links": {
+ "self": "/computers/4"
+ },
+ "relationships": {
+ "owner": {
+ "links": {
+ "related": "/computers/4/owner",
+ "self": "/computers/4/relationships/owner"
+ }
+ }
+ },
+ "type": "computer"
+ }
+ ],
+ "jsonapi": {
+ "version": "1.0"
+ },
+ "links": {
+ "self": "/persons/1"
+ }
+}
diff --git a/docs/http_snippets/snippets/relationship_api__patch_computer b/docs/http_snippets/snippets/relationship_api__patch_computer
new file mode 100644
index 0000000..2d5f016
--- /dev/null
+++ b/docs/http_snippets/snippets/relationship_api__patch_computer
@@ -0,0 +1,12 @@
+PATCH /computers/1 HTTP/1.1
+Content-Type: application/vnd.api+json
+
+{
+ "data": {
+ "type": "computer",
+ "id": "1",
+ "attributes": {
+ "serial": "New Amstrad"
+ }
+ }
+}
diff --git a/docs/http_snippets/snippets/relationship_api__patch_computer_result b/docs/http_snippets/snippets/relationship_api__patch_computer_result
new file mode 100644
index 0000000..4f7f372
--- /dev/null
+++ b/docs/http_snippets/snippets/relationship_api__patch_computer_result
@@ -0,0 +1,29 @@
+HTTP/1.1 200 OK
+Content-Type: application/vnd.api+json
+
+{
+ "data": {
+ "attributes": {
+ "serial": "New Amstrad"
+ },
+ "id": "1",
+ "links": {
+ "self": "/computers/1"
+ },
+ "relationships": {
+ "owner": {
+ "links": {
+ "related": "/computers/1/owner",
+ "self": "/computers/1/relationships/owner"
+ }
+ }
+ },
+ "type": "computer"
+ },
+ "jsonapi": {
+ "version": "1.0"
+ },
+ "links": {
+ "self": "/computers/1"
+ }
+}
diff --git a/docs/http_snippets/snippets/relationship_api__update_person_with_computer_relationship b/docs/http_snippets/snippets/relationship_api__update_person_with_computer_relationship
new file mode 100644
index 0000000..fd77275
--- /dev/null
+++ b/docs/http_snippets/snippets/relationship_api__update_person_with_computer_relationship
@@ -0,0 +1,22 @@
+PATCH /persons/1?include=computers HTTP/1.1
+Content-Type: application/vnd.api+json
+
+{
+ "data": {
+ "type": "person",
+ "id": "1",
+ "attributes": {
+ "email": "john@example.com"
+ },
+ "relationships": {
+ "computers": {
+ "data": [
+ {
+ "type": "computer",
+ "id": "3"
+ }
+ ]
+ }
+ }
+ }
+}
diff --git a/docs/http_snippets/snippets/relationship_api__update_person_with_computer_relationship_result b/docs/http_snippets/snippets/relationship_api__update_person_with_computer_relationship_result
new file mode 100644
index 0000000..13ce896
--- /dev/null
+++ b/docs/http_snippets/snippets/relationship_api__update_person_with_computer_relationship_result
@@ -0,0 +1,56 @@
+HTTP/1.1 200 OK
+Content-Type: application/vnd.api+json
+
+{
+ "data": {
+ "attributes": {
+ "display_name": "JOHN ",
+ "name": "John"
+ },
+ "id": "1",
+ "links": {
+ "self": "/persons/1"
+ },
+ "relationships": {
+ "computers": {
+ "data": [
+ {
+ "id": "3",
+ "type": "computer"
+ }
+ ],
+ "links": {
+ "related": "/persons/1/computers",
+ "self": "/persons/1/relationships/computers"
+ }
+ }
+ },
+ "type": "person"
+ },
+ "included": [
+ {
+ "attributes": {
+ "serial": "Nestor"
+ },
+ "id": "3",
+ "links": {
+ "self": "/computers/3"
+ },
+ "relationships": {
+ "owner": {
+ "links": {
+ "related": "/computers/3/owner",
+ "self": "/computers/3/relationships/owner"
+ }
+ }
+ },
+ "type": "computer"
+ }
+ ],
+ "jsonapi": {
+ "version": "1.0"
+ },
+ "links": {
+ "self": "/persons/1"
+ }
+}
diff --git a/docs/http_snippets/update_snippets_with_responses.py b/docs/http_snippets/update_snippets_with_responses.py
new file mode 100644
index 0000000..f761aee
--- /dev/null
+++ b/docs/http_snippets/update_snippets_with_responses.py
@@ -0,0 +1,185 @@
+import os
+import importlib
+import logging
+from http import HTTPStatus
+
+import requests
+import simplejson
+import argparse
+
+parser = argparse.ArgumentParser()
+parser.add_argument("prefix", help="Snippets prefix to process. Like 'minimal_api', 'relationship_', etc")
+parser.add_argument("-v", "--verbose", help="set logging level to DEBUG", action="store_true")
+
+log = logging.getLogger(__name__)
+
+SNIPPETS_DIR = "snippets"
+SORT_KEYS_ON_DUMP = True
+SNIPPET_RESULT_POSTFIX = "_result"
+REMOVE_PYTHON_SNIPPET = True
+
+
+SORTING_ORDER = [
+ "create",
+ "get",
+ "patch",
+ "update", # like patch
+ "delete",
+]
+
+ORDER_POS = {
+ i: v for i, v in enumerate(SORTING_ORDER)
+}
+
+
+class StrOrderCRUD:
+ def __init__(self, inner):
+ self.inner = inner
+
+ def __lt__(self, other):
+ index_1 = -1
+ index_2 = -1
+ for index, name in ORDER_POS.items():
+ substring = f"__{name}_"
+ if substring in self.inner:
+ index_1 = index
+ if substring in other.inner:
+ index_2 = index
+
+ if index_1 != index_2:
+ return index_1 < index_2
+
+ return self.inner < other.inner
+
+
+def run_request_for_module(module_name: str):
+ log.info("Start processing %r", module_name)
+
+ module_full_name = ".".join((SNIPPETS_DIR, module_name))
+ log.debug("import module %s", module_full_name)
+ module = importlib.import_module(module_full_name)
+
+ log.info("Process module %s", module)
+ response: requests.Response = module.response
+ log.info("Response %s", response)
+
+ http_response_text = []
+
+ response_reason = (response.reason or "")
+ if response.status_code != HTTPStatus.OK:
+ response_reason = response_reason.title()
+
+ http_response_text.append(
+ # "HTTP/1.1 201 Created"
+ "{} {} {}".format(
+ "HTTP/1.1",
+ response.status_code,
+ response_reason,
+ )
+ )
+
+ http_response_text.append(
+ "{}: {}".format(
+ "Content-Type",
+ response.headers.get('content-type'),
+ )
+ )
+ http_response_text.append("")
+
+ http_response_text.append(
+ simplejson.dumps(
+ response.json(),
+ sort_keys=SORT_KEYS_ON_DUMP,
+ indent=2,
+ ),
+ )
+
+ http_response_text.append("")
+
+ result_text = "\n".join(http_response_text)
+ log.debug("Result text:\n%s", result_text)
+
+ result_file_name = "/".join((SNIPPETS_DIR, module_name + SNIPPET_RESULT_POSTFIX))
+ with open(result_file_name, "w") as f:
+ res = f.write(result_text)
+ log.info("Wrote text (%s) to %r", res, result_file_name)
+
+ log.info("Processed %r", module_name)
+
+
+def add_help_lines(lines: list, module_name: str) -> None:
+ """
+
+ Append help lines to create smth like this:
+
+ '''
+
+ Request:
+
+ .. literalinclude:: ./http_snippets/snippets/minimal_api__create_person
+ :language: HTTP
+
+ Response:
+
+ .. literalinclude:: ./http_snippets/snippets/minimal_api__create_person_result
+ :language: HTTP
+
+ '''
+
+ """
+ literalinclude_file = ".. literalinclude:: ./http_snippets/snippets/" + module_name
+ rst_language_http = " :language: HTTP"
+
+ lines.append("")
+ lines.append("Request:")
+ lines.append("")
+ lines.append(literalinclude_file)
+ lines.append(rst_language_http)
+ lines.append("")
+ lines.append("Response:")
+ lines.append("")
+ lines.append(literalinclude_file + SNIPPET_RESULT_POSTFIX)
+ lines.append(rst_language_http)
+ lines.append("")
+
+
+def main():
+ args = parser.parse_args()
+ if args.verbose:
+ logging.basicConfig(level=logging.DEBUG)
+
+ log.warning("Starting")
+
+ available_modules = os.listdir(SNIPPETS_DIR)
+ log.debug("all available snippets: %s", available_modules)
+ modules_to_process = list(
+ filter(lambda name: name.startswith(args.prefix), available_modules)
+ )
+ modules_to_process.sort(key=StrOrderCRUD)
+ log.warning("modules to process (with order): %s", modules_to_process)
+
+ result_help_text = []
+ result_help_text.append("=" * 30)
+
+ for module_file in modules_to_process:
+ if module_file.endswith(".py"):
+ module_name = module_file[:-3]
+ try:
+ run_request_for_module(module_name)
+ except Exception:
+ log.exception("Could not process module %r, skipping", module_file)
+ else:
+ if REMOVE_PYTHON_SNIPPET:
+ os.unlink("/".join((SNIPPETS_DIR, module_file)))
+ add_help_lines(result_help_text, module_name)
+
+ result_help_text.append("=" * 30)
+ result_help_text.append("")
+
+ print("\n".join(result_help_text))
+
+ log.warning("Done")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/docs/include_related_objects.rst b/docs/include_related_objects.rst
index a025bc5..a02e3bd 100644
--- a/docs/include_related_objects.rst
+++ b/docs/include_related_objects.rst
@@ -5,9 +5,9 @@ Include related objects
.. currentmodule:: flask_combo_jsonapi
-You can include related object(s) details to responses with the querystring parameter named "include". You can use "include" parameter on any kind of route (classical CRUD route or relationships route) and any kind of http methods as long as method return data.
+You can include related object(s) details in responses with the query string parameter named "include". You can use the "include" parameter on any kind of route (classical CRUD route or relationships route) and any kind of HTTP methods as long as the method returns data.
-This features will add an additional key in result named "included"
+This feature will add an additional key in the result named "included"
Example:
@@ -174,4 +174,4 @@ Response:
}
}
-I know it is an absurd example because it will include details of related person computers and details of the person that is already in the response. But it is just for example.
+It's an absurd example because it will include details of the related person's computers and details of the person that is already in the response. But it is just for demonstration.
diff --git a/docs/index.rst b/docs/index.rst
index 3507298..fbc5252 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,9 +1,22 @@
Flask-COMBO-JSONAPI
==================
-
-.. module:: flask_combo_jsonapi
-
-**Flask-COMBO-JSONAPI** is an extension for Flask that adds support for quickly building REST APIs with huge flexibility around the JSONAPI 1.0 specification. It is designed to fit the complexity of real life environments so Flask-COMBO-JSONAPI helps you to create a logical abstraction of your data called "resource" and can interface any kind of ORMs or data storage through data layer concept.
+.. image:: https://github.com/AdCombo/flask-combo-jsonapi/workflows/Python%20tests%20and%20coverage/badge.svg
+ :alt: flask-combo-jsonapi actions
+ :target: https://github.com/AdCombo/flask-combo-jsonapi/actions
+.. image:: https://coveralls.io/repos/github/AdCombo/flask-combo-jsonapi/badge.svg
+ :alt: flask-combo-jsonapi coverage
+ :target: https://coveralls.io/github/AdCombo/flask-combo-jsonapi
+.. image:: https://img.shields.io/pypi/v/flask-combo-jsonapi.svg
+ :alt: PyPI
+ :target: http://pypi.org/p/flask-combo-jsonapi
+
+
+**Flask-COMBO-JSONAPI** is an extension for Flask that adds support for quickly building REST APIs
+with huge flexibility around the JSON:API 1.0 specification.
+It is designed to fit the complexity of real life environments
+so Flask-COMBO-JSONAPI helps you to create a logical abstraction of your data
+called "resource". It can interface any kind of ORMs or data storage
+through the concept of data layers.
Main concepts
-------------
@@ -12,16 +25,16 @@ Main concepts
:width: 900px
:alt: Architecture
-| * `JSON API 1.0 specification `_: it is a very popular specification about client server interactions for REST JSON API. It helps you work in a team because it is very precise and sharable. Thanks to this specification your API offers lots of features like a strong structure of request and response, filtering, pagination, sparse fieldsets, including related objects, great error formatting etc.
+| * `JSON:API 1.0 specification `_: this is a very popular specification for client-server interactions through a JSON-based REST API. It helps you work in a team because it is very precise and sharable. Thanks to this specification your API can offer a lot of features such as a strong structure of request and response, filtering, pagination, sparse fieldsets, including related objects, great error formatting, etc.
|
-| * **Logical data abstraction**: you usually need to expose resources to clients that don't fit your data table architecture. For example sometimes you don't want to expose all attributes of a table, compute additional attributes or create a resource that uses data from multiple data storages. Flask-COMBO-JSONAPI helps you create a logical abstraction of your data with `Marshmallow `_ / `marshmallow-jsonapi `_ so you can expose your data through a very flexible way.
+| * **Logical data abstraction**: you usually need to expose resources to clients that don't fit your data table architecture. For example sometimes you don't want to expose all attributes of a table, compute additional attributes or create a resource that uses data from multiple data storages. Flask-COMBO-JSONAPI helps you create a logical abstraction of your data with `Marshmallow `_ / `marshmallow-jsonapi `_ so you can expose your data in a very flexible way.
|
-| * **Data layer**: the data layer is a CRUD interface between your resource manager and your data. Thanks to it you can use any data storage or ORMs. There is an already full featured data layer that uses the SQLAlchemy ORM but you can create and use your own custom data layer to use data from your data storage(s). You can even create a data layer that uses multiple data storages and ORMs, send notifications or make any custom work during CRUD operations.
+| * **Data layer**: the data layer is a CRUD interface between your resource manager and your data. Thanks to this you can use any data storage or ORM. There is an already full-featured data layer that uses the SQLAlchemy ORM but you can create and use your own custom data layer to use data from your data storage. You can even create a data layer that uses multiple data storage systems and ORMs, send notifications or perform custom actions during CRUD operations.
Features
--------
-Flask-COMBO-JSONAPI has lots of features:
+Flask-COMBO-JSONAPI has many features:
* Relationship management
* Powerful filtering
@@ -36,13 +49,14 @@ Flask-COMBO-JSONAPI has lots of features:
User's Guide
------------
-This part of the documentation will show you how to get started in using
+This part of the documentation will show you how to get started using
Flask-COMBO-JSONAPI with Flask.
.. toctree::
:maxdepth: 3
installation
+ minimal_api_example
quickstart
logical_data_abstraction
resource_manager
@@ -59,6 +73,10 @@ Flask-COMBO-JSONAPI with Flask.
oauth
configuration
+
+.. include:: ./minimal_api_example.rst
+
+
API Reference
-------------
diff --git a/docs/installation.rst b/docs/installation.rst
index de63abe..432d760 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -23,9 +23,6 @@ The development version can be downloaded from `its page at GitHub
.. note::
- If you don't have virtualenv please install it before
+ If you don't have virtualenv please install it
$ pip install virtualenv
-
-
-Flask-RESTful requires Python version 2.7, 3.4 or 3.5.
\ No newline at end of file
diff --git a/docs/logical_data_abstraction.rst b/docs/logical_data_abstraction.rst
index 0661fde..8e6dcd9 100644
--- a/docs/logical_data_abstraction.rst
+++ b/docs/logical_data_abstraction.rst
@@ -5,11 +5,18 @@ Logical data abstraction
.. currentmodule:: flask_combo_jsonapi
-The first thing to do in Flask-COMBO-JSONAPI is to create a logical data abstraction. This part of the api discribes schemas of resources exposed by the api that is not the exact mapping of the data architecture. The declaration of schemas is made by `Marshmallow `_ / `marshmallow-jsonapi `_. Marshmallow is a very popular serialization / deserialization library that offers a lot of features to abstract your data architecture. Moreover there is an other library called marshmallow-jsonapi that fit the JSONAPI 1.0 specification and provides Flask integration.
+The first thing to do in Flask-COMBO-JSONAPI is to create a logical data abstraction.
+This part of the API describes schemas of resources exposed by the API
+that are not an exact mapping of the data architecture.
+The declaration of schemas is performed by `Marshmallow `_ / `marshmallow-jsonapi `_.
+Marshmallow is a very popular serialization/deserialization library
+that offers a lot of features to abstract your data architecture.
+Moreover there is another library called marshmallow-jsonapi
+that fits the JSON:API 1.0 specification and provides Flask integration.
Example:
-In this example, let's assume that we have 2 legacy models Person and Computer, and we want to create an abstraction over them.
+In this example, let's assume that we have two legacy models, Person and Computer, and we want to create an abstraction on top of them.
.. code-block:: python
@@ -76,17 +83,17 @@ Now let's create the logical abstraction to illustrate this concept.
schema='PersonSchema',
type_='person')
-You can see several differences between models and schemas exposed by the api.
+You can see several differences between models and schemas exposed by the API.
First, take a look at the Person compared to PersonSchema:
-* we can see that Person has an attribute named "password" and we don't want to expose it through the api so it is not set in PersonSchema
+* We can see that Person has an attribute named "password" and we don't want to expose it through the api so it is not set in PersonSchema
* PersonSchema has an attribute named "display_name" that is the result of concatenation of name and email
-* In the computers Relationship() defined on PersonSchema we have set the id_field to "computer_id" as that is the primary key on the Computer(db.model). Without setting id_field the relationship looks for a field called "id".
+* In the "computers" Relationship() defined on PersonSchema we have set the id_field to "computer_id" as that is the primary key on the Computer(db.model). Without setting id_field the relationship looks for a field called "id".
Second, take a look at the Computer compared to ComputerSchema:
-* we can see that the attribute computer_id is exposed as id for consistency of the api
-* we can see that the person relationship between Computer and Person is exposed in ComputerSchema as owner because it is more explicit
+* The attribute computer_id is exposed as id for consistency of the api
+* The person relationship between Computer and Person is exposed in ComputerSchema as owner because it is more explicit
-As a result you can see that you can expose your data in a very flexible way to create the api of your choice over your data architecture.
+As a result you can see that you can expose your data in a very flexible way to create the API of your choice on top of your data architecture.
diff --git a/docs/minimal_api_example.rst b/docs/minimal_api_example.rst
new file mode 100644
index 0000000..9c659d1
--- /dev/null
+++ b/docs/minimal_api_example.rst
@@ -0,0 +1,56 @@
+.. include:: ./minimal_api_head.rst
+
+Request:
+
+.. literalinclude:: ./http_snippets/snippets/minimal_api__create_person
+ :language: HTTP
+
+Response:
+
+.. literalinclude:: ./http_snippets/snippets/minimal_api__create_person_result
+ :language: HTTP
+
+
+Request:
+
+.. literalinclude:: ./http_snippets/snippets/minimal_api__get_person
+ :language: HTTP
+
+Response:
+
+.. literalinclude:: ./http_snippets/snippets/minimal_api__get_person_result
+ :language: HTTP
+
+
+Request:
+
+.. literalinclude:: ./http_snippets/snippets/minimal_api__get_persons
+ :language: HTTP
+
+Response:
+
+.. literalinclude:: ./http_snippets/snippets/minimal_api__get_persons_result
+ :language: HTTP
+
+
+Request:
+
+.. literalinclude:: ./http_snippets/snippets/minimal_api__patch_person
+ :language: HTTP
+
+Response:
+
+.. literalinclude:: ./http_snippets/snippets/minimal_api__patch_person_result
+ :language: HTTP
+
+
+Request:
+
+.. literalinclude:: ./http_snippets/snippets/minimal_api__delete_person
+ :language: HTTP
+
+Response:
+
+.. literalinclude:: ./http_snippets/snippets/minimal_api__delete_person_result
+ :language: HTTP
+
diff --git a/docs/minimal_api_head.rst b/docs/minimal_api_head.rst
new file mode 100644
index 0000000..b1f5ad7
--- /dev/null
+++ b/docs/minimal_api_head.rst
@@ -0,0 +1,18 @@
+A minimal API
+=============
+
+.. literalinclude:: ../examples/api_minimal.py
+ :language: python
+
+
+This example provides the following API structure:
+
+======================== ====== ============= ===========================
+URL method endpoint Usage
+======================== ====== ============= ===========================
+/persons GET person_list Get a collection of persons
+/persons POST person_list Create a person
+/persons/ GET person_detail Get person details
+/persons/ PATCH person_detail Update a person
+/persons/ DELETE person_detail Delete a person
+======================== ====== ============= ===========================
diff --git a/docs/oauth.rst b/docs/oauth.rst
index 3bc6c1e..8586010 100644
--- a/docs/oauth.rst
+++ b/docs/oauth.rst
@@ -5,7 +5,7 @@ OAuth
.. currentmodule:: flask_combo_jsonapi
-Flask-COMBO-JSONAPI support OAuth via `Flask-OAuthlib `_
+Flask-COMBO-JSONAPI supports OAuth via `Flask-OAuthlib `_
Example:
@@ -27,7 +27,7 @@ In this example Flask-COMBO-JSONAPI will protect all your resource methods with
oauth2.require_oauth()
-The pattern of the scope is like that ::
+The pattern of the scope is ::
_
@@ -43,7 +43,7 @@ Example ::
list_person
-If you want to customize the scope you can provide a function that computes your custom scope. The function have to looks like that:
+If you want to customize the scope you can provide a function that computes your custom scope. The function has to look like this:
.. code-block:: python
@@ -74,9 +74,9 @@ Usage example:
.. note::
- You can name the custom scope computation method as you want but you have to set the 2 required parameters: resource and method like in this previous example.
+ You can name the custom scope computation method as you want but you have to set the two required parameters "resource" and "method" as in this previous example.
-If you want to disable OAuth or make custom methods protection for a resource you can add this option to the resource manager.
+If you want to disable OAuth or create custom method protection for a resource you can add this option to the resource manager.
Example:
@@ -90,4 +90,4 @@ Example:
@oauth2.require_oauth('custom_scope')
def get(*args, **kwargs):
- return 'Hello world !'
+ return 'Hello world!'
diff --git a/docs/pagination.rst b/docs/pagination.rst
index 792a589..d23e2f7 100644
--- a/docs/pagination.rst
+++ b/docs/pagination.rst
@@ -5,16 +5,18 @@ Pagination
.. currentmodule:: flask_combo_jsonapi
-When you use the default implementation of get method on a ResourceList your results will be paginated by default. Default pagination size is 30 but you can manage it from querystring parameter named "page".
+When you use the default implementation of the get method on a ResourceList
+your results will be paginated by default.
+Default pagination size is 30 but you can manage it from querystring parameter named "page".
.. note::
- Examples are not urlencoded for a better readability
+ Examples are not URL encoded for a better readability
Size
----
-You can control page size like that:
+You can control page size like this:
.. sourcecode:: http
@@ -24,7 +26,7 @@ You can control page size like that:
Number
------
-You can control page number like that:
+You can control page number like this:
.. sourcecode:: http
@@ -34,7 +36,7 @@ You can control page number like that:
Size + Number
-------------
-Of course, you can control both like that:
+Of course, you can control both like this:
.. sourcecode:: http
@@ -44,7 +46,7 @@ Of course, you can control both like that:
Disable pagination
------------------
-You can disable pagination with size = 0
+You can disable pagination by setting size to 0
.. sourcecode:: http
diff --git a/docs/permission.rst b/docs/permission.rst
index 4151a6b..8912485 100644
--- a/docs/permission.rst
+++ b/docs/permission.rst
@@ -5,7 +5,7 @@ Permission
.. currentmodule:: flask_combo_jsonapi
-Flask-COMBO-JSONAPI provides a very agnostic permission system.
+Flask-COMBO-JSONAPI provides an agnostic permission system.
Example:
@@ -23,7 +23,7 @@ Example:
In this previous example, the API will check permission before each method call with the permission_manager function.
-The permission manager must be a function that looks like this:
+The permission_manager must be a function that looks like this:
.. code-block:: python
@@ -39,9 +39,14 @@ The permission manager must be a function that looks like this:
.. note::
- Flask-COMBO-JSONAPI use a decorator to check permission for each method named has_permission. You can provide args and kwargs to this decorators so you can retrieve this args and kwargs in the permission_manager. The default usage of the permission system does not provides any args or kwargs to the decorator.
+ Flask-COMBO-JSONAPI uses a decorator named has_permission
+ to check permission for each method.
+ You can provide args and kwargs to this decorator
+ so you can retrieve them in the permission_manager.
+ The default usage of the permission system
+ does not provide any args or kwargs to the decorator.
-If permission is denied I recommand to raise exception like that:
+If permission is denied, raising an exception is recommended:
.. code-block:: python
@@ -50,7 +55,7 @@ If permission is denied I recommand to raise exception like that:
title='Permission denied',
status='403')
-You can disable the permission system or make custom permission checking management of a resource like that:
+You can disable the permission system or create custom permission checking of a resource like this:
.. code-block:: python
@@ -66,7 +71,7 @@ You can disable the permission system or make custom permission checking managem
.. warning::
- If you want to use both permission system and oauth support to retrieve information like user from oauth (request.oauth.user) in the permission system you have to initialize permission system before to initialize oauth support because of decorators cascading.
+ If you want to use both the permission system and OAuth support to retrieve information such as a user (request.oauth.user), you have to initialize the permission system before initializing OAuth support. This is because of decorator cascading.
Example:
@@ -83,4 +88,4 @@ Example:
api = Api()
api.init_app(app)
api.permission_manager(permission_manager) # initialize permission system first
- api.oauth_manager(oauth2) # initialize oauth support second
\ No newline at end of file
+ api.oauth_manager(oauth2) # initialize oauth support second
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
index 4436a82..59c941f 100644
--- a/docs/quickstart.rst
+++ b/docs/quickstart.rst
@@ -5,236 +5,83 @@ Quickstart
.. currentmodule:: flask_combo_jsonapi
-It's time to write your first REST API. This guide assumes you have a working understanding of `Flask `_, and that you have already installed both Flask and Flask-COMBO-JSONAPI. If not, then follow the steps in the :ref:`installation` section.
+It's time to write your first advanced REST API.
+This guide assumes you have a working understanding of `Flask `_,
+and that you have already installed both Flask and Flask-COMBO-JSONAPI.
+If not, then follow the steps in the :ref:`installation` section.
-In this section you will learn basic usage of Flask-COMBO-JSONAPI around a small tutorial that use the SQLAlchemy data layer. This tutorial show you an example of a person and his computers.
+In this section you will learn basic usage of Flask-COMBO-JSONAPI
+around a small tutorial that uses the SQLAlchemy data layer.
+This tutorial shows you an example of a person and their computers.
-First example
+Advanced example
-------------
An example of Flask-COMBO-JSONAPI API looks like this:
-.. code-block:: python
-
- # -*- coding: utf-8 -*-
-
- from flask import Flask
- from flask_combo_jsonapi import Api, ResourceDetail, ResourceList, ResourceRelationship
- from flask_combo_jsonapi.exceptions import ObjectNotFound
- from flask_sqlalchemy import SQLAlchemy
- from sqlalchemy.orm.exc import NoResultFound
- from marshmallow_jsonapi.flask import Schema, Relationship
- from marshmallow_jsonapi import fields
-
- # Create the Flask application
- app = Flask(__name__)
- app.config['DEBUG'] = True
-
-
- # Initialize SQLAlchemy
- app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
- db = SQLAlchemy(app)
-
-
- # Create data storage
- class Person(db.Model):
- id = db.Column(db.Integer, primary_key=True)
- name = db.Column(db.String)
- email = db.Column(db.String)
- birth_date = db.Column(db.Date)
- password = db.Column(db.String)
-
-
- class Computer(db.Model):
- id = db.Column(db.Integer, primary_key=True)
- serial = db.Column(db.String)
- person_id = db.Column(db.Integer, db.ForeignKey('person.id'))
- person = db.relationship('Person', backref=db.backref('computers'))
-
- db.create_all()
-
-
- # Create logical data abstraction (same as data storage for this first example)
- class PersonSchema(Schema):
- class Meta:
- type_ = 'person'
- self_view = 'person_detail'
- self_view_kwargs = {'id': ''}
- self_view_many = 'person_list'
-
- id = fields.Integer(as_string=True, dump_only=True)
- name = fields.Str(required=True, load_only=True)
- email = fields.Email(load_only=True)
- birth_date = fields.Date()
- display_name = fields.Function(lambda obj: "{} <{}>".format(obj.name.upper(), obj.email))
- computers = Relationship(self_view='person_computers',
- self_view_kwargs={'id': ''},
- related_view='computer_list',
- related_view_kwargs={'id': ''},
- many=True,
- schema='ComputerSchema',
- type_='computer')
-
-
- class ComputerSchema(Schema):
- class Meta:
- type_ = 'computer'
- self_view = 'computer_detail'
- self_view_kwargs = {'id': ''}
-
- id = fields.Integer(as_string=True, dump_only=True)
- serial = fields.Str(required=True)
- owner = Relationship(attribute='person',
- self_view='computer_person',
- self_view_kwargs={'id': ''},
- related_view='person_detail',
- related_view_kwargs={'computer_id': ''},
- schema='PersonSchema',
- type_='person')
-
-
- # Create resource managers
- class PersonList(ResourceList):
- schema = PersonSchema
- data_layer = {'session': db.session,
- 'model': Person}
-
-
- class PersonDetail(ResourceDetail):
- def before_get_object(self, view_kwargs):
- if view_kwargs.get('computer_id') is not None:
- try:
- computer = self.session.query(Computer).filter_by(id=view_kwargs['computer_id']).one()
- except NoResultFound:
- raise ObjectNotFound({'parameter': 'computer_id'},
- "Computer: {} not found".format(view_kwargs['computer_id']))
- else:
- if computer.person is not None:
- view_kwargs['id'] = computer.person.id
- else:
- view_kwargs['id'] = None
-
- schema = PersonSchema
- data_layer = {'session': db.session,
- 'model': Person,
- 'methods': {'before_get_object': before_get_object}}
-
-
- class PersonRelationship(ResourceRelationship):
- schema = PersonSchema
- data_layer = {'session': db.session,
- 'model': Person}
-
-
- class ComputerList(ResourceList):
- def query(self, view_kwargs):
- query_ = self.session.query(Computer)
- if view_kwargs.get('id') is not None:
- try:
- self.session.query(Person).filter_by(id=view_kwargs['id']).one()
- except NoResultFound:
- raise ObjectNotFound({'parameter': 'id'}, "Person: {} not found".format(view_kwargs['id']))
- else:
- query_ = query_.join(Person).filter(Person.id == view_kwargs['id'])
- return query_
-
- def before_create_object(self, data, view_kwargs):
- if view_kwargs.get('id') is not None:
- person = self.session.query(Person).filter_by(id=view_kwargs['id']).one()
- data['person_id'] = person.id
-
- schema = ComputerSchema
- data_layer = {'session': db.session,
- 'model': Computer,
- 'methods': {'query': query,
- 'before_create_object': before_create_object}}
-
-
- class ComputerDetail(ResourceDetail):
- schema = ComputerSchema
- data_layer = {'session': db.session,
- 'model': Computer}
-
-
- class ComputerRelationship(ResourceRelationship):
- schema = ComputerSchema
- data_layer = {'session': db.session,
- 'model': Computer}
-
-
- # Create endpoints
- api = Api(app)
- api.route(PersonList, 'person_list', '/persons')
- api.route(PersonDetail, 'person_detail', '/persons/', '/computers//owner')
- api.route(PersonRelationship, 'person_computers', '/persons//relationships/computers')
- api.route(ComputerList, 'computer_list', '/computers', '/persons//computers')
- api.route(ComputerDetail, 'computer_detail', '/computers/')
- api.route(ComputerRelationship, 'computer_person', '/computers//relationships/owner')
-
- if __name__ == '__main__':
- # Start application
- app.run(debug=True)
-
-This example provides this api:
-
-+------------------------------------------+--------+------------------+-------------------------------------------------------+
-| url | method | endpoint | action |
-+==========================================+========+==================+=======================================================+
-| /persons | GET | person_list | Retrieve a collection of persons |
-+------------------------------------------+--------+------------------+-------------------------------------------------------+
-| /persons | POST | person_list | Create a person |
-+------------------------------------------+--------+------------------+-------------------------------------------------------+
-| /persons/ | GET | person_detail | Retrieve details of a person |
-+------------------------------------------+--------+------------------+-------------------------------------------------------+
-| /persons/ | PATCH | person_detail | Update a person |
-+------------------------------------------+--------+------------------+-------------------------------------------------------+
-| /persons/ | DELETE | person_detail | Delete a person |
-+------------------------------------------+--------+------------------+-------------------------------------------------------+
-| /persons//computers | GET | computer_list | Retrieve a collection computers related to a person |
-+------------------------------------------+--------+------------------+-------------------------------------------------------+
-| /persons//computers | POST | computer_list | Create a computer related to a person |
-+------------------------------------------+--------+------------------+-------------------------------------------------------+
-| /persons//relationship/computers | GET | person_computers | Retrieve relationships between a person and computers |
-+------------------------------------------+--------+------------------+-------------------------------------------------------+
-| /persons//relationship/computers | POST | person_computers | Create relationships between a person and computers |
-+------------------------------------------+--------+------------------+-------------------------------------------------------+
-| /persons//relationship/computers | PATCH | person_computers | Update relationships between a person and computers |
-+------------------------------------------+--------+------------------+-------------------------------------------------------+
-| /persons//relationship/computers | DELETE | person_computers | Delete relationships between a person and computers |
-+------------------------------------------+--------+------------------+-------------------------------------------------------+
-| /computers | GET | computer_list | Retrieve a collection of computers |
-+------------------------------------------+--------+------------------+-------------------------------------------------------+
-| /computers | POST | computer_list | Create a computer |
-+------------------------------------------+--------+------------------+-------------------------------------------------------+
-| /computers/ | GET | computer_detail | Retrieve details of a computer |
-+------------------------------------------+--------+------------------+-------------------------------------------------------+
-| /computers/ | PATCH | computer_detail | Update a computer |
-+------------------------------------------+--------+------------------+-------------------------------------------------------+
-| /computers/ | DELETE | computer_detail | Delete a computer |
-+------------------------------------------+--------+------------------+-------------------------------------------------------+
-| /computers//owner | GET | person_detail | Retrieve details of the owner of a computer |
-+------------------------------------------+--------+------------------+-------------------------------------------------------+
-| /computers//owner | PATCH | person_detail | Update the owner of a computer |
-+------------------------------------------+--------+------------------+-------------------------------------------------------+
-| /computers//owner | DELETE | person_detail | Delete the owner of a computer |
-+------------------------------------------+--------+------------------+-------------------------------------------------------+
-| /computers//relationship/owner | GET | person_computers | Retrieve relationships between a person and computers |
-+------------------------------------------+--------+------------------+-------------------------------------------------------+
-| /computers//relationship/owner | POST | person_computers | Create relationships between a person and computers |
-+------------------------------------------+--------+------------------+-------------------------------------------------------+
-| /computers//relationship/owner | PATCH | person_computers | Update relationships between a person and computers |
-+------------------------------------------+--------+------------------+-------------------------------------------------------+
-| /computers//relationship/owner | DELETE | person_computers | Delete relationships between a person and computers |
-+------------------------------------------+--------+------------------+-------------------------------------------------------+
+.. literalinclude:: ../examples/api.py
+ :language: python
+
+This example provides the following API:
+
++-------------------------------------------+--------+------------------+-------------------------------------------------------+
+| url | method | endpoint | action |
++===========================================+========+==================+=======================================================+
+| /persons | GET | person_list | Retrieve a collection of persons |
++-------------------------------------------+--------+------------------+-------------------------------------------------------+
+| /persons | POST | person_list | Create a person |
++-------------------------------------------+--------+------------------+-------------------------------------------------------+
+| /persons/ | GET | person_detail | Retrieve details of a person |
++-------------------------------------------+--------+------------------+-------------------------------------------------------+
+| /persons/ | PATCH | person_detail | Update a person |
++-------------------------------------------+--------+------------------+-------------------------------------------------------+
+| /persons/ | DELETE | person_detail | Delete a person |
++-------------------------------------------+--------+------------------+-------------------------------------------------------+
+| /persons//computers | GET | computer_list | Retrieve a collection computers related to a person |
++-------------------------------------------+--------+------------------+-------------------------------------------------------+
+| /persons//computers | POST | computer_list | Create a computer related to a person |
++-------------------------------------------+--------+------------------+-------------------------------------------------------+
+| /persons//relationships/computers | GET | person_computers | Retrieve relationships between a person and computers |
++-------------------------------------------+--------+------------------+-------------------------------------------------------+
+| /persons//relationships/computers | POST | person_computers | Create relationships between a person and computers |
++-------------------------------------------+--------+------------------+-------------------------------------------------------+
+| /persons//relationships/computers | PATCH | person_computers | Update relationships between a person and computers |
++-------------------------------------------+--------+------------------+-------------------------------------------------------+
+| /persons//relationships/computers | DELETE | person_computers | Delete relationships between a person and computers |
++--------------------------------------------+--------+------------------+-------------------------------------------------------+
+| /computers | GET | computer_list | Retrieve a collection of computers |
++-------------------------------------------+--------+------------------+-------------------------------------------------------+
+| /computers | POST | computer_list | Create a computer |
++-------------------------------------------+--------+------------------+-------------------------------------------------------+
+| /computers/ | GET | computer_detail | Retrieve details of a computer |
++-------------------------------------------+--------+------------------+-------------------------------------------------------+
+| /computers/ | PATCH | computer_detail | Update a computer |
++-------------------------------------------+--------+------------------+-------------------------------------------------------+
+| /computers/ | DELETE | computer_detail | Delete a computer |
++-------------------------------------------+--------+------------------+-------------------------------------------------------+
+| /computers//owner | GET | person_detail | Retrieve details of the owner of a computer |
++-------------------------------------------+--------+------------------+-------------------------------------------------------+
+| /computers//owner | PATCH | person_detail | Update the owner of a computer |
++-------------------------------------------+--------+------------------+-------------------------------------------------------+
+| /computers//owner | DELETE | person_detail | Delete the owner of a computer |
++-------------------------------------------+--------+------------------+-------------------------------------------------------+
+| /computers//relationships/owner | GET | person_computers | Retrieve relationships between a person and computers |
++-------------------------------------------+--------+------------------+-------------------------------------------------------+
+| /computers//relationships/owner | POST | person_computers | Create relationships between a person and computers |
++-------------------------------------------+--------+------------------+-------------------------------------------------------+
+| /computers//relationships/owner | PATCH | person_computers | Update relationships between a person and computers |
++-------------------------------------------+--------+------------------+-------------------------------------------------------+
+| /computers//relationships/owner | DELETE | person_computers | Delete relationships between a person and computers |
++-------------------------------------------+--------+------------------+-------------------------------------------------------+
.. warning::
- In this example, I use Flask-SQLAlchemy so you have to install it before to run the example.
+ In this example Flask-SQLAlchemy is used, so you'll need to install it before running this example.
$ pip install flask_sqlalchemy
-Save this as api.py and run it using your Python interpreter. Note that we've enabled
-`Flask debugging `_ mode to provide code reloading and better error
+Save `this file `_ as api.py and run it using your Python interpreter. Note that we've enabled
+`Flask debugging `_ mode to provide code reloading and better error
messages. ::
$ python api.py
@@ -253,577 +100,166 @@ Create object
Request:
-.. sourcecode:: http
-
- POST /computers HTTP/1.1
- Content-Type: application/vnd.api+json
- Accept: application/vnd.api+json
-
- {
- "data": {
- "type": "computer",
- "attributes": {
- "serial": "Amstrad"
- }
- }
- }
+.. literalinclude:: ./http_snippets/snippets/relationship_api__create_computer
+ :language: HTTP
Response:
-.. sourcecode:: http
+.. literalinclude:: ./http_snippets/snippets/relationship_api__create_computer_result
+ :language: HTTP
- HTTP/1.1 201 Created
- Content-Type: application/vnd.api+json
-
- {
- "data": {
- "type": "computer",
- "id": "1",
- "attributes": {
- "serial": "Amstrad"
- },
- "relationships": {
- "owner": {
- "links": {
- "related": "/computers/1/owner",
- "self": "/computers/1/relationships/owner"
- }
- }
- },
- "links": {
- "self": "/computers/1"
- }
- },
- "links": {
- "self": "/computers/1"
- },
- "jsonapi": {
- "version": "1.0"
- }
- }
List objects
~~~~~~~~~~~~
Request:
-.. sourcecode:: http
-
- GET /computers HTTP/1.1
- Accept: application/vnd.api+json
+.. literalinclude:: ./http_snippets/snippets/relationship_api__get_computers
+ :language: HTTP
Response:
-.. sourcecode:: http
+.. literalinclude:: ./http_snippets/snippets/relationship_api__get_computers_result
+ :language: HTTP
- HTTP/1.1 200 OK
- Content-Type: application/vnd.api+json
-
- {
- "data": [
- {
- "type": "computer",
- "id": "1",
- "attributes": {
- "serial": "Amstrad"
- },
- "relationships": {
- "owner": {
- "links": {
- "related": "/computers/1/owner",
- "self": "/computers/1/relationships/owner"
- }
- }
- },
- "links": {
- "self": "/computers/1"
- }
- }
- ],
- "meta": {
- "count": 1
- },
- "links": {
- "self": "/computers"
- },
- "jsonapi": {
- "version": "1.0"
- },
- }
Update object
~~~~~~~~~~~~~
Request:
-.. sourcecode:: http
-
- PATCH /computers/1 HTTP/1.1
- Content-Type: application/vnd.api+json
- Accept: application/vnd.api+json
-
- {
- "data": {
- "type": "computer",
- "id": "1",
- "attributes": {
- "serial": "Amstrad 2"
- }
- }
- }
+.. literalinclude:: ./http_snippets/snippets/relationship_api__patch_computer
+ :language: HTTP
Response:
-.. sourcecode:: http
+.. literalinclude:: ./http_snippets/snippets/relationship_api__patch_computer_result
+ :language: HTTP
- HTTP/1.1 200 OK
- Content-Type: application/vnd.api+json
-
- {
- "data": {
- "type": "computer",
- "id": "1",
- "attributes": {
- "serial": "Amstrad 2"
- },
- "relationships": {
- "owner": {
- "links": {
- "related": "/computers/1/owner",
- "self": "/computers/1/relationships/owner"
- }
- }
- },
- "links": {
- "self": "/computers/1"
- }
- },
- "links": {
- "self": "/computers/1"
- },
- "jsonapi": {
- "version": "1.0"
- }
- }
Delete object
~~~~~~~~~~~~~
Request:
-.. sourcecode:: http
-
- DELETE /computers/1 HTTP/1.1
- Accept: application/vnd.api+json
+.. literalinclude:: ./http_snippets/snippets/relationship_api__delete_computer
+ :language: HTTP
Response:
-.. sourcecode:: http
-
- HTTP/1.1 200 OK
- Content-Type: application/vnd.api+json
+.. literalinclude:: ./http_snippets/snippets/relationship_api__delete_computer_result
+ :language: HTTP
- {
- "meta": {
- "message": "Object successfully deleted"
- },
- "jsonapi": {
- "version": "1.0"
- }
- }
Relationships
-------------
-| Now let's use relationships tools. First, create 3 computers named Halo, Nestor and Comodor like in previous example.
+| Now let's use relationships tools.
+| First, create 3 computers named "Halo", "Nestor" and "Commodore".
|
-| Done ?
+| Done?
| Ok. So let's continue this tutorial.
|
-| We assume that Halo has id: 2, Nestor id: 3 and Comodor has id: 4.
+| We assume that Halo has id=2, Nestor id=3 and Commodore id=4.
Create object with related object(s)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Request:
-.. sourcecode:: http
-
- POST /persons?include=computers HTTP/1.1
- Content-Type: application/vnd.api+json
- Accept: application/vnd.api+json
-
- {
- "data": {
- "type": "person",
- "attributes": {
- "name": "John",
- "email": "john@gmail.com",
- "birth_date": "1990-12-18"
- },
- "relationships": {
- "computers": {
- "data": [
- {
- "type": "computer",
- "id": "1"
- }
- ]
- }
- }
- }
- }
+.. literalinclude:: ./http_snippets/snippets/relationship_api__create_person_with_computer_relationship
+ :language: HTTP
Response:
-.. sourcecode:: http
+.. literalinclude:: ./http_snippets/snippets/relationship_api__create_person_with_computer_relationship_result
+ :language: HTTP
+
- HTTP/1.1 201 Created
- Content-Type: application/vnd.api+json
-
- {
- "data": {
- "type": "person",
- "id": "1",
- "attributes": {
- "display_name": "JOHN ",
- "birth_date": "1990-12-18"
- },
- "links": {
- "self": "/persons/1"
- },
- "relationships": {
- "computers": {
- "data": [
- {
- "id": "1",
- "type": "computer"
- }
- ],
- "links": {
- "related": "/persons/1/computers",
- "self": "/persons/1/relationships/computers"
- }
- }
- },
- },
- "included": [
- {
- "type": "computer",
- "id": "1",
- "attributes": {
- "serial": "Amstrad"
- },
- "links": {
- "self": "/computers/1"
- },
- "relationships": {
- "owner": {
- "links": {
- "related": "/computers/1/owner",
- "self": "/computers/1/relationships/owner"
- }
- }
- }
- }
- ],
- "jsonapi": {
- "version": "1.0"
- },
- "links": {
- "self": "/persons/1"
- }
- }
-
-You can see that I have added the querystring parameter "include" to the url
+You can see that we have added the query string parameter "include" to the URL
.. sourcecode:: http
POST /persons?include=computers HTTP/1.1
-Thanks to this parameter, related computers details are included to the result. If you want to learn more: :ref:`include_related_objects`
+Thanks to this parameter, the related computers' details are included in the result. If you want to learn more: :ref:`include_related_objects`
Update object and his relationships
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Now John sell his Amstrad and buy a new computer named Nestor (id: 3). So we want to link this new computer to John. John have also made a mistake in his birth_date so let's update this 2 things in the same time.
+Now John sell his Halo (id=2) and buys a new computer named Nestor (id=3).
+So we want to link this new computer to John.
+John have also made a mistake in his email so let's update these 2 things in the same time.
Request:
-.. sourcecode:: http
-
- PATCH /persons/1?include=computers HTTP/1.1
- Content-Type: application/vnd.api+json
- Accept: application/vnd.api+json
-
- {
- "data": {
- "type": "person",
- "id": "1",
- "attributes": {
- "birth_date": "1990-10-18"
- },
- "relationships": {
- "computers": {
- "data": [
- {
- "type": "computer",
- "id": "3"
- }
- ]
- }
- }
- }
- }
+.. literalinclude:: ./http_snippets/snippets/relationship_api__update_person_with_computer_relationship
+ :language: HTTP
Response:
-.. sourcecode:: http
+.. literalinclude:: ./http_snippets/snippets/relationship_api__update_person_with_computer_relationship_result
+ :language: HTTP
- HTTP/1.1 200 OK
- Content-Type: application/vnd.api+json
-
- {
- "data": {
- "type": "person",
- "id": "1",
- "attributes": {
- "display_name": "JOHN ",
- "birth_date": "1990-10-18",
- },
- "links": {
- "self": "/persons/1"
- },
- "relationships": {
- "computers": {
- "data": [
- {
- "id": "3",
- "type": "computer"
- }
- ],
- "links": {
- "related": "/persons/1/computers",
- "self": "/persons/1/relationships/computers"
- }
- }
- },
- },
- "included": [
- {
- "type": "computer",
- "id": "3",
- "attributes": {
- "serial": "Nestor"
- },
- "relationships": {
- "owner": {
- "links": {
- "related": "/computers/3/owner",
- "self": "/computers/3/relationships/owner"
- }
- }
- },
- "links": {
- "self": "/computers/3"
- }
- }
- ],
- "links": {
- "self": "/persons/1"
- },
- "jsonapi": {
- "version": "1.0"
- }
- }
Create relationship
~~~~~~~~~~~~~~~~~~~
-Now John buy a new computer named Comodor so let's link it to John.
+Now John buys a new computer named Commodore (id=4) so let's link it to John.
Request:
-.. sourcecode:: http
+.. literalinclude:: ./http_snippets/snippets/relationship_api__create_computer_relationship_for_person
+ :language: HTTP
+
+Response:
+
+.. literalinclude:: ./http_snippets/snippets/relationship_api__create_computer_relationship_for_person_result
+ :language: HTTP
- POST /persons/1/relationships/computers HTTP/1.1
- Content-Type: application/vnd.api+json
- Accept: application/vnd.api+json
- {
- "data": [
- {
- "type": "computer",
- "id": "4"
- }
- ]
- }
+Load person with all the related computers
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Request:
+
+.. literalinclude:: ./http_snippets/snippets/relationship_api__get_person_with_computers
+ :language: HTTP
Response:
-.. sourcecode:: http
+.. literalinclude:: ./http_snippets/snippets/relationship_api__get_person_with_computers_result
+ :language: HTTP
- HTTP/1.1 200 OK
- Content-Type: application/vnd.api+json
-
- {
- "data": {
- "type": "person",
- "id": "1",
- "attributes": {
- "display_name": "JOHN ",
- "birth_date": "1990-10-18"
- },
- "relationships": {
- "computers": {
- "data": [
- {
- "id": "3",
- "type": "computer"
- },
- {
- "id": "4",
- "type": "computer"
- }
- ],
- "links": {
- "related": "/persons/1/computers",
- "self": "/persons/1/relationships/computers"
- }
- }
- },
- "links": {
- "self": "/persons/1"
- }
- },
- "included": [
- {
- "type": "computer",
- "id": "3",
- "attributes": {
- "serial": "Nestor"
- },
- "relationships": {
- "owner": {
- "links": {
- "related": "/computers/3/owner",
- "self": "/computers/3/relationships/owner"
- }
- }
- },
- "links": {
- "self": "/computers/3"
- }
- },
- {
- "type": "computer",
- "id": "4",
- "attributes": {
- "serial": "Comodor"
- },
- "relationships": {
- "owner": {
- "links": {
- "related": "/computers/4/owner",
- "self": "/computers/4/relationships/owner"
- }
- }
- },
- "links": {
- "self": "/computers/4"
- }
- }
- ],
- "links": {
- "self": "/persons/1/relationships/computers"
- },
- "jsonapi": {
- "version": "1.0"
- }
- }
+
+Check person's computers without loading actual person
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Request:
+
+.. literalinclude:: ./http_snippets/snippets/relationship_api__get_person_related_computers
+ :language: HTTP
+
+Response:
+
+.. literalinclude:: ./http_snippets/snippets/relationship_api__get_person_related_computers_result
+ :language: HTTP
Delete relationship
~~~~~~~~~~~~~~~~~~~
-Now John sell his old Nestor computer so let's unlink it from John.
+Now John sells his old Nestor computer, so let's unlink it from John.
Request:
-.. sourcecode:: http
-
- DELETE /persons/1/relationships/computers HTTP/1.1
- Content-Type: application/vnd.api+json
- Accept: application/vnd.api+json
-
- {
- "data": [
- {
- "type": "computer",
- "id": "3"
- }
- ]
- }
+.. literalinclude:: ./http_snippets/snippets/relationship_api__delete_computer_relationship
+ :language: HTTP
Response:
-.. sourcecode:: http
+.. literalinclude:: ./http_snippets/snippets/relationship_api__delete_computer_relationship_result
+ :language: HTTP
+
- HTTP/1.1 200 OK
- Content-Type: application/vnd.api+json
-
- {
- "data": {
- "type": "person",
- "id": "1",
- "attributes": {
- "display_name": "JOHN ",
- "birth_date": "1990-10-18"
- },
- "relationships": {
- "computers": {
- "data": [
- {
- "id": "4",
- "type": "computer"
- }
- ],
- "links": {
- "related": "/persons/1/computers",
- "self": "/persons/1/relationships/computers"
- }
- }
- },
- "links": {
- "self": "/persons/1"
- }
- },
- "included": [
- {
- "type": "computer",
- "id": "4",
- "attributes": {
- "serial": "Comodor"
- },
- "relationships": {
- "owner": {
- "links": {
- "related": "/computers/4/owner",
- "self": "/computers/4/relationships/owner"
- }
- }
- },
- "links": {
- "self": "/computers/4"
- }
- }
- ],
- "links": {
- "self": "/persons/1/relationships/computers"
- },
- "jsonapi": {
- "version": "1.0"
- }
- }
-
-If you want to see more examples go to `JSON API 1.0 specification `_
+If you want to see more examples visit `JSON API 1.0 specification `_
diff --git a/docs/resource_manager.rst b/docs/resource_manager.rst
index 124634a..96579f7 100644
--- a/docs/resource_manager.rst
+++ b/docs/resource_manager.rst
@@ -7,18 +7,18 @@ Resource Manager
Resource manager is the link between your logical data abstraction, your data layer and optionally other software. It is the place where logic management of your resource is located.
-Flask-COMBO-JSONAPI provides 3 kinds of resource manager with default methods implementation according to the JSONAPI 1.0 specification:
+Flask-COMBO-JSONAPI provides three kinds of resource managers with default methods implemented according to the JSON:API 1.0 specification:
-* **ResourceList**: provides get and post methods to retrieve a collection of objects or create one.
-* **ResourceDetail**: provides get, patch and delete methods to retrieve details of an object, update an object and delete an object
-* **ResourceRelationship**: provides get, post, patch and delete methods to get relationships, create relationships, update relationships and delete relationships between objects.
+* **ResourceList**: provides get and post methods to retrieve or create a collection of objects.
+* **ResourceDetail**: provides get, patch and delete methods to retrieve details of an object, update or delete it
+* **ResourceRelationship**: provides get, post, patch and delete methods to get, create, update and delete relationships between objects.
-You can rewrite each default methods implementation to make custom work. If you rewrite all default methods implementation of a resource manager or if you rewrite a method and disable access to others, you don't have to set any attribute of your resource manager.
+You can rewrite each default method implementation to customize it. If you rewrite all default methods of a resource manager or if you rewrite a method and disable access to others, you don't have to set any attributes of your resource manager.
Required attributes
-------------------
-If you want to use one of the resource manager default method implementation you have to set 2 required attributes in your resource manager: schema and data_layer.
+If you want to use one of the resource manager default method implementations you have to set two required attributes in your resource manager: schema and data_layer.
:schema: the logical data abstraction used by the resource manager. It must be a class inherited from marshmallow_jsonapi.schema.Schema.
:data_layer: data layer information used to initialize your data layer (If you want to learn more: :ref:`data_layer`)
@@ -40,28 +40,28 @@ Example:
Optional attributes
-------------------
-All resource managers are inherited from flask.views.MethodView so you can provides optional attributes to your resource manager:
+All resource managers are inherited from flask.views.MethodView so you can provide optional attributes to your resource manager:
:methods: a list of methods this resource manager can handle. If you don't specify any method, all methods are handled.
- :decorators: a tuple of decorators plugged to all methods that the resource manager can handle
+ :decorators: a tuple of decorators plugged into all methods that the resource manager can handle
-You can provide default schema kwargs for each resource manager methods with these optional attributes:
+You can provide default schema kwargs for each resource manager method with these optional attributes:
* **get_schema_kwargs**: a dict of default schema kwargs in get method
* **post_schema_kwargs**: a dict of default schema kwargs in post method
* **patch_schema_kwargs**: a dict of default schema kwargs in patch method
* **delete_schema_kwargs**: a dict of default schema kwargs in delete method
-Each method of a resource manager gets a pre and post process methods that takes view args and kwargs as parameters for the pre process methods, and the result of the method as parameter for the post process method. Thanks to this you can make custom work before and after the method process. Available methods to override are:
+Each method of a resource manager gets a pre- and postprocess method that takes view args and kwargs as parameters for the pre process methods, and the result of the method as parameter for the post process method. Thanks to this you can process custom code before and after the method processes. Available methods to override are:
- :before_get: pre process method of the get method
- :after_get: post process method of the get method
- :before_post: pre process method of the post method
- :after_post: post process method of the post method
- :before_patch: pre process method of the patch method
- :after_patch: post process method of the patch method
- :before_delete: pre process method of the delete method
- :after_delete: post process method of the delete method
+ :before_get: preprocess method of the get method
+ :after_get: postprocess method of the get method
+ :before_post: preprocess method of the post method
+ :after_post: postprocess method of the post method
+ :before_patch: preprocess method of the patch method
+ :after_patch: postprocess method of the patch method
+ :before_delete: preprocess method of the delete method
+ :after_delete: postprocess method of the delete method
Example:
@@ -82,11 +82,11 @@ Example:
get_schema_kwargs = {'only': ('name', )}
def before_patch(*args, **kwargs):
- """Make custom work here. View args and kwargs are provided as parameter
+ """Perform custom operations here. View args and kwargs are provided as parameter
"""
def after_patch(result):
- """Make custom work here. Add something to the result of the view.
+ """Perform custom operations here. Add something to the result of the view.
"""
ResourceList
@@ -94,7 +94,7 @@ ResourceList
ResourceList manager has its own optional attributes:
- :view_kwargs: if you set this flag to True view kwargs will be used to compute the list url. If you have a list url pattern with parameter like that: /persons//computers you have to set this flag to True
+ :view_kwargs: if you set this flag to True, view kwargs will be used to compute the list URL. If you have a list URL pattern with parameters such as ``/persons//computers`` you have to set this flag to True
Example:
@@ -131,9 +131,9 @@ Example:
data_layer = {'session': db.session,
'model': Person}
-This minimal ResourceDetail configuration provides GET, PATCH and DELETE interface to retrieve details of objects, update an objects and delete an object with all powerful features like sparse fieldsets and including related objects.
+This minimal ResourceDetail configuration provides a GET, PATCH and DELETE interface to retrieve details of an object, update and delete it with all-powerful features like sparse fieldsets and including related objects.
-If your schema has relationship field(s) you can update an object and also update his link(s) to related object(s) in the same time. For an example see :ref:`quickstart`.
+If your schema has relationship fields you can update an object and also update its links to (one or more) related objects at the same time. For an example see :ref:`quickstart`.
ResourceRelationship
--------------------
@@ -152,4 +152,4 @@ Example:
data_layer = {'session': db.session,
'model': Person}
-This minimal ResourceRelationship configuration provides GET, POST, PATCH and DELETE interface to retrieve relationship(s), create relationship(s), update relationship(s) and delete relationship(s) between objects with all powerful features like sparse fieldsets and including related objects.
+This minimal ResourceRelationship configuration provides a GET, POST, PATCH and DELETE interface to retrieve, create, update or delete one or more relationships between objects with all-powerful features like sparse fieldsets and including related objects.
diff --git a/docs/sorting.rst b/docs/sorting.rst
index 6dba237..ec8aa4d 100644
--- a/docs/sorting.rst
+++ b/docs/sorting.rst
@@ -5,11 +5,11 @@ Sorting
.. currentmodule:: flask_combo_jsonapi
-You can sort results with querystring parameter named "sort"
+You can sort results using the query string parameter named "sort"
.. note::
- Examples are not urlencoded for a better readability
+ Examples are not URL encoded for better readability
Example:
@@ -21,7 +21,7 @@ Example:
Multiple sort
-------------
-You can sort on multiple fields like that:
+You can sort on multiple fields like this:
.. sourcecode:: http
@@ -31,7 +31,7 @@ You can sort on multiple fields like that:
Descending sort
---------------
-You can make desc sort with the character "-" like that:
+You can in descending order using a minus symbol, "-", like this:
.. sourcecode:: http
@@ -41,7 +41,7 @@ You can make desc sort with the character "-" like that:
Multiple sort + Descending sort
-------------------------------
-Of course, you can combine both like that:
+Of course, you can combine both like this:
.. sourcecode:: http
diff --git a/docs/sparse_fieldsets.rst b/docs/sparse_fieldsets.rst
index 5833814..104bd10 100644
--- a/docs/sparse_fieldsets.rst
+++ b/docs/sparse_fieldsets.rst
@@ -5,13 +5,13 @@ Sparse fieldsets
.. currentmodule:: flask_combo_jsonapi
-You can restrict the fields returned by api with the querystring parameter called "fields". It is very useful for performance purpose because fields not returned are not resolved by api. You can use "fields" parameter on any kind of route (classical CRUD route or relationships route) and any kind of http methods as long as method return data.
+You can restrict the fields returned by your API using the query string parameter called "fields". It is very useful for performance purposes because fields not returned are not resolved by the API. You can use the "fields" parameter on any kind of route (classical CRUD route or relationships route) and any kind of HTTP methods as long as the method returns data.
.. note::
- Examples are not urlencoded for a better readability
+ Examples are not URL encoded for better readability
-The syntax of a fields is like that ::
+The syntax of the fields parameter is ::
?fields[]=
@@ -22,20 +22,20 @@ Example:
GET /persons?fields[person]=display_name HTTP/1.1
Accept: application/vnd.api+json
-In this example person's display_name is the only field returned by the api. No relationships links are returned so the response is very fast because api doesn't have to compute relationships link and it is a very costly work.
+In this example person's display_name is the only field returned by the API. No relationship links are returned so the response is very fast because the API doesn't have to do any expensive computation of relationship links.
You can manage returned fields for the entire response even for included objects
Example:
-If you don't want to compute relationships links for included computers of a person you can do something like that
+If you don't want to compute relationship links for included computers of a person you can do something like this
.. sourcecode:: http
GET /persons/1?include=computers&fields[computer]=serial HTTP/1.1
Accept: application/vnd.api+json
-And of course you can combine both like that:
+And of course you can combine both like this:
Example:
@@ -46,4 +46,4 @@ Example:
.. warning::
- If you want to use both "fields" and "include" don't forget to specify the name of the relationship in fields; if you don't the include wont work.
+ If you want to use both "fields" and "include", don't forget to specify the name of the relationship in "fields"; if you don't, the include wont work.
diff --git a/examples/api.py b/examples/api.py
index 39761e8..9516eec 100644
--- a/examples/api.py
+++ b/examples/api.py
@@ -1,5 +1,9 @@
from flask import Flask
+from marshmallow import pre_load
+from sqlalchemy import UniqueConstraint
+
from flask_combo_jsonapi import Api, ResourceDetail, ResourceList, ResourceRelationship
+from flask_combo_jsonapi.data_layers.alchemy import SqlalchemyDataLayer
from flask_combo_jsonapi.exceptions import ObjectNotFound
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm.exc import NoResultFound
@@ -11,9 +15,8 @@
app.config['DEBUG'] = True
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
-
# Initialize SQLAlchemy
-app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
+app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/api.db'
db = SQLAlchemy(app)
@@ -22,7 +25,6 @@ class Person(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String)
email = db.Column(db.String)
- birth_date = db.Column(db.Date)
password = db.Column(db.String)
@@ -32,6 +34,7 @@ class Computer(db.Model):
person_id = db.Column(db.Integer, db.ForeignKey('person.id'))
person = db.relationship('Person', backref=db.backref('computers'))
+
db.create_all()
@@ -44,17 +47,31 @@ class Meta:
self_view_many = 'person_list'
id = fields.Integer(as_string=True, dump_only=True)
- name = fields.Str(required=True, load_only=True)
+ name = fields.String(required=True)
email = fields.Email(load_only=True)
- birth_date = fields.Date()
display_name = fields.Function(lambda obj: "{} <{}>".format(obj.name.upper(), obj.email))
- computers = Relationship(self_view='person_computers',
- self_view_kwargs={'id': ''},
- related_view='computer_list',
- related_view_kwargs={'id': ''},
- many=True,
- schema='ComputerSchema',
- type_='computer')
+ computers = Relationship(
+ self_view='person_computers',
+ self_view_kwargs={'id': ''},
+ related_view='computer_list',
+ related_view_kwargs={'id': ''},
+ many=True,
+ schema='ComputerSchema',
+ type_='computer',
+ )
+
+ @pre_load
+ def remove_id_before_deserializing(self, data, **kwargs):
+ """
+ We don't want to allow editing ID on POST / PATCH
+
+ Related issues:
+ https://github.com/AdCombo/flask-combo-jsonapi/issues/34
+ https://github.com/miLibris/flask-rest-jsonapi/issues/193
+ """
+ if 'id' in data:
+ del data['id']
+ return data
class ComputerSchema(Schema):
@@ -64,57 +81,90 @@ class Meta:
self_view_kwargs = {'id': ''}
id = fields.Integer(as_string=True, dump_only=True)
- serial = fields.Str(required=True)
- owner = Relationship(attribute='person',
- self_view='computer_person',
- self_view_kwargs={'id': ''},
- related_view='person_detail',
- related_view_kwargs={'computer_id': ''},
- schema='PersonSchema',
- type_='person')
+ serial = fields.String(required=True)
+ owner = Relationship(
+ attribute='person',
+ self_view='computer_person',
+ self_view_kwargs={'id': ''},
+ related_view='person_detail',
+ related_view_kwargs={'computer_id': ''},
+ schema='PersonSchema',
+ type_='person',
+ )
+
+ @pre_load
+ def remove_id_before_deserializing(self, data, **kwargs):
+ """
+ We don't want to allow editing ID on POST / PATCH
+
+ Related issues:
+ https://github.com/AdCombo/flask-combo-jsonapi/issues/34
+ https://github.com/miLibris/flask-rest-jsonapi/issues/193
+ """
+ if 'id' in data:
+ del data['id']
+ return data
# Create resource managers
class PersonList(ResourceList):
schema = PersonSchema
- data_layer = {'session': db.session,
- 'model': Person}
+ data_layer = {
+ 'session': db.session,
+ 'model': Person,
+ }
-class PersonDetail(ResourceDetail):
+class PersonDetailSqlalchemyDataLayer(SqlalchemyDataLayer):
+
def before_get_object(self, view_kwargs):
- if view_kwargs.get('computer_id') is not None:
- try:
- computer = self.session.query(Computer).filter_by(id=view_kwargs['computer_id']).one()
- except NoResultFound:
- raise ObjectNotFound({'parameter': 'computer_id'},
- "Computer: {} not found".format(view_kwargs['computer_id']))
+ if not view_kwargs.get('computer_id'):
+ return
+ try:
+ computer = self.session.query(Computer).filter_by(
+ id=view_kwargs['computer_id'],
+ ).one()
+ except NoResultFound:
+ raise ObjectNotFound(
+ "Computer: {} not found".format(view_kwargs['computer_id']),
+ source={'parameter': 'computer_id'},
+ )
+ else:
+ if computer.person is not None:
+ view_kwargs['id'] = computer.person.id
else:
- if computer.person is not None:
- view_kwargs['id'] = computer.person.id
- else:
- view_kwargs['id'] = None
+ view_kwargs['id'] = None
+
+class PersonDetail(ResourceDetail):
schema = PersonSchema
- data_layer = {'session': db.session,
- 'model': Person,
- 'methods': {'before_get_object': before_get_object}}
+ data_layer = {
+ 'session': db.session,
+ 'model': Person,
+ 'class': PersonDetailSqlalchemyDataLayer,
+ }
class PersonRelationship(ResourceRelationship):
schema = PersonSchema
- data_layer = {'session': db.session,
- 'model': Person}
+ data_layer = {
+ 'session': db.session,
+ 'model': Person
+ }
-class ComputerList(ResourceList):
+class RelatedComputersSqlalchemyDataLayer(SqlalchemyDataLayer):
+
def query(self, view_kwargs):
query_ = self.session.query(Computer)
if view_kwargs.get('id') is not None:
try:
self.session.query(Person).filter_by(id=view_kwargs['id']).one()
except NoResultFound:
- raise ObjectNotFound({'parameter': 'id'}, "Person: {} not found".format(view_kwargs['id']))
+ raise ObjectNotFound(
+ "Person: {} not found".format(view_kwargs['id']),
+ source={'parameter': 'id'},
+ )
else:
query_ = query_.join(Person).filter(Person.id == view_kwargs['id'])
return query_
@@ -124,27 +174,35 @@ def before_create_object(self, data, view_kwargs):
person = self.session.query(Person).filter_by(id=view_kwargs['id']).one()
data['person_id'] = person.id
+
+class ComputerList(ResourceList):
schema = ComputerSchema
- data_layer = {'session': db.session,
- 'model': Computer,
- 'methods': {'query': query,
- 'before_create_object': before_create_object}}
+ data_layer = {
+ 'session': db.session,
+ 'model': Computer,
+ 'class': RelatedComputersSqlalchemyDataLayer,
+ }
class ComputerDetail(ResourceDetail):
schema = ComputerSchema
- data_layer = {'session': db.session,
- 'model': Computer}
+ data_layer = {
+ 'session': db.session,
+ 'model': Computer,
+ }
class ComputerRelationship(ResourceRelationship):
schema = ComputerSchema
- data_layer = {'session': db.session,
- 'model': Computer}
+ data_layer = {
+ 'session': db.session,
+ 'model': Computer,
+ }
# Create endpoints
api = Api(app)
+
api.route(PersonList, 'person_list', '/persons')
api.route(PersonDetail, 'person_detail', '/persons/', '/computers//owner')
api.route(PersonRelationship, 'person_computers', '/persons//relationships/computers')
diff --git a/examples/api_minimal.py b/examples/api_minimal.py
new file mode 100644
index 0000000..c205b36
--- /dev/null
+++ b/examples/api_minimal.py
@@ -0,0 +1,74 @@
+from flask import Flask
+from flask_combo_jsonapi import Api, ResourceDetail, ResourceList
+from flask_sqlalchemy import SQLAlchemy
+from marshmallow import pre_load
+from marshmallow_jsonapi.flask import Schema
+from marshmallow_jsonapi import fields
+
+# Create the Flask application and the Flask-SQLAlchemy object.
+app = Flask(__name__)
+app.config['DEBUG'] = True
+app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/api_minimal.db'
+db = SQLAlchemy(app)
+
+
+# Create model
+class Person(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ name = db.Column(db.String)
+
+
+# Create the database.
+db.create_all()
+
+
+# Create schema
+class PersonSchema(Schema):
+ class Meta:
+ type_ = 'person'
+ self_view = 'person_detail'
+ self_view_kwargs = {'id': ''}
+ self_view_many = 'person_list'
+
+ id = fields.Integer(as_string=True, dump_only=True)
+ name = fields.String()
+
+ @pre_load
+ def remove_id_before_deserializing(self, data, **kwargs):
+ """
+ We don't want to allow editing ID on POST / PATCH
+
+ Related issues:
+ https://github.com/AdCombo/flask-combo-jsonapi/issues/34
+ https://github.com/miLibris/flask-rest-jsonapi/issues/193
+ """
+ if 'id' in data:
+ del data['id']
+ return data
+
+
+# Create resource managers
+class PersonList(ResourceList):
+ schema = PersonSchema
+ data_layer = {
+ 'session': db.session,
+ 'model': Person,
+ }
+
+
+class PersonDetail(ResourceDetail):
+ schema = PersonSchema
+ data_layer = {
+ 'session': db.session,
+ 'model': Person,
+ }
+
+
+# Create the API object
+api = Api(app)
+api.route(PersonList, 'person_list', '/persons')
+api.route(PersonDetail, 'person_detail', '/persons/')
+
+# Start the flask loop
+if __name__ == '__main__':
+ app.run()
diff --git a/examples/api_nested.py b/examples/api_nested.py
index cd806c9..795e5a9 100644
--- a/examples/api_nested.py
+++ b/examples/api_nested.py
@@ -1,10 +1,13 @@
from flask import Flask
+from sqlalchemy import UniqueConstraint
+
from flask_combo_jsonapi import Api, ResourceDetail, ResourceList, ResourceRelationship
+from flask_combo_jsonapi.data_layers.alchemy import SqlalchemyDataLayer
from flask_combo_jsonapi.exceptions import ObjectNotFound
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm.exc import NoResultFound
from marshmallow_jsonapi.flask import Schema, Relationship
-from marshmallow import Schema as MarshmallowSchema
+from marshmallow import Schema as MarshmallowSchema, pre_load
from marshmallow_jsonapi import fields
# Create the Flask application
@@ -12,9 +15,8 @@
app.config['DEBUG'] = True
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
-
# Initialize SQLAlchemy
-app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
+app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/api_nested.db'
db = SQLAlchemy(app)
@@ -23,11 +25,14 @@ class Person(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String)
email = db.Column(db.String)
- birth_date = db.Column(db.Date)
password = db.Column(db.String)
- tags = db.relationship("Person_Tag", cascade="save-update, merge, delete, delete-orphan")
- single_tag = db.relationship("Person_Single_Tag", uselist=False, cascade="save-update, merge, delete, delete-orphan")
- json_tags = db.Column(db.JSON)
+ tags = db.relationship("PersonTag", cascade="save-update, merge, delete, delete-orphan")
+ single_tag = db.relationship(
+ "PersonSingleTag",
+ uselist=False,
+ cascade="save-update, merge, delete, delete-orphan",
+ )
+ json_data = db.Column(db.JSON)
class Computer(db.Model):
@@ -37,36 +42,58 @@ class Computer(db.Model):
person = db.relationship('Person', backref=db.backref('computers'))
-class Person_Tag(db.Model):
- id = db.Column(db.Integer, db.ForeignKey('person.id'), primary_key=True, index=True)
- key = db.Column(db.String, primary_key=True)
- value = db.Column(db.String, primary_key=True)
+class PersonTag(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ person_id = db.Column(db.Integer, db.ForeignKey('person.id'), index=True)
+ key = db.Column(db.String)
+ value = db.Column(db.String)
+
+ __table_args__ = (
+ UniqueConstraint(
+ 'person_id',
+ 'key',
+ 'value',
+ name='_person_key_value'
+ ),
+ )
-class Person_Single_Tag(db.Model):
- id = db.Column(db.Integer, db.ForeignKey('person.id'), primary_key=True, index=True)
+class PersonSingleTag(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ person_id = db.Column(db.Integer, db.ForeignKey('person.id'), index=True)
key = db.Column(db.String)
value = db.Column(db.String)
+ __table_args__ = (
+ UniqueConstraint(
+ 'person_id',
+ 'key',
+ 'value',
+ name='_person_key_value'
+ ),
+ )
+
+
db.create_all()
+
# Create schema
class PersonTagSchema(MarshmallowSchema):
class Meta:
type_ = 'person_tag'
- id = fields.Str(dump_only=True, load_only=True)
- key = fields.Str()
- value = fields.Str()
+ id = fields.String(dump_only=True, load_only=True)
+ key = fields.String()
+ value = fields.String()
class PersonSingleTagSchema(MarshmallowSchema):
class Meta:
type_ = 'person_single_tag'
- id = fields.Str(dump_only=True, load_only=True)
- key = fields.Str()
- value = fields.Str()
+ id = fields.String(dump_only=True, load_only=True)
+ key = fields.String()
+ value = fields.String()
# Create logical data abstraction (same as data storage for this first example)
@@ -78,18 +105,34 @@ class Meta:
self_view_many = 'person_list'
id = fields.Integer(as_string=True, dump_only=True)
- name = fields.Str(requried=True, load_only=True)
+ name = fields.String(required=True)
email = fields.Email(load_only=True)
- birth_date = fields.Date()
display_name = fields.Function(lambda obj: "{} <{}>".format(obj.name.upper(), obj.email))
- computers = Relationship(self_view='person_computers',
- self_view_kwargs={'id': ''},
- related_view='computer_list',
- related_view_kwargs={'id': ''},
- many=True,
- schema='ComputerSchema',
- type_='computer')
tags = fields.Nested(PersonTagSchema, many=True)
+ single_tag = fields.Nested(PersonSingleTagSchema, many=False)
+ json_data = fields.Dict(dump_only=True)
+ computers = Relationship(
+ self_view='person_computers',
+ self_view_kwargs={'id': ''},
+ related_view='computer_list',
+ related_view_kwargs={'id': ''},
+ many=True,
+ schema='ComputerSchema',
+ type_='computer',
+ )
+
+ @pre_load
+ def remove_id_before_deserializing(self, data, **kwargs):
+ """
+ We don't want to allow editing ID on POST / PATCH
+
+ Related issues:
+ https://github.com/AdCombo/flask-combo-jsonapi/issues/34
+ https://github.com/miLibris/flask-rest-jsonapi/issues/193
+ """
+ if 'id' in data:
+ del data['id']
+ return data
class ComputerSchema(Schema):
@@ -99,58 +142,90 @@ class Meta:
self_view_kwargs = {'id': ''}
id = fields.Integer(as_string=True, dump_only=True)
- serial = fields.Str(requried=True)
- owner = Relationship(attribute='person',
- self_view='computer_person',
- self_view_kwargs={'id': ''},
- related_view='person_detail',
- related_view_kwargs={'computer_id': ''},
- schema='PersonSchema',
- type_='person')
-
+ serial = fields.String(required=True)
+ owner = Relationship(
+ attribute='person',
+ self_view='computer_person',
+ self_view_kwargs={'id': ''},
+ related_view='person_detail',
+ related_view_kwargs={'computer_id': ''},
+ schema='PersonSchema',
+ type_='person',
+ )
+
+ @pre_load
+ def remove_id_before_deserializing(self, data, **kwargs):
+ """
+ We don't want to allow editing ID on POST / PATCH
+
+ Related issues:
+ https://github.com/AdCombo/flask-combo-jsonapi/issues/34
+ https://github.com/miLibris/flask-rest-jsonapi/issues/193
+ """
+ if 'id' in data:
+ del data['id']
+ return data
# Create resource managers
class PersonList(ResourceList):
schema = PersonSchema
- data_layer = {'session': db.session,
- 'model': Person}
+ data_layer = {
+ 'session': db.session,
+ 'model': Person,
+ }
-class PersonDetail(ResourceDetail):
+class PersonDetailSqlalchemyDataLayer(SqlalchemyDataLayer):
+
def before_get_object(self, view_kwargs):
- if view_kwargs.get('computer_id') is not None:
- try:
- computer = self.session.query(Computer).filter_by(id=view_kwargs['computer_id']).one()
- except NoResultFound:
- raise ObjectNotFound({'parameter': 'computer_id'},
- "Computer: {} not found".format(view_kwargs['computer_id']))
+ if not view_kwargs.get('computer_id'):
+ return
+ try:
+ computer = self.session.query(Computer).filter_by(
+ id=view_kwargs['computer_id'],
+ ).one()
+ except NoResultFound:
+ raise ObjectNotFound(
+ "Computer: {} not found".format(view_kwargs['computer_id']),
+ source={'parameter': 'computer_id'},
+ )
+ else:
+ if computer.person is not None:
+ view_kwargs['id'] = computer.person.id
else:
- if computer.person is not None:
- view_kwargs['id'] = computer.person.id
- else:
- view_kwargs['id'] = None
+ view_kwargs['id'] = None
+
+class PersonDetail(ResourceDetail):
schema = PersonSchema
- data_layer = {'session': db.session,
- 'model': Person,
- 'methods': {'before_get_object': before_get_object}}
+ data_layer = {
+ 'session': db.session,
+ 'model': Person,
+ 'class': PersonDetailSqlalchemyDataLayer,
+ }
class PersonRelationship(ResourceRelationship):
schema = PersonSchema
- data_layer = {'session': db.session,
- 'model': Person}
+ data_layer = {
+ 'session': db.session,
+ 'model': Person
+ }
-class ComputerList(ResourceList):
+class RelatedComputersSqlalchemyDataLayer(SqlalchemyDataLayer):
+
def query(self, view_kwargs):
query_ = self.session.query(Computer)
if view_kwargs.get('id') is not None:
try:
self.session.query(Person).filter_by(id=view_kwargs['id']).one()
except NoResultFound:
- raise ObjectNotFound({'parameter': 'id'}, "Person: {} not found".format(view_kwargs['id']))
+ raise ObjectNotFound(
+ "Person: {} not found".format(view_kwargs['id']),
+ source={'parameter': 'id'},
+ )
else:
query_ = query_.join(Person).filter(Person.id == view_kwargs['id'])
return query_
@@ -160,27 +235,35 @@ def before_create_object(self, data, view_kwargs):
person = self.session.query(Person).filter_by(id=view_kwargs['id']).one()
data['person_id'] = person.id
+
+class ComputerList(ResourceList):
schema = ComputerSchema
- data_layer = {'session': db.session,
- 'model': Computer,
- 'methods': {'query': query,
- 'before_create_object': before_create_object}}
+ data_layer = {
+ 'session': db.session,
+ 'model': Computer,
+ 'class': RelatedComputersSqlalchemyDataLayer,
+ }
class ComputerDetail(ResourceDetail):
schema = ComputerSchema
- data_layer = {'session': db.session,
- 'model': Computer}
+ data_layer = {
+ 'session': db.session,
+ 'model': Computer,
+ }
class ComputerRelationship(ResourceRelationship):
schema = ComputerSchema
- data_layer = {'session': db.session,
- 'model': Computer}
+ data_layer = {
+ 'session': db.session,
+ 'model': Computer,
+ }
# Create endpoints
api = Api(app)
+
api.route(PersonList, 'person_list', '/persons')
api.route(PersonDetail, 'person_detail', '/persons/', '/computers//owner')
api.route(PersonRelationship, 'person_computers', '/persons//relationships/computers')
diff --git a/flask_combo_jsonapi/data_layers/alchemy.py b/flask_combo_jsonapi/data_layers/alchemy.py
index 880ef7e..ee7d499 100644
--- a/flask_combo_jsonapi/data_layers/alchemy.py
+++ b/flask_combo_jsonapi/data_layers/alchemy.py
@@ -1,4 +1,8 @@
"""This module is a CRUD interface between resource managers and the sqlalchemy ORM"""
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from sqlalchemy.orm import Session as SessionType
from sqlalchemy.orm.exc import NoResultFound
from sqlalchemy.orm.collections import InstrumentedList
@@ -33,6 +37,8 @@
class SqlalchemyDataLayer(BaseDataLayer):
"""Sqlalchemy data layer"""
+ if TYPE_CHECKING:
+ session: "SessionType"
def __init__(self, kwargs):
"""Initialize an instance of SqlalchemyDataLayer
diff --git a/flask_combo_jsonapi/resource.py b/flask_combo_jsonapi/resource.py
index b15e550..8700705 100644
--- a/flask_combo_jsonapi/resource.py
+++ b/flask_combo_jsonapi/resource.py
@@ -419,7 +419,11 @@ def _get_validated_json_payload(self, related_type_) -> dict:
@check_method_requirements
def post(self, *args, **kwargs):
- """Add / create relationship(s)"""
+ """
+ Add / create relationship(s)
+
+ https://jsonapi.org/format/#crud-updating-to-many-relationships
+ """
relationship_field, model_relationship_field, related_type_, related_id_field = self._get_relationship_data()
json_data = self._get_validated_json_payload(related_type_)
self.before_post(args, kwargs, json_data=json_data)
@@ -441,7 +445,23 @@ def post(self, *args, **kwargs):
@check_method_requirements
def patch(self, *args, **kwargs):
- """Update a relationship"""
+ """
+ Update a relationship
+
+ # https://jsonapi.org/format/#crud-updating-relationship-responses-200
+
+ > If a server accepts an update but also changes the targeted relationship(s)
+ > in other ways than those specified by the request,
+ > it MUST return a 200 OK response.
+ > The response document MUST include a representation
+ > of the updated relationship(s).
+
+ > A server MUST return a 200 OK status code if an update is successful,
+ > the client’s current data remain up to date,
+ > and the server responds only with top-level meta data.
+ > In this case the server MUST NOT include a representation
+ > of the updated relationship(s).
+ """
relationship_field, model_relationship_field, related_type_, related_id_field = self._get_relationship_data()
json_data = self._get_validated_json_payload(related_type_)
diff --git a/requirements.txt b/requirements.txt
index d503c22..7831a46 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
simplejson
-Flask>=1.0.1
+Flask>=1.0.1,<2
marshmallow==3.2.1
marshmallow_jsonapi==0.24.0
-sqlalchemy
+sqlalchemy<1.4
diff --git a/setup.py b/setup.py
index 5ec144b..948693c 100644
--- a/setup.py
+++ b/setup.py
@@ -1,7 +1,7 @@
import os
from setuptools import setup, find_packages
-__version__ = "1.0.6"
+__version__ = "1.0.7"
requirements_filepath = os.path.join(os.path.dirname(__name__), "requirements.txt")