Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add serialisation server module #191

Merged
merged 18 commits into from Oct 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
@@ -1,6 +1,9 @@

# Unreleased

## New Features
* Dynamic serialisation of data tables rows
https://github.com/anvilistas/anvil-extras/pull/191

## Updates:
* storage supports `datetime` and `date` objects
https://github.com/anvilistas/anvil-extras/pull/179
Expand Down
138 changes: 138 additions & 0 deletions docs/guides/modules/serialisation.rst
@@ -0,0 +1,138 @@
Serialisation
=============
A server module that provides dynamic serialisation of data table rows.
s-cork marked this conversation as resolved.
Show resolved Hide resolved

A single data table row is converted to a dictionary of simple Python types.
A set of rows is converted to a list of those dictionaries.

At present, media columns and multiple link columns are not supported.
Date and Datetime columns are converted to strings in iso format.

Usage
-----
Let's imagine we have a data table named 'books' with columns 'title' and 'publication_date'.

In a server module, import and call the function `datatable_schema` to get a `marshmallow <https://marshmallow.readthedocs.io/en/stable/>`_ Schema instance:

.. code-block:: python

from anvil.tables import app_tables
from anvil_extras.serialisation import datatable_schema
from pprint import pprint

schema = datatable_schema("books")

To serialise a row from the books table, call the schema's `dump` method:

.. code-block:: python

book = app_tables.books.get(title="Fluent Python")
result = schema.dump(book)
pprint(result)

>> {"publication_date": "2015-08-01", "title": "Fluent Python"}

To serialise several rows from the books table, set the `many` argument to True:

.. code-block:: python

books = app_tables.books.search()
result = schema.dump(books, many=True)
pprint(result)

>> [{'publication_date': '2015-08-01', 'title': 'Fluent Python'},
>> {'publication_date': '2015-01-01', 'title': 'Practical Vim'},
>> {'publication_date': None, 'title': "The Hitch Hiker's Guide to the Galaxy"}]


To exclude the publication date from the result, pass its name to the server function:

.. code-block:: python

from anvil.tables import app_tables
from anvil_extras.serialisation import datatable_schema
from pprint import pprint

schema = datatable_schema("books", ignore_columns="publication_date")
books = app_tables.books.search()
result = schema.dump(books, many=True)
pprint(result)

>> [{'title': 'Fluent Python'},
>> {'title': 'Practical Vim'},
>> {'title': "The Hitch Hiker's Guide to the Galaxy"}]

You can also pass a list of column names to ignore.

If you want the row id included in the results, set the `with_id` argument:

.. code-block:: python

from anvil.tables import app_tables
from anvil_extras.serialisation import datatable_schema
from pprint import pprint

schema = datatable_schema("books", ignore_columns="publication_date", with_id=True)
books = app_tables.books.search()
result = schema.dump(books, many=True)
pprint(result)

>> [{'_id': '[169162,297786594]', 'title': 'Fluent Python'},
>> {'_id': '[169162,297786596]', 'title': 'Practical Vim'},
>> {'_id': '[169162,297786597]',
>> 'title': "The Hitch Hiker's Guide to the Galaxy"}]


Linked Tables
+++++++++++++
Let's imagine we also have an 'authors' table with a 'name' column and that we've added
an 'author' linked column to the books table.

To include the author in the results for a books search, create a dict to define, for each table, the linked columns in that table the linked table they refer to:

.. code-block:: python

from anvil.tables import app_tables
from anvil_extras.serialisation import datatable_schema
from pprint import pprint

# The books table has one linked column named 'author' and that is a link to the 'authors' table
linked_tables = {"books": {"author": "authors"}}
schema = datatable_schema(
"books",
ignore_columns="publication_date",
linked_tables=linked_tables,
)
books = app_tables.books.search()
result = schema.dump(books, many=True)
pprint(result)

>> [{'author': {'name': 'Luciano Ramalho'}, 'title': 'Fluent Python'},
>> {'author': {'name': 'Drew Neil'}, 'title': 'Practical Vim'},
>> {'author': {'name': 'Douglas Adams'},
>> 'title': "The Hitch Hiker's Guide to the Galaxy"}]

Finally, let's imagine the 'authors' table has a 'date_of_birth' column but we don't want to include that in the results:


.. code-block:: python

from anvil.tables import app_tables
from anvil_extras.serialisation import datatable_schema
from pprint import pprint

linked_tables = {"books": {"author": "authors"}}
ignore_columns = {"books": "publication_date", "authors": "date_of_birth"}
schema = datatable_schema(
"books",
ignore_columns=ignore_columns,
linked_tables=linked_tables,
)
books = app_tables.books.search()
result = schema.dump(books, many=True)
pprint(result)

>> [{'author': {'name': 'Luciano Ramalho'}, 'title': 'Fluent Python'},
>> {'author': {'name': 'Drew Neil'}, 'title': 'Practical Vim'},
>> {'author': {'name': 'Douglas Adams'},
>> 'title': "The Hitch Hiker's Guide to the Galaxy"}]
98 changes: 98 additions & 0 deletions server_code/serialisation.py
@@ -0,0 +1,98 @@
# SPDX-License-Identifier: MIT
#
# Copyright (c) 2021 The Anvil Extras project team members listed at
# https://github.com/anvilistas/anvil-extras/graphs/contributors
#
# This software is published at https://github.com/anvilistas/anvil-extras

from anvil.tables import app_tables

import marshmallow

__version__ = "1.8.1"

anvil_to_marshmallow = {
"bool": marshmallow.fields.Boolean,
"date": marshmallow.fields.Date,
"datetime": marshmallow.fields.DateTime,
"number": marshmallow.fields.Number,
"string": marshmallow.fields.Str,
"simpleObject": marshmallow.fields.Raw,
}


def _exclusions(table_name, ignore_columns):
"""Generate a list of columns to exclude from serialisation for a given table name

Parameters
----------
table_name : str
The name of a data table within the app
ignore_columns : list, tuple, dict or str
A list or tuple of column names to ignore, a dict mapping
table names to such lists or tuples, or a string with a single column name

Returns
-------
list
of column names
"""
if isinstance(ignore_columns, (list, tuple)):
return ignore_columns
elif isinstance(ignore_columns, dict):
return ignore_columns[table_name]
elif isinstance(ignore_columns, str):
return [ignore_columns]
else:
return []


def datatable_schema(
table_name, ignore_columns=None, linked_tables=None, with_id=False
):
"""Generate a marshmallow Schema dynamically from a table name

Parameters
----------
table_name : str
The name of a data table within the app
ignore_columns : list, tuple, dict or str
A list or tuple of column names to ignore, a dict mapping
table names to such lists or tuples, or a string with a single column name
linked_tables : dict
mapping a table name to a dict which, in turn, maps a column name to a linked
table name
with_id : boolean
whether the internal anvil id should be included in the serialised output

Returns
-------
marshmallow.Schema
"""
table = getattr(app_tables, table_name)
exclusions = _exclusions(table_name, ignore_columns)
if linked_tables is None:
linked_tables = {}

try:
schema_definition = {
column["name"]: anvil_to_marshmallow[column["type"]]()
for column in table.list_columns()
if column["type"] != "liveObject" and column["name"] not in exclusions
}
except KeyError as e:
raise ValueError(f"{e} columns are not supported")

if table_name in linked_tables:
linked_schema_definition = {
column: marshmallow.fields.Nested(
datatable_schema(linked_table, ignore_columns, linked_tables, with_id)
)
for column, linked_table in linked_tables[table_name].items()
}
schema_definition = {**schema_definition, **linked_schema_definition}

if with_id:
schema_definition["_id"] = marshmallow.fields.Function(lambda row: row.get_id())

return marshmallow.Schema.from_dict(schema_definition)()