From 8402b30fbac95ab9e94cc80e0d3cc2ce9329cb0c Mon Sep 17 00:00:00 2001 From: Yordis Prieto Lazo Date: Fri, 7 Apr 2023 21:06:10 -0400 Subject: [PATCH] feat: add OnePiece.Oban package --- apps/one_piece_oban/.formatter.exs | 4 + apps/one_piece_oban/.gitignore | 26 +++ apps/one_piece_oban/CHANGELOG.md | 7 + apps/one_piece_oban/LICENSE | 21 ++ apps/one_piece_oban/README.md | 9 + apps/one_piece_oban/coveralls.json | 5 + apps/one_piece_oban/lib/one_piece/myapp.ex | 4 + apps/one_piece_oban/lib/one_piece/oban.ex | 218 ++++++++++++++++++ .../lib/one_piece/oban/query.ex | 93 ++++++++ apps/one_piece_oban/mix.exs | 112 +++++++++ apps/one_piece_oban/priv/plts/.gitignore | 1 + apps/one_piece_oban/test/pepeg_test.exs | 7 + apps/one_piece_oban/test/test_helper.exs | 1 + mix.lock | 3 + 14 files changed, 511 insertions(+) create mode 100644 apps/one_piece_oban/.formatter.exs create mode 100644 apps/one_piece_oban/.gitignore create mode 100644 apps/one_piece_oban/CHANGELOG.md create mode 100644 apps/one_piece_oban/LICENSE create mode 100644 apps/one_piece_oban/README.md create mode 100644 apps/one_piece_oban/coveralls.json create mode 100644 apps/one_piece_oban/lib/one_piece/myapp.ex create mode 100644 apps/one_piece_oban/lib/one_piece/oban.ex create mode 100644 apps/one_piece_oban/lib/one_piece/oban/query.ex create mode 100644 apps/one_piece_oban/mix.exs create mode 100644 apps/one_piece_oban/priv/plts/.gitignore create mode 100644 apps/one_piece_oban/test/pepeg_test.exs create mode 100644 apps/one_piece_oban/test/test_helper.exs diff --git a/apps/one_piece_oban/.formatter.exs b/apps/one_piece_oban/.formatter.exs new file mode 100644 index 0000000..9799113 --- /dev/null +++ b/apps/one_piece_oban/.formatter.exs @@ -0,0 +1,4 @@ +[ + line_length: 120, + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/apps/one_piece_oban/.gitignore b/apps/one_piece_oban/.gitignore new file mode 100644 index 0000000..23333dd --- /dev/null +++ b/apps/one_piece_oban/.gitignore @@ -0,0 +1,26 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +one_piece_commanded-*.tar + +# Temporary files for e.g. tests +/tmp diff --git a/apps/one_piece_oban/CHANGELOG.md b/apps/one_piece_oban/CHANGELOG.md new file mode 100644 index 0000000..062d744 --- /dev/null +++ b/apps/one_piece_oban/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +## Unreleased + +## v0.1.0 - 2023-04-07 + +- Initial release diff --git a/apps/one_piece_oban/LICENSE b/apps/one_piece_oban/LICENSE new file mode 100644 index 0000000..d979a5c --- /dev/null +++ b/apps/one_piece_oban/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023-Present Straw Hat, LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/one_piece_oban/README.md b/apps/one_piece_oban/README.md new file mode 100644 index 0000000..7b95f1a --- /dev/null +++ b/apps/one_piece_oban/README.md @@ -0,0 +1,9 @@ +# OnePiece.Clock + +A Swiss Army Knife for Oban. + +## Documentation + +### References + +- [API Reference](api-reference.html) diff --git a/apps/one_piece_oban/coveralls.json b/apps/one_piece_oban/coveralls.json new file mode 100644 index 0000000..41d96e3 --- /dev/null +++ b/apps/one_piece_oban/coveralls.json @@ -0,0 +1,5 @@ +{ + "coverage_options": { + "treat_no_relevant_lines_as_covered": true + } +} diff --git a/apps/one_piece_oban/lib/one_piece/myapp.ex b/apps/one_piece_oban/lib/one_piece/myapp.ex new file mode 100644 index 0000000..3fb9873 --- /dev/null +++ b/apps/one_piece_oban/lib/one_piece/myapp.ex @@ -0,0 +1,4 @@ +defmodule MyApp.Oban do + use OnePiece.Oban, + otp_app: :one_piece_oban +end diff --git a/apps/one_piece_oban/lib/one_piece/oban.ex b/apps/one_piece_oban/lib/one_piece/oban.ex new file mode 100644 index 0000000..b5e5df0 --- /dev/null +++ b/apps/one_piece_oban/lib/one_piece/oban.ex @@ -0,0 +1,218 @@ +defmodule OnePiece.Oban do + @moduledoc """ + Provides a simplified interface to the `Oban` library. + """ + + @doc """ + It creates a facade for the `Oban` functions. + + It allows you to avoid having to pass the `t:Oban.name/0` value to the `Oban` functions, as it is automatically set to + the module name. As well as allowing you to configure the `Oban` instance in the application's configuration using + the module name under a given OTP application key. + + ## Examples + + ### In an application module + + defmodule MyApp.Oban do + use OnePiece.Oban, + otp_app: :my_app, + repo: MyApp.Repo + end + + Now you can register the `MyApp.Oban` module in the application's supervision tree: + + defmodule MyApp.Application do + use Application + + def start(_type, _args) do + children = [ + MyApp.Repo, + MyApp.Oban + ] + + opts = [strategy: :one_for_one, name: MyApp.Supervisor] + Supervisor.start_link(children, opts) + end + end + + ### Avoiding the need of passing the `Oban` instance + + Instead of calling `Oban` functions passing the `Oban` instance, you can use the `OnePiece.Oban` module directly: + + OnePiece.Oban.insert(MyApp.MyJob.new(args: %{ "field" => "value" })) + """ + defmacro __using__(opts \\ []) do + {opts, child_opts} = Keyword.split(opts, [:otp_app]) + otp_app = Keyword.fetch!(opts, :otp_app) + + quote do + @doc """ + Returns the Oban child spec for the application. This should be added to the application's supervision tree. + + The `:name` option is ignored and set to `#{inspect(__MODULE__)}`. + + ## Examples + + defmodule MyApp.Application do + use Application + + children = [ + {#{inspect(__MODULE__)}, prefix: "special"} + ] + + opts = [strategy: :one_for_one, name: MyApp.Supervisor] + Supervisor.start_link(children, opts) + end + """ + def child_spec(opts) do + unquote(child_opts) + |> Keyword.merge(Application.get_env(unquote(otp_app), __MODULE__, [])) + |> Keyword.merge(opts) + |> Keyword.put(:name, __MODULE__) + |> Oban.child_spec() + end + + @doc """ + A facade for `Oban.cancel_all_jobs/2` that uses the `#{inspect(__MODULE__)}` module as the `t:Oban.name/0` argument. + """ + @spec cancel_all_jobs(queryable :: Ecto.Queryable.t()) :: {:ok, non_neg_integer()} + def cancel_all_jobs(queryable) do + Oban.insert(__MODULE__, queryable) + end + + @doc """ + A facade for `Oban.cancel_job/2` that uses the `#{inspect(__MODULE__)}` module as the `t:Oban.name/0` argument. + """ + @spec cancel_job(job_or_id :: Oban.Job.t() | integer()) :: :ok + def cancel_job(job_or_id) do + Oban.cancel_job(__MODULE__, job_or_id) + end + + @doc """ + A facade for `Oban.check_queue/2` that uses the `#{inspect(__MODULE__)}` module as the `t:Oban.name/0` argument. + """ + @spec check_queue(opts :: [{:queue, Oban.queue_name()}]) :: Oban.queue_state() + def check_queue(opts) do + Oban.check_queue(__MODULE__, opts) + end + + @doc """ + A facade for `Oban.config/1` that uses the `#{inspect(__MODULE__)}` module as the `t:Oban.name/0` argument. + """ + @spec config :: Oban.Config.t() + def config do + Oban.config(__MODULE__) + end + + @doc """ + A facade for `Oban.drain_queue/2` that uses the `#{inspect(__MODULE__)}` module as the `t:Oban.name/0` argument. + """ + @spec drain_queue(opts :: [Oban.drain_option()]) :: Oban.drain_result() + def drain_queue(opts) do + Oban.drain_queue(__MODULE__, opts) + end + + @doc """ + A facade for `Oban.insert/3` that uses the `#{inspect(__MODULE__)}` module as the `t:Oban.name/0` argument. + """ + @spec insert(changeset :: Oban.Job.changeset(), opts :: Keyword.t()) :: + {:ok, Oban.Job.t()} | {:error, Oban.Job.changeset() | term()} + def insert(changeset, opts \\ []) do + Oban.insert(__MODULE__, changeset, opts) + end + + @doc """ + A facade for `Oban.insert/5` that uses the `#{inspect(__MODULE__)}` module as the `t:Oban.name/0` argument. + """ + @spec insert(Oban.multi(), Oban.multi_name(), Oban.changeset_or_fun(), Keyword.t()) :: Oban.multi() + def insert(multi, multi_name, changeset, opts \\ []) do + Oban.insert(__MODULE__, multi, multi_name, changeset, opts) + end + + @doc """ + A facade for `Oban.insert!/3` that uses the `#{inspect(__MODULE__)}` module as the `t:Oban.name/0` argument. + """ + @spec insert!(Job.changeset(), opts :: Keyword.t()) :: Job.t() + def insert!(changeset, opts \\ []) do + Oban.insert!(__MODULE__, changeset, opts) + end + + @doc """ + A facade for `Oban.insert_all/3` that uses the `#{inspect(__MODULE__)}` module as the `t:Oban.name/0` argument. + """ + @spec insert_all( + Oban.changesets_or_wrapper() | Oban.multi_name(), + Keyword.t() | Oban.changesets_or_wrapper_or_fun() + ) :: [Job.t()] | Oban.multi() + def insert_all(changesets, opts) do + Oban.insert_all(__MODULE__, changesets, opts) + end + + @doc """ + A facade for `Oban.insert_all/5` that uses the `#{inspect(__MODULE__)}` module as the `t:Oban.name/0` argument. + """ + @spec insert_all(Oban.multi(), Oban.multi_name(), Oban.changesets_or_wrapper_or_fun(), Keyword.t()) :: + Oban.multi() + def insert_all(multi, multi_name, changesets, opts) do + Oban.insert_all(__MODULE__, multi, multi_name, changesets, opts) + end + + @doc """ + A facade for `Oban.start_queue/2` that uses the `#{inspect(__MODULE__)}` module as the `t:Oban.name/0` argument. + """ + @spec start_queue(opts :: Keyword.t()) :: :ok + def start_queue(opts) do + Oban.start_queue(__MODULE__, opts) + end + + @doc """ + A facade for `Oban.pause_queue/2` that uses the `#{inspect(__MODULE__)}` module as the `t:Oban.name/0` argument. + """ + @spec pause_queue(opts :: [Oban.queue_option()]) :: :ok + def pause_queue(opts) do + Oban.pause_queue(__MODULE__, opts) + end + + @doc """ + A facade for `Oban.resume_queue/2` that uses the `#{inspect(__MODULE__)}` module as the `t:Oban.name/0` argument. + """ + @spec resume_queue(opts :: [Oban.queue_option()]) :: :ok + def resume_queue(opts) do + Oban.resume_queue(__MODULE__, opts) + end + + @doc """ + A facade for `Oban.scale_queue/2` that uses the `#{inspect(__MODULE__)}` module as the `t:Oban.name/0` argument. + """ + @spec scale_queue(opts :: [Oban.queue_option()]) :: :ok + def scale_queue(opts) do + Oban.scale_queue(__MODULE__, opts) + end + + @doc """ + A facade for `Oban.stop_queue/2` that uses the `#{inspect(__MODULE__)}` module as the `t:Oban.name/0` argument. + """ + @spec stop_queue(opts :: [Oban.queue_option()]) :: :ok + def stop_queue(opts) do + Oban.stop_queue(__MODULE__, opts) + end + + @doc """ + A facade for `Oban.retry_job/2` that uses the `#{inspect(__MODULE__)}` module as the `t:Oban.name/0` argument. + """ + @spec retry_job(job_or_id :: Job.t() | integer()) :: :ok + def retry_job(job_or_id) do + Oban.retry_job(__MODULE__, job_or_id) + end + + @doc """ + A facade for `Oban.retry_all_jobs/2` that uses the `#{inspect(__MODULE__)}` module as the `t:Oban.name/0` argument. + """ + @spec retry_all_jobs(queryable :: Ecto.Queryable.t()) :: {:ok, non_neg_integer()} + def retry_all_jobs(queryable) do + Oban.retry_all_jobs(__MODULE__, queryable) + end + end + end +end diff --git a/apps/one_piece_oban/lib/one_piece/oban/query.ex b/apps/one_piece_oban/lib/one_piece/oban/query.ex new file mode 100644 index 0000000..6044670 --- /dev/null +++ b/apps/one_piece_oban/lib/one_piece/oban/query.ex @@ -0,0 +1,93 @@ +defmodule OnePiece.Oban.Query do + @moduledoc """ + This module provides a series of helper functions to interact with `Oban.Job` queries. + + It simplifies the process of querying `Oban.Job` jobs by providing functions for common querying scenarios. + """ + + import Ecto.Query, only: [from: 2] + + @doc """ + Creates a new Oban.Job query. + """ + @spec new() :: Ecto.Queryable.t() + def new do + Oban.Job + end + + @doc """ + Filters the query by the worker name. + + ## Examples + + OnePiece.Oban.Query.new() + |> Oban.Query.where_worker("MyApp.MyWorker") + |> Oban.cancel_all_jobs() + """ + @spec where_worker(query :: Ecto.Queryable.t(), worker_name :: String.t()) :: Ecto.Query.t() + def where_worker(query, worker_name) do + from(j in query, where: j.worker == ^worker_name) + end + + @doc """ + Filters the query by the given queue name or job module. + + When a module is given, the queue name is inferred from the module `queue` configuration. + + ## Examples + + OnePiece.Oban.Query.new() + |> Oban.Query.where_queue(MyApp.Job) + |> Repo.all() + + OnePiece.Oban.Query.new() + |> Oban.Query.where_queue("default") + |> Repo.all() + """ + @spec where_queue(query :: Ecto.Queryable.t(), queue_name_or_job_mod :: String.t() | module()) :: Ecto.Query.t() + def where_queue(query, queue_name) when is_binary(queue_name) do + from(j in query, where: j.queue == ^queue_name) + end + + def where_queue(query, job_module) when is_atom(job_module) do + queue_name = + job_module + |> queue_name() + |> Kernel.to_string() + + where_queue(query, queue_name) + end + + @doc """ + Filters the query to only include jobs whose arguments contain the given `args` map. + + ## Examples + + OnePiece.Oban.Query.new() + |> Oban.Query.where_queue(MyApp.ConfirmationEmailJob) + |> Oban.Query.where_contains_args(%{"user_id" => "user_id"}) + |> Repo.one() + """ + @spec where_contains_args(query :: Ecto.Queryable.t(), args :: map()) :: Ecto.Query.t() + def where_contains_args(query, args) do + from(j in query, where: fragment("? @> ?", j.args, ^args)) + end + + @doc """ + Filters the query to only include jobs which are in the 'scheduled' state, i.e., jobs that are cancellable. + + ## Examples + + OnePiece.Oban.Query.new() + |> Oban.Query.where_cancellable() + |> Repo.all() + """ + @spec where_cancellable(query :: Ecto.Queryable.t()) :: Ecto.Query.t() + def where_cancellable(query) do + from(j in query, where: j.state in ~w[scheduled]) + end + + defp queue_name(job_module) do + Keyword.get(job_module.__opts__(), :queue) + end +end diff --git a/apps/one_piece_oban/mix.exs b/apps/one_piece_oban/mix.exs new file mode 100644 index 0000000..98fbb61 --- /dev/null +++ b/apps/one_piece_oban/mix.exs @@ -0,0 +1,112 @@ +defmodule OnePiece.Oban.MixProject do + use Mix.Project + + @app :one_piece_oban + @version "0.1.0" + @elixir_version "~> 1.13" + @source_url "https://github.com/straw-hat-team/beam-monorepo" + + def project do + [ + build_path: "../../_build", + config_path: "../../config/config.exs", + deps_path: "../../deps", + lockfile: "../../mix.lock", + name: "OnePiece.Oban", + description: "A Swiss Army Knife for Oban", + app: @app, + version: @version, + elixir: @elixir_version, + elixirc_paths: elixirc_paths(Mix.env()), + start_permanent: Mix.env() == :prod, + deps: deps(), + aliases: aliases(), + test_coverage: test_coverage(), + preferred_cli_env: preferred_cli_env(), + package: package(), + docs: docs(), + dialyzer: dialyzer() + ] + end + + def application do + [ + extra_applications: [:logger] + ] + end + + defp deps do + [ + {:ecto, "~> 3.10"}, + {:oban, "~> 2.15"}, + + # Tools + {:dialyxir, ">= 0.0.0", only: [:dev], runtime: false}, + {:credo, ">= 0.0.0", only: [:dev, :test], runtime: false}, + {:excoveralls, ">= 0.0.0", only: [:test], runtime: false}, + {:ex_doc, ">= 0.0.0", only: [:dev], runtime: false} + ] + end + + defp elixirc_paths(:test), do: ["lib", "test/support"] + defp elixirc_paths(_), do: ["lib"] + + defp aliases do + [ + test: ["test --trace"] + ] + end + + defp test_coverage do + [tool: ExCoveralls] + end + + defp preferred_cli_env do + [ + "coveralls.html": :test, + "coveralls.json": :test, + coveralls: :test + ] + end + + defp dialyzer do + [ + plt_core_path: "priv/plts" + ] + end + + defp package do + [ + name: @app, + files: [ + ".formatter.exs", + "lib", + "mix.exs", + "README*", + "LICENSE*" + ], + maintainers: ["Yordis Prieto"], + licenses: ["MIT"], + links: %{ + "GitHub" => @source_url + } + ] + end + + defp docs do + [ + main: "readme", + homepage_url: @source_url, + source_url_pattern: "#{@source_url}/blob/#{@app}@v#{@version}/apps/#{@app}/%{path}#L%{line}", + extras: [ + "README.md", + "CHANGELOG.md" + ], + groups_for_extras: [ + "How-to": ~r/docs\/how-to\/.?/, + Explanations: ~r/docs\/explanations\/.?/, + References: ~r/docs\/references\/.?/ + ] + ] + end +end diff --git a/apps/one_piece_oban/priv/plts/.gitignore b/apps/one_piece_oban/priv/plts/.gitignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/apps/one_piece_oban/priv/plts/.gitignore @@ -0,0 +1 @@ +* diff --git a/apps/one_piece_oban/test/pepeg_test.exs b/apps/one_piece_oban/test/pepeg_test.exs new file mode 100644 index 0000000..d051110 --- /dev/null +++ b/apps/one_piece_oban/test/pepeg_test.exs @@ -0,0 +1,7 @@ +defmodule PepegTest do + use ExUnit.Case, async: true + + test "the truth" do + assert MyApp.Oban.config() |> dbg() + end +end diff --git a/apps/one_piece_oban/test/test_helper.exs b/apps/one_piece_oban/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/apps/one_piece_oban/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start() diff --git a/mix.lock b/mix.lock index 58897d4..18c0cea 100644 --- a/mix.lock +++ b/mix.lock @@ -4,10 +4,12 @@ "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, "commanded": {:hex, :commanded, "1.4.2", "edc11a5faea8fbaf7afed3b8f422ad49758c12c19f960da9ea93696465a6d49d", [:mix], [{:backoff, "~> 1.1", [hex: :backoff, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_registry, "~> 0.2 or ~> 0.3", [hex: :telemetry_registry, repo: "hexpm", optional: false]}], "hexpm", "4e90854da0aab8294ec2206eea89658367c077625a340cb3e806b4cf712a61fa"}, "credo": {:hex, :credo, "1.6.7", "323f5734350fd23a456f2688b9430e7d517afb313fbd38671b8a4449798a7854", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "41e110bfb007f7eda7f897c10bf019ceab9a0b269ce79f015d54b0dcf4fc7dd3"}, + "db_connection": {:hex, :db_connection, "2.5.0", "bb6d4f30d35ded97b29fe80d8bd6f928a1912ca1ff110831edcd238a1973652c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c92d5ba26cd69ead1ff7582dbb860adeedfff39774105a4f1c92cbb654b55aa2"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "dialyxir": {:hex, :dialyxir, "1.3.0", "fd1672f0922b7648ff9ce7b1b26fcf0ef56dda964a459892ad15f6b4410b5284", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "00b2a4bcd6aa8db9dcb0b38c1225b7277dca9bc370b6438715667071a304696f"}, "earmark_parser": {:hex, :earmark_parser, "1.4.31", "a93921cdc6b9b869f519213d5bc79d9e218ba768d7270d46fdcf1c01bacff9e2", [:mix], [], "hexpm", "317d367ee0335ef037a87e46c91a2269fef6306413f731e8ec11fc45a7efd059"}, "ecto": {:hex, :ecto, "3.10.2", "6b887160281a61aa16843e47735b8a266caa437f80588c3ab80a8a960e6abe37", [: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", "6a895778f0d7648a4b34b486af59a1c8009041fbdf2b17f1ac215eb829c60235"}, + "ecto_sql": {:hex, :ecto_sql, "3.10.1", "6ea6b3036a0b0ca94c2a02613fd9f742614b5cfe494c41af2e6571bb034dd94c", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.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", "f6a25bdbbd695f12c8171eaff0851fa4c8e72eec1e98c7364402dda9ce11c56b"}, "elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ex_doc": {:hex, :ex_doc, "0.29.4", "6257ecbb20c7396b1fe5accd55b7b0d23f44b6aa18017b415cb4c2b91d997729", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2c6699a737ae46cb61e4ed012af931b57b699643b24dabe2400a8168414bc4f5"}, @@ -23,6 +25,7 @@ "mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, + "oban": {:hex, :oban, "2.15.2", "8f934a49db39163633965139c8846d8e24c2beb4180f34a005c2c7c3f69a6aa2", [:mix], [{:ecto_sql, "~> 3.6", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0f4a579ea48fc7489e0d84facf8b01566e142bdc6542d7dabce32c10e664f1e9"}, "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, "plug": {:hex, :plug, "1.14.2", "cff7d4ec45b4ae176a227acd94a7ab536d9b37b942c8e8fa6dfc0fff98ff4d80", [: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", "842fc50187e13cf4ac3b253d47d9474ed6c296a8732752835ce4a86acdf68d13"}, "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},