├── .dockerignore ├── .formatter.exs ├── .github └── workflows │ └── release.yml ├── .gitignore ├── Dockerfile ├── README.md ├── apps ├── hany │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ │ ├── hany.ex │ │ └── hany │ │ │ ├── application.ex │ │ │ ├── repo.ex │ │ │ ├── repo │ │ │ ├── local.ex │ │ │ └── migrator.ex │ │ │ └── user.ex │ ├── mix.exs │ ├── priv │ │ └── repo │ │ │ ├── migrations │ │ │ ├── .formatter.exs │ │ │ └── 20230226154351_create_users.exs │ │ │ └── seeds.exs │ └── test │ │ ├── support │ │ └── data_case.ex │ │ └── test_helper.exs ├── hany_cluster │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ │ ├── hany_cluster.ex │ │ └── hany_cluster │ │ │ ├── application.ex │ │ │ ├── connector.ex │ │ │ ├── rpc.ex │ │ │ ├── strategy.ex │ │ │ └── strategy │ │ │ ├── gce.ex │ │ │ └── local_epmd.ex │ ├── mix.exs │ └── test │ │ ├── hany_cluster_test.exs │ │ └── test_helper.exs ├── hany_ml │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── lib │ │ ├── hany_ml.ex │ │ └── hany_ml │ │ │ ├── application.ex │ │ │ └── local.ex │ ├── mix.exs │ └── test │ │ └── test_helper.exs └── hany_web │ ├── .formatter.exs │ ├── .gitignore │ ├── README.md │ ├── assets │ ├── css │ │ ├── app.css │ │ └── phoenix.css │ ├── js │ │ └── app.js │ └── vendor │ │ └── topbar.js │ ├── lib │ ├── hany_web.ex │ └── hany_web │ │ ├── application.ex │ │ ├── controllers │ │ └── page_controller.ex │ │ ├── endpoint.ex │ │ ├── router.ex │ │ ├── telemetry.ex │ │ ├── templates │ │ ├── layout │ │ │ ├── app.html.heex │ │ │ ├── live.html.heex │ │ │ └── root.html.heex │ │ └── page │ │ │ └── index.html.heex │ │ └── views │ │ ├── error_helpers.ex │ │ ├── error_view.ex │ │ ├── layout_view.ex │ │ └── page_view.ex │ ├── mix.exs │ ├── priv │ └── static │ │ ├── favicon.ico │ │ ├── images │ │ └── phoenix.png │ │ └── robots.txt │ └── test │ ├── hany_web │ ├── controllers │ │ └── page_controller_test.exs │ └── views │ │ ├── error_view_test.exs │ │ ├── layout_view_test.exs │ │ └── page_view_test.exs │ ├── support │ └── conn_case.ex │ └── test_helper.exs ├── config ├── config.exs ├── dev.exs ├── prod.exs ├── runtime.exs └── test.exs ├── docker-compose.yml ├── mix.exs └── mix.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Temporary files, for example, from tests. 23 | /tmp 24 | 25 | 26 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["mix.exs", "config/*.exs"], 3 | subdirectories: ["apps/*"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: "release" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | inputs: 9 | 10 | jobs: 11 | build-and-push: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | release: ["hany", "hany_web", "hany_ml", "hany_livebook"] 16 | steps: 17 | - 18 | name: Checkout 19 | uses: actions/checkout@v3 20 | - 21 | name: Set up QEMU 22 | uses: docker/setup-qemu-action@v2 23 | - 24 | name: Set up Docker Buildx 25 | uses: docker/setup-buildx-action@v2 26 | - 27 | name: Login to GHCR 28 | uses: docker/login-action@v2 29 | with: 30 | registry: ghcr.io 31 | username: ${{ github.actor }} 32 | password: ${{ secrets.GITHUB_TOKEN }} 33 | - 34 | name: Build and push (base) 35 | uses: docker/build-push-action@v3 36 | with: 37 | push: true 38 | platforms: | 39 | linux/amd64 40 | tags: | 41 | ghcr.io/${{ github.repository }}/${{ matrix.release }}:latest 42 | ghcr.io/${{ github.repository }}/${{ matrix.release }}:${{ github.sha }} 43 | build-args: | 44 | _MIX_ENV=prod 45 | _RELEASE_NAME=${{ matrix.release }} 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Temporary files, for example, from tests. 23 | /tmp 24 | 25 | 26 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM hexpm/elixir:1.14.2-erlang-25.2.3-ubuntu-jammy-20221130 as builder 2 | ARG _MIX_ENV 3 | ARG _RELEASE_NAME 4 | WORKDIR /src 5 | COPY . . 6 | RUN DEBIAN_FRONTEND=noninteractive \ 7 | && apt-get update \ 8 | && apt-get -y install --no-install-recommends \ 9 | curl \ 10 | ca-certificates \ 11 | build-essential 12 | RUN mix local.rebar --force 13 | RUN mix local.hex --force 14 | RUN mix deps.get 15 | RUN MIX_ENV=${_MIX_ENV} mix release.setup 16 | RUN MIX_ENV=${_MIX_ENV} mix release ${_RELEASE_NAME} 17 | RUN mv _build/${_MIX_ENV}/rel/${_RELEASE_NAME} /opt/release 18 | RUN mv /opt/release/bin/${_RELEASE_NAME} /opt/release/bin/app 19 | 20 | FROM hexpm/elixir:1.14.2-erlang-25.2.3-ubuntu-jammy-20221130 as runner 21 | WORKDIR /opt/release 22 | COPY --from=builder /opt/release . 23 | CMD /opt/release/bin/app start 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elixir Distributed Application Example 2 | 3 | Screen Shot 2023-02-28 at 2 15 58 PM 4 | 5 | [2023 Seoul Elixir Meetup - Journey of the Elixir distributed application.pdf](https://github.com/rudebono/elixir-da-example/files/10869527/2023.Seoul.Elixir.Meetup.-.Journey.of.the.Elixir.distributed.application.pdf) 6 | -------------------------------------------------------------------------------- /apps/hany/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:ecto], 3 | inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | subdirectories: ["priv/*/migrations"] 5 | ] 6 | -------------------------------------------------------------------------------- /apps/hany/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | hany-*.tar 24 | -------------------------------------------------------------------------------- /apps/hany/README.md: -------------------------------------------------------------------------------- 1 | # Hany 2 | 3 | **TODO: Add description** 4 | -------------------------------------------------------------------------------- /apps/hany/lib/hany.ex: -------------------------------------------------------------------------------- 1 | defmodule Hany do 2 | @moduledoc """ 3 | Hany keeps the contexts that define your domain 4 | and business logic. 5 | 6 | Contexts are also responsible for managing your data, regardless 7 | if it comes from the database, an external API or others. 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /apps/hany/lib/hany/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Hany.Application do 2 | use Application 3 | 4 | @impl true 5 | def start(_type, _args) do 6 | [ 7 | Hany.Repo.Local, 8 | Hany.Repo.Migrator, 9 | {Phoenix.PubSub, name: Hany.PubSub} 10 | ] 11 | |> Supervisor.start_link(strategy: :one_for_one, name: __MODULE__) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /apps/hany/lib/hany/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule Hany.Repo do 2 | alias HanyCluster.Rpc 3 | 4 | def config() do 5 | __MODULE__.__exec__(:config, []) 6 | end 7 | 8 | def aggregate(queryable, aggregate, opts \\ []) do 9 | __MODULE__.__exec__(:aggregate, [queryable, aggregate, opts]) 10 | end 11 | 12 | def aggregate(queryable, aggregate, field, opts) do 13 | __MODULE__.__exec__(:aggregate, [queryable, aggregate, field, opts]) 14 | end 15 | 16 | def all(queryable, opts \\ []) do 17 | __MODULE__.__exec__(:all, [queryable, opts]) 18 | end 19 | 20 | def delete(struct_or_changeset, opts \\ []) do 21 | __MODULE__.__exec__(:delete, [struct_or_changeset, opts]) 22 | end 23 | 24 | def delete!(struct_or_changeset, opts \\ []) do 25 | __MODULE__.__exec__(:delete!, [struct_or_changeset, opts]) 26 | end 27 | 28 | def delete_all(queryable, opts \\ []) do 29 | __MODULE__.__exec__(:delete_all, [queryable, opts]) 30 | end 31 | 32 | def exists?(queryable, opts \\ []) do 33 | __MODULE__.__exec__(:exists?, [queryable, opts]) 34 | end 35 | 36 | def get(queryable, id, opts \\ []) do 37 | __MODULE__.__exec__(:get, [queryable, id, opts]) 38 | end 39 | 40 | def get!(queryable, id, opts \\ []) do 41 | __MODULE__.__exec__(:get!, [queryable, id, opts]) 42 | end 43 | 44 | def get_by(queryable, clauses, opts \\ []) do 45 | __MODULE__.__exec__(:get_by, [queryable, clauses, opts]) 46 | end 47 | 48 | def get_by!(queryable, clauses, opts \\ []) do 49 | __MODULE__.__exec__(:get_by!, [queryable, clauses, opts]) 50 | end 51 | 52 | def get_dynamic_repo() do 53 | __MODULE__.__exec__(:get_dynamic_repo, []) 54 | end 55 | 56 | def insert(struct_or_changeset, opts \\ []) do 57 | __MODULE__.__exec__(:insert, [struct_or_changeset, opts]) 58 | end 59 | 60 | def insert!(struct_or_changeset, opts \\ []) do 61 | __MODULE__.__exec__(:insert!, [struct_or_changeset, opts]) 62 | end 63 | 64 | def insert_all(schema_or_source, entries_or_query, opts \\ []) do 65 | __MODULE__.__exec__(:insert_all, [schema_or_source, entries_or_query, opts]) 66 | end 67 | 68 | def insert_or_update(changeset, opts \\ []) do 69 | __MODULE__.__exec__(:insert_or_update, [changeset, opts]) 70 | end 71 | 72 | def insert_or_update!(changeset, opts \\ []) do 73 | __MODULE__.__exec__(:insert_or_update!, [changeset, opts]) 74 | end 75 | 76 | def one(queryable, opts \\ []) do 77 | __MODULE__.__exec__(:one, [queryable, opts]) 78 | end 79 | 80 | def one!(queryable, opts \\ []) do 81 | __MODULE__.__exec__(:one!, [queryable, opts]) 82 | end 83 | 84 | def preload(structs_or_struct_or_nil, preloads, opts \\ []) do 85 | __MODULE__.__exec__(:preload, [ 86 | structs_or_struct_or_nil, 87 | preloads, 88 | opts 89 | ]) 90 | end 91 | 92 | def prepare_query(operation, query, opts \\ []) do 93 | __MODULE__.__exec__(:prepare_query, [operation, query, opts]) 94 | end 95 | 96 | def put_dynamic_repo(name_or_pid) do 97 | __MODULE__.__exec__(:put_dynamic_repo, [name_or_pid]) 98 | end 99 | 100 | def query(query, params \\ [], opts \\ []) do 101 | __MODULE__.__exec__(:query, [query, params, opts]) 102 | end 103 | 104 | def query!(query, params \\ [], opts \\ []) do 105 | __MODULE__.__exec__(:query!, [query, params, opts]) 106 | end 107 | 108 | def reload(struct_or_structs, opts \\ []) do 109 | __MODULE__.__exec__(:reload, [struct_or_structs, opts]) 110 | end 111 | 112 | def reload!(struct_or_structs, opts \\ []) do 113 | __MODULE__.__exec__(:reload!, [struct_or_structs, opts]) 114 | end 115 | 116 | def rollback(value) do 117 | __MODULE__.__exec__(:rollback, [value]) 118 | end 119 | 120 | def stream(queryable, opts \\ []) do 121 | __MODULE__.__exec__(:stream, [queryable, opts]) 122 | end 123 | 124 | def transaction(fun_or_multi, opts \\ []) do 125 | __MODULE__.__exec__(:transaction, [fun_or_multi, opts]) 126 | end 127 | 128 | def update(changeset, opts \\ []) do 129 | __MODULE__.__exec__(:update, [changeset, opts]) 130 | end 131 | 132 | def update!(changeset, opts \\ []) do 133 | __MODULE__.__exec__(:update!, [changeset, opts]) 134 | end 135 | 136 | def update_all(queryable, updates, opts \\ []) do 137 | __MODULE__.__exec__(:update_all, [queryable, updates, opts]) 138 | end 139 | 140 | def __exec__(func, args) do 141 | Rpc.call(__MODULE__.Local, func, args) 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /apps/hany/lib/hany/repo/local.ex: -------------------------------------------------------------------------------- 1 | defmodule Hany.Repo.Local do 2 | use Ecto.Repo, otp_app: :hany, adapter: Ecto.Adapters.Postgres 3 | 4 | def migrate() do 5 | application = Application.get_application(__MODULE__) 6 | path = Application.app_dir(application, Application.get_env(application, __MODULE__)[:priv]) 7 | versions = Ecto.Migrator.run(__MODULE__, path, :up, all: true) 8 | {:ok, versions} 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /apps/hany/lib/hany/repo/migrator.ex: -------------------------------------------------------------------------------- 1 | defmodule Hany.Repo.Migrator do 2 | use GenServer 3 | 4 | def start_link(_arguments) do 5 | GenServer.start_link(__MODULE__, [], name: __MODULE__) 6 | end 7 | 8 | def init(_arguments) do 9 | # For Master <-> Read Replica PostgreSQL Nodes 10 | # case HanyCluster.is_primary_region() do 11 | # true -> Hany.Repo.Local.migrate() 12 | # false -> {:ok, []} 13 | # end 14 | Hany.Repo.Local.migrate() 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /apps/hany/lib/hany/user.ex: -------------------------------------------------------------------------------- 1 | defmodule Hany.User do 2 | use Ecto.Schema 3 | import Ecto.Changeset 4 | 5 | schema "users" do 6 | field :name, :string 7 | timestamps() 8 | end 9 | 10 | def changeset(struct, params \\ %{}) do 11 | struct 12 | |> cast(params, [:name]) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /apps/hany/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Hany.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :hany, 7 | version: "0.1.0", 8 | build_path: "../../_build", 9 | config_path: "../../config/config.exs", 10 | deps_path: "../../deps", 11 | lockfile: "../../mix.lock", 12 | elixir: "~> 1.12", 13 | elixirc_paths: elixirc_paths(Mix.env()), 14 | start_permanent: Mix.env() == :prod, 15 | aliases: aliases(), 16 | deps: deps() 17 | ] 18 | end 19 | 20 | # Configuration for the OTP application. 21 | # 22 | # Type `mix help compile.app` for more information. 23 | def application do 24 | [ 25 | mod: {Hany.Application, []}, 26 | extra_applications: [:logger, :runtime_tools] 27 | ] 28 | end 29 | 30 | # Specifies which paths to compile per environment. 31 | defp elixirc_paths(:test), do: ["lib", "test/support"] 32 | defp elixirc_paths(_), do: ["lib"] 33 | 34 | # Specifies your project dependencies. 35 | # 36 | # Type `mix help deps` for examples and options. 37 | defp deps do 38 | [ 39 | {:hany_cluster, in_umbrella: true, runtime: true}, 40 | {:phoenix_pubsub, "~> 2.0"}, 41 | {:ecto_sql, "~> 3.6"}, 42 | {:postgrex, ">= 0.0.0"}, 43 | {:jason, "~> 1.2"} 44 | ] 45 | end 46 | 47 | # Aliases are shortcuts or tasks specific to the current project. 48 | # 49 | # See the documentation for `Mix` for more info on aliases. 50 | defp aliases do 51 | [ 52 | setup: ["deps.get", "ecto.setup"], 53 | "release.setup": [], 54 | "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 55 | "ecto.reset": ["ecto.drop", "ecto.setup"], 56 | test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"] 57 | ] 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /apps/hany/priv/repo/migrations/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:ecto_sql], 3 | inputs: ["*.exs"] 4 | ] 5 | -------------------------------------------------------------------------------- /apps/hany/priv/repo/migrations/20230226154351_create_users.exs: -------------------------------------------------------------------------------- 1 | defmodule Hany.Repo.Local.Migrations.CreateUsers do 2 | use Ecto.Migration 3 | 4 | def change() do 5 | create table(:users) do 6 | add :name, :string 7 | timestamps() 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /apps/hany/priv/repo/seeds.exs: -------------------------------------------------------------------------------- 1 | # Script for populating the database. You can run it as: 2 | # 3 | # mix run priv/repo/seeds.exs 4 | # 5 | # Inside the script, you can read and write to any of your 6 | # repositories directly: 7 | # 8 | # Hany.Repo.Local.insert!(%Hany.SomeSchema{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will fail if something goes wrong. 12 | -------------------------------------------------------------------------------- /apps/hany/test/support/data_case.ex: -------------------------------------------------------------------------------- 1 | defmodule Hany.DataCase do 2 | @moduledoc """ 3 | This module defines the setup for tests requiring 4 | access to the application's data layer. 5 | 6 | You may define functions here to be used as helpers in 7 | your tests. 8 | 9 | Finally, if the test case interacts with the database, 10 | we enable the SQL sandbox, so changes done to the database 11 | are reverted at the end of every test. If you are using 12 | PostgreSQL, you can even run database tests asynchronously 13 | by setting `use Hany.DataCase, async: true`, although 14 | this option is not recommended for other databases. 15 | """ 16 | 17 | use ExUnit.CaseTemplate 18 | 19 | using do 20 | quote do 21 | alias Hany.Repo.Local 22 | 23 | import Ecto 24 | import Ecto.Changeset 25 | import Ecto.Query 26 | import Hany.DataCase 27 | end 28 | end 29 | 30 | setup tags do 31 | Hany.DataCase.setup_sandbox(tags) 32 | :ok 33 | end 34 | 35 | @doc """ 36 | Sets up the sandbox based on the test tags. 37 | """ 38 | def setup_sandbox(tags) do 39 | pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Hany.Repo.Local, shared: not tags[:async]) 40 | on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end) 41 | end 42 | 43 | @doc """ 44 | A helper that transforms changeset errors into a map of messages. 45 | 46 | assert {:error, changeset} = Accounts.create_user(%{password: "short"}) 47 | assert "password is too short" in errors_on(changeset).password 48 | assert %{password: ["password is too short"]} = errors_on(changeset) 49 | 50 | """ 51 | def errors_on(changeset) do 52 | Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> 53 | Regex.replace(~r"%{(\w+)}", message, fn _, key -> 54 | opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string() 55 | end) 56 | end) 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /apps/hany/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | Ecto.Adapters.SQL.Sandbox.mode(Hany.Repo.Local, :manual) 3 | -------------------------------------------------------------------------------- /apps/hany_cluster/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /apps/hany_cluster/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | hany_cluster-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /apps/hany_cluster/README.md: -------------------------------------------------------------------------------- 1 | # HanyCluster 2 | 3 | **TODO: Add description** 4 | 5 | ## Installation 6 | 7 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 8 | by adding `hany_cluster` to your list of dependencies in `mix.exs`: 9 | 10 | ```elixir 11 | def deps do 12 | [ 13 | {:hany_cluster, "~> 0.1.0"} 14 | ] 15 | end 16 | ``` 17 | 18 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 19 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 20 | be found at . 21 | 22 | -------------------------------------------------------------------------------- /apps/hany_cluster/lib/hany_cluster.ex: -------------------------------------------------------------------------------- 1 | defmodule HanyCluster do 2 | def is_started_application(application) do 3 | Application.started_applications() 4 | |> Enum.any?(fn {app, _description, _vsn} -> app === application end) 5 | end 6 | 7 | def find_node(application) do 8 | Node.list() 9 | |> Enum.reduce([], fn node, nodes -> 10 | if :rpc.call(node, HanyCluster, :is_started_application, [application]) do 11 | [node | nodes] 12 | else 13 | nodes 14 | end 15 | end) 16 | |> Enum.random() 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /apps/hany_cluster/lib/hany_cluster/application.ex: -------------------------------------------------------------------------------- 1 | defmodule HanyCluster.Application do 2 | use Application 3 | 4 | @impl true 5 | def start(_type, _args) do 6 | [{HanyCluster.Connector, Application.get_env(:hany_cluster, :strategy)}] 7 | |> Supervisor.start_link(strategy: :one_for_one, name: __MODULE__) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /apps/hany_cluster/lib/hany_cluster/connector.ex: -------------------------------------------------------------------------------- 1 | defmodule HanyCluster.Connector do 2 | use GenServer 3 | 4 | require Logger 5 | 6 | def start_link(strategy) do 7 | GenServer.start_link(__MODULE__, strategy, name: __MODULE__) 8 | end 9 | 10 | @impl true 11 | def init(strategy) do 12 | :global_group.monitor_nodes(true) 13 | for node <- strategy.nodes(), do: connect(node) 14 | loop() 15 | {:ok, strategy} 16 | end 17 | 18 | @impl true 19 | def handle_info({:nodeup, node}, strategy) do 20 | Logger.info("#{inspect(node)} connected") 21 | {:noreply, strategy} 22 | end 23 | 24 | @impl true 25 | def handle_info({:nodedown, node}, strategy) do 26 | Logger.info("#{inspect(node)} disconnected") 27 | {:noreply, strategy} 28 | end 29 | 30 | @impl true 31 | def handle_info(:loop, strategy) do 32 | for node <- strategy.nodes(), do: connect(node) 33 | loop() 34 | {:noreply, strategy} 35 | end 36 | 37 | defp connect(node) do 38 | if node not in [Node.self() | Node.list(:connected)], do: Node.connect(node) 39 | end 40 | 41 | defp loop() do 42 | Process.send_after(__MODULE__, :loop, 3_000) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /apps/hany_cluster/lib/hany_cluster/rpc.ex: -------------------------------------------------------------------------------- 1 | defmodule HanyCluster.Rpc do 2 | def call(module, function, args) do 3 | application = Application.get_application(module) 4 | 5 | if HanyCluster.is_started_application(application) do 6 | apply(module, function, args) 7 | else 8 | application 9 | |> HanyCluster.find_node() 10 | |> :rpc.call(module, function, args) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /apps/hany_cluster/lib/hany_cluster/strategy.ex: -------------------------------------------------------------------------------- 1 | defmodule HanyCluster.Strategy do 2 | @callback nodes() :: [node()] | [] 3 | end 4 | -------------------------------------------------------------------------------- /apps/hany_cluster/lib/hany_cluster/strategy/gce.ex: -------------------------------------------------------------------------------- 1 | defmodule HanyCluster.Strategy.Gce do 2 | @behaviour HanyCluster.Strategy 3 | 4 | @cacerts CAStore.file_path() 5 | |> File.read!() 6 | |> :public_key.pem_decode() 7 | |> Enum.map(fn {:Certificate, der, _} -> der end) 8 | 9 | @impl true 10 | def nodes() do 11 | {:ok, {{'HTTP/1.1', 200, 'OK'}, _headers, project_id}} = 12 | :httpc.request( 13 | :get, 14 | { 15 | "http://metadata.google.internal/computeMetadata/v1/project/project-id", 16 | [{'Metadata-Flavor', 'Google'}, {'content-type', 'application/text'}] 17 | }, 18 | [], 19 | [{:body_format, :binary}] 20 | ) 21 | 22 | {:ok, {{'HTTP/1.1', 200, 'OK'}, _headers, body}} = 23 | :httpc.request( 24 | :get, 25 | { 26 | "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token", 27 | [{'Metadata-Flavor', 'Google'}, {'content-type', 'application/json'}] 28 | }, 29 | [], 30 | [{:body_format, :binary}] 31 | ) 32 | 33 | {:ok, %{"access_token" => access_token, "token_type" => token_type}} = Jason.decode(body) 34 | 35 | {:ok, {{'HTTP/1.1', 200, 'OK'}, _headers, body}} = 36 | :httpc.request( 37 | :get, 38 | { 39 | "https://compute.googleapis.com/compute/v1/projects/#{project_id}/aggregated/instances", 40 | [ 41 | {'Authorization', '#{token_type} #{access_token}'}, 42 | {'content-type', 'application/json'} 43 | ] 44 | }, 45 | [ 46 | {:ssl, 47 | [ 48 | {:verify, :verify_peer}, 49 | {:cacerts, @cacerts}, 50 | {:customize_hostname_check, 51 | [{:match_fun, :public_key.pkix_verify_hostname_match_fun(:https)}]} 52 | ]} 53 | ], 54 | [{:body_format, :binary}] 55 | ) 56 | 57 | [name, _host] = 58 | Node.self() 59 | |> Atom.to_charlist() 60 | |> :string.split('@') 61 | 62 | {:ok, %{"items" => items}} = Jason.decode(body) 63 | 64 | items 65 | |> Enum.filter(fn 66 | {_zone, %{"instances" => [_ | _]}} -> true 67 | {_zone, _} -> false 68 | end) 69 | |> Enum.reduce([], fn {_zone, %{"instances" => instances}}, acc -> acc ++ instances end) 70 | |> Enum.map(fn %{"selfLink" => self_link} -> URI.parse(self_link) end) 71 | |> Enum.map(fn %URI{path: path} -> Path.split(path) end) 72 | |> Enum.map(fn [ 73 | "/", 74 | "compute", 75 | "v1", 76 | "projects", 77 | project_id, 78 | "zones", 79 | zone, 80 | "instances", 81 | instance 82 | ] -> 83 | :"#{name}@#{instance}.#{zone}.c.#{project_id}.internal" 84 | end) 85 | rescue 86 | _ -> [] 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /apps/hany_cluster/lib/hany_cluster/strategy/local_epmd.ex: -------------------------------------------------------------------------------- 1 | defmodule HanyCluster.Strategy.Epmd do 2 | @behaviour HanyCluster.Strategy 3 | 4 | @impl true 5 | def nodes() do 6 | {:ok, names} = :erl_epmd.names() 7 | [_name, host] = Node.self() |> Atom.to_charlist() |> :string.split('@') 8 | Enum.map(names, fn {name, _} -> :"#{name ++ '@' ++ host}" end) 9 | rescue 10 | _ -> [] 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /apps/hany_cluster/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule HanyCluster.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :hany_cluster, 7 | version: "0.1.0", 8 | build_path: "../../_build", 9 | config_path: "../../config/config.exs", 10 | deps_path: "../../deps", 11 | lockfile: "../../mix.lock", 12 | elixir: "~> 1.14", 13 | start_permanent: Mix.env() == :prod, 14 | deps: deps(), 15 | aliases: aliases() 16 | ] 17 | end 18 | 19 | def application() do 20 | [ 21 | extra_applications: [:logger, :runtime_tools, :inets, :ssl, :public_key], 22 | mod: {HanyCluster.Application, []} 23 | ] 24 | end 25 | 26 | defp deps() do 27 | [ 28 | {:jason, "~> 1.3"}, 29 | {:castore, "~> 0.1.17"} 30 | ] 31 | end 32 | 33 | defp aliases() do 34 | [ 35 | setup: ["deps.get"], 36 | "release.setup": [] 37 | ] 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /apps/hany_cluster/test/hany_cluster_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HanyClusterTest do 2 | use ExUnit.Case 3 | doctest HanyCluster 4 | 5 | test "greets the world" do 6 | assert HanyCluster.hello() == :world 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /apps/hany_cluster/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /apps/hany_ml/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /apps/hany_ml/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | hany_ml-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | -------------------------------------------------------------------------------- /apps/hany_ml/README.md: -------------------------------------------------------------------------------- 1 | # HanyMl 2 | 3 | **TODO: Add description** 4 | 5 | ## Installation 6 | 7 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 8 | by adding `hany_ml` to your list of dependencies in `mix.exs`: 9 | 10 | ```elixir 11 | def deps do 12 | [ 13 | {:hany_ml, "~> 0.1.0"} 14 | ] 15 | end 16 | ``` 17 | 18 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 19 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 20 | be found at . 21 | 22 | -------------------------------------------------------------------------------- /apps/hany_ml/lib/hany_ml.ex: -------------------------------------------------------------------------------- 1 | defmodule HanyMl do 2 | alias HanyCluster.Rpc 3 | 4 | def run(image) do 5 | __MODULE__.__exec__(:run, [image]) 6 | end 7 | 8 | def __exec__(func, args) do 9 | Rpc.call(__MODULE__.Local, func, args) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /apps/hany_ml/lib/hany_ml/application.ex: -------------------------------------------------------------------------------- 1 | defmodule HanyMl.Application do 2 | use Application 3 | 4 | @impl true 5 | def start(_type, _args) do 6 | [HanyMl.Local] 7 | |> Supervisor.start_link(strategy: :one_for_one, name: __MODULE__) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /apps/hany_ml/lib/hany_ml/local.ex: -------------------------------------------------------------------------------- 1 | defmodule HanyMl.Local do 2 | use GenServer 3 | 4 | def start_link(_) do 5 | GenServer.start_link(__MODULE__, nil, name: __MODULE__) 6 | end 7 | 8 | @impl true 9 | def init(_) do 10 | {:ok, resnet} = Bumblebee.load_model({:hf, "microsoft/resnet-50"}) 11 | {:ok, featurizer} = Bumblebee.load_featurizer({:hf, "microsoft/resnet-50"}) 12 | {:ok, Bumblebee.Vision.image_classification(resnet, featurizer)} 13 | end 14 | 15 | @impl true 16 | def handle_call({:run, image}, _from, serving) do 17 | image = image.data |> Nx.from_binary(:u8) |> Nx.reshape({image.height, image.width, 3}) 18 | {:reply, Nx.Serving.run(serving, image), serving} 19 | end 20 | 21 | def run(image) do 22 | GenServer.call(__MODULE__, {:run, image}) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /apps/hany_ml/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule HanyMl.MixProject do 2 | use Mix.Project 3 | 4 | def project() do 5 | [ 6 | app: :hany_ml, 7 | version: "0.1.0", 8 | build_path: "../../_build", 9 | config_path: "../../config/config.exs", 10 | deps_path: "../../deps", 11 | lockfile: "../../mix.lock", 12 | elixir: "~> 1.14", 13 | start_permanent: Mix.env() == :prod, 14 | deps: deps(), 15 | aliases: aliases() 16 | ] 17 | end 18 | 19 | def application() do 20 | [ 21 | extra_applications: [:logger], 22 | mod: {HanyMl.Application, []} 23 | ] 24 | end 25 | 26 | defp deps() do 27 | [ 28 | {:hany_cluster, in_umbrella: true, runtime: true}, 29 | {:bumblebee, "~> 0.1.2"}, 30 | {:exla, ">= 0.0.0"}, 31 | {:nx, "~> 0.4.1"} 32 | ] 33 | end 34 | 35 | defp aliases() do 36 | [ 37 | setup: ["deps.get"], 38 | "release.setup": [] 39 | ] 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /apps/hany_ml/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /apps/hany_web/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:phoenix], 3 | inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /apps/hany_web/.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | hany_web-*.tar 24 | 25 | # Ignore assets that are produced by build tools. 26 | /priv/static/assets/ 27 | 28 | # Ignore digested assets cache. 29 | /priv/static/cache_manifest.json 30 | 31 | # In case you use Node.js/npm, you want to ignore these. 32 | npm-debug.log 33 | /assets/node_modules/ 34 | 35 | -------------------------------------------------------------------------------- /apps/hany_web/README.md: -------------------------------------------------------------------------------- 1 | # HanyWeb 2 | 3 | To start your Phoenix server: 4 | 5 | * Install dependencies with `mix deps.get` 6 | * Create and migrate your database with `mix ecto.setup` 7 | * Start Phoenix endpoint with `mix phx.server` 8 | 9 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 10 | 11 | Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html). 12 | 13 | ## Learn more 14 | 15 | * Official website: https://www.phoenixframework.org/ 16 | * Guides: https://hexdocs.pm/phoenix/overview.html 17 | * Docs: https://hexdocs.pm/phoenix 18 | * Forum: https://elixirforum.com/c/phoenix-forum 19 | * Source: https://github.com/phoenixframework/phoenix 20 | -------------------------------------------------------------------------------- /apps/hany_web/assets/css/app.css: -------------------------------------------------------------------------------- 1 | /* This file is for your main application CSS */ 2 | @import "./phoenix.css"; 3 | 4 | /* Alerts and form errors used by phx.new */ 5 | .alert { 6 | padding: 15px; 7 | margin-bottom: 20px; 8 | border: 1px solid transparent; 9 | border-radius: 4px; 10 | } 11 | .alert-info { 12 | color: #31708f; 13 | background-color: #d9edf7; 14 | border-color: #bce8f1; 15 | } 16 | .alert-warning { 17 | color: #8a6d3b; 18 | background-color: #fcf8e3; 19 | border-color: #faebcc; 20 | } 21 | .alert-danger { 22 | color: #a94442; 23 | background-color: #f2dede; 24 | border-color: #ebccd1; 25 | } 26 | .alert p { 27 | margin-bottom: 0; 28 | } 29 | .alert:empty { 30 | display: none; 31 | } 32 | .invalid-feedback { 33 | color: #a94442; 34 | display: block; 35 | margin: -1rem 0 2rem; 36 | } 37 | 38 | /* LiveView specific classes for your customization */ 39 | .phx-no-feedback.invalid-feedback, 40 | .phx-no-feedback .invalid-feedback { 41 | display: none; 42 | } 43 | 44 | .phx-click-loading { 45 | opacity: 0.5; 46 | transition: opacity 1s ease-out; 47 | } 48 | 49 | .phx-loading{ 50 | cursor: wait; 51 | } 52 | 53 | .phx-modal { 54 | opacity: 1!important; 55 | position: fixed; 56 | z-index: 1; 57 | left: 0; 58 | top: 0; 59 | width: 100%; 60 | height: 100%; 61 | overflow: auto; 62 | background-color: rgba(0,0,0,0.4); 63 | } 64 | 65 | .phx-modal-content { 66 | background-color: #fefefe; 67 | margin: 15vh auto; 68 | padding: 20px; 69 | border: 1px solid #888; 70 | width: 80%; 71 | } 72 | 73 | .phx-modal-close { 74 | color: #aaa; 75 | float: right; 76 | font-size: 28px; 77 | font-weight: bold; 78 | } 79 | 80 | .phx-modal-close:hover, 81 | .phx-modal-close:focus { 82 | color: black; 83 | text-decoration: none; 84 | cursor: pointer; 85 | } 86 | 87 | .fade-in-scale { 88 | animation: 0.2s ease-in 0s normal forwards 1 fade-in-scale-keys; 89 | } 90 | 91 | .fade-out-scale { 92 | animation: 0.2s ease-out 0s normal forwards 1 fade-out-scale-keys; 93 | } 94 | 95 | .fade-in { 96 | animation: 0.2s ease-out 0s normal forwards 1 fade-in-keys; 97 | } 98 | .fade-out { 99 | animation: 0.2s ease-out 0s normal forwards 1 fade-out-keys; 100 | } 101 | 102 | @keyframes fade-in-scale-keys{ 103 | 0% { scale: 0.95; opacity: 0; } 104 | 100% { scale: 1.0; opacity: 1; } 105 | } 106 | 107 | @keyframes fade-out-scale-keys{ 108 | 0% { scale: 1.0; opacity: 1; } 109 | 100% { scale: 0.95; opacity: 0; } 110 | } 111 | 112 | @keyframes fade-in-keys{ 113 | 0% { opacity: 0; } 114 | 100% { opacity: 1; } 115 | } 116 | 117 | @keyframes fade-out-keys{ 118 | 0% { opacity: 1; } 119 | 100% { opacity: 0; } 120 | } 121 | -------------------------------------------------------------------------------- /apps/hany_web/assets/css/phoenix.css: -------------------------------------------------------------------------------- 1 | /* Includes some default style for the starter application. 2 | * This can be safely deleted to start fresh. 3 | */ 4 | 5 | /* Milligram v1.4.1 https://milligram.github.io 6 | * Copyright (c) 2020 CJ Patoilo Licensed under the MIT license 7 | */ 8 | 9 | *,*:after,*:before{box-sizing:inherit}html{box-sizing:border-box;font-size:62.5%}body{color:#000000;font-family:'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;font-size:1.6em;font-weight:300;letter-spacing:.01em;line-height:1.6}blockquote{border-left:0.3rem solid #d1d1d1;margin-left:0;margin-right:0;padding:1rem 1.5rem}blockquote *:last-child{margin-bottom:0}.button,button,input[type='button'],input[type='reset'],input[type='submit']{background-color:#0069d9;border:0.1rem solid #0069d9;border-radius:.4rem;color:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.8rem;letter-spacing:.1rem;line-height:3.8rem;padding:0 3.0rem;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap}.button:focus,.button:hover,button:focus,button:hover,input[type='button']:focus,input[type='button']:hover,input[type='reset']:focus,input[type='reset']:hover,input[type='submit']:focus,input[type='submit']:hover{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}.button[disabled],button[disabled],input[type='button'][disabled],input[type='reset'][disabled],input[type='submit'][disabled]{cursor:default;opacity:.5}.button[disabled]:focus,.button[disabled]:hover,button[disabled]:focus,button[disabled]:hover,input[type='button'][disabled]:focus,input[type='button'][disabled]:hover,input[type='reset'][disabled]:focus,input[type='reset'][disabled]:hover,input[type='submit'][disabled]:focus,input[type='submit'][disabled]:hover{background-color:#0069d9;border-color:#0069d9}.button.button-outline,button.button-outline,input[type='button'].button-outline,input[type='reset'].button-outline,input[type='submit'].button-outline{background-color:transparent;color:#0069d9}.button.button-outline:focus,.button.button-outline:hover,button.button-outline:focus,button.button-outline:hover,input[type='button'].button-outline:focus,input[type='button'].button-outline:hover,input[type='reset'].button-outline:focus,input[type='reset'].button-outline:hover,input[type='submit'].button-outline:focus,input[type='submit'].button-outline:hover{background-color:transparent;border-color:#606c76;color:#606c76}.button.button-outline[disabled]:focus,.button.button-outline[disabled]:hover,button.button-outline[disabled]:focus,button.button-outline[disabled]:hover,input[type='button'].button-outline[disabled]:focus,input[type='button'].button-outline[disabled]:hover,input[type='reset'].button-outline[disabled]:focus,input[type='reset'].button-outline[disabled]:hover,input[type='submit'].button-outline[disabled]:focus,input[type='submit'].button-outline[disabled]:hover{border-color:inherit;color:#0069d9}.button.button-clear,button.button-clear,input[type='button'].button-clear,input[type='reset'].button-clear,input[type='submit'].button-clear{background-color:transparent;border-color:transparent;color:#0069d9}.button.button-clear:focus,.button.button-clear:hover,button.button-clear:focus,button.button-clear:hover,input[type='button'].button-clear:focus,input[type='button'].button-clear:hover,input[type='reset'].button-clear:focus,input[type='reset'].button-clear:hover,input[type='submit'].button-clear:focus,input[type='submit'].button-clear:hover{background-color:transparent;border-color:transparent;color:#606c76}.button.button-clear[disabled]:focus,.button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,button.button-clear[disabled]:hover,input[type='button'].button-clear[disabled]:focus,input[type='button'].button-clear[disabled]:hover,input[type='reset'].button-clear[disabled]:focus,input[type='reset'].button-clear[disabled]:hover,input[type='submit'].button-clear[disabled]:focus,input[type='submit'].button-clear[disabled]:hover{color:#0069d9}code{background:#f4f5f6;border-radius:.4rem;font-size:86%;margin:0 .2rem;padding:.2rem .5rem;white-space:nowrap}pre{background:#f4f5f6;border-left:0.3rem solid #0069d9;overflow-y:hidden}pre>code{border-radius:0;display:block;padding:1rem 1.5rem;white-space:pre}hr{border:0;border-top:0.1rem solid #f4f5f6;margin:3.0rem 0}input[type='color'],input[type='date'],input[type='datetime'],input[type='datetime-local'],input[type='email'],input[type='month'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],input[type='week'],input:not([type]),textarea,select{-webkit-appearance:none;background-color:transparent;border:0.1rem solid #d1d1d1;border-radius:.4rem;box-shadow:none;box-sizing:inherit;height:3.8rem;padding:.6rem 1.0rem .7rem;width:100%}input[type='color']:focus,input[type='date']:focus,input[type='datetime']:focus,input[type='datetime-local']:focus,input[type='email']:focus,input[type='month']:focus,input[type='number']:focus,input[type='password']:focus,input[type='search']:focus,input[type='tel']:focus,input[type='text']:focus,input[type='url']:focus,input[type='week']:focus,input:not([type]):focus,textarea:focus,select:focus{border-color:#0069d9;outline:0}select{background:url('data:image/svg+xml;utf8,') center right no-repeat;padding-right:3.0rem}select:focus{background-image:url('data:image/svg+xml;utf8,')}select[multiple]{background:none;height:auto}textarea{min-height:6.5rem}label,legend{display:block;font-size:1.6rem;font-weight:700;margin-bottom:.5rem}fieldset{border-width:0;padding:0}input[type='checkbox'],input[type='radio']{display:inline}.label-inline{display:inline-block;font-weight:normal;margin-left:.5rem}.container{margin:0 auto;max-width:112.0rem;padding:0 2.0rem;position:relative;width:100%}.row{display:flex;flex-direction:column;padding:0;width:100%}.row.row-no-padding{padding:0}.row.row-no-padding>.column{padding:0}.row.row-wrap{flex-wrap:wrap}.row.row-top{align-items:flex-start}.row.row-bottom{align-items:flex-end}.row.row-center{align-items:center}.row.row-stretch{align-items:stretch}.row.row-baseline{align-items:baseline}.row .column{display:block;flex:1 1 auto;margin-left:0;max-width:100%;width:100%}.row .column.column-offset-10{margin-left:10%}.row .column.column-offset-20{margin-left:20%}.row .column.column-offset-25{margin-left:25%}.row .column.column-offset-33,.row .column.column-offset-34{margin-left:33.3333%}.row .column.column-offset-40{margin-left:40%}.row .column.column-offset-50{margin-left:50%}.row .column.column-offset-60{margin-left:60%}.row .column.column-offset-66,.row .column.column-offset-67{margin-left:66.6666%}.row .column.column-offset-75{margin-left:75%}.row .column.column-offset-80{margin-left:80%}.row .column.column-offset-90{margin-left:90%}.row .column.column-10{flex:0 0 10%;max-width:10%}.row .column.column-20{flex:0 0 20%;max-width:20%}.row .column.column-25{flex:0 0 25%;max-width:25%}.row .column.column-33,.row .column.column-34{flex:0 0 33.3333%;max-width:33.3333%}.row .column.column-40{flex:0 0 40%;max-width:40%}.row .column.column-50{flex:0 0 50%;max-width:50%}.row .column.column-60{flex:0 0 60%;max-width:60%}.row .column.column-66,.row .column.column-67{flex:0 0 66.6666%;max-width:66.6666%}.row .column.column-75{flex:0 0 75%;max-width:75%}.row .column.column-80{flex:0 0 80%;max-width:80%}.row .column.column-90{flex:0 0 90%;max-width:90%}.row .column .column-top{align-self:flex-start}.row .column .column-bottom{align-self:flex-end}.row .column .column-center{align-self:center}@media (min-width: 40rem){.row{flex-direction:row;margin-left:-1.0rem;width:calc(100% + 2.0rem)}.row .column{margin-bottom:inherit;padding:0 1.0rem}}a{color:#0069d9;text-decoration:none}a:focus,a:hover{color:#606c76}dl,ol,ul{list-style:none;margin-top:0;padding-left:0}dl dl,dl ol,dl ul,ol dl,ol ol,ol ul,ul dl,ul ol,ul ul{font-size:90%;margin:1.5rem 0 1.5rem 3.0rem}ol{list-style:decimal inside}ul{list-style:circle inside}.button,button,dd,dt,li{margin-bottom:1.0rem}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote,dl,figure,form,ol,p,pre,table,ul{margin-bottom:2.5rem}table{border-spacing:0;display:block;overflow-x:auto;text-align:left;width:100%}td,th{border-bottom:0.1rem solid #e1e1e1;padding:1.2rem 1.5rem}td:first-child,th:first-child{padding-left:0}td:last-child,th:last-child{padding-right:0}@media (min-width: 40rem){table{display:table;overflow-x:initial}}b,strong{font-weight:bold}p{margin-top:0}h1,h2,h3,h4,h5,h6{font-weight:300;letter-spacing:-.1rem;margin-bottom:2.0rem;margin-top:0}h1{font-size:4.6rem;line-height:1.2}h2{font-size:3.6rem;line-height:1.25}h3{font-size:2.8rem;line-height:1.3}h4{font-size:2.2rem;letter-spacing:-.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-.05rem;line-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}img{max-width:100%}.clearfix:after{clear:both;content:' ';display:table}.float-left{float:left}.float-right{float:right} 10 | 11 | /* General style */ 12 | h1{font-size: 3.6rem; line-height: 1.25} 13 | h2{font-size: 2.8rem; line-height: 1.3} 14 | h3{font-size: 2.2rem; letter-spacing: -.08rem; line-height: 1.35} 15 | h4{font-size: 1.8rem; letter-spacing: -.05rem; line-height: 1.5} 16 | h5{font-size: 1.6rem; letter-spacing: 0; line-height: 1.4} 17 | h6{font-size: 1.4rem; letter-spacing: 0; line-height: 1.2} 18 | pre{padding: 1em;} 19 | 20 | .container{ 21 | margin: 0 auto; 22 | max-width: 80.0rem; 23 | padding: 0 2.0rem; 24 | position: relative; 25 | width: 100% 26 | } 27 | select { 28 | width: auto; 29 | } 30 | 31 | /* Phoenix promo and logo */ 32 | .phx-hero { 33 | text-align: center; 34 | border-bottom: 1px solid #e3e3e3; 35 | background: #eee; 36 | border-radius: 6px; 37 | padding: 3em 3em 1em; 38 | margin-bottom: 3rem; 39 | font-weight: 200; 40 | font-size: 120%; 41 | } 42 | .phx-hero input { 43 | background: #ffffff; 44 | } 45 | .phx-logo { 46 | min-width: 300px; 47 | margin: 1rem; 48 | display: block; 49 | } 50 | .phx-logo img { 51 | width: auto; 52 | display: block; 53 | } 54 | 55 | /* Headers */ 56 | header { 57 | width: 100%; 58 | background: #fdfdfd; 59 | border-bottom: 1px solid #eaeaea; 60 | margin-bottom: 2rem; 61 | } 62 | header section { 63 | align-items: center; 64 | display: flex; 65 | flex-direction: column; 66 | justify-content: space-between; 67 | } 68 | header section :first-child { 69 | order: 2; 70 | } 71 | header section :last-child { 72 | order: 1; 73 | } 74 | header nav ul, 75 | header nav li { 76 | margin: 0; 77 | padding: 0; 78 | display: block; 79 | text-align: right; 80 | white-space: nowrap; 81 | } 82 | header nav ul { 83 | margin: 1rem; 84 | margin-top: 0; 85 | } 86 | header nav a { 87 | display: block; 88 | } 89 | 90 | @media (min-width: 40.0rem) { /* Small devices (landscape phones, 576px and up) */ 91 | header section { 92 | flex-direction: row; 93 | } 94 | header nav ul { 95 | margin: 1rem; 96 | } 97 | .phx-logo { 98 | flex-basis: 527px; 99 | margin: 2rem 1rem; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /apps/hany_web/assets/js/app.js: -------------------------------------------------------------------------------- 1 | // We import the CSS which is extracted to its own file by esbuild. 2 | // Remove this line if you add a your own CSS build pipeline (e.g postcss). 3 | import "../css/app.css" 4 | 5 | // If you want to use Phoenix channels, run `mix help phx.gen.channel` 6 | // to get started and then uncomment the line below. 7 | // import "./user_socket.js" 8 | 9 | // You can include dependencies in two ways. 10 | // 11 | // The simplest option is to put them in assets/vendor and 12 | // import them using relative paths: 13 | // 14 | // import "../vendor/some-package.js" 15 | // 16 | // Alternatively, you can `npm install some-package --prefix assets` and import 17 | // them using a path starting with the package name: 18 | // 19 | // import "some-package" 20 | // 21 | 22 | // Include phoenix_html to handle method=PUT/DELETE in forms and buttons. 23 | import "phoenix_html" 24 | // Establish Phoenix Socket and LiveView configuration. 25 | import {Socket} from "phoenix" 26 | import {LiveSocket} from "phoenix_live_view" 27 | import topbar from "../vendor/topbar" 28 | 29 | let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") 30 | let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}}) 31 | 32 | // Show progress bar on live navigation and form submits 33 | topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"}) 34 | window.addEventListener("phx:page-loading-start", info => topbar.show()) 35 | window.addEventListener("phx:page-loading-stop", info => topbar.hide()) 36 | 37 | // connect if there are any LiveViews on the page 38 | liveSocket.connect() 39 | 40 | // expose liveSocket on window for web console debug logs and latency simulation: 41 | // >> liveSocket.enableDebug() 42 | // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session 43 | // >> liveSocket.disableLatencySim() 44 | window.liveSocket = liveSocket 45 | 46 | -------------------------------------------------------------------------------- /apps/hany_web/assets/vendor/topbar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license MIT 3 | * topbar 1.0.0, 2021-01-06 4 | * https://buunguyen.github.io/topbar 5 | * Copyright (c) 2021 Buu Nguyen 6 | */ 7 | (function (window, document) { 8 | "use strict"; 9 | 10 | // https://gist.github.com/paulirish/1579671 11 | (function () { 12 | var lastTime = 0; 13 | var vendors = ["ms", "moz", "webkit", "o"]; 14 | for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { 15 | window.requestAnimationFrame = 16 | window[vendors[x] + "RequestAnimationFrame"]; 17 | window.cancelAnimationFrame = 18 | window[vendors[x] + "CancelAnimationFrame"] || 19 | window[vendors[x] + "CancelRequestAnimationFrame"]; 20 | } 21 | if (!window.requestAnimationFrame) 22 | window.requestAnimationFrame = function (callback, element) { 23 | var currTime = new Date().getTime(); 24 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 25 | var id = window.setTimeout(function () { 26 | callback(currTime + timeToCall); 27 | }, timeToCall); 28 | lastTime = currTime + timeToCall; 29 | return id; 30 | }; 31 | if (!window.cancelAnimationFrame) 32 | window.cancelAnimationFrame = function (id) { 33 | clearTimeout(id); 34 | }; 35 | })(); 36 | 37 | var canvas, 38 | progressTimerId, 39 | fadeTimerId, 40 | currentProgress, 41 | showing, 42 | addEvent = function (elem, type, handler) { 43 | if (elem.addEventListener) elem.addEventListener(type, handler, false); 44 | else if (elem.attachEvent) elem.attachEvent("on" + type, handler); 45 | else elem["on" + type] = handler; 46 | }, 47 | options = { 48 | autoRun: true, 49 | barThickness: 3, 50 | barColors: { 51 | 0: "rgba(26, 188, 156, .9)", 52 | ".25": "rgba(52, 152, 219, .9)", 53 | ".50": "rgba(241, 196, 15, .9)", 54 | ".75": "rgba(230, 126, 34, .9)", 55 | "1.0": "rgba(211, 84, 0, .9)", 56 | }, 57 | shadowBlur: 10, 58 | shadowColor: "rgba(0, 0, 0, .6)", 59 | className: null, 60 | }, 61 | repaint = function () { 62 | canvas.width = window.innerWidth; 63 | canvas.height = options.barThickness * 5; // need space for shadow 64 | 65 | var ctx = canvas.getContext("2d"); 66 | ctx.shadowBlur = options.shadowBlur; 67 | ctx.shadowColor = options.shadowColor; 68 | 69 | var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0); 70 | for (var stop in options.barColors) 71 | lineGradient.addColorStop(stop, options.barColors[stop]); 72 | ctx.lineWidth = options.barThickness; 73 | ctx.beginPath(); 74 | ctx.moveTo(0, options.barThickness / 2); 75 | ctx.lineTo( 76 | Math.ceil(currentProgress * canvas.width), 77 | options.barThickness / 2 78 | ); 79 | ctx.strokeStyle = lineGradient; 80 | ctx.stroke(); 81 | }, 82 | createCanvas = function () { 83 | canvas = document.createElement("canvas"); 84 | var style = canvas.style; 85 | style.position = "fixed"; 86 | style.top = style.left = style.right = style.margin = style.padding = 0; 87 | style.zIndex = 100001; 88 | style.display = "none"; 89 | if (options.className) canvas.classList.add(options.className); 90 | document.body.appendChild(canvas); 91 | addEvent(window, "resize", repaint); 92 | }, 93 | topbar = { 94 | config: function (opts) { 95 | for (var key in opts) 96 | if (options.hasOwnProperty(key)) options[key] = opts[key]; 97 | }, 98 | show: function () { 99 | if (showing) return; 100 | showing = true; 101 | if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId); 102 | if (!canvas) createCanvas(); 103 | canvas.style.opacity = 1; 104 | canvas.style.display = "block"; 105 | topbar.progress(0); 106 | if (options.autoRun) { 107 | (function loop() { 108 | progressTimerId = window.requestAnimationFrame(loop); 109 | topbar.progress( 110 | "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2) 111 | ); 112 | })(); 113 | } 114 | }, 115 | progress: function (to) { 116 | if (typeof to === "undefined") return currentProgress; 117 | if (typeof to === "string") { 118 | to = 119 | (to.indexOf("+") >= 0 || to.indexOf("-") >= 0 120 | ? currentProgress 121 | : 0) + parseFloat(to); 122 | } 123 | currentProgress = to > 1 ? 1 : to; 124 | repaint(); 125 | return currentProgress; 126 | }, 127 | hide: function () { 128 | if (!showing) return; 129 | showing = false; 130 | if (progressTimerId != null) { 131 | window.cancelAnimationFrame(progressTimerId); 132 | progressTimerId = null; 133 | } 134 | (function loop() { 135 | if (topbar.progress("+.1") >= 1) { 136 | canvas.style.opacity -= 0.05; 137 | if (canvas.style.opacity <= 0.05) { 138 | canvas.style.display = "none"; 139 | fadeTimerId = null; 140 | return; 141 | } 142 | } 143 | fadeTimerId = window.requestAnimationFrame(loop); 144 | })(); 145 | }, 146 | }; 147 | 148 | if (typeof module === "object" && typeof module.exports === "object") { 149 | module.exports = topbar; 150 | } else if (typeof define === "function" && define.amd) { 151 | define(function () { 152 | return topbar; 153 | }); 154 | } else { 155 | this.topbar = topbar; 156 | } 157 | }.call(this, window, document)); 158 | -------------------------------------------------------------------------------- /apps/hany_web/lib/hany_web.ex: -------------------------------------------------------------------------------- 1 | defmodule HanyWeb do 2 | @moduledoc """ 3 | The entrypoint for defining your web interface, such 4 | as controllers, views, channels and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use HanyWeb, :controller 9 | use HanyWeb, :view 10 | 11 | The definitions below will be executed for every view, 12 | controller, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. Instead, define any helper function in modules 17 | and import those modules here. 18 | """ 19 | 20 | def controller do 21 | quote do 22 | use Phoenix.Controller, namespace: HanyWeb 23 | 24 | import Plug.Conn 25 | alias HanyWeb.Router.Helpers, as: Routes 26 | end 27 | end 28 | 29 | def view do 30 | quote do 31 | use Phoenix.View, 32 | root: "lib/hany_web/templates", 33 | namespace: HanyWeb 34 | 35 | # Import convenience functions from controllers 36 | import Phoenix.Controller, 37 | only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1] 38 | 39 | # Include shared imports and aliases for views 40 | unquote(view_helpers()) 41 | end 42 | end 43 | 44 | def live_view do 45 | quote do 46 | use Phoenix.LiveView, 47 | layout: {HanyWeb.LayoutView, "live.html"} 48 | 49 | unquote(view_helpers()) 50 | end 51 | end 52 | 53 | def live_component do 54 | quote do 55 | use Phoenix.LiveComponent 56 | 57 | unquote(view_helpers()) 58 | end 59 | end 60 | 61 | def component do 62 | quote do 63 | use Phoenix.Component 64 | 65 | unquote(view_helpers()) 66 | end 67 | end 68 | 69 | def router do 70 | quote do 71 | use Phoenix.Router 72 | 73 | import Plug.Conn 74 | import Phoenix.Controller 75 | import Phoenix.LiveView.Router 76 | end 77 | end 78 | 79 | def channel do 80 | quote do 81 | use Phoenix.Channel 82 | end 83 | end 84 | 85 | defp view_helpers do 86 | quote do 87 | # Use all HTML functionality (forms, tags, etc) 88 | use Phoenix.HTML 89 | 90 | # Import LiveView and .heex helpers (live_render, live_patch, <.form>, etc) 91 | import Phoenix.LiveView.Helpers 92 | import Phoenix.Component 93 | 94 | # Import basic rendering functionality (render, render_layout, etc) 95 | import Phoenix.View 96 | 97 | import HanyWeb.ErrorHelpers 98 | alias HanyWeb.Router.Helpers, as: Routes 99 | end 100 | end 101 | 102 | @doc """ 103 | When used, dispatch to the appropriate controller/view/etc. 104 | """ 105 | defmacro __using__(which) when is_atom(which) do 106 | apply(__MODULE__, which, []) 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /apps/hany_web/lib/hany_web/application.ex: -------------------------------------------------------------------------------- 1 | defmodule HanyWeb.Application do 2 | # See https://hexdocs.pm/elixir/Application.html 3 | # for more information on OTP Applications 4 | @moduledoc false 5 | 6 | use Application 7 | 8 | @impl true 9 | def start(_type, _args) do 10 | children = [ 11 | # Start the Telemetry supervisor 12 | HanyWeb.Telemetry, 13 | # Start the Endpoint (http/https) 14 | HanyWeb.Endpoint 15 | # Start a worker by calling: HanyWeb.Worker.start_link(arg) 16 | # {HanyWeb.Worker, arg} 17 | ] 18 | 19 | # See https://hexdocs.pm/elixir/Supervisor.html 20 | # for other strategies and supported options 21 | opts = [strategy: :one_for_one, name: HanyWeb.Supervisor] 22 | Supervisor.start_link(children, opts) 23 | end 24 | 25 | # Tell Phoenix to update the endpoint configuration 26 | # whenever the application is updated. 27 | @impl true 28 | def config_change(changed, _new, removed) do 29 | HanyWeb.Endpoint.config_change(changed, removed) 30 | :ok 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /apps/hany_web/lib/hany_web/controllers/page_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule HanyWeb.PageController do 2 | use HanyWeb, :controller 3 | 4 | def index(conn, _params) do 5 | render(conn, "index.html") 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /apps/hany_web/lib/hany_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule HanyWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :hany_web 3 | 4 | # The session will be stored in the cookie and signed, 5 | # this means its contents can be read but not tampered with. 6 | # Set :encryption_salt if you would also like to encrypt it. 7 | @session_options [ 8 | store: :cookie, 9 | key: "_hany_web_key", 10 | signing_salt: "q5ipgJRs" 11 | ] 12 | 13 | socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]] 14 | 15 | # Serve at "/" the static files from "priv/static" directory. 16 | # 17 | # You should set gzip to true if you are running phx.digest 18 | # when deploying your static files in production. 19 | plug Plug.Static, 20 | at: "/", 21 | from: :hany_web, 22 | gzip: false, 23 | only: ~w(assets fonts images favicon.ico robots.txt) 24 | 25 | # Code reloading can be explicitly enabled under the 26 | # :code_reloader configuration of your endpoint. 27 | if code_reloading? do 28 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket 29 | plug Phoenix.LiveReloader 30 | plug Phoenix.CodeReloader 31 | plug Phoenix.Ecto.CheckRepoStatus, otp_app: :hany_web 32 | end 33 | 34 | plug Phoenix.LiveDashboard.RequestLogger, 35 | param_key: "request_logger", 36 | cookie_key: "request_logger" 37 | 38 | plug Plug.RequestId 39 | plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] 40 | 41 | plug Plug.Parsers, 42 | parsers: [:urlencoded, :multipart, :json], 43 | pass: ["*/*"], 44 | json_decoder: Phoenix.json_library() 45 | 46 | plug Plug.MethodOverride 47 | plug Plug.Head 48 | plug Plug.Session, @session_options 49 | plug HanyWeb.Router 50 | end 51 | -------------------------------------------------------------------------------- /apps/hany_web/lib/hany_web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule HanyWeb.Router do 2 | use HanyWeb, :router 3 | 4 | pipeline :browser do 5 | plug :accepts, ["html"] 6 | plug :fetch_session 7 | plug :fetch_live_flash 8 | plug :put_root_layout, {HanyWeb.LayoutView, :root} 9 | plug :protect_from_forgery 10 | plug :put_secure_browser_headers 11 | end 12 | 13 | pipeline :api do 14 | plug :accepts, ["json"] 15 | end 16 | 17 | scope "/", HanyWeb do 18 | pipe_through :browser 19 | 20 | get "/", PageController, :index 21 | end 22 | 23 | # Other scopes may use custom stacks. 24 | # scope "/api", HanyWeb do 25 | # pipe_through :api 26 | # end 27 | 28 | # Enables LiveDashboard only for development 29 | # 30 | # If you want to use the LiveDashboard in production, you should put 31 | # it behind authentication and allow only admins to access it. 32 | # If your application does not have an admins-only section yet, 33 | # you can use Plug.BasicAuth to set up some basic authentication 34 | # as long as you are also using SSL (which you should anyway). 35 | if Mix.env() in [:dev, :test] do 36 | import Phoenix.LiveDashboard.Router 37 | 38 | scope "/" do 39 | pipe_through :browser 40 | 41 | live_dashboard "/dashboard", metrics: HanyWeb.Telemetry 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /apps/hany_web/lib/hany_web/telemetry.ex: -------------------------------------------------------------------------------- 1 | defmodule HanyWeb.Telemetry do 2 | use Supervisor 3 | import Telemetry.Metrics 4 | 5 | def start_link(arg) do 6 | Supervisor.start_link(__MODULE__, arg, name: __MODULE__) 7 | end 8 | 9 | @impl true 10 | def init(_arg) do 11 | children = [ 12 | # Telemetry poller will execute the given period measurements 13 | # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics 14 | {:telemetry_poller, measurements: periodic_measurements(), period: 10_000} 15 | # Add reporters as children of your supervision tree. 16 | # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()} 17 | ] 18 | 19 | Supervisor.init(children, strategy: :one_for_one) 20 | end 21 | 22 | def metrics do 23 | [ 24 | # Phoenix Metrics 25 | summary("phoenix.endpoint.stop.duration", 26 | unit: {:native, :millisecond} 27 | ), 28 | summary("phoenix.router_dispatch.stop.duration", 29 | tags: [:route], 30 | unit: {:native, :millisecond} 31 | ), 32 | 33 | # Database Metrics 34 | summary("hany.repo.query.total_time", 35 | unit: {:native, :millisecond}, 36 | description: "The sum of the other measurements" 37 | ), 38 | summary("hany.repo.query.decode_time", 39 | unit: {:native, :millisecond}, 40 | description: "The time spent decoding the data received from the database" 41 | ), 42 | summary("hany.repo.query.query_time", 43 | unit: {:native, :millisecond}, 44 | description: "The time spent executing the query" 45 | ), 46 | summary("hany.repo.query.queue_time", 47 | unit: {:native, :millisecond}, 48 | description: "The time spent waiting for a database connection" 49 | ), 50 | summary("hany.repo.query.idle_time", 51 | unit: {:native, :millisecond}, 52 | description: 53 | "The time the connection spent waiting before being checked out for the query" 54 | ), 55 | 56 | # VM Metrics 57 | summary("vm.memory.total", unit: {:byte, :kilobyte}), 58 | summary("vm.total_run_queue_lengths.total"), 59 | summary("vm.total_run_queue_lengths.cpu"), 60 | summary("vm.total_run_queue_lengths.io") 61 | ] 62 | end 63 | 64 | defp periodic_measurements do 65 | [ 66 | # A module, function and arguments to be invoked periodically. 67 | # This function must call :telemetry.execute/3 and a metric must be added above. 68 | # {HanyWeb, :count_users, []} 69 | ] 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /apps/hany_web/lib/hany_web/templates/layout/app.html.heex: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | <%= @inner_content %> 5 |
6 | -------------------------------------------------------------------------------- /apps/hany_web/lib/hany_web/templates/layout/live.html.heex: -------------------------------------------------------------------------------- 1 |
2 | 5 | 6 | 9 | 10 | <%= @inner_content %> 11 |
12 | -------------------------------------------------------------------------------- /apps/hany_web/lib/hany_web/templates/layout/root.html.heex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= live_title_tag assigns[:page_title] || "Hany", suffix: " · Phoenix Framework" %> 9 | 10 | 11 | 12 | 13 |
14 |
15 | 23 | 26 |
27 |
28 | <%= @inner_content %> 29 | 30 | 31 | -------------------------------------------------------------------------------- /apps/hany_web/lib/hany_web/templates/page/index.html.heex: -------------------------------------------------------------------------------- 1 |
2 |

Welcome to Phoenix!

3 |

Peace of mind from prototype to production

4 |
5 | 6 |
7 | 21 | 41 |
42 | -------------------------------------------------------------------------------- /apps/hany_web/lib/hany_web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule HanyWeb.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | use Phoenix.HTML 7 | 8 | @doc """ 9 | Generates tag for inlined form input errors. 10 | """ 11 | def error_tag(form, field) do 12 | Enum.map(Keyword.get_values(form.errors, field), fn error -> 13 | content_tag(:span, translate_error(error), 14 | class: "invalid-feedback", 15 | phx_feedback_for: input_name(form, field) 16 | ) 17 | end) 18 | end 19 | 20 | @doc """ 21 | Translates an error message. 22 | """ 23 | def translate_error({msg, opts}) do 24 | # Because the error messages we show in our forms and APIs 25 | # are defined inside Ecto, we need to translate them dynamically. 26 | Enum.reduce(opts, msg, fn {key, value}, acc -> 27 | String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end) 28 | end) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /apps/hany_web/lib/hany_web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HanyWeb.ErrorView do 2 | use HanyWeb, :view 3 | 4 | # If you want to customize a particular status code 5 | # for a certain format, you may uncomment below. 6 | # def render("500.html", _assigns) do 7 | # "Internal Server Error" 8 | # end 9 | 10 | # By default, Phoenix returns the status message from 11 | # the template name. For example, "404.html" becomes 12 | # "Not Found". 13 | def template_not_found(template, _assigns) do 14 | Phoenix.Controller.status_message_from_template(template) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /apps/hany_web/lib/hany_web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HanyWeb.LayoutView do 2 | use HanyWeb, :view 3 | 4 | # Phoenix LiveDashboard is available only in development by default, 5 | # so we instruct Elixir to not warn if the dashboard route is missing. 6 | @compile {:no_warn_undefined, {Routes, :live_dashboard_path, 2}} 7 | end 8 | -------------------------------------------------------------------------------- /apps/hany_web/lib/hany_web/views/page_view.ex: -------------------------------------------------------------------------------- 1 | defmodule HanyWeb.PageView do 2 | use HanyWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /apps/hany_web/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule HanyWeb.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :hany_web, 7 | version: "0.1.0", 8 | build_path: "../../_build", 9 | config_path: "../../config/config.exs", 10 | deps_path: "../../deps", 11 | lockfile: "../../mix.lock", 12 | elixir: "~> 1.12", 13 | elixirc_paths: elixirc_paths(Mix.env()), 14 | compilers: Mix.compilers(), 15 | start_permanent: Mix.env() == :prod, 16 | aliases: aliases(), 17 | deps: deps() 18 | ] 19 | end 20 | 21 | # Configuration for the OTP application. 22 | # 23 | # Type `mix help compile.app` for more information. 24 | def application do 25 | [ 26 | mod: {HanyWeb.Application, []}, 27 | extra_applications: [:logger, :runtime_tools] 28 | ] 29 | end 30 | 31 | # Specifies which paths to compile per environment. 32 | defp elixirc_paths(:test), do: ["lib", "test/support"] 33 | defp elixirc_paths(_), do: ["lib"] 34 | 35 | # Specifies your project dependencies. 36 | # 37 | # Type `mix help deps` for examples and options. 38 | defp deps do 39 | [ 40 | {:hany, in_umbrella: true, runtime: false}, 41 | {:hany_cluster, in_umbrella: true, runtime: true}, 42 | {:phoenix, "~> 1.6.15"}, 43 | {:phoenix_ecto, "~> 4.4"}, 44 | {:phoenix_html, "~> 3.0"}, 45 | {:phoenix_live_reload, "~> 1.2", only: :dev}, 46 | {:phoenix_live_view, "~> 0.18.3"}, 47 | {:floki, ">= 0.30.0", only: :test}, 48 | {:phoenix_live_dashboard, "~> 0.7.2"}, 49 | {:esbuild, "~> 0.4", runtime: Mix.env() == :dev}, 50 | {:telemetry_metrics, "~> 0.6"}, 51 | {:telemetry_poller, "~> 1.0"}, 52 | {:jason, "~> 1.2"}, 53 | {:plug_cowboy, "~> 2.5.2"} 54 | ] 55 | end 56 | 57 | # Aliases are shortcuts or tasks specific to the current project. 58 | # 59 | # See the documentation for `Mix` for more info on aliases. 60 | defp aliases do 61 | [ 62 | setup: ["deps.get"], 63 | test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"], 64 | "release.setup": ["esbuild default --minify", "phx.digest"] 65 | ] 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /apps/hany_web/priv/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rudebono/elixir-da-example/11af589bbdfc36acda91ad9738b3b00786488d8c/apps/hany_web/priv/static/favicon.ico -------------------------------------------------------------------------------- /apps/hany_web/priv/static/images/phoenix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rudebono/elixir-da-example/11af589bbdfc36acda91ad9738b3b00786488d8c/apps/hany_web/priv/static/images/phoenix.png -------------------------------------------------------------------------------- /apps/hany_web/priv/static/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /apps/hany_web/test/hany_web/controllers/page_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HanyWeb.PageControllerTest do 2 | use HanyWeb.ConnCase 3 | 4 | test "GET /", %{conn: conn} do 5 | conn = get(conn, "/") 6 | assert html_response(conn, 200) =~ "Welcome to Phoenix!" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /apps/hany_web/test/hany_web/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HanyWeb.ErrorViewTest do 2 | use HanyWeb.ConnCase, async: true 3 | 4 | # Bring render/3 and render_to_string/3 for testing custom views 5 | import Phoenix.View 6 | 7 | test "renders 404.html" do 8 | assert render_to_string(HanyWeb.ErrorView, "404.html", []) == "Not Found" 9 | end 10 | 11 | test "renders 500.html" do 12 | assert render_to_string(HanyWeb.ErrorView, "500.html", []) == "Internal Server Error" 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /apps/hany_web/test/hany_web/views/layout_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HanyWeb.LayoutViewTest do 2 | use HanyWeb.ConnCase, async: true 3 | 4 | # When testing helpers, you may want to import Phoenix.HTML and 5 | # use functions such as safe_to_string() to convert the helper 6 | # result into an HTML string. 7 | # import Phoenix.HTML 8 | end 9 | -------------------------------------------------------------------------------- /apps/hany_web/test/hany_web/views/page_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HanyWeb.PageViewTest do 2 | use HanyWeb.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /apps/hany_web/test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule HanyWeb.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | import other functionality to make it easier 8 | to build common data structures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | we enable the SQL sandbox, so changes done to the database 12 | are reverted at the end of every test. If you are using 13 | PostgreSQL, you can even run database tests asynchronously 14 | by setting `use HanyWeb.ConnCase, async: true`, although 15 | this option is not recommended for other databases. 16 | """ 17 | 18 | use ExUnit.CaseTemplate 19 | 20 | using do 21 | quote do 22 | # Import conveniences for testing with connections 23 | import Plug.Conn 24 | import Phoenix.ConnTest 25 | import HanyWeb.ConnCase 26 | 27 | alias HanyWeb.Router.Helpers, as: Routes 28 | 29 | # The default endpoint for testing 30 | @endpoint HanyWeb.Endpoint 31 | end 32 | end 33 | 34 | setup tags do 35 | Hany.DataCase.setup_sandbox(tags) 36 | {:ok, conn: Phoenix.ConnTest.build_conn()} 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /apps/hany_web/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | Ecto.Adapters.SQL.Sandbox.mode(Hany.Repo.Local, :manual) 3 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :hany, ecto_repos: [Hany.Repo.Local] 4 | 5 | config :hany, Hany.Repo.Local, priv: "priv/repo" 6 | 7 | config :hany_web, ecto_repos: [Hany.Repo.Local], generators: [context_app: :hany] 8 | 9 | config :hany_web, HanyWeb.Endpoint, 10 | url: [host: "localhost"], 11 | render_errors: [view: HanyWeb.ErrorView, accepts: ~w(html json), layout: false], 12 | pubsub_server: Hany.PubSub, 13 | live_view: [signing_salt: "XzNU9OtT"] 14 | 15 | config :esbuild, 16 | version: "0.14.29", 17 | default: [ 18 | args: 19 | ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*), 20 | cd: Path.expand("../apps/hany_web/assets", __DIR__), 21 | env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)} 22 | ] 23 | 24 | config :logger, :console, format: "$time $metadata[$level] $message\n", metadata: [:request_id] 25 | 26 | config :phoenix, :json_library, Jason 27 | 28 | config :mime, :types, %{"text/plain" => ["livemd"]} 29 | 30 | config :livebook, 31 | app_service_name: nil, 32 | app_service_url: nil, 33 | authentication_mode: :token, 34 | feature_flags: [], 35 | force_ssl_host: nil, 36 | learn_notebooks: [], 37 | plugs: [], 38 | shutdown_callback: nil, 39 | storage: Livebook.Storage.Ets, 40 | update_instructions_url: nil, 41 | within_iframe: false, 42 | allowed_uri_schemes: [], 43 | iframe_port: 4003 44 | 45 | config :livebook, LivebookWeb.Endpoint, 46 | url: [host: "localhost"], 47 | pubsub_server: Livebook.PubSub, 48 | live_view: [signing_salt: "XzNU9OtT"] 49 | 50 | config :nx, default_backend: EXLA.Backend 51 | 52 | import_config "#{config_env()}.exs" 53 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :hany_cluster, strategy: HanyCluster.Strategy.Epmd 4 | 5 | config :hany, Hany.Repo.Local, 6 | username: "postgres", 7 | password: "postgres", 8 | hostname: "localhost", 9 | database: "hany_dev", 10 | stacktrace: true, 11 | show_sensitive_data_on_connection_error: true, 12 | pool_size: 10, 13 | port: 5432 14 | 15 | config :hany_web, HanyWeb.Endpoint, 16 | http: [ip: {127, 0, 0, 1}, port: 4000], 17 | check_origin: false, 18 | code_reloader: true, 19 | debug_errors: true, 20 | secret_key_base: "y6D26+l7RT5ZokEQjrYw3BZUnNSKYqRgXGv1eRLfBXtulKARLnQnVikk0lcFkrD3", 21 | watchers: [esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]}], 22 | live_reload: [ 23 | patterns: [ 24 | ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", 25 | ~r"lib/hany_web/(live|views)/.*(ex)$", 26 | ~r"lib/hany_web/templates/.*(eex)$" 27 | ] 28 | ] 29 | 30 | config :logger, :console, format: "[$level] $message\n" 31 | 32 | config :phoenix, :plug_init_mode, :runtime 33 | config :phoenix, :stacktrace_depth, 20 34 | 35 | config :livebook, LivebookWeb.Endpoint, 36 | http: [ip: {127, 0, 0, 1}, port: 4002], 37 | pubsub_server: Livebook.PubSub, 38 | server: false 39 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :hany_cluster, strategy: HanyCluster.Strategy.Gce 4 | 5 | config :hany_web, HanyWeb.Endpoint, 6 | url: [scheme: "https", host: "da.rudebono.dev", port: 443], 7 | cache_static_manifest: "priv/static/cache_manifest.json", 8 | check_origin: false, 9 | server: true 10 | 11 | config :logger, level: :info 12 | 13 | config :livebook, LivebookWeb.Endpoint, 14 | url: [scheme: "https", host: "livebook.rudebono.dev", port: 443, path: "/"], 15 | http: [ip: {0, 0, 0, 0, 0, 0, 0, 0}, port: 4002], 16 | check_origin: false, 17 | server: true 18 | -------------------------------------------------------------------------------- /config/runtime.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | if config_env() == :prod do 4 | Livebook.config_runtime() 5 | 6 | config :hany, Hany.Repo.Local, 7 | username: System.get_env("PG_USERNAME", "postgres"), 8 | password: System.get_env("PG_PASSWORD", "postgres"), 9 | hostname: System.get_env("PG_HOSATNAME", "localhost"), 10 | database: "hany_prod", 11 | port: String.to_integer(System.get_env("PG_PORT", "5432")) 12 | 13 | config :hany_web, HanyWeb.Endpoint, 14 | http: [ 15 | ip: {0, 0, 0, 0, 0, 0, 0, 0}, 16 | port: String.to_integer(System.get_env("HANYWEB_PORT", "4000")) 17 | ], 18 | secret_key_base: 19 | System.get_env( 20 | "SECRET_KEY_BASE", 21 | "y6D26+l7RT5ZokEQjrYw3BZUnNSKYqRgXGv1eRLfBXtulKARLnQnVikk0lcFkrD3" 22 | ) 23 | 24 | config :livebook, LivebookWeb.Endpoint, 25 | http: [ 26 | ip: {0, 0, 0, 0, 0, 0, 0, 0}, 27 | port: String.to_integer(System.get_env("LIVEBOOK_PORT", "4002")) 28 | ], 29 | secret_key_base: 30 | System.get_env( 31 | "SECRET_KEY_BASE", 32 | "y6D26+l7RT5ZokEQjrYw3BZUnNSKYqRgXGv1eRLfBXtulKARLnQnVikk0lcFkrD3" 33 | ) 34 | end 35 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :hany_cluster, strategy: HanyCluster.Strategy.Epmd 4 | 5 | config :hany, Hany.Repo.Local, 6 | username: "postgres", 7 | password: "postgres", 8 | hostname: "localhost", 9 | database: "hany_test#{System.get_env("MIX_TEST_PARTITION")}", 10 | pool: Ecto.Adapters.SQL.Sandbox, 11 | pool_size: 10, 12 | port: 5432 13 | 14 | config :hany_web, HanyWeb.Endpoint, 15 | http: [ip: {127, 0, 0, 1}, port: 4001], 16 | secret_key_base: "G1W9Xa9RI016nDE8izj9Qr6GiLhUL8iVF69BLWuMXHwrvhkLL6bVzi9u+IRlOI4w", 17 | server: false 18 | 19 | config :logger, level: :warn 20 | 21 | config :phoenix, :plug_init_mode, :runtime 22 | 23 | config :livebook, LivebookWeb.Endpoint, server: false 24 | 25 | config :livebook, LivebookWeb.Endpoint, 26 | http: [ip: {127, 0, 0, 1}, port: 4002], 27 | pubsub_server: Livebook.PubSub, 28 | server: false 29 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | postgres: 5 | image: postgres:13.10-alpine 6 | ports: 7 | - 5432:5432 8 | environment: 9 | POSTGRES_PASSWORD: postgres 10 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Hany.Umbrella.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | apps_path: "apps", 7 | version: "0.1.0", 8 | start_permanent: Mix.env() == :prod, 9 | deps: deps(), 10 | aliases: aliases(), 11 | releases: releases() 12 | ] 13 | end 14 | 15 | defp deps() do 16 | [ 17 | {:livebook, "~> 0.8.1"}, 18 | {:kino, "~> 0.8.0"} 19 | ] 20 | end 21 | 22 | defp aliases() do 23 | [ 24 | setup: ["cmd mix setup"], 25 | "release.setup": ["cmd mix release.setup"], 26 | "ecto.reset": ["cmd --app hany mix ecto.reset"] 27 | ] 28 | end 29 | 30 | defp releases() do 31 | [ 32 | hany: [ 33 | include_executables_for: [:unix], 34 | applications: [ 35 | livebook: :load, 36 | hany: :permanent, 37 | hany_cluster: :permanent, 38 | hany_ml: :load, 39 | hany_web: :load 40 | ] 41 | ], 42 | hany_web: [ 43 | include_executables_for: [:unix], 44 | applications: [ 45 | livebook: :load, 46 | hany: :load, 47 | hany_cluster: :permanent, 48 | hany_ml: :load, 49 | hany_web: :permanent 50 | ] 51 | ], 52 | hany_ml: [ 53 | include_executables_for: [:unix], 54 | applications: [ 55 | livebook: :load, 56 | hany: :load, 57 | hany_cluster: :permanent, 58 | hany_ml: :permanent, 59 | hany_web: :load 60 | ] 61 | ], 62 | hany_livebook: [ 63 | include_executables_for: [:unix], 64 | applications: [ 65 | livebook: :permanent, 66 | kino: :permanent, 67 | hany: :load, 68 | hany_cluster: :permanent, 69 | hany_ml: :load, 70 | hany_web: :load 71 | ] 72 | ] 73 | ] 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "aws_signature": {:hex, :aws_signature, "0.3.1", "67f369094cbd55ffa2bbd8cc713ede14b195fcfb45c86665cd7c5ad010276148", [:rebar3], [], "hexpm", "50fc4dc1d1f7c2d0a8c63f455b3c66ecd74c1cf4c915c768a636f9227704a674"}, 3 | "axon": {:hex, :axon, "0.3.1", "d2f678871d439ff623c50eecb255e2e89de9e2cd5d4bb7a40d9a80f3a46b86d1", [:mix], [{:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.4.0", [hex: :nx, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1", [hex: :table_rex, repo: "hexpm", optional: true]}], "hexpm", "01e0c085a3f26d0cccd44e29a2f308c537ad71d573bb853130f1fd484caccd55"}, 4 | "bumblebee": {:hex, :bumblebee, "0.1.2", "bdc596400b0039df80400c49a9a3c8f0df2000427006dbbc02ba8c308f2b3881", [:mix], [{:axon, "~> 0.3.1", [hex: :axon, repo: "hexpm", optional: false]}, {:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nx, "~> 0.4.1", [hex: :nx, repo: "hexpm", optional: false]}, {:nx_image, "~> 0.1.0", [hex: :nx_image, repo: "hexpm", optional: false]}, {:progress_bar, "~> 2.0", [hex: :progress_bar, repo: "hexpm", optional: false]}, {:tokenizers, "~> 0.2.0", [hex: :tokenizers, repo: "hexpm", optional: false]}, {:unpickler, "~> 0.1.0", [hex: :unpickler, repo: "hexpm", optional: false]}], "hexpm", "b360f533e30ac3e0a397aeb33f7f7ae100d26d1efe28f91a5774034bac45f29e"}, 5 | "castore": {:hex, :castore, "0.1.20", "62a0126cbb7cb3e259257827b9190f88316eb7aa3fdac01fd6f2dfd64e7f46e9", [:mix], [], "hexpm", "a020b7650529c986c454a4035b6b13a328e288466986307bea3aadb4c95ac98a"}, 6 | "complex": {:hex, :complex, "0.4.3", "84db4aad241099a8785446ac6eacf498bf3a60634a0e45c7745d875714ddbf98", [:mix], [], "hexpm", "2ceda96ebddcc22697974f1a2666d4cc5dfdd34f8cd8c4f9dced037bcb41eeb5"}, 7 | "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, 8 | "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"}, 9 | "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, 10 | "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"}, 11 | "db_connection": {:hex, :db_connection, "2.4.3", "3b9aac9f27347ec65b271847e6baeb4443d8474289bd18c1d6f4de655b70c94d", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c127c15b0fa6cfb32eed07465e05da6c815b032508d4ed7c116122871df73c12"}, 12 | "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, 13 | "earmark_parser": {:hex, :earmark_parser, "1.4.29", "149d50dcb3a93d9f3d6f3ecf18c918fb5a2d3c001b5d3305c926cddfbd33355b", [:mix], [], "hexpm", "4902af1b3eb139016aed210888748db8070b8125c2342ce3dcae4f38dcc63503"}, 14 | "ecto": {:hex, :ecto, "3.9.4", "3ee68e25dbe0c36f980f1ba5dd41ee0d3eb0873bccae8aeaf1a2647242bffa35", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "de5f988c142a3aa4ec18b85a4ec34a2390b65b24f02385c1144252ff6ff8ee75"}, 15 | "ecto_sql": {:hex, :ecto_sql, "3.9.2", "34227501abe92dba10d9c3495ab6770e75e79b836d114c41108a4bf2ce200ad5", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9.2", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1eb5eeb4358fdbcd42eac11c1fbd87e3affd7904e639d77903c1358b2abd3f70"}, 16 | "elixir_make": {:hex, :elixir_make, "0.7.5", "784cc00f5fa24239067cc04d449437dcc5f59353c44eb08f188b2b146568738a", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "c3d63e8d5c92fa3880d89ecd41de59473fa2e83eeb68148155e25e8b95aa2887"}, 17 | "esbuild": {:hex, :esbuild, "0.6.1", "a774bfa7b4512a1211bf15880b462be12a4c48ed753a170c68c63b2c95888150", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "569f7409fb5a932211573fc20e2a930a0d5cf3377c5b4f6506c651b1783a1678"}, 18 | "exla": {:hex, :exla, "0.4.2", "7d5008c36c942de75efddffe4a4e6aac98da722261b7188b23b1363282a146a8", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nx, "~> 0.4.2", [hex: :nx, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:xla, "~> 0.4.0", [hex: :xla, repo: "hexpm", optional: false]}], "hexpm", "c7c5d70073c30ca4fee3981d992d27f2a2c1d8333b012ab8d0f7330c3624ee79"}, 19 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, 20 | "floki": {:hex, :floki, "0.34.2", "5fad07ef153b3b8ec110b6b155ec3780c4b2c4906297d0b4be1a7162d04a7e02", [:mix], [], "hexpm", "26b9d50f0f01796bc6be611ca815c5e0de034d2128e39cc9702eee6b66a4d1c8"}, 21 | "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, 22 | "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, 23 | "livebook": {:hex, :livebook, "0.8.1", "bdbb69189fd04d487fe42f02740911c6d6d3d2e0b28d4e7e9ec484d24ceb581d", [:mix], [{:aws_signature, "0.3.1", [hex: :aws_signature, repo: "hexpm", optional: false]}, {:castore, "0.1.20", [hex: :castore, repo: "hexpm", optional: false]}, {:connection, "1.1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:earmark_parser, "1.4.29", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:ecto, "3.9.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:jason, "1.4.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mint_web_socket, "1.0.2", [hex: :mint_web_socket, repo: "hexpm", optional: false]}, {:phoenix, "1.6.15", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_ecto, "4.4.0", [hex: :phoenix_ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "3.2.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_dashboard, "0.7.2", [hex: :phoenix_live_dashboard, repo: "hexpm", optional: false]}, {:phoenix_live_view, "0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:plug_cowboy, "2.5.2", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:protobuf, "0.8.0", [hex: :protobuf, repo: "hexpm", optional: false]}, {:telemetry_metrics, "0.6.1", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_poller, "1.0.0", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "b92fd0799dd15a425fc56e5abc391fae8825d476087b9972595e9d261158204e"}, 24 | "mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"}, 25 | "mint": {:hex, :mint, "1.4.2", "50330223429a6e1260b2ca5415f69b0ab086141bc76dc2fbf34d7c389a6675b2", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "ce75a5bbcc59b4d7d8d70f8b2fc284b1751ffb35c7b6a6302b5192f8ab4ddd80"}, 26 | "mint_web_socket": {:hex, :mint_web_socket, "1.0.2", "0933a4c82f2376e35569b2255cdce94f2e3f993c0d5b04c360460cb8beda7154", [:mix], [{:mint, ">= 1.4.0", [hex: :mint, repo: "hexpm", optional: false]}], "hexpm", "067c5e15439be060f2ab57c468ee4ab29e39cb20b498ed990cb94f62db0efc3a"}, 27 | "nx": {:hex, :nx, "0.4.2", "444e9cc1b1e95edf8c9d9d9f22635349a0cd60cb6a07d4954f3016b2d6d178d7", [:mix], [{:complex, "~> 0.4.3", [hex: :complex, repo: "hexpm", optional: false]}], "hexpm", "9d8f110cf733c4bbc86f0a5fe08f6537e106c39bbcb6dfabc7ef33f14f12edb3"}, 28 | "nx_image": {:hex, :nx_image, "0.1.0", "ae10fa41fa95126f934d6160ef4320f7db583535fb868415f2562fe19969d245", [:mix], [{:nx, "~> 0.4", [hex: :nx, repo: "hexpm", optional: false]}], "hexpm", "60a2928164cdca540b4c180ff25579b97a5f2a650fc890d40db3e1a7798c93ad"}, 29 | "phoenix": {:hex, :phoenix, "1.6.15", "0a1d96bbc10747fd83525370d691953cdb6f3ccbac61aa01b4acb012474b047d", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d70ab9fbf6b394755ea88b644d34d79d8b146e490973151f248cacd122d20672"}, 30 | "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"}, 31 | "phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"}, 32 | "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.7.2", "97cc4ff2dba1ebe504db72cb45098cb8e91f11160528b980bd282cc45c73b29c", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0e5fdf063c7a3b620c566a30fcf68b7ee02e5e46fe48ee46a6ec3ba382dc05b7"}, 33 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"}, 34 | "phoenix_live_view": {:hex, :phoenix_live_view, "0.18.3", "2e3d009422addf8b15c3dccc65ce53baccbe26f7cfd21d264680b5867789a9c1", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c8845177a866e017dcb7083365393c8f00ab061b8b6b2bda575891079dce81b2"}, 35 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"}, 36 | "phoenix_template": {:hex, :phoenix_template, "1.0.1", "85f79e3ad1b0180abb43f9725973e3b8c2c3354a87245f91431eec60553ed3ef", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "157dc078f6226334c91cb32c1865bf3911686f8bcd6bcff86736f6253e6993ee"}, 37 | "phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"}, 38 | "plug": {:hex, :plug, "1.14.0", "ba4f558468f69cbd9f6b356d25443d0b796fbdc887e03fa89001384a9cac638f", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bf020432c7d4feb7b3af16a0c2701455cbbbb95e5b6866132cb09eb0c29adc14"}, 39 | "plug_cowboy": {:hex, :plug_cowboy, "2.5.2", "62894ccd601cf9597e2c23911ff12798a8a18d237e9739f58a6b04e4988899fe", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ea6e87f774c8608d60c8d34022a7d073bd7680a0a013f049fc62bf35efea1044"}, 40 | "plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"}, 41 | "postgrex": {:hex, :postgrex, "0.16.5", "fcc4035cc90e23933c5d69a9cd686e329469446ef7abba2cf70f08e2c4b69810", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "edead639dc6e882618c01d8fc891214c481ab9a3788dfe38dd5e37fd1d5fb2e8"}, 42 | "progress_bar": {:hex, :progress_bar, "2.0.1", "7b40200112ae533d5adceb80ff75fbe66dc753bca5f6c55c073bfc122d71896d", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "2519eb58a2f149a3a094e729378256d8cb6d96a259ec94841bd69fdc71f18f87"}, 43 | "protobuf": {:hex, :protobuf, "0.8.0", "61b27d6fd50e7b1b2eb0ee17c1f639906121f4ef965ae0994644eb4c68d4647d", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "3644ed846fd6f5e3b5c2cd617aa8344641e230edf812a45365fee7622bccd25a"}, 44 | "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, 45 | "rustler_precompiled": {:hex, :rustler_precompiled, "0.6.1", "160b545bce8bf9a3f1b436b2c10f53574036a0db628e40f393328cbbe593602f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "0dd269fa261c4e3df290b12031c575fff07a542749f7b0e8b744d72d66c43600"}, 46 | "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, 47 | "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, 48 | "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"}, 49 | "tokenizers": {:hex, :tokenizers, "0.2.0", "3aa9811396680f849803f6a3978a310a653059613592710ce5f883d67ff17a33", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.5", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "2496fd44cf96bcefc70e75cf7e34126de8b63ccb9ad35967d6d5d8661cbdb6b7"}, 50 | "unpickler": {:hex, :unpickler, "0.1.0", "c2262c0819e6985b761e7107546cef96a485f401816be5304a65fdd200d5bd6a", [:mix], [], "hexpm", "e2b3f61e62406187ac52afead8a63bfb4e49394028993f3c4c42712743cab79e"}, 51 | "xla": {:hex, :xla, "0.4.4", "c3a8ed1f579bda949df505e49ff65415c8281d991fbd6ae1d8f3c5d0fd155f54", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "484f3f9011db3c9f1ff1e98eecefd382f3882a07ada540fd58803db1d2dab671"}, 52 | } 53 | --------------------------------------------------------------------------------