diff --git a/.travis.yml b/.travis.yml index 05c00fa..0978935 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ script: - mix dialyzer --halt-exit-status - mix xref unreachable - mix credo --ignore todo + - mix docs # We want to cache Dialyzer PLTs, in order not to have to rebuild them # every time. Dialyzer will automatically rebuild them when needed. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a50da91 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2019 Game Analytics Limited + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 14eb276..af6612a 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,137 @@ # ElixirDruid +===== [![Build Status](https://travis-ci.com/GameAnalytics/elixir_druid.svg?token=7iC72mSUZcJMSAvPBsAL&branch=master)](https://travis-ci.com/GameAnalytics/elixir_druid) -A library for sending requests to [Druid][druid], based on -[HTTPoison][httpoison]. +An open-source client library for sending requests to [Apache Druid][druid] from applications written in Elixir. The project uses [HTTPoison][httpoison] as an HTTP client for sending queries. [druid]: http://druid.io/ [httpoison]: https://github.com/edgurgel/httpoison +## Getting Started + +Add ElixirDruid as a dependency to your project. + +[//]: # (TODO - Replace GitHub dep with Hex.pm below) + +```elixir +defp deps do + [ + {:elixir_druid, github: "GameAnalytics/elixir_druid"} + ] +end +``` + +## Configuration + +ElixirDruid requires a Druid Broker profile to be defined in the configuration of your application. + +```elixir +config :elixir_druid, + request_timeout: 120_000, + query_priority: 0, + broker_profiles: [ + default: [ + base_url: "https://druid-broker-host:9088", + cacertfile: "path/to/druid-certificate.crt", + http_username: "username", + http_password: "password" + ] + ] +``` + +* `request_timeout`: Query timeout in millis to be used in [`Context`](context-druid-doc-link) of all Druid queries. +* `query_priority`: Priority to be used in [`Context`](context-druid-doc-link) of all Druid queries. + +[context-druid-doc-link]: http://druid.io/docs/latest/querying/query-context.html + ## Usage Build a query like this: ```elixir use ElixirDruid + q = from "my_datasource", query_type: "timeseries", - intervals: ["2018-05-29T00:00:00+00:00/2018-06-05T00:00:00+00:00"], + intervals: ["2019-03-01T00:00:00+00:00/2019-03-04T00:00:00+00:00"], granularity: :day, filter: dimensions.foo == "bar", - aggregations: [event_count: count(), - unique_ids: hyperUnique(:user_unique)] + aggregations: [event_count: count(), + unique_id_count: hyperUnique(:user_unique)] ``` And then send it: ```elixir -ElixirDruid.post_query q, :default +ElixirDruid.post_query(q, :default) ``` -`:default` is a configuration profile pointing to your Druid server. -See `config/config.exs`, where you can change the profile or add new -ones. +Where `:default` is a configuration profile pointing to your Druid server. The default value for the profile argument is `:default`, so if you only need a single configuration you can omit it: ```elixir -ElixirDruid.post_query q +ElixirDruid.post_query(q) ``` + +Response example: +```elixir +{:ok, + [ + %{ + "result" => %{ + "event_count" => 7544, + "unique_id_count" => 43.18210933535 + }, + "timestamp" => "2019-03-01T00:00:00.000Z" + }, + %{ + "result" => %{ + "event_count" => 1051, + "unique_id_count" => 104.02052398847 + }, + "timestamp" => "2019-03-02T00:00:00.000Z" + }, + %{ + "result" => %{ + "event_count" => 4591, + "unique_id_count" => 79.19885795313 + }, + "timestamp" => "2019-03-03T00:00:00.000Z" + } + ]} +``` + +## Troubleshooting + +You can check correctness of your configuration by requesting status from Druid Broker. A successfull response will look like this. + +```elixir +iex(1)> ElixirDruid.status(:default) +{:ok, + %{ + "memory" => %{...}, + "modules" => [...], + "version" => "0.13.0" + }} +``` + +## Contributions +We'd love to accept your contributions in a form of patches, bug reports and new features! + +Before opening a pull request please make sure your changes pass all the tests. + +## License +Except as otherwise noted this software is licensed under the [Apache License, Version 2.0]((http://www.apache.org/licenses/LICENSE-2.0)) + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the +License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. + +The code was Copyright 2018-2019 Game Analytics Limited and/or its affiliates. diff --git a/config/config.exs b/config/config.exs index f5f8590..99275bb 100644 --- a/config/config.exs +++ b/config/config.exs @@ -34,9 +34,9 @@ config :elixir_druid, query_priority: 0, broker_profiles: [ default: [ - base_url: "http://localhost:9088", - cacertfile: "ca.crt", - http_username: "username", - http_password: "secret" + base_url: "https://druid-broker-host:9088", + cacertfile: "path/to/druid-certificate.crt", + http_username: "username", + http_password: "password" ] ] diff --git a/lib/elixir_druid.ex b/lib/elixir_druid.ex index 4de2640..2577424 100644 --- a/lib/elixir_druid.ex +++ b/lib/elixir_druid.ex @@ -6,8 +6,76 @@ end defmodule ElixirDruid do @moduledoc """ - Documentation for ElixirDruid. + Post a query to Druid Broker or request its status. + + Use ElixirDruid.Query to build a query. + + ## Examples + + Build a query like this: + + ```elixir + use ElixirDruid + + q = from "my_datasource", + query_type: "timeseries", + intervals: ["2019-03-01T00:00:00+00:00/2019-03-04T00:00:00+00:00"], + granularity: :day, + filter: dimensions.foo == "bar", + aggregations: [event_count: count(), + unique_id_count: hyperUnique(:user_unique)] + ``` + + And then send it: + + ```elixir + ElixirDruid.post_query(q, :default) + ``` + + Where `:default` is a configuration profile pointing to your Druid server. + + The default value for the profile argument is `:default`, so if you + only need a single configuration you can omit it: + + ```elixir + ElixirDruid.post_query(q) + ``` + + Response example: + ```elixir + {:ok, + [ + %{ + "result" => %{ + "event_count" => 7544, + "unique_id_count" => 43.18210933535 + }, + "timestamp" => "2019-03-01T00:00:00.000Z" + }, + %{ + "result" => %{ + "event_count" => 1051, + "unique_id_count" => 104.02052398847 + }, + "timestamp" => "2019-03-02T00:00:00.000Z" + }, + %{ + "result" => %{ + "event_count" => 4591, + "unique_id_count" => 79.19885795313 + }, + "timestamp" => "2019-03-03T00:00:00.000Z" + } + ]} + ``` + + To request status from Broker run + ```elixir + ElixirDruid.status(:default) + ``` + """ + @moduledoc since: "1.0.0" @spec post_query(%ElixirDruid.Query{}, atom()) :: {:ok, term()} | {:error, HTTPoison.Error.t() | Jason.DecodeError.t() | ElixirDruid.Error.t()} @@ -29,7 +97,7 @@ defmodule ElixirDruid do @spec status(atom) :: {:ok, term()} | {:error, HTTPoison.Error.t() | Jason.DecodeError.t() | ElixirDruid.Error.t()} - def status(profile) do + def status(profile \\ :default) do url_path = "/status" body = "" headers = [] @@ -38,7 +106,7 @@ defmodule ElixirDruid do end @spec status!(atom) :: term() - def status!(profile) do + def status!(profile \\ :default) do case status(profile) do {:ok, response} -> response {:error, error} -> raise error diff --git a/lib/elixir_druid/query.ex b/lib/elixir_druid/query.ex index 37e68b3..e180c79 100644 --- a/lib/elixir_druid/query.ex +++ b/lib/elixir_druid/query.ex @@ -12,6 +12,53 @@ defmodule ElixirDruid.Query do # A query has type ElixirDruid.query.t() @type t :: %__MODULE__{} + @doc """ + Use `from` macro to build Druid queries. See [Druid documentation](http://druid.io/docs/latest/querying/querying.html) to learn about + available fields and general query object structure. + + ## Examples + + ```elixir + iex(1)> use ElixirDruid + ElixirDruid.Query + iex(2)> q = from "my_datasource", + ...(2)> query_type: "timeseries", + ...(2)> intervals: ["2019-03-01T00:00:00+00:00/2019-03-04T00:00:00+00:00"], + ...(2)> granularity: :day, + ...(2)> filter: dimensions.foo == "bar", + ...(2)> aggregations: [event_count: count(), + ...(2)> unique_id_count: hyperUnique(:user_unique)] + %ElixirDruid.Query{ + aggregations: [ + %{name: :event_count, type: "count"}, + %{fieldName: :user_unique, name: :unique_id_count, type: :hyperUnique} + ], + analysis_types: nil, + bound: nil, + context: %{priority: 0, timeout: 120000}, + data_source: "my_datasource", + dimension: nil, + dimensions: nil, + filter: %{dimension: "foo", type: "selector", value: "bar"}, + granularity: :day, + intervals: ["2019-03-01T00:00:00+00:00/2019-03-04T00:00:00+00:00"], + limit: nil, + limit_spec: nil, + merge: nil, + metric: nil, + post_aggregations: nil, + query: nil, + query_type: "timeseries", + search_dimensions: nil, + sort: nil, + threshold: nil, + to_include: nil, + virtual_columns: nil + } + ``` + + """ + @doc since: "1.0.0" defmacro from(source, kw) do query_fields = List.foldl(kw, [], &build_query/2) quote generated: true, bind_quoted: [source: source, query_fields: query_fields] do diff --git a/mix.exs b/mix.exs index 3526b8a..ebca01e 100644 --- a/mix.exs +++ b/mix.exs @@ -4,10 +4,31 @@ defmodule ElixirDruid.MixProject do def project do [ app: :elixir_druid, - version: "0.1.0", + version: System.cmd("git", ["describe", "--tags"]) |> elem(0) |> String.trim_trailing |> String.trim_leading("v"), elixir: "~> 1.8.1", start_permanent: Mix.env() == :prod, - deps: deps() + description: "Client library for sending requests to Druid.", + source_url: "https://github.com/gameanalytics/elixir_druid", + package: package(), + deps: deps(), + + # Docs + name: "ElixirDruid", + source_url: "https://github.com/GameAnalytics/elixir_druid", + homepage_url: "https://github.com/GameAnalytics/elixir_druid", + docs: [ + main: "ElixirDruid", # The main page in the docs + extras: ["README.md"] + ] + ] + end + + defp package do + [ + files: ["config", "lib", "mix.exs", "README*", "LICENSE*"], + maintainers: ["Magnus Henoch"], + licenses: ["Apache-2.0"], + links: %{github: "https://github.com/gameanalytics/elixir_druid"} ] end @@ -25,7 +46,8 @@ defmodule ElixirDruid.MixProject do {:httpoison, "~> 1.0"}, {:timex, "~> 3.1"}, {:dialyxir, "~> 1.0-rc.3", only: [:dev], runtime: false}, - {:credo, "~> 0.9.3", only: [:dev, :test], runtime: false} + {:credo, "~> 0.9.3", only: [:dev, :test], runtime: false}, + {:ex_doc, "~> 0.19.3", only: :dev, runtime: false}, ] end end diff --git a/mix.lock b/mix.lock index b52535a..6767086 100644 --- a/mix.lock +++ b/mix.lock @@ -4,14 +4,19 @@ "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"}, "credo": {:hex, :credo, "0.9.3", "76fa3e9e497ab282e0cf64b98a624aa11da702854c52c82db1bf24e54ab7c97a", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, "dialyxir": {:hex, :dialyxir, "1.0.0-rc.5", "c9c2379c59cf2dfc74690f48866e33ffb55ff660e5e02405c14614d204efdc4f", [:mix], [{:erlex, "~> 0.2.1", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm"}, + "earmark": {:hex, :earmark, "1.3.2", "b840562ea3d67795ffbb5bd88940b1bed0ed9fa32834915125ea7d02e35888a5", [:mix], [], "hexpm"}, "erlex": {:hex, :erlex, "0.2.1", "cee02918660807cbba9a7229cae9b42d1c6143b768c781fa6cee1eaf03ad860b", [:mix], [], "hexpm"}, + "ex_doc": {:hex, :ex_doc, "0.19.3", "3c7b0f02851f5fc13b040e8e925051452e41248f685e40250d7e40b07b9f8c10", [:mix], [{:earmark, "~> 1.2", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, "gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm"}, "hackney": {:hex, :hackney, "1.13.0", "24edc8cd2b28e1c652593833862435c80661834f6c9344e84b6a2255e7aeef03", [:rebar3], [{:certifi, "2.3.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.2", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, "httpoison": {:hex, :httpoison, "1.2.0", "2702ed3da5fd7a8130fc34b11965c8cfa21ade2f232c00b42d96d4967c39a3a3", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "idna": {:hex, :idna, "5.1.2", "e21cb58a09f0228a9e0b95eaa1217f1bcfc31a1aaa6e1fdf2f53a33f7dbd9494", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, "jason": {:hex, :jason, "1.1.1", "d3ccb840dfb06f2f90a6d335b536dd074db748b3e7f5b11ab61d239506585eb2", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, + "makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, + "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"}, "parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm"}, "poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"},