Packages
livebook
0.19.7
0.19.8
0.19.7
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.6
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.3
0.17.2
0.17.1
0.17.0
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.14.0-rc.1
0.14.0-rc.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.2
0.1.1
0.1.0
Automate code & data workflows with interactive notebooks
Current section
Files
Jump to
Current section
Files
lib/livebook/runtime/dependencies.ex
defmodule Livebook.Runtime.Dependencies do
@doc """
Finds or adds a `Mix.install/2` call to `code` and modifies it to
include the given Mix deps.
A config may be given, in which case it is added or merged into the
`Mix.install/2` `:config` option.
"""
@spec insert_mix_dependencies(String.t(), list(Livebook.Runtime.dependency())) ::
{:ok, String.t()} | {:error, String.t()}
def insert_mix_dependencies(code, dependencies) do
deps = Enum.map(dependencies, & &1.dep)
config = Enum.reduce(dependencies, [], &Livebook.Utils.keyword_deep_merge(&2, &1.config))
add_mix_deps(code, deps, config)
end
defp add_mix_deps(code, deps, config) do
with {:ok, ast, comments} <- string_to_quoted_with_comments(code),
{:ok, ast} <- insert_deps(ast, deps, config),
do: {:ok, format(ast, comments)}
end
defp string_to_quoted_with_comments(code) do
try do
to_quoted_opts = [
literal_encoder: &{:ok, {:__block__, &2, [&1]}},
token_metadata: true,
unescape: false
]
{ast, comments} = Code.string_to_quoted_with_comments!(code, to_quoted_opts)
{:ok, ast, comments}
rescue
error -> {:error, Exception.format(:error, error)}
end
end
defp insert_deps(ast, deps, config) do
with :error <- update_install(ast, deps, config) do
dep_nodes = Enum.map(deps, &dep_node/1)
install_args =
[{:__block__, [newlines: 1], [dep_nodes]}] ++
case config do
[] -> []
config -> [[config: Macro.escape(config)]]
end
install_node =
{{:., [], [{:__aliases__, [], [:Mix]}, :install]}, [newlines: 1], install_args}
{:ok, prepend_node(ast, install_node)}
end
end
defp format(ast, comments) do
ast
|> Code.quoted_to_algebra(comments: comments)
|> Inspect.Algebra.format(90)
|> IO.iodata_to_binary()
end
defp prepend_node({:__block__, meta, nodes}, node) do
{:__block__, meta, [node | nodes]}
end
defp prepend_node(ast, node) do
{:__block__, [], [node, ast]}
end
defp update_install(
{{:., _, [{:__aliases__, _, [:Mix]}, :install]} = target, meta1,
[{:__block__, meta2, [dep_nodes]} | args]},
deps,
config
) do
args =
case {args, config} do
{args, []} ->
args
{[], config} ->
[[config: Macro.escape(config)]]
{[opts | other_args], config} ->
opts = map_maybe_block(opts, &update_config_in_opts(&1, config))
[opts | other_args]
end
meta1 = Keyword.put(meta1, :newlines, 1)
new_dep_nodes = for dep <- deps, not has_dep?(dep_nodes, dep), do: dep_node(dep)
{:ok, {target, meta1, [{:__block__, meta2, [dep_nodes ++ new_dep_nodes]} | args]}}
end
defp update_install({:__block__, meta, nodes}, deps, config) do
{nodes, found} =
Enum.map_reduce(nodes, _found = false, fn
node, false ->
case update_install(node, deps, config) do
{:ok, node} -> {node, true}
_ -> {node, false}
end
node, true ->
{node, true}
end)
if found do
{:ok, {:__block__, meta, nodes}}
else
:error
end
end
defp update_install(_node, _deps, _config), do: :error
defp has_dep?(deps, dep) do
name = elem(dep, 0)
Enum.any?(deps, fn
{:__block__, _, [{{:__block__, _, [^name]}, _}]} -> true
{:{}, _, [{:__block__, _, [^name]} | _]} -> true
_ -> false
end)
end
defp dep_node(dep), do: {:__block__, [], [Macro.escape(dep)]}
defp update_config_in_opts(
[{{:__block__, meta1, [:config]}, {:__block__, meta2, [current_config]}} | tail],
config
) do
config = append_config(current_config, config)
[{{:__block__, meta1, [:config]}, {:__block__, meta2, [config]}} | tail]
end
defp update_config_in_opts([head | tail], config),
do: [head | update_config_in_opts(tail, config)]
defp update_config_in_opts([], config), do: [config: Macro.escape(config)]
defp append_config(current_config, config) do
# Note: the current config has literals wrapped in a :__block__,
# while the new one doesn't.
# If the given key is already in the config, we ignore it
existing_keys = for {{:__block__, _meta, [key]}, _value} <- current_config, do: key
config = Keyword.drop(config, existing_keys)
# We need to wrap literals in a block, as in the rest of the AST
to_quoted_opts = [literal_encoder: &{:ok, {:__block__, &2, [&1]}}]
{:__block__, _, [items]} =
config
|> Macro.to_string()
|> Code.string_to_quoted!(to_quoted_opts)
current_config ++ items
end
defp map_maybe_block({:__block__, meta, [value]}, fun), do: {:__block__, meta, [fun.(value)]}
defp map_maybe_block(value, fun), do: fun.(value)
@doc """
Parses a plain Elixir term from its string representation.
## Examples
iex> Livebook.Runtime.Dependencies.parse_term(~s|{:req, "~> 0.5.0"}|)
{:ok, {:req, "~> 0.5.0"}}
iex> Livebook.Runtime.Dependencies.parse_term(~s|{:req, "~> 0.5.0", runtime: false, meta: "data"}|)
{:ok, {:req, "~> 0.5.0", runtime: false, meta: "data"}}
iex> Livebook.Runtime.Dependencies.parse_term(~s|%{name: "Jake", numbers: [1, 2, 3.4]}|)
{:ok, %{name: "Jake", numbers: [1, 2, 3.4]}}
iex> Livebook.Runtime.Dependencies.parse_term(~s|{:call, Enum.count([])}|)
:error
iex> Livebook.Runtime.Dependencies.parse_term(~s|Enum|)
:error
"""
@spec parse_term(String.t()) :: {:ok, term()} | :error
def parse_term(string) do
case Code.string_to_quoted(string) do
{:ok, ast} -> unescape_term(ast)
{:error, _} -> :error
end
end
defp unescape_term(node)
when is_atom(node)
when is_integer(node)
when is_float(node)
when is_binary(node),
do: {:ok, node}
defp unescape_term([]), do: {:ok, []}
defp unescape_term([head | tail]) do
with {:ok, head} <- unescape_term(head),
{:ok, tail} <- unescape_term(tail),
do: {:ok, [head | tail]}
end
defp unescape_term({left, right}) do
with {:ok, left} <- unescape_term(left),
{:ok, right} <- unescape_term(right),
do: {:ok, {left, right}}
end
defp unescape_term({:{}, _, nodes}) do
with {:ok, terms} <- unescape_term(nodes), do: {:ok, List.to_tuple(terms)}
end
defp unescape_term({:%{}, _, nodes}) do
with {:ok, terms} <- unescape_term(nodes), do: {:ok, Map.new(terms)}
end
defp unescape_term(_node), do: :error
@doc """
Searches packages within the given list.
"""
@spec search_packages_in_list(list(Livebook.Runtime.package_details()), String.t()) ::
{:ok, list(Livebook.Runtime.package_details())}
def search_packages_in_list(packages, search) do
matching_packages = Enum.filter(packages, &String.starts_with?(&1.name, search))
{:ok, matching_packages}
end
@doc """
Searches for packages on Hex.
## Options
* `:api_url` - the base URL for Hex API requests. Optional
"""
@spec search_packages_on_hex(String.t(), keyword()) ::
{:ok, list(Livebook.Runtime.package_details())} | {:error, String.t()}
def search_packages_on_hex(search, opts \\ [])
def search_packages_on_hex("", _opts), do: {:ok, []}
def search_packages_on_hex(search, opts) do
api_url = opts[:api_url] || "https://hex.pm/api"
req = Req.new(base_url: api_url) |> Livebook.Utils.req_attach_defaults()
params = [search: "name:#{search}*", sort: "recent_downloads"]
case Req.get(req, url: "/packages", params: params) do
{:ok, %{status: 200} = resp} ->
packages =
resp.body
|> Enum.map(&parse_package/1)
|> reorder_packages(search)
{:ok, packages}
{:ok, %{status: status}} ->
{:error, "unexpected response, HTTP status #{status}"}
{:error, exception} ->
{:error, "failed to make a request, reason: #{Exception.message(exception)}}"}
end
end
defp parse_package(package) do
{:ok, dep} = parse_term(package["configs"]["mix.exs"])
%{
name: package["name"],
version: package["latest_stable_version"] || package["latest_version"],
description: package["meta"]["description"],
url: package["html_url"],
dependency: %{dep: dep, config: []}
}
end
defp reorder_packages(packages, search) do
case Enum.find_index(packages, &(&1.name == search)) do
nil ->
packages
exact_idx ->
{package, packages} = List.pop_at(packages, exact_idx)
[package | packages]
end
end
end