Skip to main content

A tool for creating HTML documents programmatically with Python

Project description

Phractal

Phractal is a tool for creating documents programmatically with Python, powered by Pydantic and Jinja2.

Phractal solves the complexity that can arise when templating complex documents in Python by using nesting. Hence the name; a Phractal document is a fractal arrangement of simple components nested inside one another. Phractal is inspired by React.js's component-driven development style, with the additional utility of Pydantic-enforced type-checking before the page is rendered, making debugging a breeze!

In addition to building your own pages from the ground up, any package that produces HTML output can be integrated into your Phractal documents. For example, Plotly graphs can be inserted into your components with just a few lines of code.

Phractal was originally designed to automatically generate bespoke templated statistical reports in the style of pandas-profiling. This is just the tip of the iceberg, though: anywhere you need to generate a document programmatically, Phractal is useful. Try creating invoices with it!


Key Features

There are two main tools that Practal makes available for document building:

phractal.Phraction

phractal.ValidatedCachedProperty

In the simple examples below, Phractal is barely more efficient than basic Jinja templating. The more complex your documents become, the more Phractal can help.

phractal.Phraction

The base unit of construction of a Phractal document - analagous to React.Component.

  • Renders a supplied template as an HTML string, automatically inserting its field values into the template where they are referenced.
  • Performs Pydantic-based field validation/type-checking.

Args:

  • template: A Jinja2-formatted string defining the HTML structure of the Phraction.

Methods:

Example - phraction useage:

from phractal import Phraction

# A Phraction to render a simple "hello" message
class HelloPara(Phraction):
    template = "<p>Hello {{ name }}</p>"
    name: str 

# Creating an instance of the Phraction
hello_para = HelloPara(name="Murderbot")

# Practions can be rendered to HTML strings using their __str__ method.
print(hello_para)

# Alternatively we can save to file.
# Adding HTML boilerplate here is a good idea.
# hello_para.with_boilerplate().save("./hello_para.html")

Output:

<p>Hello Murderbot</p>

Example - with_boilerplate useage:

from phractal import Phraction

class MyDoc(Phraction):
    template="<p>{{ msg }}</p>"
    msg: str

my_doc = MyDoc(msg="Hi!")

# Note that the returned object is an instance subclassed from the "Phraction" class.
print(isinstance(my_doc.with_boilerplate(), Phraction))
print(my_doc.with_boilerplate())

# We can save with boilerplate like so.
my_doc.with_boilerplate().save("./my_doc.html")

Output:

True

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Phractal Document</title>
  </head>
  <body>
	<p>Hi!</p>
  </body>
</html>


phractal.ValidatedCachedProperty

A decorator that turns a method on a class into a property which is cached and validated at time of first access, and inserted into the template where its name is referenced. Note that this can be used to nest Phractions!

  • Validation is based on return type hints (type hints are compulsory).
  • Supports Pydantic style parameterized generics. (Ex: list[int] matches [1, 2, 3] but not ["a", "b","c"])
  • Arbitrary type hinting is allowed. Ex: def foo(self) -> list[MyCustomClass]:
  • Value is calculated and cached at first access.
  • phractal.ValidatedCachedProperty is subclassed from the inbuilt functools.cached_property.
  • To avoid caching and validation, use the built-in @property decorator.
  • To skip validation but still cache the result, we recommend using the typing.Any hint. Ex: def foo(self) -> Any:

Example 1:

from phractal import Phraction, ValidatedCachedProperty

# A Phraction to display calculated total prices with tax
class TaxBox(Phraction):
    template = '''
        <p>
            Gross: {{ "$%.2f"|format(gross_amount) }}
            Total (incl. tax): {{ "$%.2f"|format(total_amount) }}
        </p>
    '''
    gross_amount: float

    @ValidatedCachedProperty
    def total_amount(self):
        return self.gross_amount*1.1

# Creating an instance of the Phraction
my_taxbox = TaxBox(gross_amount=1000)

# Rendering to the console as an HTML string using __str__
print(my_taxbox)

Output:

<p>
    Gross: $1000.00
    Total (incl. tax): $1100.00
</p>

Example 2 (nesting):

from phractal import Phraction, ValidatedCachedProperty

# Phraction to render a simple "hello" message as an H1 heading
class HelloHeading(Phraction):
    template = "<h1>Hello {{ name }}</h1>"
    name: str 

# A Phraction to render a list item
class ClientItem(Phraction):
    template = "<li>{{ client_name }}</li>"
    client_name: str

# A Phraction to contain the heading and the list items as nested components
class HelloDiv(Phraction):
    template = '''
        <div>
            {{ hello_para }}
            Here's a list of clients:
            <ol>
                {% for client_item in clients %}
                    {{ client_item }}
                {% endfor %}
            </ol>
        </div>
    '''

    name: str
    client_names: list[str]
    
    # Validated and cached (calculated at access-time)
    @ValidatedCachedProperty
    def hello_para(self) -> HelloPara: 
        return HelloPara(name=self.name)
    
    # Neither validated nor cached.
    @property
    def clients(self):
        return [
            ClientItem(
                client_name=client_name
            ) for client_name in self.client_names
        ]

# Creating an instance of the top-level/parent Phraction
#   (No need to create instances for the nested Phractions;
#   the parent Phraction does this for us when it is rendered)
my_div = HelloDiv(
    name="Murderbot",
    client_names=[
        "Dr. Mensah",
        "Dr. Arada",
        "Dr. Ratthi",
        "Dr. Bharadwaj"
    ]
)

# Rendering to the console with __str__
print(my_div)

Output:

<div>
    <h1>Hello Murderbot</h1>
    <p>Here's a list of clients:</p>
    <ol>
        <li>Dr. Mensah</li>
        <li>Dr. Arada</li>
        <li>Dr. Ratthi</li>
        <li>Dr. Bharadwaj</li>
    </ol>
</div>

Incorporating Other Packages

To incorporate output from other packages that generate document assets, simply incorporate these outputs into your Phractions using @Property or @ValidatedCachedProperty.

Example - Plotly:

Here we incorporate a Plotly Gauge Plot.

from phractal import Phraction, ValidatedCachedProperty
from pydantic import Field
import plotly.graph_objects as go
import plotly.offline

# A Phraction that renders a gauge plot
class GaugePlot(Phraction):
    template = """
        <div style="width: 700px; height: 400px;">
            {{ gauge }}
        </div>
    """
        
    label: str
    numerator: int = Field(ge=0)

    # This works because plotly.offline.plot returns the plot as an HTML string 
    #   when the output_type='div' kwarg is supplied.
    @ValidatedCachedProperty
    def gauge(self) -> str:
        fig = go.Figure(go.Indicator(
            mode = "gauge+number",
            value = self.numerator/100,
            number = { "valueformat": "%" },
            domain = {'x': [0, 1], 'y': [0, 1]},
            title = {'text': self.label},
            gauge = {
                'axis': {'range': [None, 1]},
            },
        ))
        return plotly.offline.plot(
            fig, 
            include_plotlyjs=False, 
            output_type='div'
        )

# A parent Phraction to combine three GaugePlot Phractions under a heading
class AcademicPerformance(Phraction):
    # Plotly wants us to include plotly-latest.min.js to render the plot
    #   The best place for both of this is here in the top-level Phraction.
    template = """
    <script src="https://cdn.plot.ly/plotly-latest.min.js"></script> 
    <h1>Academic Performance</h1>
    {% for plot in plots %}
        {{ plot }}
    {% endfor %}  
    """
    
    grades: list[tuple]

    # Nesting those gauge plots
    @ValidatedCachedProperty
    def plots(self) -> list:
        return [
            GaugePlot(
                label=label, 
                numerator=numerator
            ) for (label, numerator) in self.grades
        ]

# Data to feed to the instance
grades = [
    ("English", 82), 
    ("Art", 64), 
    ("History", 79)
]

# Creating the instance
academic_performance = AcademicPerformance(grades=grades)

# The output here is a bit longer
# Let's save it to file instead of printing to console
academic_performance.with_boilerplate(bootstrap=True).save("./test.html")

Output:

A series of gauge graphs rendered in an HTML Document

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

Phractal-1.0.1.tar.gz (10.2 kB view hashes)

Uploaded Source

Built Distribution

Phractal-1.0.1-py3-none-any.whl (12.6 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page