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…
Code used to render the above
For a full breakdown of this code, see the user guide.
@bloqs_provider
def server_time() -> pybloqs.BaseBlock:
return pybloqs.Block(
datetime.now().isoformat(),
title="The time is now",
title_level=3,
)
pybloqs.server.serve_block(
pybloqs.Block(
[
"Use the drop-down to see a feature.",
Select(
{
"Polling block": bloqs_provider(
lambda: pybloqs.Block(
server_time.poll("1s"),
title="Polling block",
title_level=2,
)
),
"Tabs": bloqs_provider(
lambda: Tabs(
{
"Random Data": random_data,
"Server time": server_time,
},
title="Tabs",
title_level=2,
),
),
"On-demand loading": bloqs_provider(
lambda: pybloqs.VStack(
[
server_time,
pybloqs.Block(
"Please scroll...",
height="500px",
),
server_time,
],
title="On-demand loading",
title_level=2,
)
),
"Refresh": bloqs_provider(
lambda: Refresh(
server_time,
title="Refresh",
title_level=2,
)
),
},
),
],
title="pybloqs.server in action",
title_level=1,
),
"/kitchen_sink",
)
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
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.
BloqsProvider
s 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.