Working with Lazy And Recursive Access of Data

Note
The example reads the shop data described in The Business Layer.

Purpose

A report often needs data that is related to its root object at several levels: a customer, its account, its account manager, the other customers of that manager. The .NET version of this page builds such data with a custom C# type whose indexers fetch each property, related entity and list of child entities lazily, when the template first asks for it.

In Python, the same data is a structure of nested dicts and lists, and the template reaches every level by member access: root.Entities.AccountManager.Name. The structure may also refer back to itself. A dict that appears in several places, such as an account manager that each of its customers points to, is one object for the engine, so a template can follow the references round as far as it needs. See Using Data Sources for how the engine reads Python data.

Creating Template

The template reads the properties of the root entity, its related entities, its child entities, and then goes from the root to its account manager and back to the manager’s customers:

Using properties of the root entity
<<[root["CustomerName"]]>>
<<[root.ShippingAddress]>>

Using related entities of the root entity
<<[root.Entities.Account.Login]>>
<<[root.Entities.AccountManager.Name]>>

Using child entities of the root entity
<<foreach [order in root.Children.Orders]>><<[order.OrderNumber]>>: <<[order.Product.ProductName]>>
<</foreach>>
Invoice | Amount due
<<foreach [invoice in root.Children.Invoices]>><<[invoice.Number]>> | <<[invoice.Amount]>><</foreach>>

Following the references back
<<foreach [customer in root.Entities.AccountManager.Customers]>><<[customer.CustomerName]>> (<<[customer.Entities.Account.Login]>>) is managed by <<[customer.Entities.AccountManager.Name]>>, who manages <<[customer.Entities.AccountManager.Customers.Count()]>> customers
<</foreach>>

The invoices are a table-row data band in the template.

Download Template

Get the template from here.

Generating The Report

The code loads Quayside Bistro and Saltmarsh Deli from customers.json, gives each its related and child entities as nested dicts, and puts both in the list of customers of their account manager, Mei Tanaka. Each customer’s AccountManager is that same manager dict, so the data is circular: manager, customer, manager again.

import json

from groupdocs.assembly import DataSourceInfo, DocumentAssembler

with open("customers.json", encoding="utf-8") as f:
    customers = {c["CustomerName"]: c for c in json.load(f)}

manager = {"Name": "Mei Tanaka", "Customers": []}


def add_entities(customer, login):
    # Related entities and child entities of a customer, as nested dicts and lists
    customer["Entities"] = {"Account": {"Login": login}, "AccountManager": manager}
    customer["Children"] = {
        "Orders": customer["Order"],
        "Invoices": [{"Number": "INV-%d" % o["OrderNumber"], "Amount": o["Price"]} for o in customer["Order"]],
    }
    # The manager refers back to the customer: the structure is circular
    manager["Customers"].append(customer)
    return customer


root = add_entities(customers["Quayside Bistro"], "quayside-bistro")
add_entities(customers["Saltmarsh Deli"], "saltmarsh-deli")

assembler = DocumentAssembler()
assembler.assemble_document("recursive-customer-data.docx", "recursive-customer-data-report.docx", DataSourceInfo(root, "root"))

The following sample file is used in this example: recursive-customer-data.docx

The following sample file is used in this example: customers.json

Binary file (DOCX, 8 KB)

Download full output

Accessing a Dict by Key

A dict passed to the engine also has an indexer, so <<[root["CustomerName"]]>> reads a value by its key, as in the template above. Use it for a key that is not an identifier, such as <<[row["First Name"]]>>. The indexer returns the value as object, however, which the engine cannot index again or read members of. The indexer chain of the .NET template, root.Entities["Account"]["Login"], fails in Python with:

Can not resolve an indexer on type 'System.Object'.

Write such a chain with member access instead, root.Entities.Account.Login, as the template above does.

When the Engine Reads the Data

Python data is read eagerly. DataSourceInfo(root, "root") copies the whole structure into .NET objects when it is created, so:

  • The data must be complete before DataSourceInfo is created. A change made to a dict afterwards does not reach the report.
  • A value cannot be computed on demand. The engine reads only dicts, lists, tuples and sets, and the values str, int, float, decimal.Decimal, bool, None, datetime, date, time and bytes. Another Python object, such as an instance of a class with properties, raises a TypeError, and the __getitem__ of a dict subclass is not called.
  • A list cannot contain itself: that raises a ValueError. A dict can be referred to from any number of places, itself included.

To keep a large report fast, load only the data its template reads before you call assemble_document.

Close
Loading

Analyzing your prompt, please hold on...

An error occurred while retrieving the results. Please refresh the page and try again.