ruby_garmin_connect 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +21 -0
- data/LICENSE +21 -0
- data/README.md +295 -0
- data/lib/garmin_connect/api/activities.rb +190 -0
- data/lib/garmin_connect/api/badges.rb +94 -0
- data/lib/garmin_connect/api/body_composition.rb +127 -0
- data/lib/garmin_connect/api/devices.rb +118 -0
- data/lib/garmin_connect/api/health.rb +139 -0
- data/lib/garmin_connect/api/metrics.rb +177 -0
- data/lib/garmin_connect/api/user.rb +39 -0
- data/lib/garmin_connect/api/wellness.rb +45 -0
- data/lib/garmin_connect/api/workouts.rb +62 -0
- data/lib/garmin_connect/auth/oauth1_token.rb +43 -0
- data/lib/garmin_connect/auth/oauth2_token.rb +70 -0
- data/lib/garmin_connect/auth/sso.rb +303 -0
- data/lib/garmin_connect/auth/token_store.rb +79 -0
- data/lib/garmin_connect/client.rb +213 -0
- data/lib/garmin_connect/connection.rb +148 -0
- data/lib/garmin_connect/errors.rb +46 -0
- data/lib/garmin_connect/version.rb +5 -0
- data/lib/garmin_connect.rb +35 -0
- metadata +136 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
5
|
+
module GarminConnect
|
|
6
|
+
# The main entry point for interacting with the Garmin Connect API.
|
|
7
|
+
#
|
|
8
|
+
# @example Basic usage
|
|
9
|
+
# client = GarminConnect::Client.new(email: "user@example.com", password: "secret")
|
|
10
|
+
# client.login
|
|
11
|
+
# puts client.daily_summary
|
|
12
|
+
#
|
|
13
|
+
# @example Resume from saved tokens
|
|
14
|
+
# client = GarminConnect::Client.new(token_dir: "~/.garminconnect")
|
|
15
|
+
# client.login
|
|
16
|
+
# puts client.heart_rates
|
|
17
|
+
#
|
|
18
|
+
# @example With MFA handler
|
|
19
|
+
# client = GarminConnect::Client.new(
|
|
20
|
+
# email: "user@example.com",
|
|
21
|
+
# password: "secret",
|
|
22
|
+
# mfa_handler: -> { print "MFA code: "; gets.chomp }
|
|
23
|
+
# )
|
|
24
|
+
# client.login
|
|
25
|
+
class Client
|
|
26
|
+
include API::User
|
|
27
|
+
include API::Health
|
|
28
|
+
include API::Activities
|
|
29
|
+
include API::BodyComposition
|
|
30
|
+
include API::Metrics
|
|
31
|
+
include API::Devices
|
|
32
|
+
include API::Badges
|
|
33
|
+
include API::Workouts
|
|
34
|
+
include API::Wellness
|
|
35
|
+
|
|
36
|
+
DEFAULT_TOKEN_DIR = File.expand_path("~/.garminconnect")
|
|
37
|
+
|
|
38
|
+
attr_reader :connection
|
|
39
|
+
|
|
40
|
+
# @param email [String, nil] Garmin account email
|
|
41
|
+
# @param password [String, nil] Garmin account password
|
|
42
|
+
# @param domain [String] "garmin.com" or "garmin.cn"
|
|
43
|
+
# @param token_dir [String, nil] directory for token persistence (default: ~/.garminconnect)
|
|
44
|
+
# @param token_string [String, nil] base64-encoded token string (alternative to token_dir)
|
|
45
|
+
# @param mfa_handler [Proc, nil] called when MFA is needed; must return the code
|
|
46
|
+
def initialize(email: nil, password: nil, domain: "garmin.com",
|
|
47
|
+
token_dir: DEFAULT_TOKEN_DIR, token_string: nil,
|
|
48
|
+
mfa_handler: nil)
|
|
49
|
+
@email = email
|
|
50
|
+
@password = password
|
|
51
|
+
@domain = domain
|
|
52
|
+
@token_dir = token_dir ? File.expand_path(token_dir) : nil
|
|
53
|
+
@token_string = token_string
|
|
54
|
+
@mfa_handler = mfa_handler
|
|
55
|
+
|
|
56
|
+
@display_name = nil
|
|
57
|
+
@full_name = nil
|
|
58
|
+
@unit_system = nil
|
|
59
|
+
@user_profile_pk = nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Authenticate with Garmin Connect.
|
|
63
|
+
# Attempts to resume from saved tokens first, falling back to a fresh login.
|
|
64
|
+
#
|
|
65
|
+
# @return [self]
|
|
66
|
+
def login
|
|
67
|
+
if resume_from_tokens
|
|
68
|
+
setup_user_info
|
|
69
|
+
elsif @email && @password
|
|
70
|
+
fresh_login
|
|
71
|
+
else
|
|
72
|
+
raise AuthenticationError, "No credentials or saved tokens available"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
self
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Check if the client is authenticated with valid tokens.
|
|
79
|
+
def authenticated?
|
|
80
|
+
@connection && !@oauth2_token.nil?
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Save the current tokens to the configured token directory.
|
|
84
|
+
# @param directory [String, nil] override the default token directory
|
|
85
|
+
def save_tokens(directory = @token_dir)
|
|
86
|
+
raise Error, "No tokens to save" unless @oauth1_token && @oauth2_token
|
|
87
|
+
|
|
88
|
+
Auth::TokenStore.save(
|
|
89
|
+
directory || DEFAULT_TOKEN_DIR,
|
|
90
|
+
oauth1_token: @oauth1_token,
|
|
91
|
+
oauth2_token: @oauth2_token
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Serialize tokens to a base64-encoded string.
|
|
96
|
+
# @return [String]
|
|
97
|
+
def dump_tokens
|
|
98
|
+
raise Error, "No tokens to dump" unless @oauth1_token && @oauth2_token
|
|
99
|
+
|
|
100
|
+
Auth::TokenStore.dumps(oauth1_token: @oauth1_token, oauth2_token: @oauth2_token)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Log out (clears in-memory tokens).
|
|
104
|
+
def logout
|
|
105
|
+
@oauth1_token = nil
|
|
106
|
+
@oauth2_token = nil
|
|
107
|
+
@connection = nil
|
|
108
|
+
@display_name = nil
|
|
109
|
+
@full_name = nil
|
|
110
|
+
@unit_system = nil
|
|
111
|
+
@user_profile_pk = nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private
|
|
115
|
+
|
|
116
|
+
def resume_from_tokens
|
|
117
|
+
oauth1, oauth2 = load_tokens
|
|
118
|
+
return false unless oauth1 && oauth2
|
|
119
|
+
|
|
120
|
+
@oauth1_token = oauth1
|
|
121
|
+
@oauth2_token = oauth2
|
|
122
|
+
@domain = oauth1.domain || @domain
|
|
123
|
+
build_connection
|
|
124
|
+
|
|
125
|
+
true
|
|
126
|
+
rescue GarminConnect::Error
|
|
127
|
+
false
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def load_tokens
|
|
131
|
+
if @token_string
|
|
132
|
+
Auth::TokenStore.loads(@token_string)
|
|
133
|
+
elsif @token_dir && Dir.exist?(@token_dir)
|
|
134
|
+
Auth::TokenStore.load(@token_dir)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def fresh_login
|
|
139
|
+
@oauth1_token, @oauth2_token = Auth::SSO.login(
|
|
140
|
+
email: @email,
|
|
141
|
+
password: @password,
|
|
142
|
+
domain: @domain,
|
|
143
|
+
mfa_handler: @mfa_handler
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
build_connection
|
|
147
|
+
save_tokens if @token_dir
|
|
148
|
+
setup_user_info
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def build_connection
|
|
152
|
+
@connection = Connection.new(
|
|
153
|
+
oauth1_token: @oauth1_token,
|
|
154
|
+
oauth2_token: @oauth2_token,
|
|
155
|
+
domain: @domain,
|
|
156
|
+
token_dir: @token_dir
|
|
157
|
+
)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def setup_user_info
|
|
161
|
+
settings = user_settings
|
|
162
|
+
@unit_system = settings&.dig("userData", "measurementSystem")
|
|
163
|
+
|
|
164
|
+
profile = connection.get("/userprofile-service/userprofile/profile")
|
|
165
|
+
@display_name = profile&.dig("displayName")
|
|
166
|
+
@full_name = profile&.dig("fullName")
|
|
167
|
+
@user_profile_pk = profile&.dig("profileId") || settings&.dig("id")
|
|
168
|
+
rescue HTTPError
|
|
169
|
+
# Non-fatal: display_name may not be available
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Exposed for API modules that need it.
|
|
173
|
+
def user_profile_pk
|
|
174
|
+
@user_profile_pk
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# --- Date helpers ---
|
|
178
|
+
|
|
179
|
+
def today
|
|
180
|
+
Date.today.strftime("%Y-%m-%d")
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def format_date(date)
|
|
184
|
+
case date
|
|
185
|
+
when Date, Time, DateTime
|
|
186
|
+
date.strftime("%Y-%m-%d")
|
|
187
|
+
when String
|
|
188
|
+
date
|
|
189
|
+
else
|
|
190
|
+
date.to_s
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Auto-chunk large date ranges into smaller windows.
|
|
195
|
+
# @param start_date [Date, String]
|
|
196
|
+
# @param end_date [Date, String]
|
|
197
|
+
# @param max_days [Integer] maximum days per chunk
|
|
198
|
+
def chunked_request(start_date, end_date, max_days)
|
|
199
|
+
start_d = start_date.is_a?(Date) ? start_date : Date.parse(start_date.to_s)
|
|
200
|
+
end_d = end_date.is_a?(Date) ? end_date : Date.parse(end_date.to_s)
|
|
201
|
+
results = []
|
|
202
|
+
|
|
203
|
+
while start_d <= end_d
|
|
204
|
+
chunk_end = [start_d + max_days - 1, end_d].min
|
|
205
|
+
chunk = yield(start_d, chunk_end)
|
|
206
|
+
results.concat(Array(chunk))
|
|
207
|
+
start_d = chunk_end + 1
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
results
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "faraday/retry"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module GarminConnect
|
|
8
|
+
# Handles authenticated HTTP requests to the Garmin Connect API.
|
|
9
|
+
# Automatically refreshes expired OAuth2 tokens using the OAuth1 token.
|
|
10
|
+
class Connection
|
|
11
|
+
API_BASE = "https://connectapi.garmin.com"
|
|
12
|
+
USER_AGENT = "GCM-iOS-5.19.1.2"
|
|
13
|
+
|
|
14
|
+
RETRY_OPTIONS = {
|
|
15
|
+
max: 3,
|
|
16
|
+
interval: 0.5,
|
|
17
|
+
backoff_factor: 2,
|
|
18
|
+
retry_statuses: [408, 500, 502, 503, 504]
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
attr_reader :domain
|
|
22
|
+
|
|
23
|
+
def initialize(oauth1_token:, oauth2_token:, domain: "garmin.com", token_dir: nil)
|
|
24
|
+
@oauth1_token = oauth1_token
|
|
25
|
+
@oauth2_token = oauth2_token
|
|
26
|
+
@domain = domain || "garmin.com"
|
|
27
|
+
@token_dir = token_dir
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Make an authenticated GET request.
|
|
31
|
+
def get(path, params: {})
|
|
32
|
+
request(:get, path, params: params)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Make an authenticated POST request with a JSON body.
|
|
36
|
+
def post(path, body: nil, params: {})
|
|
37
|
+
request(:post, path, body: body, params: params)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Make an authenticated PUT request with a JSON body.
|
|
41
|
+
def put(path, body: nil, params: {})
|
|
42
|
+
request(:put, path, body: body, params: params)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Make an authenticated DELETE request.
|
|
46
|
+
def delete(path, params: {})
|
|
47
|
+
request(:delete, path, params: params)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Download raw bytes (for FIT/GPX/TCX file downloads).
|
|
51
|
+
def download(path, params: {})
|
|
52
|
+
ensure_valid_token!
|
|
53
|
+
resp = connection.get(path) do |req|
|
|
54
|
+
req.headers["Authorization"] = @oauth2_token.authorization_header
|
|
55
|
+
req.params = params unless params.empty?
|
|
56
|
+
end
|
|
57
|
+
handle_errors!(resp)
|
|
58
|
+
resp.body
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Upload a file (multipart form).
|
|
62
|
+
def upload(path, file_path:, file_name: nil)
|
|
63
|
+
ensure_valid_token!
|
|
64
|
+
name = file_name || File.basename(file_path)
|
|
65
|
+
payload = { file: Faraday::Multipart::FilePart.new(file_path, nil, name) }
|
|
66
|
+
|
|
67
|
+
resp = upload_connection.post(path) do |req|
|
|
68
|
+
req.headers["Authorization"] = @oauth2_token.authorization_header
|
|
69
|
+
req.body = payload
|
|
70
|
+
end
|
|
71
|
+
handle_errors!(resp)
|
|
72
|
+
parse_response(resp)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def request(method, path, body: nil, params: {})
|
|
78
|
+
ensure_valid_token!
|
|
79
|
+
|
|
80
|
+
resp = connection.public_send(method, path) do |req|
|
|
81
|
+
req.headers["Authorization"] = @oauth2_token.authorization_header
|
|
82
|
+
req.params = params unless params.empty?
|
|
83
|
+
|
|
84
|
+
if body && %i[post put].include?(method)
|
|
85
|
+
req.headers["Content-Type"] = "application/json"
|
|
86
|
+
req.body = body.is_a?(String) ? body : JSON.generate(body)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
handle_errors!(resp)
|
|
91
|
+
parse_response(resp)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def ensure_valid_token!
|
|
95
|
+
return unless @oauth2_token.expired?
|
|
96
|
+
|
|
97
|
+
@oauth2_token = Auth::SSO.refresh(@oauth1_token, domain: @domain)
|
|
98
|
+
Auth::TokenStore.save_oauth2(@token_dir, oauth2_token: @oauth2_token) if @token_dir
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def connection
|
|
102
|
+
@connection ||= Faraday.new(url: api_base) do |f|
|
|
103
|
+
f.request :retry, **RETRY_OPTIONS
|
|
104
|
+
f.adapter Faraday.default_adapter
|
|
105
|
+
end.tap do |conn|
|
|
106
|
+
conn.headers["User-Agent"] = USER_AGENT
|
|
107
|
+
conn.headers["Accept"] = "application/json"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def upload_connection
|
|
112
|
+
@upload_connection ||= Faraday.new(url: api_base) do |f|
|
|
113
|
+
f.request :multipart
|
|
114
|
+
f.request :retry, **RETRY_OPTIONS
|
|
115
|
+
f.adapter Faraday.default_adapter
|
|
116
|
+
end.tap do |conn|
|
|
117
|
+
conn.headers["User-Agent"] = USER_AGENT
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def api_base
|
|
122
|
+
"https://connectapi.#{@domain}"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def parse_response(resp)
|
|
126
|
+
return nil if resp.status == 204
|
|
127
|
+
return resp.body if resp.body.nil? || resp.body.empty?
|
|
128
|
+
|
|
129
|
+
JSON.parse(resp.body)
|
|
130
|
+
rescue JSON::ParserError
|
|
131
|
+
resp.body
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def handle_errors!(resp)
|
|
135
|
+
return if resp.success?
|
|
136
|
+
|
|
137
|
+
error_class = HTTP_ERRORS.fetch(resp.status) do
|
|
138
|
+
resp.status >= 500 ? ServerError : HTTPError
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
raise error_class.new(
|
|
142
|
+
"Garmin API error: HTTP #{resp.status}",
|
|
143
|
+
status: resp.status,
|
|
144
|
+
body: resp.body
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GarminConnect
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
# Authentication & login errors
|
|
7
|
+
class AuthenticationError < Error; end
|
|
8
|
+
class LoginError < AuthenticationError; end
|
|
9
|
+
class MFARequiredError < AuthenticationError
|
|
10
|
+
attr_reader :mfa_html
|
|
11
|
+
|
|
12
|
+
def initialize(message = "MFA code required", mfa_html: nil)
|
|
13
|
+
@mfa_html = mfa_html
|
|
14
|
+
super(message)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class TokenExpiredError < AuthenticationError; end
|
|
19
|
+
|
|
20
|
+
# HTTP response errors
|
|
21
|
+
class HTTPError < Error
|
|
22
|
+
attr_reader :status, :body
|
|
23
|
+
|
|
24
|
+
def initialize(message = nil, status: nil, body: nil)
|
|
25
|
+
@status = status
|
|
26
|
+
@body = body
|
|
27
|
+
super(message || "HTTP #{status}")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class BadRequestError < HTTPError; end
|
|
32
|
+
class UnauthorizedError < HTTPError; end
|
|
33
|
+
class ForbiddenError < HTTPError; end
|
|
34
|
+
class NotFoundError < HTTPError; end
|
|
35
|
+
class TooManyRequestsError < HTTPError; end
|
|
36
|
+
class ServerError < HTTPError; end
|
|
37
|
+
|
|
38
|
+
# Maps HTTP status codes to error classes
|
|
39
|
+
HTTP_ERRORS = {
|
|
40
|
+
400 => BadRequestError,
|
|
41
|
+
401 => UnauthorizedError,
|
|
42
|
+
403 => ForbiddenError,
|
|
43
|
+
404 => NotFoundError,
|
|
44
|
+
429 => TooManyRequestsError
|
|
45
|
+
}.freeze
|
|
46
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "garmin_connect/version"
|
|
4
|
+
require_relative "garmin_connect/errors"
|
|
5
|
+
require_relative "garmin_connect/auth/oauth1_token"
|
|
6
|
+
require_relative "garmin_connect/auth/oauth2_token"
|
|
7
|
+
require_relative "garmin_connect/auth/token_store"
|
|
8
|
+
require_relative "garmin_connect/auth/sso"
|
|
9
|
+
require_relative "garmin_connect/connection"
|
|
10
|
+
require_relative "garmin_connect/api/user"
|
|
11
|
+
require_relative "garmin_connect/api/health"
|
|
12
|
+
require_relative "garmin_connect/api/activities"
|
|
13
|
+
require_relative "garmin_connect/api/body_composition"
|
|
14
|
+
require_relative "garmin_connect/api/metrics"
|
|
15
|
+
require_relative "garmin_connect/api/devices"
|
|
16
|
+
require_relative "garmin_connect/api/badges"
|
|
17
|
+
require_relative "garmin_connect/api/workouts"
|
|
18
|
+
require_relative "garmin_connect/api/wellness"
|
|
19
|
+
require_relative "garmin_connect/client"
|
|
20
|
+
|
|
21
|
+
module GarminConnect
|
|
22
|
+
class << self
|
|
23
|
+
# Convenience method to create and login a client in one step.
|
|
24
|
+
#
|
|
25
|
+
# @example
|
|
26
|
+
# client = GarminConnect.login(email: "user@example.com", password: "secret")
|
|
27
|
+
# client.daily_summary
|
|
28
|
+
#
|
|
29
|
+
# @example Resume from tokens
|
|
30
|
+
# client = GarminConnect.login(token_dir: "~/.garminconnect")
|
|
31
|
+
def login(**options)
|
|
32
|
+
Client.new(**options).login
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ruby_garmin_connect
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- DRBragg
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: faraday
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: faraday-cookie_jar
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 0.0.7
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 0.0.7
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: faraday-follow_redirects
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0.3'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0.3'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: faraday-retry
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '2.0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '2.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: oauth
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '1.1'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '1.1'
|
|
82
|
+
description: A comprehensive Ruby wrapper for the Garmin Connect API, providing access
|
|
83
|
+
to health, fitness, activity, and device data.
|
|
84
|
+
email:
|
|
85
|
+
- drbragg@gmail.com
|
|
86
|
+
executables: []
|
|
87
|
+
extensions: []
|
|
88
|
+
extra_rdoc_files: []
|
|
89
|
+
files:
|
|
90
|
+
- CHANGELOG.md
|
|
91
|
+
- LICENSE
|
|
92
|
+
- README.md
|
|
93
|
+
- lib/garmin_connect.rb
|
|
94
|
+
- lib/garmin_connect/api/activities.rb
|
|
95
|
+
- lib/garmin_connect/api/badges.rb
|
|
96
|
+
- lib/garmin_connect/api/body_composition.rb
|
|
97
|
+
- lib/garmin_connect/api/devices.rb
|
|
98
|
+
- lib/garmin_connect/api/health.rb
|
|
99
|
+
- lib/garmin_connect/api/metrics.rb
|
|
100
|
+
- lib/garmin_connect/api/user.rb
|
|
101
|
+
- lib/garmin_connect/api/wellness.rb
|
|
102
|
+
- lib/garmin_connect/api/workouts.rb
|
|
103
|
+
- lib/garmin_connect/auth/oauth1_token.rb
|
|
104
|
+
- lib/garmin_connect/auth/oauth2_token.rb
|
|
105
|
+
- lib/garmin_connect/auth/sso.rb
|
|
106
|
+
- lib/garmin_connect/auth/token_store.rb
|
|
107
|
+
- lib/garmin_connect/client.rb
|
|
108
|
+
- lib/garmin_connect/connection.rb
|
|
109
|
+
- lib/garmin_connect/errors.rb
|
|
110
|
+
- lib/garmin_connect/version.rb
|
|
111
|
+
homepage: https://github.com/drbragg/garmin_connect
|
|
112
|
+
licenses:
|
|
113
|
+
- MIT
|
|
114
|
+
metadata:
|
|
115
|
+
homepage_uri: https://github.com/drbragg/garmin_connect
|
|
116
|
+
source_code_uri: https://github.com/drbragg/garmin_connect
|
|
117
|
+
changelog_uri: https://github.com/drbragg/garmin_connect/blob/main/CHANGELOG.md
|
|
118
|
+
rubygems_mfa_required: 'true'
|
|
119
|
+
rdoc_options: []
|
|
120
|
+
require_paths:
|
|
121
|
+
- lib
|
|
122
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
123
|
+
requirements:
|
|
124
|
+
- - ">="
|
|
125
|
+
- !ruby/object:Gem::Version
|
|
126
|
+
version: '3.1'
|
|
127
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - ">="
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '0'
|
|
132
|
+
requirements: []
|
|
133
|
+
rubygems_version: 3.6.9
|
|
134
|
+
specification_version: 4
|
|
135
|
+
summary: Ruby client for the Garmin Connect API
|
|
136
|
+
test_files: []
|