waves-ruby 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/CONTRIBUTING.md +38 -0
- data/README.md +94 -0
- data/exe/waves +6 -0
- data/lib/waves/cli/app.rb +238 -0
- data/lib/waves/cli/formatter.rb +149 -0
- data/lib/waves/cli.rb +22 -0
- data/lib/waves/client.rb +107 -0
- data/lib/waves/config.rb +45 -0
- data/lib/waves/errors.rb +12 -0
- data/lib/waves/request.rb +13 -0
- data/lib/waves/version.rb +5 -0
- data/lib/waves.rb +18 -0
- data/waves-ruby.gemspec +23 -0
- metadata +70 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: ba842c75819ad196345a640bb058aaa398b5ed6ada2e7610ff15d080b0613d1b
|
|
4
|
+
data.tar.gz: 6ea6b97c6b33a6503dc9921761b23fb86d9a0851a307294d248134259038c00f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 273137f5386f5fa57d420644e1c0e083f98d658957995023434ce8ae0acecf4897785e65319c72d3ef44e2d3d562c90a634a2b02879cd8a76575755b90a359e1
|
|
7
|
+
data.tar.gz: 64d920b54c4e272d2a334c51844aa912478e37e6e988f9bb982106262387beec817030c688882225c06c82285c08fa06cdd3a98e10e447c69c5eb21fb3797b5d
|
data/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Local Setup
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
bundle install
|
|
7
|
+
bundle exec rspec
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Copy `.env.example` to `.env` if you want to run live API verification locally.
|
|
11
|
+
|
|
12
|
+
## Testing Policy
|
|
13
|
+
|
|
14
|
+
- `.env` is for local development only and must never be committed.
|
|
15
|
+
- Use `WAVES_API_TOKEN` from `.env` for opt-in live verification only.
|
|
16
|
+
- Raw VCR recordings belong in `spec/cassettes/local/` and must stay untracked.
|
|
17
|
+
- Committed fixtures and example cassettes must be sanitized.
|
|
18
|
+
- Do not commit real meeting titles, summaries, snippets, notes, transcripts, or signed media URLs.
|
|
19
|
+
|
|
20
|
+
## Live Verification Workflow
|
|
21
|
+
|
|
22
|
+
1. Copy `.env.example` to `.env`.
|
|
23
|
+
2. Set `WAVES_API_TOKEN` in `.env`.
|
|
24
|
+
3. Run `WAVES_LIVE=1 bundle exec rspec spec/waves/live/client_live_spec.rb`.
|
|
25
|
+
4. If you want to record local cassettes for the entire live suite, run `WAVES_LIVE=1 WAVES_RECORD_LIVE=1 bundle exec rspec spec/waves/live/client_live_spec.rb`.
|
|
26
|
+
5. Keep any raw recordings in `spec/cassettes/local/`.
|
|
27
|
+
6. Optionally set `WAVES_SESSION_ID` and `WAVES_SEARCH_QUERY` in `.env` if you want deterministic session and search coverage.
|
|
28
|
+
7. Convert any useful example into a sanitized fixture under `spec/fixtures/waves/` or a scrubbed cassette under `spec/cassettes/examples/`.
|
|
29
|
+
8. Commit only sanitized artifacts.
|
|
30
|
+
|
|
31
|
+
## Sanitization Checklist
|
|
32
|
+
|
|
33
|
+
- Replace tokens and `Authorization` headers.
|
|
34
|
+
- Replace real meeting titles, snippets, summaries, notes, and transcript text.
|
|
35
|
+
- Replace signed or temporary media URLs.
|
|
36
|
+
- Replace participant emails and other personal identifiers.
|
|
37
|
+
|
|
38
|
+
The helper patterns in [spec/support/sanitizer.rb](/Users/avi/Development/code/waves-rubygem-cli-api/spec/support/sanitizer.rb) are intended to make that scrubbing repeatable, but contributors are still responsible for reviewing the final artifact before committing it.
|
data/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Waves Ruby
|
|
2
|
+
|
|
3
|
+
Ruby API client and CLI for the Wave API.
|
|
4
|
+
|
|
5
|
+
## CLI
|
|
6
|
+
|
|
7
|
+
Available commands:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
waves login
|
|
11
|
+
waves account
|
|
12
|
+
waves list
|
|
13
|
+
waves search "meeting content"
|
|
14
|
+
waves show SESSION_ID
|
|
15
|
+
waves transcript SESSION_ID
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Use `--json` on any read command for machine-readable output:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
waves list --json
|
|
22
|
+
waves account --json
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
You can provide credentials with `--token`, `WAVES_API_TOKEN`, or a token saved locally by `waves login`.
|
|
26
|
+
|
|
27
|
+
## Ruby API
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
client = Waves::Client.new(token: ENV.fetch("WAVES_API_TOKEN"))
|
|
31
|
+
|
|
32
|
+
client.account
|
|
33
|
+
client.list_sessions(limit: 10)
|
|
34
|
+
client.search_sessions(query: "roadmap")
|
|
35
|
+
client.session("session_123")
|
|
36
|
+
client.transcript("session_123")
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Development
|
|
40
|
+
|
|
41
|
+
Install dependencies:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
bundle install
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Run the test suite:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
bundle exec rspec
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Live API Verification
|
|
54
|
+
|
|
55
|
+
Copy `.env.example` to `.env` and set a real token:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
cp .env.example .env
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Then set:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
WAVES_API_TOKEN=your_wave_api_token
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Live specs are opt-in and skipped by default. Run them locally with:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
WAVES_LIVE=1 bundle exec rspec spec/waves/live/client_live_spec.rb
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
That exercises the current live coverage for:
|
|
74
|
+
|
|
75
|
+
- `account`
|
|
76
|
+
- `list_sessions`
|
|
77
|
+
- `stats`
|
|
78
|
+
- `session`
|
|
79
|
+
- `transcript`
|
|
80
|
+
- `search_sessions`
|
|
81
|
+
|
|
82
|
+
`session`, `transcript`, and `search_sessions` auto-discover test inputs from your first listed session. You can make them deterministic by setting `WAVES_SESSION_ID` and `WAVES_SEARCH_QUERY` in `.env`.
|
|
83
|
+
|
|
84
|
+
To record local-only cassettes for every live example in the gitignored directory:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
WAVES_LIVE=1 WAVES_RECORD_LIVE=1 bundle exec rspec spec/waves/live/client_live_spec.rb
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Those recordings stay under `spec/cassettes/local/` and must not be committed. If you want to keep an example response in the repo, convert it into a sanitized fixture under `spec/fixtures/waves/` or a scrubbed example cassette under `spec/cassettes/examples/`.
|
|
91
|
+
|
|
92
|
+
Use the sanitization patterns in [spec/support/sanitizer.rb](/Users/avi/Development/code/waves-rubygem-cli-api/spec/support/sanitizer.rb) before committing any captured API data.
|
|
93
|
+
|
|
94
|
+
See [CONTRIBUTING.md](/Users/avi/Development/code/waves-rubygem-cli-api/CONTRIBUTING.md) for the full policy.
|
data/exe/waves
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "io/console"
|
|
4
|
+
require "optparse"
|
|
5
|
+
|
|
6
|
+
module Waves
|
|
7
|
+
module CLI
|
|
8
|
+
class CommandError < StandardError; end
|
|
9
|
+
|
|
10
|
+
class App
|
|
11
|
+
COMMANDS = %w[login account list search show transcript].freeze
|
|
12
|
+
|
|
13
|
+
def initialize(argv:, stdin:, stdout:, stderr:, env:, config:, client_class:)
|
|
14
|
+
@argv = argv.dup
|
|
15
|
+
@stdin = stdin
|
|
16
|
+
@stdout = stdout
|
|
17
|
+
@stderr = stderr
|
|
18
|
+
@env = env
|
|
19
|
+
@config = config
|
|
20
|
+
@client_class = client_class
|
|
21
|
+
@formatter = Formatter.new(stdout: stdout)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def start
|
|
25
|
+
options, args = parse_global_options(@argv)
|
|
26
|
+
command = args.shift
|
|
27
|
+
|
|
28
|
+
return print_help if command.nil?
|
|
29
|
+
return print_help(args.first) if command == "help"
|
|
30
|
+
return print_help(command) if options[:help]
|
|
31
|
+
|
|
32
|
+
dispatch(command, args, options)
|
|
33
|
+
rescue OptionParser::ParseError, CommandError, Waves::Error => error
|
|
34
|
+
print_error(error, json: options&.fetch(:json, false) || false)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def parse_global_options(argv)
|
|
40
|
+
options = {
|
|
41
|
+
help: false,
|
|
42
|
+
json: false,
|
|
43
|
+
token: nil
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
parser = OptionParser.new do |opts|
|
|
47
|
+
opts.on("--json") { options[:json] = true }
|
|
48
|
+
opts.on("--token TOKEN") { |token| options[:token] = token }
|
|
49
|
+
opts.on("-h", "--help") { options[:help] = true }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
args = argv.dup
|
|
53
|
+
parser.permute!(args)
|
|
54
|
+
|
|
55
|
+
[options, args]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def dispatch(command, args, options)
|
|
59
|
+
case command
|
|
60
|
+
when "login"
|
|
61
|
+
run_login(args, options)
|
|
62
|
+
when "account"
|
|
63
|
+
ensure_no_arguments!("account", args)
|
|
64
|
+
render_payload(fetch_client(options).account, options) { |payload| @formatter.print_account(payload) }
|
|
65
|
+
when "list"
|
|
66
|
+
ensure_no_arguments!("list", args)
|
|
67
|
+
render_payload(fetch_client(options).list_sessions, options) { |payload| @formatter.print_list(payload) }
|
|
68
|
+
when "search"
|
|
69
|
+
run_search(args, options)
|
|
70
|
+
when "show"
|
|
71
|
+
run_show(args, options)
|
|
72
|
+
when "transcript"
|
|
73
|
+
run_transcript(args, options)
|
|
74
|
+
else
|
|
75
|
+
raise CommandError, "Unknown command: #{command}"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def run_login(args, options)
|
|
80
|
+
ensure_no_arguments!("login", args)
|
|
81
|
+
|
|
82
|
+
token = normalize_token(options[:token]) || normalize_token(prompt_for_token)
|
|
83
|
+
raise CommandError, "Token cannot be empty" if token.to_s.empty?
|
|
84
|
+
|
|
85
|
+
@config.save_token(token)
|
|
86
|
+
|
|
87
|
+
payload = {
|
|
88
|
+
"config_path" => @config.config_path,
|
|
89
|
+
"message" => "Token saved"
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
render_payload(payload, options) do
|
|
93
|
+
@stdout.puts "Saved Wave API token to #{@config.config_path}"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def run_search(args, options)
|
|
98
|
+
query = args.join(" ").strip
|
|
99
|
+
raise CommandError, "Usage: waves search QUERY" if query.empty?
|
|
100
|
+
|
|
101
|
+
render_payload(fetch_client(options).search_sessions(query: query), options) do |payload|
|
|
102
|
+
@formatter.print_search(payload)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def run_show(args, options)
|
|
107
|
+
session_id = require_single_argument!("show", "SESSION_ID", args)
|
|
108
|
+
|
|
109
|
+
render_payload(fetch_client(options).session(session_id), options) do |payload|
|
|
110
|
+
@formatter.print_session(payload)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def run_transcript(args, options)
|
|
115
|
+
session_id = require_single_argument!("transcript", "SESSION_ID", args)
|
|
116
|
+
|
|
117
|
+
render_payload(fetch_client(options).transcript(session_id), options) do |payload|
|
|
118
|
+
@formatter.print_transcript(payload)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def fetch_client(options)
|
|
123
|
+
@client_class.new(token: resolve_token(options[:token]))
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def resolve_token(explicit_token)
|
|
127
|
+
normalize_token(explicit_token) ||
|
|
128
|
+
normalize_token(@env["WAVES_API_TOKEN"]) ||
|
|
129
|
+
normalize_token(@config.stored_token)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def render_payload(payload, options)
|
|
133
|
+
if options[:json]
|
|
134
|
+
@formatter.print_json(payload)
|
|
135
|
+
else
|
|
136
|
+
yield payload
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
0
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def print_error(error, json:)
|
|
143
|
+
if json
|
|
144
|
+
@stderr.puts JSON.pretty_generate(
|
|
145
|
+
"error" => {
|
|
146
|
+
"message" => error.message,
|
|
147
|
+
"type" => error.class.name.split("::").last.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
else
|
|
151
|
+
@stderr.puts "Error: #{error.message}"
|
|
152
|
+
@stderr.puts
|
|
153
|
+
@stderr.puts help_text if error.is_a?(CommandError) && error.message.start_with?("Unknown command:")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
1
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def print_help(command = nil)
|
|
160
|
+
text = command ? help_text(command) : help_text
|
|
161
|
+
|
|
162
|
+
if text.nil?
|
|
163
|
+
@stderr.puts "Error: Unknown command: #{command}"
|
|
164
|
+
@stderr.puts
|
|
165
|
+
@stderr.puts help_text
|
|
166
|
+
return 1
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
@stdout.puts text
|
|
170
|
+
0
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def help_text(command = nil)
|
|
174
|
+
return top_level_help if command.nil?
|
|
175
|
+
|
|
176
|
+
{
|
|
177
|
+
"login" => "Usage: waves login [--token TOKEN] [--json]",
|
|
178
|
+
"account" => "Usage: waves account [--token TOKEN] [--json]",
|
|
179
|
+
"list" => "Usage: waves list [--token TOKEN] [--json]",
|
|
180
|
+
"search" => "Usage: waves search QUERY [--token TOKEN] [--json]",
|
|
181
|
+
"show" => "Usage: waves show SESSION_ID [--token TOKEN] [--json]",
|
|
182
|
+
"transcript" => "Usage: waves transcript SESSION_ID [--token TOKEN] [--json]"
|
|
183
|
+
}[command]
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def top_level_help
|
|
187
|
+
<<~TEXT
|
|
188
|
+
Usage: waves COMMAND [options]
|
|
189
|
+
|
|
190
|
+
Commands:
|
|
191
|
+
login
|
|
192
|
+
account
|
|
193
|
+
list
|
|
194
|
+
search QUERY
|
|
195
|
+
show SESSION_ID
|
|
196
|
+
transcript SESSION_ID
|
|
197
|
+
|
|
198
|
+
Global options:
|
|
199
|
+
--json
|
|
200
|
+
--token TOKEN
|
|
201
|
+
-h, --help
|
|
202
|
+
TEXT
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def ensure_no_arguments!(command, args)
|
|
206
|
+
return if args.empty?
|
|
207
|
+
|
|
208
|
+
raise CommandError, help_text(command)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def require_single_argument!(command, argument_name, args)
|
|
212
|
+
return args.first if args.length == 1 && !args.first.to_s.empty?
|
|
213
|
+
|
|
214
|
+
raise CommandError, "Usage: waves #{command} #{argument_name}"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def prompt_for_token
|
|
218
|
+
@stderr.print "Wave API token: "
|
|
219
|
+
|
|
220
|
+
token = if @stdin.respond_to?(:noecho) && @stdin.tty?
|
|
221
|
+
@stdin.noecho(&:gets)
|
|
222
|
+
else
|
|
223
|
+
@stdin.gets
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
@stderr.puts if @stdin.tty?
|
|
227
|
+
token
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def normalize_token(token)
|
|
231
|
+
value = token.to_s.strip
|
|
232
|
+
return if value.empty?
|
|
233
|
+
|
|
234
|
+
value
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Waves
|
|
6
|
+
module CLI
|
|
7
|
+
class Formatter
|
|
8
|
+
def initialize(stdout:)
|
|
9
|
+
@stdout = stdout
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def print_json(payload)
|
|
13
|
+
@stdout.puts JSON.pretty_generate(payload)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def print_account(account)
|
|
17
|
+
print_record(account)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def print_list(response)
|
|
21
|
+
sessions = Array(response["sessions"])
|
|
22
|
+
|
|
23
|
+
if sessions.empty?
|
|
24
|
+
@stdout.puts "No sessions found"
|
|
25
|
+
return
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
print_collection(sessions)
|
|
29
|
+
|
|
30
|
+
return if response["next_cursor"].to_s.empty?
|
|
31
|
+
|
|
32
|
+
@stdout.puts
|
|
33
|
+
@stdout.puts "Next cursor: #{response["next_cursor"]}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def print_search(response)
|
|
37
|
+
results = Array(response["results"])
|
|
38
|
+
|
|
39
|
+
if results.empty?
|
|
40
|
+
@stdout.puts "No results found"
|
|
41
|
+
return
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
print_collection(results, include_snippet: true)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def print_session(session)
|
|
48
|
+
print_record(session)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def print_transcript(response)
|
|
52
|
+
transcript = response["transcript"]
|
|
53
|
+
|
|
54
|
+
case transcript
|
|
55
|
+
when String
|
|
56
|
+
@stdout.puts transcript
|
|
57
|
+
when Array
|
|
58
|
+
transcript.each do |entry|
|
|
59
|
+
@stdout.puts transcript_line(entry)
|
|
60
|
+
end
|
|
61
|
+
else
|
|
62
|
+
print_record(response)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def print_collection(records, include_snippet: false)
|
|
69
|
+
records.each_with_index do |record, index|
|
|
70
|
+
@stdout.puts summary_line(record)
|
|
71
|
+
|
|
72
|
+
detail = detail_line(record)
|
|
73
|
+
@stdout.puts " #{detail}" unless detail.nil?
|
|
74
|
+
|
|
75
|
+
if include_snippet && !record["snippet"].to_s.empty?
|
|
76
|
+
@stdout.puts " #{record["snippet"]}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
@stdout.puts unless index == records.length - 1
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def print_record(record)
|
|
84
|
+
record.each do |key, value|
|
|
85
|
+
label = humanize_key(key)
|
|
86
|
+
|
|
87
|
+
case value
|
|
88
|
+
when Hash, Array
|
|
89
|
+
@stdout.puts "#{label}:"
|
|
90
|
+
JSON.pretty_generate(value).each_line do |line|
|
|
91
|
+
@stdout.puts " #{line.rstrip}"
|
|
92
|
+
end
|
|
93
|
+
when String
|
|
94
|
+
if value.include?("\n")
|
|
95
|
+
@stdout.puts "#{label}:"
|
|
96
|
+
value.each_line do |line|
|
|
97
|
+
@stdout.puts " #{line.rstrip}"
|
|
98
|
+
end
|
|
99
|
+
else
|
|
100
|
+
@stdout.puts "#{label}: #{value}"
|
|
101
|
+
end
|
|
102
|
+
else
|
|
103
|
+
@stdout.puts "#{label}: #{value}"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def summary_line(record)
|
|
109
|
+
identifier = record["id"] || "(no id)"
|
|
110
|
+
title = record["title"] || record["name"]
|
|
111
|
+
|
|
112
|
+
return identifier if title.to_s.empty?
|
|
113
|
+
|
|
114
|
+
"#{identifier} #{title}"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def detail_line(record)
|
|
118
|
+
details = []
|
|
119
|
+
details << record["type"]
|
|
120
|
+
details << record["status"]
|
|
121
|
+
details << record["created_at"]
|
|
122
|
+
details << record["started_at"]
|
|
123
|
+
details << record["updated_at"]
|
|
124
|
+
details << "#{record["duration_seconds"]}s" if record["duration_seconds"]
|
|
125
|
+
details.reject! { |value| value.to_s.empty? }
|
|
126
|
+
|
|
127
|
+
return if details.empty?
|
|
128
|
+
|
|
129
|
+
details.join(" ")
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def transcript_line(entry)
|
|
133
|
+
return entry unless entry.is_a?(Hash)
|
|
134
|
+
|
|
135
|
+
prefix = [entry["speaker"], entry["timestamp"]].reject { |value| value.to_s.empty? }.join(" @ ")
|
|
136
|
+
text = entry["text"].to_s
|
|
137
|
+
|
|
138
|
+
return text if prefix.empty?
|
|
139
|
+
return prefix if text.empty?
|
|
140
|
+
|
|
141
|
+
"#{prefix}: #{text}"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def humanize_key(key)
|
|
145
|
+
key.to_s.split("_").map(&:capitalize).join(" ")
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
data/lib/waves/cli.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "cli/formatter"
|
|
4
|
+
require_relative "cli/app"
|
|
5
|
+
|
|
6
|
+
module Waves
|
|
7
|
+
module CLI
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def start(argv, stdin: $stdin, stdout: $stdout, stderr: $stderr, env: ENV, config: Waves::Config, client_class: Waves::Client)
|
|
11
|
+
App.new(
|
|
12
|
+
argv: argv,
|
|
13
|
+
stdin: stdin,
|
|
14
|
+
stdout: stdout,
|
|
15
|
+
stderr: stderr,
|
|
16
|
+
env: env,
|
|
17
|
+
config: config,
|
|
18
|
+
client_class: client_class
|
|
19
|
+
).start
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/waves/client.rb
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "httparty"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Waves
|
|
7
|
+
class Client
|
|
8
|
+
include HTTParty
|
|
9
|
+
|
|
10
|
+
base_uri "https://api.wave.co/v1"
|
|
11
|
+
|
|
12
|
+
def initialize(token: nil, timeout: Waves.timeout)
|
|
13
|
+
@token = token || Config.token
|
|
14
|
+
@timeout = timeout
|
|
15
|
+
|
|
16
|
+
raise InvalidConfigError, "Missing Wave API token" if @token.to_s.empty?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def list_sessions(limit: nil, cursor: nil, since: nil, type: nil)
|
|
20
|
+
request(:get, "/sessions", query: {
|
|
21
|
+
limit: limit,
|
|
22
|
+
cursor: cursor,
|
|
23
|
+
since: since,
|
|
24
|
+
type: type
|
|
25
|
+
})
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def session(id)
|
|
29
|
+
request(:get, "/sessions/#{id}")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def search_sessions(query:, limit: nil)
|
|
33
|
+
request(:post, "/sessions/search", body: {
|
|
34
|
+
query: query,
|
|
35
|
+
limit: limit
|
|
36
|
+
})
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def transcript(id)
|
|
40
|
+
request(:get, "/sessions/#{id}/transcript")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def stats(since: nil, until_time: nil, until_: nil)
|
|
44
|
+
request(:get, "/sessions/stats", query: {
|
|
45
|
+
since: since,
|
|
46
|
+
until: until_time || until_
|
|
47
|
+
})
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def account
|
|
51
|
+
request(:get, "/account")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def request(method, path, query: nil, body: nil)
|
|
57
|
+
options = {
|
|
58
|
+
headers: headers,
|
|
59
|
+
timeout: @timeout
|
|
60
|
+
}
|
|
61
|
+
options[:query] = Request.compact_hash(query) if query
|
|
62
|
+
options[:body] = JSON.generate(Request.compact_hash(body)) if body
|
|
63
|
+
|
|
64
|
+
response = self.class.public_send(method, path, options)
|
|
65
|
+
parse_response(response)
|
|
66
|
+
rescue SocketError, HTTParty::Error, Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED => e
|
|
67
|
+
raise ConnectionError, e.message
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def parse_response(response)
|
|
71
|
+
parsed = parse_body(response)
|
|
72
|
+
return parsed if response.success?
|
|
73
|
+
|
|
74
|
+
error = parsed.is_a?(Hash) ? parsed["error"] || {} : {}
|
|
75
|
+
message = error["message"] || response.body
|
|
76
|
+
|
|
77
|
+
case response.code.to_i
|
|
78
|
+
when 401
|
|
79
|
+
raise AuthenticationError, message
|
|
80
|
+
when 403
|
|
81
|
+
raise AuthorizationError, message
|
|
82
|
+
when 404
|
|
83
|
+
raise NotFoundError, message
|
|
84
|
+
when 429
|
|
85
|
+
raise RateLimitError, message
|
|
86
|
+
else
|
|
87
|
+
raise APIError, message
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def parse_body(response)
|
|
92
|
+
return {} if response.body.to_s.empty?
|
|
93
|
+
|
|
94
|
+
JSON.parse(response.body)
|
|
95
|
+
rescue JSON::ParserError
|
|
96
|
+
response.body
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def headers
|
|
100
|
+
{
|
|
101
|
+
"Accept" => "application/json",
|
|
102
|
+
"Authorization" => "Bearer #{@token}",
|
|
103
|
+
"Content-Type" => "application/json"
|
|
104
|
+
}
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
data/lib/waves/config.rb
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Waves
|
|
6
|
+
module Config
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def config_dir
|
|
10
|
+
File.join(Dir.home, ".config", "waves")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def config_path
|
|
14
|
+
File.join(config_dir, "config.json")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def token
|
|
18
|
+
ENV["WAVES_API_TOKEN"] || stored_token
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def stored_token
|
|
22
|
+
load["token"]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def save_token(token)
|
|
26
|
+
raise ArgumentError, "token is required" if token.to_s.empty?
|
|
27
|
+
|
|
28
|
+
FileUtils.mkdir_p(config_dir)
|
|
29
|
+
File.write(config_path, JSON.pretty_generate("token" => token))
|
|
30
|
+
File.chmod(0o600, config_path)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def clear
|
|
34
|
+
File.delete(config_path) if File.exist?(config_path)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def load
|
|
38
|
+
return {} unless File.exist?(config_path)
|
|
39
|
+
|
|
40
|
+
JSON.parse(File.read(config_path))
|
|
41
|
+
rescue JSON::ParserError
|
|
42
|
+
raise InvalidConfigError, "Invalid config file at #{config_path}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
data/lib/waves/errors.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Waves
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
class InvalidConfigError < Error; end
|
|
6
|
+
class AuthenticationError < Error; end
|
|
7
|
+
class AuthorizationError < Error; end
|
|
8
|
+
class NotFoundError < Error; end
|
|
9
|
+
class RateLimitError < Error; end
|
|
10
|
+
class APIError < Error; end
|
|
11
|
+
class ConnectionError < Error; end
|
|
12
|
+
end
|
data/lib/waves.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
require_relative "waves/config"
|
|
6
|
+
require_relative "waves/errors"
|
|
7
|
+
require_relative "waves/request"
|
|
8
|
+
require_relative "waves/version"
|
|
9
|
+
require_relative "waves/client"
|
|
10
|
+
require_relative "waves/cli"
|
|
11
|
+
|
|
12
|
+
module Waves
|
|
13
|
+
class << self
|
|
14
|
+
attr_accessor :timeout
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
self.timeout = 10
|
|
18
|
+
end
|
data/waves-ruby.gemspec
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require_relative "lib/waves/version"
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |spec|
|
|
4
|
+
spec.name = "waves-ruby"
|
|
5
|
+
spec.version = Waves::VERSION
|
|
6
|
+
spec.authors = ["Avi Flombaum"]
|
|
7
|
+
spec.email = ["git@avi.nyc"]
|
|
8
|
+
spec.summary = "Ruby API client and CLI for the Wave API"
|
|
9
|
+
spec.description = "A small Ruby API client and CLI for working with the Wave API."
|
|
10
|
+
spec.homepage = "https://github.com/aviflombaum/waves-gems"
|
|
11
|
+
spec.license = "MIT"
|
|
12
|
+
spec.required_ruby_version = ">= 3.2"
|
|
13
|
+
|
|
14
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
15
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/releases/tag/v#{spec.version}"
|
|
16
|
+
|
|
17
|
+
spec.files = Dir["*.md", "*.gemspec", "exe/*", "lib/**/*.rb"]
|
|
18
|
+
spec.bindir = "exe"
|
|
19
|
+
spec.executables = ["waves"]
|
|
20
|
+
spec.require_paths = ["lib"]
|
|
21
|
+
|
|
22
|
+
spec.add_dependency "httparty"
|
|
23
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: waves-ruby
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Avi Flombaum
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: httparty
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
description: A small Ruby API client and CLI for working with the Wave API.
|
|
27
|
+
email:
|
|
28
|
+
- git@avi.nyc
|
|
29
|
+
executables:
|
|
30
|
+
- waves
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- CONTRIBUTING.md
|
|
35
|
+
- README.md
|
|
36
|
+
- exe/waves
|
|
37
|
+
- lib/waves.rb
|
|
38
|
+
- lib/waves/cli.rb
|
|
39
|
+
- lib/waves/cli/app.rb
|
|
40
|
+
- lib/waves/cli/formatter.rb
|
|
41
|
+
- lib/waves/client.rb
|
|
42
|
+
- lib/waves/config.rb
|
|
43
|
+
- lib/waves/errors.rb
|
|
44
|
+
- lib/waves/request.rb
|
|
45
|
+
- lib/waves/version.rb
|
|
46
|
+
- waves-ruby.gemspec
|
|
47
|
+
homepage: https://github.com/aviflombaum/waves-gems
|
|
48
|
+
licenses:
|
|
49
|
+
- MIT
|
|
50
|
+
metadata:
|
|
51
|
+
source_code_uri: https://github.com/aviflombaum/waves-gems
|
|
52
|
+
changelog_uri: https://github.com/aviflombaum/waves-gems/releases/tag/v0.1.0
|
|
53
|
+
rdoc_options: []
|
|
54
|
+
require_paths:
|
|
55
|
+
- lib
|
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '3.2'
|
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
62
|
+
requirements:
|
|
63
|
+
- - ">="
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: '0'
|
|
66
|
+
requirements: []
|
|
67
|
+
rubygems_version: 4.0.6
|
|
68
|
+
specification_version: 4
|
|
69
|
+
summary: Ruby API client and CLI for the Wave API
|
|
70
|
+
test_files: []
|