Packages

A native Elixir DataFrame API for Apache Spark, over Spark Connect.

Current section

Files

Jump to
latu mix.exs
Raw

mix.exs

defmodule Latu.MixProject do
use Mix.Project
@version "0.1.0"
@source_url "https://github.com/zero-one-group/latu"
def project do
[
app: :latu,
version: @version,
elixir: "~> 1.20",
start_permanent: Mix.env() == :prod,
# Enforced here rather than per-run with `--force --warnings-as-errors`: any file the
# incremental compiler touches is held to it, and the checks stop paying to recompile
# the ~7k generated proto lines every time. See docs/decisions.md.
elixirc_options: elixirc_options(Mix.env()),
# test/support is loaded by test_helper.exs, not by the test loader.
test_ignore_filters: [&String.starts_with?(&1, "test/support/")],
deps: deps(),
aliases: aliases(),
description: "A native Elixir DataFrame API for Apache Spark, over Spark Connect.",
package: package(),
docs: docs(),
source_url: @source_url
]
end
defp package do
[
licenses: ["Apache-2.0"],
links: %{
"GitHub" => @source_url,
"Changelog" => "https://hexdocs.pm/latu/changelog.html",
"Spark Connect" => "https://spark.apache.org/docs/latest/spark-connect-overview.html"
},
# Hex's defaults plus `usage-rules.md`, which tooling reads from the dep. `priv/` carries
# the harvested docs and groups the compile refuses without, and the vendored protos.
# `assets/` is not shipped: nothing in the package reads it, and the README's images are
# absolute URLs.
files: ~w(lib priv .formatter.exs mix.exs README.md LICENSE CHANGELOG.md usage-rules.md)
]
end
defp docs do
[
name: "Latu",
main: "readme",
# The README opens with the lockup image rather than an `# Latu` heading, so ExDoc is told
# the page title; the tile is the sidebar logo and the favicon. See assets/README.md.
logo: "assets/latu-avatar.svg",
favicon: "assets/latu-avatar.svg",
assets: %{"assets" => "assets"},
extras: [
{"README.md", title: "Latu"},
"CHANGELOG.md",
"docs/guides/quick-start.md",
"docs/guides/cookbook.md",
"docs/guides/from-pyspark.md",
"docs/guides/from-explorer.md",
"docs/cheatsheet.cheatmd",
"usage-rules.md",
"docs/deviations.md",
"CONTRIBUTING.md"
],
groups_for_extras: [
Guides: [
"docs/guides/quick-start.md",
"docs/guides/cookbook.md",
"docs/guides/from-pyspark.md",
"docs/guides/from-explorer.md"
],
Reference: ["docs/cheatsheet.cheatmd", "usage-rules.md", "docs/deviations.md"]
],
# `mix docs` runs with warnings as errors, so the exemptions are named here. It warns on a
# reference to something `@moduledoc false`, and Latu has three kinds it will always have:
# the generated protocol modules, the transport, and the internals `docs/deviations.md`
# documents *because* they are internals. Anything else is a real defect and fails the
# build.
#
# Two knobs, not interchangeable (ExDoc's source): prose references consult
# `skip_code_autolink_to` with the **term**; typespecs consult only
# `skip_undefined_reference_warnings_on`, by page, module or file — never the term — so a
# spec naming a generated protocol type can only be silenced per *module*. No file appears
# in either list: every relative link in the docs resolves to an extra, and
# `test/latu/docs_test.exs` checks that more strictly than ExDoc does.
skip_code_autolink_to: fn term ->
String.starts_with?(term, "Latu.Protocol.") or
String.starts_with?(term, "Latu.Client") or
String.starts_with?(term, "Latu.Plan.Inspect") or
String.starts_with?(term, "Latu.Result.Literal") or
String.starts_with?(term, "Latu.Functions.Registry") or
term == "Latu.Session.pin/2"
end,
# `Latu.Plan`'s types *are* the protocol's — `@type relation :: Proto.Relation.t()` — and
# `Latu.Result.decode/2` takes `[Latu.Client.batch()]`. Both name modules that are
# `@moduledoc false` on purpose, so ExDoc has no page to link and says so once per spec.
#
# What this costs, stated plainly: an undefined *type* in a spec on these two modules
# would not be reported here. Two other checks cover it — the compiler, since `mix.exs`
# sets `warnings_as_errors` for `lib/` and an unknown type in a `@spec` is a compile
# warning; and `test/latu/examples_test.exs`, which resolves every `Mod.fun/arity` in
# every docstring against real exports, including these two modules' prose.
skip_undefined_reference_warnings_on: fn id ->
id in ["Latu.Plan", "Latu.Result"]
end,
warnings_as_errors: true,
groups_for_modules: [
Verbs: [Latu],
Expressions: [Latu.Column, Latu.Functions, Latu.Window],
Session: [Latu.Session, Latu.Retry, Latu.Catalog],
# Livebook rendering has no module of its own: `lib/latu/kino.ex` is a `Kino.Render`
# impl for `Latu.DataFrame` behind an optional dep, and nothing else.
"Results and observability": [
Latu.Result,
Latu.Error,
Latu.ExecutionInfo,
Latu.Progress,
Latu.Telemetry
],
# Inert data a verb takes or returns, plus the plan layer itself. You read these; you
# rarely name one.
"Plans and carriers": [
Latu.Plan,
Latu.DataFrame,
Latu.GroupedData,
Latu.MergeInto,
Latu.CaseWhen,
Latu.Subquery
]
],
# `Latu.Functions` is 671 name/arity pairs, and ExDoc hands this function the `:module`,
# `:name` and `:arity` of every entry -- so one config function groups the whole reference
# by Spark's own categories, where `@doc group:` would have meant fifty attributes inside
# a generated module. See `Latu.Functions.Groups`.
default_group_for_doc: fn metadata ->
if metadata[:module] == Latu.Functions, do: Latu.Functions.Groups.of(metadata[:name])
end,
# The release tag. Every "view source" link on hexdocs points at it, so it moves only when
# the version does.
source_ref: "v#{@version}",
source_url: @source_url
]
end
defp elixirc_options(:prod), do: []
defp elixirc_options(_env), do: [warnings_as_errors: true]
# `check` and `check.all` run `mix test`, which refuses to run outside the test env.
def cli do
[preferred_envs: [check: :test, "check.all": :test]]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:decimal, "~> 2.0 or ~> 3.0"},
{:explorer, "~> 0.12"},
{:grpc, "~> 1.0"},
{:gun, "~> 2.4"},
{:kino, "~> 0.12", optional: true},
{:protobuf, "~> 0.17"},
{:telemetry, "~> 1.0"},
# 0.36 is the floor: `:default_group_for_doc` and `:group` doc metadata land there.
{:ex_doc, "~> 0.36", only: :dev, runtime: false}
]
end
defp aliases do
[
"proto.generate": &generate_proto/1,
fixtures: &generate_fixtures/1,
# `--warnings-as-errors` because `mix test` compiles test files itself, outside
# `elixirc_options`; without it a warning in test/ is the one kind CI lets through.
check: [
"format --check-formatted",
"compile",
"test --warnings-as-errors"
],
"check.all": [
"format --check-formatted",
"compile",
"test --include integration --warnings-as-errors"
]
]
end
# The integration suite's data files. Generated rather than committed — dev/README.md.
defp generate_fixtures(_) do
# Absolute: System.cmd/3 sends a *relative* command to :os.find_executable/1, which
# searches PATH rather than the cwd, so a relative path here fails with :enoent.
python = Path.expand(Path.join(~w(dev .venv bin python)))
if not File.exists?(python) do
Mix.raise("#{python} not found; create it as dev/README.md's one-time setup says")
end
{output, status} = System.cmd(python, ["dev/make_data_fixtures.py"], stderr_to_stdout: true)
if status != 0, do: Mix.raise("make_data_fixtures.py failed:\n#{output}")
Mix.shell().info("Wrote the integration suite's data files to fixtures/")
end
defp generate_proto(_) do
output_dir = "lib/latu/protocol/generated"
tmp_dir = Path.join(System.tmp_dir!(), "latu-proto-#{System.unique_integer([:positive])}")
File.rm_rf!(output_dir)
File.mkdir_p!(output_dir)
File.mkdir_p!(tmp_dir)
try do
proto_files = Path.wildcard("priv/proto/spark/connect/*.proto") |> Enum.sort()
opts = "plugins=grpc,package_prefix=latu.protocol"
args = ["-I", "priv/proto", "--elixir_out=#{opts}:#{tmp_dir}" | proto_files]
{output, status} = System.cmd("protoc", args, stderr_to_stdout: true)
if status != 0, do: Mix.raise("protoc failed:\n#{output}")
# protoc-gen-elixir nests output twice over; flatten it. See dev/README.md.
sources = Path.wildcard(Path.join(tmp_dir, "**/*.pb.ex")) |> Enum.sort()
if sources == [], do: Mix.raise("protoc produced no .pb.ex files under #{tmp_dir}")
generated =
for source <- sources do
dest = Path.join(output_dir, Path.basename(source))
if File.exists?(dest), do: Mix.raise("name collision on #{Path.basename(source)}")
File.cp!(source, dest)
dest
end
Mix.shell().info("Generated #{length(generated)} modules:\n#{Enum.join(generated, "\n")}")
after
File.rm_rf!(tmp_dir)
end
end
end