PyBloqs Server#

As mentioned elsewhere, PyBloqs is a library for creating components that can be easily slotted together to display data and create reports. These reports can be rendered to HTML or PDF or other formats and the components are called “blocks” or “PyBloqs”.

On the other hand, pybloqs.server is a framework for serving dynamic and interactive PyBloqs.

With pybloqs.server you can create dashboards, lightweight reports with heavy computation, and reports containing up-to-the-second data.

Attention

Spinning up a pybloqs server just for you. Please wait…

User Guide#

Serving PyBloqs#

Let us consider a simple block. I’ve defined the Pandas dataframes planets_dataframe and chebyshev_dataframe elsewhere.

sample_block = pybloqs.Block(
    [
        "Here are the planets and some Chebyshev polynomials:",
        pybloqs.HStack(
            [
                planets_dataframe,
                chebyshev_dataframe.plot(height=200).update_layout(
                    margin={"l": 0, "r": 0, "t": 0, "b": 0},
                ),
            ]
        ),
    ],
    title="My report",
    title_level=2,
)

Using pybloqs.server we can serve this PyBloq from a flask server:

import pybloqs.server

pybloqs.server.serve_block(sample_block, "/sample_block")

pybloqs.server.app.run()

This renders as the following.

PyBloqs server provides you with the Flask application object it is using to serve the HTML at pybloqs.server.app. You can then either a WSGI server, or run it locally by calling app.run().

The serve_block function can be called multiple times to serve multiple blocks on different endpoints. You can also set the page title and favicon of the resulting page.

Providers#

So far we have just served a static block. We might as well have run

sample_block.save('sample_block.html')

and

$ python -m http.server

The power of pybloqs.server comes from its on-the-fly block generation and rendering. This is powered by block providers.

At its simplest, a block provider is simply a function that returns a block, decorated with the @bloqs_provider decorator.

@bloqs_provider
def server_time() -> pybloqs.BaseBlock:
    return pybloqs.Block(
        datetime.now().isoformat(),
        title="The time is now",
        title_level=2,
    )

Calling this function naturally returns the block which we can then serve

pybloqs.server.serve_block(server_time(), "/server_time_frozen")

However, if we don’t call the provider at the point that we register the block, then pybloqs.server will run the provider each time the endpoint is hit:

pybloqs.server.serve_block(server_time, "/server_time")

Note that the first report above shows the time that the server started up while the second shows the time the iframe was loaded.

Using providers in this way, you can build a report in PyBloqs and have it render with up-to-the-moment data. You can even have only part of the report be dynamic and the rest static:

mixed_report = pybloqs.HStack(
    [
        pybloqs.Block(
            planets_dataframe,
            title="Planets",
            title_level=2,
        ),
        # This is a bloqs provider!
        server_time,
    ]
)

pybloqs.server.serve_block(mixed_report, "/mixed_report")

Dynamic blocks are only served when they are needed. For example, in this report, the second current_time block will show the time that it scrolled into view which will be different from the first current_time block.

very_tall = pybloqs.VStack(
    [
        server_time,
        pybloqs.Block(
            "Please scroll...",
            height="500px",
        ),
        server_time,
    ]
)

pybloqs.server.serve_block(very_tall, "/very_tall")

Providers can have parameters, as they behave like normal functions.

@bloqs_provider
def greet(
    name: str,
) -> pybloqs.BaseBlock:
    return pybloqs.Block(
        f"Nice to meet you, {name}.",
        title="Hello",
        title_level=2,
    )


pybloqs.server.serve_block(
    pybloqs.HStack(
        [
            greet("Ada"),
            greet("Alan"),
            greet("Alonzo"),
        ]
    ),
    "/greet",
)

However, using the with_parameter or with_parameters function, you can add parameters while not actually evaluating the provider.

pybloqs.server.serve_block(
    pybloqs.HStack(
        [
            greet.with_parameter("Ada"),
            greet.with_parameter("Alan"),
            greet.with_parameter("Alonzo"),
        ]
    ),
    "/lazy_greet",
)

You should think about with_parameters much like functools.partial. It has the signature:

class BloqsProvider:
    def with_parameters(self, *args, **kwargs) -> "BloqsProvider": ...
    def __call__(self, *args, **kwargs) -> pybloqs.BaseBlock: ...

    with_parameter = with_parameters


Useful Blocks#

The module pybloqs.server.block provides a number of useful PyBloqs for use with a PyBloqs server. These blocks are unlikely to function as you expect unless the root block is served with pybloqs.server.

Tabs#

The Tab block dynamically loads the contents of each tab when it is displayed.

from pybloqs.server.block import Tabs

tabs = pybloqs.VStack(
    [
        "Here is a tab block:",
        Tabs(
            {
                "Random data": random_data,
                "Server time": server_time,
            }
        ),
    ]
)

pybloqs.server.serve_block(tabs, "/tabs")

Note that tabs takes a dictionary of tab labels to providers. You can also use a provider which takes a single string parameter:

tabs_with_provider = Tabs(
    options=["Ada", "Alan", "Alonzo"],
    provider=greet,
)

pybloqs.server.serve_block(
    tabs_with_provider,
    "/tabs_with_provider",
)

Selections#

The Select block exposes an identical API to the Tab block, but allows the user to choose the option by using a HTML selection box in the top right corner of the block.

from pybloqs.server.block import Select

select = Select(
    options=["Ada", "Alan", "Alonzo"],
    provider=greet,
)

pybloqs.server.serve_block(select, "/select")

Polling#

If you have a provider, you can easily construct a polling block that periodicially updates with the contents renderded by that provider.

polling_block: pybloqs.BaseBlock = server_time.poll("5s")
# or pybloqs.server.block.Poll(server_time, "5s")

pybloqs.server.serve_block(polling_block, "/poll")

Refreshing#

On the other hand if you only want to reload the contents when the user requests it manually, you can use a refreshing block..

from pybloqs.server.block import Refresh

refresh_block = Refresh(server_time)

pybloqs.server.serve_block(refresh_block, "/refresh")

How it Works#

PyBloqs server is built using HTMX with a standard python webserver backend, currently Flask.

HTMX aims to provide, among other things, a clean API to insert snippets of HTML within a document. PyBloqs fundamentally are each snippets of HTML, so it a natural fit.

The @bloqs_provider decorator wraps a function that generates a PyBloq in a new block type called a BloqsProvider. This block presents the get_fragment function which just returns the HTML content of the block, similarly to BaseBlock._write_block. The decorator then registers this function with the server at an endpoint given by the id of the block provider.

BloqsProviders also present as a BaseBlock themselves, but the content they render is (very similar to)

<div hx-trigger="revealed" hx-get="/{ID OF BLOQS PROVIDER}">
    Loading
</div>

This snippet instructs HTMX to make an AJAX request to /{ID OF BLOQS PROVIDER} and replace the entire <div/> with the contents of the response.

You can open the console of your browser’s development tools now, and see the HTTP requests being made in order to render the above examples. You can also look at the server source file directly.

Caveats#

Resources#

When used to render static HTML or images, PyBloqs has a mechanism to prevent resources (CSS and JavaScript) required by blocks from being embedded multiple times if multiple blocks require them.

PyBloqs server also attempts to not duplicate resources. If a request indicates that it hasn’t received a particular resource yet, then the provider’s get_fragment function will serve the resource next to the content. The fact that this resource has been sent is then stored in the hx-headers tag of the <body/> of the report. This instructs HTMX to send the data in the headers of every AJAX request it makes (which is how the provider knew that it hadn’t served the resource in the first place).

Parameters#

If you make a request to /{ID OF BLOQS PROVIDER}?some=parameters&args=here&args=and&args=there it will call the providing function with *args=('here', 'and', 'there'), **kwargs={'some': 'parameter'}. You can generate this link by calling with_parameter (or with_parameters) on a block provider. This will give you back a new provider which will render with the correct URL.

Loading…#

Because providing the block might take some time for the server, the stub contains a Loading... message so the user does not just see a white page. By default this is a small <canvas/> that shows the game of life playing out, but this can be customised.