What’s new in Livebook v0.7
Mix.install([
{:kino, "~> 0.7.0"},
{:kino_db, "~> 0.2.0"},
{:explorer, "~> 0.3.1"},
{:req, "~> 0.3.1"},
{:postgrex, "~> 0.16.5"},
{:req_athena, "~> 0.1.1"}
])
Intro
This is an accompanying notebook to the Livebook v0.7 announcement blog post.
Secrets management
Secrets inside Code cells
We know we should not hardcode passwords, API tokens, and sensitive data in our code or notebook like this:
api_username = "postman"
api_password = "password"
Req.get!("https://postman-echo.com/basic-auth", auth: {api_username, api_password})
To deal with sensitive data, Livebook has a feature called Secrets.
You can add a secret using the Secrets menu on the sidebar.
Once you have created a secret, you can use it using the System.fetch_env!/1
, adding a “LB_” namespace to the name of the secret you created.
For example, let’s say you create a secret with the name API_USERNAME
. You can call that secret from your code like this:
api_username = System.fetch_env!("LB_API_USERNAME")
Livebook will automatically detect when your code is trying to use a secret that was not setted up before.
For example, if you evaluate the cell below and the API_USERNAME
or API_PASSWORD
secret were not setted up, Livebook you ask you to create that secret.
You can create those secrets with the following value and evaluate the cell below again:
-
API_USERNAME
: postman -
API_PASSWORD
: password
api_username = System.fetch_env!("LB_API_USERNAME")
api_password = System.fetch_env!("LB_API_PASSWORD")
Req.get!("https://postman-echo.com/basic-auth", auth: {api_username, api_password})
Secrets inside Database Connection Smart cell
The new Secrets feature is integrated with Database Connection Smart cells.
So, when you’re creating a connection to PostgreSQL or Amazon Athena, Livebook will give you the option to use a Secret for the database password.
To see how that works, evaluate the following cell and click in the Password field in the form below:
opts = [
hostname: "localhost",
port: 5432,
username: "postgres",
password: "",
database: ""
]
{:ok, conn} = Kino.start_child({Postgrex, opts})
Or, evaluate the following cell and click in the Secret Access Key field in the form below:
Visual representations of the running system
An example of visualizing two processes exchanging a few messages
Let’s say you want to visualize how that piece of code is exchanging messages when it runs:
parent = self()
child =
spawn(fn ->
receive do
:ping -> send(parent, :pong)
end
end)
send(child, :ping)
receive do
:pong -> :ponged!
end
All you need to do is wrap your code with Kino.Process.render_seq_trace/2
:
Kino.Process.render_seq_trace(fn ->
parent = self()
child =
spawn(fn ->
receive do
:ping -> send(parent, :pong)
end
end)
send(child, :ping)
receive do
:pong -> :ponged!
end
end)
An example of visualizing more than two processes exchanging messages
Kino.Process.render_seq_trace(fn ->
1..4
|> Task.async_stream(fn i ->
i
end)
|> Stream.run()
end)
Visualize supervision trees
To visualize a supervision tree, call Kino.Process.render_sup_tree/2
with the supervisor’s PID:
{:ok, supervisor_pid} =
Supervisor.start_link(
[
{Task, fn -> Process.sleep(:infinity) end},
{Agent, fn -> [] end}
],
strategy: :one_for_one
)
Kino.Process.render_sup_tree(supervisor_pid)
Automatic detection of a supervisor through its PID
Livebook will also automatically show you a supervision tree if the last line in your code cell is the PID of a supervisor:
{:ok, supervisor_pid} =
Supervisor.start_link(
[
{Task, fn -> Process.sleep(:infinity) end},
{Agent, fn -> [] end}
],
strategy: :one_for_one
)
supervisor_pid
Automatic detection of an application tree through its atom
Given the name of an application as an atom, Livebook will automatically render the application tree:
:kino
Interactive user interface to visualize and edit Elixir pipelines
An example of using Livebook’s dgb interactive user interface with a simple pipeline
"Elixir is cool!"
|> String.trim_trailing("!")
|> String.split()
|> List.first()
|> dbg()
An example of using Livebook’s dgb interactive user interface with a pipeline that is doing some data analysis
alias Explorer.DataFrame
alias Explorer.Series
Explorer.Datasets.iris()
|> DataFrame.filter_with(&Series.equal(&1["species"], "Iris-virginica"))
|> DataFrame.select(["sepal_length", "sepal_width", "petal_length", "petal_width"])
|> DataFrame.arrange(desc: "sepal_width")
|> DataFrame.rename(["sepal length", "sepal width", "petal lenght", "petal width"])
|> DataFrame.to_rows()
|> dbg()
Organize your cell output with layouts
Tabs layout
data = [
%{id: 1, name: "Elixir", website: "https://elixir-lang.org"},
%{id: 2, name: "Erlang", website: "https://www.erlang.org"}
]
Kino.Layout.tabs(
Table: Kino.DataTable.new(data),
Raw: data
)
Grid layout
urls = [
"https://images.unsplash.com/photo-1603203040743-24aced6793b4?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=580&h=580&q=80",
"https://images.unsplash.com/photo-1578339850459-76b0ac239aa2?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=580&h=580&q=80",
"https://images.unsplash.com/photo-1633479397973-4e69efa75df2?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=580&h=580&q=80",
"https://images.unsplash.com/photo-1597838816882-4435b1977fbe?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=580&h=580&q=80",
"https://images.unsplash.com/photo-1629778712393-4f316eee143e?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=580&h=580&q=80",
"https://images.unsplash.com/photo-1638667168629-58c2516fbd22?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=580&h=580&q=80"
]
images =
for {url, i} <- Enum.with_index(urls, 1) do
image = Kino.Markdown.new("![](#{url})")
label = Kino.Markdown.new("**Image #{i}**")
Kino.Layout.grid([image, label], boxed: true)
end
Kino.Layout.grid(images, columns: 3)