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 details)

Uploaded Source

Built Distribution

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

Uploaded Python 3

File details

Details for the file Phractal-1.0.1.tar.gz.

File metadata

  • Download URL: Phractal-1.0.1.tar.gz
  • Upload date:
  • Size: 10.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.1 CPython/3.11.2

File hashes

Hashes for Phractal-1.0.1.tar.gz
Algorithm Hash digest
SHA256 809bb3a02461af01d86849428d385c5a42d5b77b7c076a184df9659f4c55081c
MD5 ad847a3588d5de7d734f38252fa77c30
BLAKE2b-256 c4ec0895ecf1b35fa8fb205c647d3b8414ed3dbc027892bd33c278ba9d689990

See more details on using hashes here.

File details

Details for the file Phractal-1.0.1-py3-none-any.whl.

File metadata

  • Download URL: Phractal-1.0.1-py3-none-any.whl
  • Upload date:
  • Size: 12.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.1 CPython/3.11.2

File hashes

Hashes for Phractal-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 a21085d6b75fd5e455fd10b9d6b0fd69f5d9de6d60f9fdc3ef3a784d474f29f8
MD5 27c8743af12008077ad4afa2988a1543
BLAKE2b-256 f3fb5bab8ef5efecdc74757776c2127864300aaa6e6260de245718c55fda89a6

See more details on using hashes here.

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