From 0b53e3dfd8cb5e021f7ef89bbf6751cd73907dbe Mon Sep 17 00:00:00 2001 From: arathunku Date: Tue, 8 Jul 2025 19:56:55 +0200 Subject: [PATCH] Extend API coverage - activities, sleep --- .github/workflows/elixir-build-and-test.yml | 2 +- CHANGELOG.md | 5 +- README.md | 29 ++++++-- lib/nimrag.ex | 13 ++-- lib/nimrag/api/activity.ex | 32 ++++---- lib/nimrag/api/activity_list.ex | 33 +++++---- lib/nimrag/api/data.ex | 73 ++++++++++++++++++- lib/nimrag/api/profile.ex | 2 +- lib/nimrag/api/sleep_daily.ex | 20 ++--- lib/nimrag/auth.ex | 17 +++-- mix.exs | 4 +- mix.lock | 17 ++--- ...rvice__activities__search__activities.json | 1 - 13 files changed, 170 insertions(+), 78 deletions(-) diff --git a/.github/workflows/elixir-build-and-test.yml b/.github/workflows/elixir-build-and-test.yml index 1717aa3..6ee9b79 100644 --- a/.github/workflows/elixir-build-and-test.yml +++ b/.github/workflows/elixir-build-and-test.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup Elixir Project uses: ./.github/actions/elixir-setup diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ad3d3a..82b0940 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,13 @@ - Add `Nimrag.steps_weekly` - Add `Nimrag.sleep_daily` - Add `Nimrag.user_settings` -- Rename struct `Nimrag.Api.Activity` to `Nimrag.Api.ActivityList` (data struct for elements of activities list) - Add `Nimrag.activity` - Add `Nimrag.activity_details` +## Breaking changes + +- `Nimrag.activity_list/2` returned struct is now `Nimrag.Api.ActivityList` and uses `activity_id` instead of `:id` + ## 0.1.0 (2024-03-29) Init release diff --git a/README.md b/README.md index 0372993..6c7bffb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Nimrag -[![Actions Status](https://github.com/arathunku/nimrag/actions/workflows/elixir-build-and-test.yml/badge.svg)](https://github.com/arathunku/nimrag/actions/workflows/elixir-build-and-test.yml) +[![Actions Status](https://github.com/arathunku/nimrag/actions/workflows/elixir-build-and-test.yml/badge.svg)](https://github.com/arathunku/nimrag/actions/workflows/elixir-build-and-test.yml) [![Hex.pm](https://img.shields.io/hexpm/v/nimrag.svg?style=flat)](https://hex.pm/packages/nimrag) [![Documentation](https://img.shields.io/badge/hex-docs-lightgreen.svg?style=flat)](https://hexdocs.pm/nimrag) [![License](https://img.shields.io/hexpm/l/nimrag.svg?style=flat)](https://github.com/arathunku/nimrag/blob/main/LICENSE.md) @@ -34,11 +34,11 @@ https://github.com/arathunku/nimrag/assets/749393/7246f688-4820-4276-96de-5d8ed7 Garmin doesn't have any official public API for individuals, only businesses. It means we're required to use username, password and (optionally) MFA code to obtain OAuth tokens. OAuth1 token is valid for up to a year and it's used to generate -OAuth2 token that expires quickly, OAuth2 token is used for making the API calls. -After OAuth1 token expires, we need to repeat the authentication process. +OAuth2 token that expires quickly, OAuth2 token is used for making the API calls. +After OAuth1 token expires, we need to repeat the authentication process. -Please see `Nimrag.Auth` docs for more details about authentication, -and see `Nimrag.Credentials` on how to avoid providing plaintext credentials directly in code. +Please see `Nimrag.Auth` docs for more details about authentication, +and see `Nimrag.Credentials` on how to avoid providing plaintext credentials directly in code. ```elixir # If you're using it for the first time, we need to get OAuth Tokens first. @@ -63,7 +63,10 @@ client = Nimrag.Client.new() |> Nimrag.Client.with_auth(Nimrag.Credentials.read_ {:ok, %Nimrag.Api.Profile{} = profile, client} = Nimrag.profile(client) # Fetch your latest activity -{:ok, %Nimrag.Api.Activity{} = activity, client} = Nimrag.last_activity(client) +{:ok, %Nimrag.Api.ActivityList{} = activity, client} = Nimrag.last_activity(client) + +# Fetch your latest activity details +{:ok, %Nimrag.Api.ActivityDetails{} = activity, client} = Nimrag.activity_details(client, activity.activity_id) # Call at the end of the session to cache new OAuth2 token :ok = Nimrag.Credentials.write_fs_oauth2_token(client) @@ -86,7 +89,7 @@ automatically updated when it's near expiration. There's at this moment no extensive coverage of API endpoints, feel free to submit PR with new structs and endpoints, see [Contributing](#contributing). -### Rate limit +### Rate limit By default, Nimrag uses [Hammer](https://github.com/ExHammer/hammer) for rate limiting requests. If you are already using `Hammer`, you can configure backend key via config. @@ -111,7 +114,7 @@ You can discover new endpoints by setting up [mitmproxy](https://mitmproxy.org/) traffic from mobile app or website. You can also take a look at [python-garminconnect](https://github.com/cyberjunky/python-garminconnect/blob/master/garminconnect/__init__.py). -For local setup, the project has minimal dependencies and is easy to install +For local setup, the project has minimal dependencies and is easy to install ```sh # fork and clone the repo @@ -142,6 +145,16 @@ $ mix check 1. Define new [`Schematic`](https://github.com/mhanberg/schematic) schema in `Nimrag.Api`, and ensure all tests pass. + +### Data structure + +- Any method returning API's data returns it as a struct, with known fields. +- When Garmin removes any of the fields, they'll be removed here too. +- Garmin API is consistenly inconsistent where it comes to timestamps - sometimes they're formatted, +sometimes they're unix timestamp, even for the fields on the same name. Nimrag changes all datetimes +without timezone into NaiveDateTime "as is", and all "GMT" timestamps by Garmin are DateTime with Etc/UTC +timezone. All fields with NaiveDateTime/DateTime end with `_at` + ## License Copyright © 2024 Michal Forys diff --git a/lib/nimrag.ex b/lib/nimrag.ex index 9edb16c..4ed0fe1 100644 --- a/lib/nimrag.ex +++ b/lib/nimrag.ex @@ -100,7 +100,8 @@ defmodule Nimrag do Note: this doesn't return the same data structure as a list of activities! """ @spec activity(Client.t(), integer()) :: {:ok, Api.Activity.t(), Client.t()} | error() - def activity(client, id), do: client |> activity_req(id) |> response_as_data(Api.Activity) + def activity(client, id) when is_integer(id) or is_bitstring(id), + do: client |> activity_req(id) |> response_as_data(Api.Activity) def activity_req(client, id), do: get(client, url: "/activity-service/activity/:id", path_params: [id: id]) @@ -110,7 +111,7 @@ defmodule Nimrag do """ @spec activity_details(Client.t(), integer()) :: {:ok, Api.ActivityDetails.t(), Client.t()} | error() - def activity_details(client, id), + def activity_details(client, id) when is_integer(id) or is_bitstring(id), do: client |> activity_details_req(id) |> response_as_data(Api.ActivityDetails) def activity_details_req(client, id), @@ -124,7 +125,7 @@ defmodule Nimrag do {:ok, list(Api.ActivityList.t()), Client.t()} | error() @spec activities(Client.t(), offset :: integer(), limit :: integer()) :: {:ok, list(Api.ActivityList.t()), Client.t()} | error() - def activities(client, offset \\ 0, limit \\ 10) do + def activities(client, offset \\ 0, limit \\ 10) when is_integer(offset) and is_integer(limit) do client |> activities_req(offset, limit) |> response_as_data(Api.ActivityList) end @@ -163,7 +164,8 @@ defmodule Nimrag do {:ok, binary(), Client.t()} | error() @spec download_activity(Client.t(), activity_id :: integer(), :csv) :: {:ok, binary(), Client.t()} | error() - def download_activity(client, activity_id, :raw) do + def download_activity(client, activity_id, :raw) + when is_integer(activity_id) or is_bitstring(activity_id) do with {:ok, %{body: body, status: 200}, client} <- download_activity_req(client, prefix_url: "download-service/files/activity", @@ -210,7 +212,8 @@ defmodule Nimrag do {:ok, list(Api.SleepDaily.t()), Client.t()} | error() @spec sleep_daily(Client.t(), username :: String.t(), date :: Date.t(), integer()) :: {:ok, list(Api.SleepDaily.t()), Client.t()} | error() - def sleep_daily(client, username, date \\ Date.utc_today(), buffer_minutes \\ 60) do + def sleep_daily(client, username, date \\ Date.utc_today(), buffer_minutes \\ 60) + when is_bitstring(username) do client |> sleep_daily_req(username, date, buffer_minutes) |> response_as_data(Api.SleepDaily) end diff --git a/lib/nimrag/api/activity.ex b/lib/nimrag/api/activity.ex index e1bd125..4e26686 100644 --- a/lib/nimrag/api/activity.ex +++ b/lib/nimrag/api/activity.ex @@ -6,43 +6,47 @@ defmodule Nimrag.Api.Activity do @type t() :: %__MODULE__{ distance: float(), duration: float(), - average_hr: float(), - max_hr: float(), - elevation_gain: float(), - elevation_loss: float() + average_hr: nil | float(), + max_hr: nil | float(), + elevation_gain: nil | float(), + elevation_loss: nil | float(), + start_local_at: NaiveDateTime.t(), + start_at: DateTime.t() } - defstruct ~w( - id distance duration average_hr max_hr elevation_gain elevation_loss - )a + defstruct ~w(distance duration average_hr max_hr elevation_gain elevation_loss start_local_at start_at)a def schematic() do schema(__MODULE__, %{ field(:distance) => float(), field(:duration) => float(), - {"maxHR", :max_hr} => float(), - {"averageHR", :average_hr} => float(), - field(:elevation_gain) => float(), - field(:elevation_loss) => float() + optional({"maxHR", :max_hr}) => nullable(float()), + optional({"averageHR", :average_hr}) => nullable(float()), + optional(field(:elevation_gain)) => nullable(float()), + optional(field(:elevation_loss)) => nullable(float()), + {"startTimeLocal", :start_local_at} => naive_datetime(), + {"startTimeGMT", :start_at} => gmt_datetime_as_datetime() }) end end @type t() :: %__MODULE__{ - id: integer(), + activity_id: integer(), activity_name: String.t(), activity_type: ActivityType.t(), + description: nil | String.t(), summary: __MODULE__.Summary.t() } defstruct ~w( - id activity_name activity_type summary + activity_id activity_name activity_type summary description )a def schematic() do schema(__MODULE__, %{ - {"activityId", :id} => int(), + field(:activity_id) => int(), field(:activity_name) => str(), + optional(field(:description)) => nullable(str()), {"activityTypeDTO", :activity_type} => ActivityType.schematic(), {"summaryDTO", :summary} => __MODULE__.Summary.schematic() }) diff --git a/lib/nimrag/api/activity_list.ex b/lib/nimrag/api/activity_list.ex index 8455437..15774ee 100644 --- a/lib/nimrag/api/activity_list.ex +++ b/lib/nimrag/api/activity_list.ex @@ -3,38 +3,39 @@ defmodule Nimrag.Api.ActivityList do alias Nimrag.Api.ActivityType @type t() :: %__MODULE__{ - id: integer(), + activity_id: integer(), distance: float(), duration: float(), activity_name: String.t(), begin_at: DateTime.t(), start_local_at: NaiveDateTime.t(), - average_hr: float(), - max_hr: float(), - elevation_gain: float(), - elevation_loss: float(), + average_hr: nil | float(), + max_hr: nil | float(), + elevation_gain: nil | float(), + elevation_loss: nil | float(), description: nil | String.t(), activity_type: ActivityType.t() } defstruct ~w( - id distance duration begin_at start_local_at activity_name - average_hr max_hr elevation_gain elevation_loss description activity_type + activity_id distance duration begin_at start_local_at activity_name description + average_hr max_hr elevation_gain elevation_loss activity_type )a def schematic() do schema(__MODULE__, %{ - {"beginTimestamp", :begin_at} => timestamp_datetime(), + # TODO: check methods + {"beginTimestamp", :begin_at} => timestamp_as_datetime(), {"startTimeLocal", :start_local_at} => naive_datetime(), - {"activityId", :id} => int(), + field(:activity_id) => int(), field(:activity_name) => str(), - :distance => float(), - :duration => float(), - {"averageHR", :average_hr} => float(), - {"maxHR", :max_hr} => float(), - field(:elevationGain) => float(), - field(:elevationLoss) => float(), - field(:description) => nullable(str()), + optional(field(:description)) => nullable(str()), + field(:distance) => float(), + field(:duration) => float(), + optional({"averageHR", :average_hr}) => nullable(float()), + optional({"maxHR", :max_hr}) => nullable(float()), + optional(field(:elevationGain)) => nullable(float()), + optional(field(:elevationLoss)) => nullable(float()), field(:activity_type) => ActivityType.schematic() }) end diff --git a/lib/nimrag/api/data.ex b/lib/nimrag/api/data.ex index 0f8deea..1e35d2f 100644 --- a/lib/nimrag/api/data.ex +++ b/lib/nimrag/api/data.ex @@ -26,11 +26,14 @@ defmodule Nimrag.Api.Data do end end - def timestamp_datetime() do + def timestamp_as_datetime() do raw( fn - i, :to -> is_number(i) and match?({:ok, _}, DateTime.from_unix(i, :millisecond)) - i, :from -> match?(%DateTime{}, i) + i, :to -> + is_number(i) and match?({:ok, _}, DateTime.from_unix(i, :millisecond)) + + i, :from -> + match?(%DateTime{}, i) end, transform: fn i, :to -> @@ -43,6 +46,28 @@ defmodule Nimrag.Api.Data do ) end + def timestamp_as_naive_datetime() do + raw( + fn + i, :to -> + is_number(i) and + match?({:ok, _}, DateTime.from_unix(i, :millisecond)) + + i, :from -> + match?(%NaiveDateTime{}, i) + end, + transform: fn + i, :to -> + {:ok, dt} = DateTime.from_unix(i, :millisecond) + DateTime.to_naive(dt) + + v, :from -> + DateTime.from_naive(v, "Etc/UTC") + |> DateTime.to_unix(:millisecond) + end + ) + end + def naive_datetime() do raw( fn @@ -60,6 +85,48 @@ defmodule Nimrag.Api.Data do ) end + def gmt_timestamp_as_datetime() do + raw( + fn + i, :to -> + is_binary(i) and + match?({:ok, _}, NaiveDateTime.from_iso8601(i) |> DateTime.from_naive!("Etc/UTC")) + + i, :from -> + match?(%NaiveDateTime{}, i) + end, + transform: fn + i, :to -> + {:ok, dt} = NaiveDateTime.from_iso8601(i) + DateTime.from_naive!(dt, "Etc/UTC") + + i, :from -> + DateTime.to_iso8601(i) + end + ) + end + + def gmt_datetime_as_datetime() do + raw( + fn + i, :to -> + is_binary(i) and + match?({:ok, _}, NaiveDateTime.from_iso8601!(i) |> DateTime.from_naive("Etc/UTC")) + + i, :from -> + match?(%NaiveDateTime{}, i) + end, + transform: fn + i, :to -> + {:ok, dt} = NaiveDateTime.from_iso8601(i) + DateTime.from_naive!(dt, "Etc/UTC") + + i, :from -> + DateTime.to_iso8601(i) + end + ) + end + def date() do raw( fn diff --git a/lib/nimrag/api/profile.ex b/lib/nimrag/api/profile.ex index 115a8ca..2d27673 100644 --- a/lib/nimrag/api/profile.ex +++ b/lib/nimrag/api/profile.ex @@ -152,7 +152,7 @@ defmodule Nimrag.Api.Profile do field(:favorite_activity_types) => list(str()), field(:favorite_cycling_activity_types) => list(str()), field(:full_name) => str(), - field(:garmin_guid) => nullable(str()), + optional(field(:garmin_guid)) => nullable(str()), field(:id) => int(), field(:level_is_viewed) => bool(), field(:level_point_threshold) => int(), diff --git a/lib/nimrag/api/sleep_daily.ex b/lib/nimrag/api/sleep_daily.ex index 397db68..5eb2389 100644 --- a/lib/nimrag/api/sleep_daily.ex +++ b/lib/nimrag/api/sleep_daily.ex @@ -9,10 +9,10 @@ defmodule Nimrag.Api.SleepDaily do calendar_date: Date.t(), sleep_time_seconds: integer(), nap_time_seconds: integer(), - sleep_start_timestamp_local: DateTime.t(), - sleep_end_timestamp_local: DateTime.t() + sleep_start_local_at: NaiveDateTime.t(), + sleep_end_local_at: NaiveDateTime.t() } - defstruct ~w(id calendar_date sleep_time_seconds nap_time_seconds sleep_start_timestamp_local sleep_end_timestamp_local)a + defstruct ~w(id calendar_date sleep_time_seconds nap_time_seconds sleep_start_local_at sleep_end_local_at)a def schematic() do schema(__MODULE__, %{ @@ -20,8 +20,8 @@ defmodule Nimrag.Api.SleepDaily do field(:calendar_date) => date(), field(:sleep_time_seconds) => int(), field(:nap_time_seconds) => int(), - field(:sleep_start_timestamp_local) => timestamp_datetime(), - field(:sleep_end_timestamp_local) => timestamp_datetime() + {"sleepStartTimestampLocal", :sleep_start_local_at} => timestamp_as_naive_datetime() + # {"sleepEndTimestampLocal", :sleep_end_local_at} => timestamp_as_naive_datetime() }) end end @@ -30,16 +30,16 @@ defmodule Nimrag.Api.SleepDaily do use Nimrag.Api.Data @type t() :: %__MODULE__{ - start_gmt: DateTime.t(), - end_gmt: DateTime.t(), + start_at: DateTime.t(), + end_at: DateTime.t(), activity_level: float() } - defstruct ~w(start_gmt end_gmt activity_level)a + defstruct ~w(start_at end_at activity_level)a def schematic() do schema(__MODULE__, %{ - {"startGMT", :start_gmt} => naive_datetime(), - {"endGMT", :end_gmt} => naive_datetime(), + {"startGMT", :start_at} => gmt_datetime_as_datetime(), + {"endGMT", :end_at} => gmt_datetime_as_datetime(), field(:activity_level) => float() }) end diff --git a/lib/nimrag/auth.ex b/lib/nimrag/auth.ex index 1ce704b..528f628 100644 --- a/lib/nimrag/auth.ex +++ b/lib/nimrag/auth.ex @@ -45,7 +45,7 @@ defmodule Nimrag.Auth do {:ok, signin_response} <- signin_req(sso, embed_response), {:ok, signin_post_response} <- submit_signin_req(sso, signin_response, credentials), - cookie = get_cookie(signin_response), + cookie = get_cookie_header(signin_response), {:ok, signin_post_response} <- maybe_handle_mfa(sso, signin_post_response, cookie, credentials), {:ok, ticket} <- get_ticket(signin_post_response), @@ -196,7 +196,7 @@ defmodule Nimrag.Auth do uri = response |> get_location() |> URI.parse() sso.client - |> Req.Request.put_header("cookie", Enum.uniq(cookie ++ get_cookie(response))) + |> set_cookie_header(Enum.uniq(cookie ++ get_cookie_header(response))) |> Req.Request.put_header( "referer", "#{sso.url}/verifyMFA/loginEnterMfaCode" @@ -225,7 +225,7 @@ defmodule Nimrag.Auth do ) end - defp get_cookie(%Req.Response{} = response), + defp get_cookie_header(%Req.Response{} = response), do: Req.Response.get_header(response, "set-cookie") defp get_location(%Req.Response{} = response), @@ -247,9 +247,12 @@ defmodule Nimrag.Auth do end end + defp set_cookie_header(%Req.Request{} = req, cookie), + do: Req.Request.put_header(req, "cookie", cookie |> Enum.join("; ")) + defp submit_mfa_req(sso, csrf_token, cookie, mfa_code) do sso.client - |> Req.Request.put_header("cookie", cookie) + |> set_cookie_header(cookie) |> Req.Request.put_header("referer", "#{sso.url}/verifyMFA") |> Req.post( url: "/verifyMFA/loginEnterMfaCode", @@ -270,7 +273,7 @@ defmodule Nimrag.Auth do defp get_mfa(sso, cookie, retry) do sso.client - |> Req.Request.put_header("cookie", cookie) + |> set_cookie_header(cookie) |> Req.Request.put_header("referer", "#{sso.url}/signin") |> Req.get( url: "/verifyMFA/loginEnterMfaCode", @@ -295,7 +298,7 @@ defmodule Nimrag.Auth do defp signin_req(sso, %Req.Response{} = prev_resp) do sso.client - |> Req.Request.put_header("cookie", get_cookie(prev_resp)) + |> set_cookie_header(get_cookie_header(prev_resp)) |> Req.Request.put_header("referer", "#{sso.url}/embed") |> Req.get( url: "/signin", @@ -307,7 +310,7 @@ defmodule Nimrag.Auth do defp submit_signin_req(sso, %Req.Response{} = prev_resp, credentials) do with {:ok, csrf_token} <- get_csrf_token(prev_resp) do sso.client - |> Req.Request.put_header("cookie", get_cookie(prev_resp)) + |> set_cookie_header(get_cookie_header(prev_resp)) |> Req.Request.put_header("referer", "#{sso.url}/signin") |> Req.post( url: "/signin", diff --git a/mix.exs b/mix.exs index 188e057..18db900 100644 --- a/mix.exs +++ b/mix.exs @@ -65,11 +65,11 @@ defmodule Nimrag.MixProject do defp deps do [ - {:req, "~> 0.5.0"}, + {:req, "~> 0.5.10"}, {:oauther, "~> 1.1"}, {:jason, "~> 1.4"}, {:recase, "~> 0.7"}, - {:schematic, "~> 0.3"}, + {:schematic, "~> 0.5.1"}, {:hammer, "~> 6.2"}, {:plug, "~> 1.0", only: [:test]}, {:excoveralls, "~> 0.18.1", only: [:dev, :test], runtime: false}, diff --git a/mix.lock b/mix.lock index 02c5537..e84d8be 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,5 @@ %{ "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, - "castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"}, "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"}, "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, @@ -8,25 +7,25 @@ "ex_doc": {:hex, :ex_doc, "0.33.0", "690562b153153c7e4d455dc21dab86e445f66ceba718defe64b0ef6f0bd83ba0", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "3f69adc28274cb51be37d09b03e4565232862a4b10288a3894587b0131412124"}, "excoveralls": {:hex, :excoveralls, "0.18.1", "a6f547570c6b24ec13f122a5634833a063aec49218f6fff27de9df693a15588c", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "d65f79db146bb20399f23046015974de0079668b9abb2f5aac074d078da60b8d"}, "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, - "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, + "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"}, "hammer": {:hex, :hammer, "6.2.1", "5ae9c33e3dceaeb42de0db46bf505bd9c35f259c8defb03390cd7556fea67ee2", [:mix], [{:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm", "b9476d0c13883d2dc0cc72e786bac6ac28911fba7cc2e04b70ce6a6d9c4b2bdc"}, - "hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"}, + "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"}, "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, - "mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"}, + "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "oauther": {:hex, :oauther, "1.3.0", "82b399607f0ca9d01c640438b34d74ebd9e4acd716508f868e864537ecdb1f76", [:mix], [], "hexpm", "78eb888ea875c72ca27b0864a6f550bc6ee84f2eeca37b093d3d833fbcaec04e"}, - "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, - "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, + "plug": {:hex, :plug, "1.17.0", "a0832e7af4ae0f4819e0c08dd2e7482364937aea6a8a997a679f2cbb7e026b2e", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6692046652a69a00a5a21d0b7e11fcf401064839d59d6b8787f23af55b1e6bc"}, + "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, "recase": {:hex, :recase, "0.7.0", "3f2f719f0886c7a3b7fe469058ec539cb7bbe0023604ae3bce920e186305e5ae", [:mix], [], "hexpm", "36f5756a9f552f4a94b54a695870e32f4e72d5fad9c25e61bc4a3151c08a4e0c"}, - "req": {:hex, :req, "0.5.6", "8fe1eead4a085510fe3d51ad854ca8f20a622aae46e97b302f499dfb84f726ac", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cfaa8e720945d46654853de39d368f40362c2641c4b2153c886418914b372185"}, - "schematic": {:hex, :schematic, "0.3.1", "be633c1472959dc0ace22dd0e1f1445b099991fec39f6d6e5273d35ebd217ac4", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "52c419b5c405286e2d0369b9ca472b00b850c59a8b0bdf0dd69172ad4418d5ea"}, + "req": {:hex, :req, "0.5.10", "a3a063eab8b7510785a467f03d30a8d95f66f5c3d9495be3474b61459c54376c", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "8a604815743f8a2d3b5de0659fa3137fa4b1cffd636ecb69b30b2b9b2c2559be"}, + "schematic": {:hex, :schematic, "0.5.1", "be4b2c03115d5a593459c11a7249a6fbb45855947d9653e9250455dcd7df1d42", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02f913c97e6e04ccdaa02004679a7a16bb16fe0449583ad647e296d8e8961546"}, "styler": {:hex, :styler, "0.11.9", "2595393b94e660cd6e8b582876337cc50ff047d184ccbed42fdad2bfd5d78af5", [:mix], [], "hexpm", "8b7806ba1fdc94d0a75127c56875f91db89b75117fcc67572661010c13e1f259"}, - "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, } diff --git a/test/fixtures/api/__activitylist-service__activities__search__activities.json b/test/fixtures/api/__activitylist-service__activities__search__activities.json index 433f9b1..7e861ae 100644 --- a/test/fixtures/api/__activitylist-service__activities__search__activities.json +++ b/test/fixtures/api/__activitylist-service__activities__search__activities.json @@ -250,7 +250,6 @@ "minCda": null, "normPower": 340.0, "avgVerticalRatio": 8.300000190734863, - "elevationGain": 43.0, "caloriesConsumed": null, "avgFlow": null, "rightBalance": null,