lexdrill 0.14.0 → 0.16.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 12866aced5d028c5f5951d0a201a7351090cc27adb617c73e317b80df3dcaa0f
4
- data.tar.gz: 2e14d88608088fdbb6bd1a0071171696182deb828b840826bd3e7a7964f1f887
3
+ metadata.gz: 36e9bb140cbbb757b2c9703b8cd122a9fd62a3faa108c4cc0072755391acb65f
4
+ data.tar.gz: b3502832141a5b7e751f69c5b6d8e729c640f5896300053b96f99fe068968b73
5
5
  SHA512:
6
- metadata.gz: 8c8947fc580a75406e2a02b164092d49dc0bf66663ed55f0b5d395374e9f169b6bb330e84cdebedbb0ea91f5af88334c8bfa5f0532699b122b9d29d01eae7d50
7
- data.tar.gz: 1b7c635562627735a1f85bc1656398f8782bcfaead000c24be594ade643748e89b2e0993215c853139fce1c3587758640c5b73e43e11b6de9293aedcb85fcf59
6
+ metadata.gz: f89f5aa67e5ad3170d1cd956a4c0d53ffca66993f5b94ad55a3f9ef6e32bbaa8befb8d522e03506c761826a4273b1f86f72c4e92c2230c6d2c4b2906149bb70b
7
+ data.tar.gz: ac7a2afdfbdff3fe694b4ec90f30eaa7c418707a9370697033cadabf0d2b389f68f5356404ab62afbff30f685d16316399777721861ffe31c2e95970047d974f
data/README.md CHANGED
@@ -182,54 +182,103 @@ anything.
182
182
 
183
183
  ### Google Sheets export
184
184
 
185
- `drill export` mirrors your word list (text only, no stats) into a tab of a
186
- Google Sheet you own, using **your own Google account** — not a service
187
- account key. That distinction matters: a service-account private key would
188
- have to be embedded in this published gem, meaning anyone who `gem install`s
189
- lexdrill would get it too. Instead, `drill` uses the OAuth 2.0 **Device
190
- Authorization Grant** ("visit this URL, enter this code," the same style of
191
- flow `gcloud auth login` uses) you approve access to your own account
192
- once, and the resulting token is cached locally at
193
- `~/.drill.gcp-token.json` (mode `0600`), never published or shared.
185
+ `drill export <sheet-name>` mirrors your word list (text only, no stats)
186
+ into a tab of a Google Sheet you own; `drill import <sheet-name>` reads it
187
+ back. There are two ways to authenticate pick whichever you prefer:
188
+
189
+ - **`drill remote <url>`** a **service account** you create yourself. No
190
+ interactive sign-in, ever; you share the spreadsheet with the service
191
+ account's email address once, and `drill` uses a local private key file
192
+ to sign its own requests.
193
+ - **`drill oauth <url>`** your **personal Google login**, via a one-time
194
+ interactive OAuth approval. No GCP service account needed, but you (or
195
+ anyone using the gem) has to click through a sign-in flow once.
196
+
197
+ If both are configured, whichever you set **more recently** wins — running
198
+ `drill oauth <url>` after `drill remote <url>` switches `export`/`import`
199
+ over to the personal-login flow, and vice versa.
200
+
201
+ Either way, `export` always **overwrites** the named tab (creating it
202
+ first if it doesn't exist yet) with the current word list, one phrase per
203
+ row, so it stays an exact mirror even if the list shrinks. `import` is the
204
+ reverse: it reads column A of the tab (ignoring any other columns, like an
205
+ old export's show counts) and **replaces** your local `.drill.txt` —
206
+ useful for editing the list in Sheets and pulling changes back down, or
207
+ seeding a fresh machine from an existing sheet.
208
+
209
+ #### Option A: service account (`drill remote`)
210
+
211
+ No embedded secret, no interactive consent screen — but it does require a
212
+ one-time Google Cloud Console setup, and the resulting private key file is
213
+ a real secret you must keep local.
214
+
215
+ 1. Go to <https://console.cloud.google.com>, select a project (reuse the
216
+ one from Option B if you've already set that up), and make sure
217
+ **Google Sheets API** is enabled (**APIs & Services → Library**).
218
+ 2. **IAM & Admin → Service Accounts → "+ CREATE SERVICE ACCOUNT"**. Name it
219
+ anything (e.g. `lexdrill-export`); skip granting it any project-level
220
+ roles — it doesn't need any, since access comes from sharing the
221
+ document directly.
222
+ 3. Click into the new service account → **Keys → Add Key → Create new key
223
+ → JSON → Create**. This downloads a `.json` file — **this is a real
224
+ secret**, equivalent to a password. Never commit it, publish it, or
225
+ share it with anyone.
226
+ 4. Note the service account's email (looks like
227
+ `name@your-project-id.iam.gserviceaccount.com` — also the `client_email`
228
+ field in the downloaded JSON).
229
+ 5. Open your target Google Sheet → **Share** → paste that email → grant
230
+ **Editor** access → uncheck "Notify people" → Share.
231
+ 6. Save the downloaded key file to `~/.drill.gcp-service-account.json` on
232
+ your machine (`drill` reads it from that fixed path and sets it to mode
233
+ `0600`; it's never embedded in the gem or committed anywhere).
234
+
235
+ ```bash
236
+ drill remote 'https://docs.google.com/spreadsheets/d/1opBP4APL5SUvepm9qwjIYRNtDZdoY1Ee87F5PWdxaMg/edit?usp=sharing'
237
+ drill export Sheet1
238
+ ```
239
+
240
+ No sign-in prompt — every call authenticates silently by signing a fresh,
241
+ short-lived JWT with the local key file.
242
+
243
+ #### Option B: personal login (`drill oauth`)
244
+
245
+ Uses the OAuth 2.0 **Device Authorization Grant** ("visit this URL, enter
246
+ this code," the same style of flow `gcloud auth login` uses) — you approve
247
+ access to your own account once, and the resulting token is cached locally
248
+ at `~/.drill.gcp-token.json` (mode `0600`), never published or shared.
194
249
 
195
250
  The OAuth client id/secret embedded in `lib/lexdrill/google_auth.rb` are
196
251
  **not** secret for this use — Google's own docs say client credentials for
197
252
  "TVs and Limited Input devices" / installed-app clients aren't treated as
198
- confidential. The actual secret is your personal refresh token, which is
199
- generated only after you interactively approve, and stays on your machine.
200
-
201
- #### One-time Google Cloud setup (you do this yourself)
253
+ confidential (this is fundamentally different from the service account's
254
+ private key above, which genuinely is a secret). The actual secret is your
255
+ personal refresh token, generated only after you interactively approve.
202
256
 
203
- 1. Go to <https://console.cloud.google.com>, create or select a project.
204
- 2. **APIs & Services → Library** → search "Google Sheets API" → Enable.
205
- 3. **APIs & Services → OAuth consent screen**: User type **External**;
257
+ 1. Go to <https://console.cloud.google.com>, create or select a project,
258
+ enable **Google Sheets API**.
259
+ 2. **APIs & Services → OAuth consent screen**: User type **External**;
206
260
  publishing status **Testing** is fine — add your own Google account
207
261
  under "Test users".
208
- 4. **APIs & Services → Credentials → Create Credentials → OAuth client
262
+ 3. **APIs & Services → Credentials → Create Credentials → OAuth client
209
263
  ID** → Application type **"TVs and Limited Input devices"** (no redirect
210
264
  URI needed). Copy the generated Client ID and Client secret into
211
265
  `CLIENT_ID`/`CLIENT_SECRET` in `lib/lexdrill/google_auth.rb`.
212
- 5. "Testing" publishing status has historically imposed a 7-day
266
+ 4. "Testing" publishing status has historically imposed a 7-day
213
267
  refresh-token expiry for some scopes. If `drill export` starts asking
214
268
  you to re-authorize every week, switch the consent screen to **"In
215
269
  production"** — for the `spreadsheets` scope (not a "restricted" scope)
216
270
  this just adds an "unverified app" click-through on first consent, no
217
271
  Google review required.
218
272
 
219
- #### Using it
220
-
221
273
  ```bash
222
- drill remote 'https://docs.google.com/spreadsheets/d/1opBP4APL5SUvepm9qwjIYRNtDZdoY1Ee87F5PWdxaMg/edit?usp=sharing'
274
+ drill oauth 'https://docs.google.com/spreadsheets/d/1opBP4APL5SUvepm9qwjIYRNtDZdoY1Ee87F5PWdxaMg/edit?usp=sharing'
223
275
  drill export Sheet1
224
276
  ```
225
277
 
226
- The first `export` prints a URL and a short code — visit it, sign in with
227
- the Google account that has edit access to that spreadsheet, and approve.
228
- Every export after that is silent (the cached token refreshes itself
229
- automatically). If `<sheet-name>` doesn't exist yet as a tab in the
230
- spreadsheet, `export` creates it; either way it always **overwrites** the
231
- tab's contents with the current word list text (one word/phrase per row, no
232
- stats), so it stays an exact mirror even if the list shrinks.
278
+ The first `export`/`import` prints a URL and a short code — visit it, sign
279
+ in with the Google account that has edit access to that spreadsheet, and
280
+ approve. Every call after that is silent (the cached token refreshes
281
+ itself automatically).
233
282
 
234
283
  ### Commands
235
284
 
@@ -248,5 +297,7 @@ stats), so it stays an exact mirror even if the list shrinks.
248
297
  | `drill stats` | Print all items as `<count>\t<phrase>` (tab-separated), sorted by show count, highest first |
249
298
  | `drill rand <n>` | `drill next` shows a word ~1-in-`n` times (`n=1` is every time, the default) |
250
299
  | `drill go <number>` | Jump so the next `drill next` shows item `<number>` (1-based, see `drill list`) — prints nothing itself; refuses a graduated item; has no effect while `drill beat rand` is active, since that mode ignores the counter entirely |
251
- | `drill remote <url>` | Set the Google Sheet to export to (global, parses the spreadsheet id out of a normal share URL) |
252
- | `drill export <sheet-name>` | Export the word list text to the named tab (created if it doesn't exist), overwriting it; first run triggers a one-time Google device-flow sign-in |
300
+ | `drill remote <url>` | Set the Google Sheet used by the service account flow (global, parses the spreadsheet id out of a normal share URL); whichever of `drill remote`/`drill oauth` was set more recently wins |
301
+ | `drill oauth <url>` | Set the Google Sheet used by the OAuth (personal-login) flow (global, parses the spreadsheet id out of a normal share URL); whichever of `drill remote`/`drill oauth` was set more recently wins |
302
+ | `drill export <sheet-name>` | Export the word list text to the named tab (created if it doesn't exist), overwriting it; uses whichever of `drill remote`/`drill oauth` was configured most recently (first OAuth use triggers a one-time Google device-flow sign-in) |
303
+ | `drill import <sheet-name>` | Replace the local word list with column A of the named tab |
data/lib/lexdrill/cli.rb CHANGED
@@ -19,7 +19,9 @@ class Lexdrill::CLI
19
19
  run_rand: %w[rand],
20
20
  run_go: %w[go],
21
21
  run_remote: %w[remote],
22
- run_export: %w[export]
22
+ run_oauth: %w[oauth],
23
+ run_export: %w[export],
24
+ run_import: %w[import]
23
25
  }.freeze
24
26
 
25
27
  def self.start(argv = ARGV)
@@ -71,9 +73,13 @@ class Lexdrill::CLI
71
73
  drill stats Print items as <count>\t<phrase>, tab-separated, highest count first
72
74
  drill rand <n> drill next shows a word ~1-in-n times (n=1 is every time)
73
75
  drill go <number> Jump so the next `next` shows item <number> (1-based, see drill list)
74
- drill remote <url> Set the Google Sheet to export to (global, ~/.drill.remote)
76
+ drill remote <url> Set the Google Sheet used by a local service account key
77
+ (~/.drill.gcp-service-account.json) — no interactive sign-in
78
+ drill oauth <url> Set the Google Sheet used by the OAuth (personal-login) flow
75
79
  drill export <sheet-name> Export the word list text to the given tab (overwrites its
76
- contents); first run opens a one-time Google device-flow sign-in
80
+ contents); uses whichever of drill remote/drill oauth was set
81
+ more recently (oauth opens a one-time sign-in on first use)
82
+ drill import <sheet-name> Replace the local word list with column A of the given tab
77
83
  HELP
78
84
  0
79
85
  end
@@ -311,13 +317,58 @@ class Lexdrill::CLI
311
317
  1
312
318
  end
313
319
 
320
+ def run_oauth
321
+ url = argv[1]
322
+ return print_oauth_usage unless url
323
+
324
+ Lexdrill::OauthRemote.set(url)
325
+ puts "oauth spreadsheet set: #{Lexdrill::OauthRemote.spreadsheet_id}"
326
+ 0
327
+ rescue ArgumentError
328
+ print_invalid_oauth_url(url)
329
+ end
330
+
331
+ def print_oauth_usage
332
+ warn "usage: drill oauth <google-sheets-url>"
333
+ 1
334
+ end
335
+
336
+ def print_invalid_oauth_url(url)
337
+ warn "drill: could not find a spreadsheet id in #{url.inspect}"
338
+ 1
339
+ end
340
+
341
+ # Uses whichever of `drill remote` (service account) / `drill oauth`
342
+ # (personal login) was set more recently, so switching between them with
343
+ # either command always takes effect. Returns [spreadsheet_id,
344
+ # access_token], or nil if neither is configured.
345
+ def sheets_target
346
+ case latest_remote_kind
347
+ when :remote then [Lexdrill::Remote.spreadsheet_id, Lexdrill::ServiceAccountAuth.fetch_token!]
348
+ when :oauth then [Lexdrill::OauthRemote.spreadsheet_id, Lexdrill::GoogleAuth.ensure_token!]
349
+ end
350
+ end
351
+
352
+ def latest_remote_kind
353
+ remote_set = Lexdrill::Remote.configured?
354
+ oauth_set = Lexdrill::OauthRemote.configured?
355
+ return :remote if remote_set && (!oauth_set || newer?(Lexdrill::Remote::PATH, Lexdrill::OauthRemote::PATH))
356
+ return :oauth if oauth_set
357
+
358
+ nil
359
+ end
360
+
361
+ def newer?(path, other_path) = File.mtime(path) >= File.mtime(other_path)
362
+
314
363
  def run_export
315
364
  sheet_name = argv[1]
316
365
  return print_export_usage unless sheet_name
317
- return print_no_remote unless Lexdrill::Remote.configured?
318
366
 
319
- perform_export(sheet_name)
320
- rescue Lexdrill::GoogleAuth::AuthError => error
367
+ target = sheets_target
368
+ return print_no_remote unless target
369
+
370
+ perform_export(sheet_name, *target)
371
+ rescue Lexdrill::GoogleAuth::AuthError, Lexdrill::ServiceAccountAuth::AuthError => error
321
372
  warn "drill: #{error.message}"
322
373
  1
323
374
  rescue Lexdrill::SheetsClient::ApiError => error
@@ -327,10 +378,9 @@ class Lexdrill::CLI
327
378
  1
328
379
  end
329
380
 
330
- def perform_export(sheet_name)
331
- token = Lexdrill::GoogleAuth.ensure_token!
381
+ def perform_export(sheet_name, spreadsheet_id, token)
332
382
  rows = export_rows
333
- Lexdrill::SheetsClient.overwrite_sheet(Lexdrill::Remote.spreadsheet_id, sheet_name, rows, token)
383
+ Lexdrill::SheetsClient.overwrite_sheet(spreadsheet_id, sheet_name, rows, token)
334
384
  puts "exported #{rows.size} word(s) to #{sheet_name.inspect}"
335
385
  0
336
386
  end
@@ -345,7 +395,45 @@ class Lexdrill::CLI
345
395
  end
346
396
 
347
397
  def print_no_remote
348
- warn "drill: no remote spreadsheet configured; run `drill remote <url>` first"
398
+ warn "drill: no remote spreadsheet configured; run `drill remote <url>` (service account) " \
399
+ "or `drill oauth <url>` (personal login) first"
400
+ 1
401
+ end
402
+
403
+ def run_import
404
+ sheet_name = argv[1]
405
+ return print_import_usage unless sheet_name
406
+
407
+ target = sheets_target
408
+ return print_no_remote unless target
409
+
410
+ perform_import(sheet_name, *target)
411
+ rescue Lexdrill::GoogleAuth::AuthError, Lexdrill::ServiceAccountAuth::AuthError => error
412
+ warn "drill: #{error.message}"
413
+ 1
414
+ rescue Lexdrill::SheetsClient::ApiError => error
415
+ print_sheets_api_error(error, sheet_name)
416
+ rescue Lexdrill::HTTPClient::NetworkError => error
417
+ warn "drill: network error talking to Google (#{error.message})"
418
+ 1
419
+ end
420
+
421
+ def perform_import(sheet_name, spreadsheet_id, token)
422
+ words = Lexdrill::SheetsClient.read_column(spreadsheet_id, sheet_name, token)
423
+ return print_empty_import(sheet_name) if words.empty?
424
+
425
+ File.write(Lexdrill::WordList::PATH, "#{words.join("\n")}\n", encoding: "UTF-8")
426
+ puts "imported #{words.size} word(s) from #{sheet_name.inspect}"
427
+ 0
428
+ end
429
+
430
+ def print_import_usage
431
+ warn "usage: drill import <sheet-name>"
432
+ 1
433
+ end
434
+
435
+ def print_empty_import(sheet_name)
436
+ warn "drill: #{sheet_name.inspect} has no data to import"
349
437
  1
350
438
  end
351
439
 
@@ -353,8 +441,8 @@ class Lexdrill::CLI
353
441
  status = error.status
354
442
  message = error.message
355
443
  case status
356
- when 404 then warn "drill: spreadsheet not found or not accessible with this Google account (check `drill remote`)"
357
- when 403 then warn "drill: access denied — the signed-in Google account needs edit access to the spreadsheet"
444
+ when 404 then warn "drill: spreadsheet not found or not accessible (check `drill remote`/`drill oauth`)"
445
+ when 403 then warn "drill: access denied — make sure the spreadsheet is shared with the right account"
358
446
  when 400 then warn "drill: #{message} (is #{sheet_name.inspect} a real tab in the spreadsheet?)"
359
447
  else warn "drill: Google Sheets API error (#{status}): #{message}"
360
448
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Global "which Google Sheet to export to via the OAuth device-flow login"
4
+ # setting. Lives at ~/.drill.oauth-remote as a plain spreadsheet id, parsed
5
+ # out of a normal Google Sheets share URL. Set via `drill oauth <url>`.
6
+ # See Lexdrill::Remote for the separate, service-account-based flow.
7
+ module Lexdrill::OauthRemote
8
+ PATH = File.join(Dir.home, ".drill.oauth-remote")
9
+ URL_PATTERN = %r{/spreadsheets/d/([a-zA-Z0-9_-]+)}
10
+
11
+ def self.set(url)
12
+ id = extract_id(url)
13
+ raise ArgumentError, "no spreadsheet id found in #{url.inspect}" unless id
14
+
15
+ File.write(PATH, id)
16
+ end
17
+
18
+ def self.extract_id(url)
19
+ url[URL_PATTERN, 1]
20
+ end
21
+
22
+ def self.configured?
23
+ File.exist?(PATH)
24
+ end
25
+
26
+ def self.spreadsheet_id
27
+ File.read(PATH).strip if configured?
28
+ end
29
+ end
@@ -1,7 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Global "which Google Sheet to export to" setting. Lives at ~/.drill.remote
4
- # as a plain spreadsheet id, parsed out of a normal Google Sheets share URL.
3
+ # Global "which Google Sheet to export to via a service account" setting.
4
+ # Lives at ~/.drill.remote as a plain spreadsheet id, parsed out of a normal
5
+ # Google Sheets share URL. Set via `drill remote <url>` — the user shares
6
+ # that spreadsheet with their service account's email directly in Google
7
+ # Sheets, so no interactive Google sign-in is needed at export/import time.
8
+ # See Lexdrill::OauthRemote for the separate, personal-login-based flow.
5
9
  module Lexdrill::Remote
6
10
  PATH = File.join(Dir.home, ".drill.remote")
7
11
  URL_PATTERN = %r{/spreadsheets/d/([a-zA-Z0-9_-]+)}
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "openssl"
5
+ require "base64"
6
+
7
+ # Authenticates to the Sheets API as a Google service account, using the
8
+ # JWT Bearer Token flow (RFC 7523) — no interactive consent, no refresh
9
+ # token to cache: a fresh short-lived JWT is signed and exchanged for an
10
+ # access token on every call. The service account's private key lives in a
11
+ # JSON file the user downloads themselves from Google Cloud Console and
12
+ # saves locally at PATH below — it is never embedded in gem source, never
13
+ # published, and lexdrill never writes or transmits it anywhere except in
14
+ # the signed JWT sent directly to Google's own token endpoint.
15
+ module Lexdrill::ServiceAccountAuth
16
+ PATH = File.join(Dir.home, ".drill.gcp-service-account.json")
17
+ TOKEN_URL = "https://oauth2.googleapis.com/token"
18
+ SCOPE = "https://www.googleapis.com/auth/spreadsheets"
19
+ GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer"
20
+ TOKEN_LIFETIME = 3600
21
+
22
+ class AuthError < StandardError; end
23
+
24
+ def self.configured?
25
+ File.exist?(PATH)
26
+ end
27
+
28
+ def self.fetch_token!
29
+ key = load_key
30
+ jwt = build_jwt(key)
31
+ exchange_jwt(jwt)
32
+ end
33
+
34
+ def self.load_key
35
+ JSON.parse(File.read(PATH))
36
+ rescue Errno::ENOENT
37
+ raise AuthError, "no service account key found at #{PATH} — see README's service account setup"
38
+ rescue JSON::ParserError
39
+ raise AuthError, "#{PATH} is not valid JSON"
40
+ end
41
+ private_class_method :load_key
42
+
43
+ def self.build_jwt(key)
44
+ claims = jwt_claims(key.fetch("client_email"))
45
+ signing_input = "#{encode_segment(jwt_header)}.#{encode_segment(claims)}"
46
+ signature = sign(signing_input, key.fetch("private_key"))
47
+ "#{signing_input}.#{Base64.urlsafe_encode64(signature, padding: false)}"
48
+ end
49
+ private_class_method :build_jwt
50
+
51
+ def self.jwt_header
52
+ { "alg" => "RS256", "typ" => "JWT" }
53
+ end
54
+ private_class_method :jwt_header
55
+
56
+ def self.jwt_claims(client_email)
57
+ now = Time.now.to_i
58
+ { "iss" => client_email, "scope" => SCOPE, "aud" => TOKEN_URL, "iat" => now, "exp" => now + TOKEN_LIFETIME }
59
+ end
60
+ private_class_method :jwt_claims
61
+
62
+ def self.encode_segment(hash)
63
+ Base64.urlsafe_encode64(JSON.generate(hash), padding: false)
64
+ end
65
+ private_class_method :encode_segment
66
+
67
+ def self.sign(signing_input, private_key_pem)
68
+ OpenSSL::PKey::RSA.new(private_key_pem).sign(OpenSSL::Digest.new("SHA256"), signing_input)
69
+ end
70
+ private_class_method :sign
71
+
72
+ def self.exchange_jwt(jwt)
73
+ response = Lexdrill::HTTPClient.post_form(TOKEN_URL, { "grant_type" => GRANT_TYPE, "assertion" => jwt })
74
+ handle_token_response(response)
75
+ end
76
+ private_class_method :exchange_jwt
77
+
78
+ def self.handle_token_response(response)
79
+ body = JSON.parse(response.body)
80
+ return body.fetch("access_token") if response.code == 200
81
+
82
+ raise AuthError, "Google rejected the service account credentials: #{body['error_description'] || body['error']}"
83
+ end
84
+ private_class_method :handle_token_response
85
+ end
@@ -4,9 +4,10 @@ require "uri"
4
4
  require "json"
5
5
 
6
6
  # Thin wrapper around the Google Sheets API v4 REST endpoints needed by
7
- # `drill export`: create the target tab if it doesn't already exist, clear
8
- # its contents, write new rows into it, then auto-fit column A's width to
9
- # the content so long phrases aren't visually truncated.
7
+ # `drill export`/`drill import`: create the target tab if it doesn't already
8
+ # exist, clear its contents, write new rows into it, then auto-fit column
9
+ # A's width to the content so long phrases aren't visually truncated; or,
10
+ # for import, read column A back out of an existing tab.
10
11
  module Lexdrill::SheetsClient
11
12
  BASE_URL = "https://sheets.googleapis.com/v4/spreadsheets"
12
13
 
@@ -68,6 +69,14 @@ module Lexdrill::SheetsClient
68
69
  handle_response(Lexdrill::HTTPClient.json_put(url, body: body, headers: auth_header(access_token)))
69
70
  end
70
71
 
72
+ # Reads column A back out of an existing tab (ignoring any other
73
+ # columns, e.g. show counts from an older export), skipping blank cells.
74
+ def self.read_column(spreadsheet_id, sheet_name, access_token)
75
+ url = "#{BASE_URL}/#{spreadsheet_id}/values/#{encoded_range(sheet_name)}"
76
+ data = handle_response(Lexdrill::HTTPClient.json_get(url, headers: auth_header(access_token)))
77
+ data.fetch("values", []).filter_map { |row| row[0]&.strip }.reject(&:empty?)
78
+ end
79
+
71
80
  def self.auth_header(token)
72
81
  { "Authorization" => "Bearer #{token}" }
73
82
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lexdrill
4
- VERSION = "0.14.0"
4
+ VERSION = "0.16.0"
5
5
  end
data/lib/lexdrill.rb CHANGED
@@ -10,8 +10,10 @@ require_relative "lexdrill/word_list"
10
10
  require_relative "lexdrill/toggle"
11
11
  require_relative "lexdrill/rand"
12
12
  require_relative "lexdrill/remote"
13
+ require_relative "lexdrill/oauth_remote"
13
14
  require_relative "lexdrill/http_client"
14
15
  require_relative "lexdrill/google_auth"
16
+ require_relative "lexdrill/service_account_auth"
15
17
  require_relative "lexdrill/sheets_client"
16
18
  require_relative "lexdrill/shell_snippet"
17
19
  require_relative "lexdrill/inspector"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lexdrill
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.0
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Siarhei Kisliak
@@ -36,8 +36,10 @@ files:
36
36
  - lib/lexdrill/http_client.rb
37
37
  - lib/lexdrill/inspector.rb
38
38
  - lib/lexdrill/line_formatter.rb
39
+ - lib/lexdrill/oauth_remote.rb
39
40
  - lib/lexdrill/rand.rb
40
41
  - lib/lexdrill/remote.rb
42
+ - lib/lexdrill/service_account_auth.rb
41
43
  - lib/lexdrill/sheets_client.rb
42
44
  - lib/lexdrill/shell_snippet.rb
43
45
  - lib/lexdrill/stats.rb