fizzy-cli 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 +17 -0
- data/LICENSE.txt +21 -0
- data/README.md +194 -0
- data/bin/fizzy +13 -0
- data/lib/fizzy/auth.rb +37 -0
- data/lib/fizzy/cli/auth.rb +110 -0
- data/lib/fizzy/cli/base.rb +68 -0
- data/lib/fizzy/cli/boards.rb +57 -0
- data/lib/fizzy/cli/cards.rb +178 -0
- data/lib/fizzy/cli/columns.rb +61 -0
- data/lib/fizzy/cli/comments.rb +61 -0
- data/lib/fizzy/cli/notifications.rb +42 -0
- data/lib/fizzy/cli/pins.rb +30 -0
- data/lib/fizzy/cli/reactions.rb +49 -0
- data/lib/fizzy/cli/steps.rb +55 -0
- data/lib/fizzy/cli/tags.rb +17 -0
- data/lib/fizzy/cli/users.rb +50 -0
- data/lib/fizzy/cli.rb +62 -0
- data/lib/fizzy/client.rb +124 -0
- data/lib/fizzy/errors.rb +21 -0
- data/lib/fizzy/formatter.rb +37 -0
- data/lib/fizzy/paginator.rb +43 -0
- data/lib/fizzy/version.rb +5 -0
- data/lib/fizzy.rb +13 -0
- metadata +88 -0
data/lib/fizzy/client.rb
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fizzy
|
|
4
|
+
Response = Data.define(:body, :headers, :status)
|
|
5
|
+
|
|
6
|
+
class Client
|
|
7
|
+
BASE_URL = "https://app.fizzy.do"
|
|
8
|
+
|
|
9
|
+
attr_reader :account_slug
|
|
10
|
+
|
|
11
|
+
def initialize(token:, account_slug:)
|
|
12
|
+
@token = token
|
|
13
|
+
@account_slug = account_slug
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def get(path, params: {})
|
|
17
|
+
request(:get, path, params: params)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def post(path, body: nil)
|
|
21
|
+
request(:post, path, body: body)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def put(path, body: nil)
|
|
25
|
+
request(:put, path, body: body)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def patch(path, body: nil)
|
|
29
|
+
request(:patch, path, body: body)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def delete(path)
|
|
33
|
+
request(:delete, path)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def connection
|
|
39
|
+
@connection ||= begin
|
|
40
|
+
uri = URI(BASE_URL)
|
|
41
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
42
|
+
http.use_ssl = true
|
|
43
|
+
http.open_timeout = 5
|
|
44
|
+
http.read_timeout = 30
|
|
45
|
+
http.start
|
|
46
|
+
http
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def request(method, path, body: nil, params: {})
|
|
51
|
+
uri = URI("#{BASE_URL}#{path}")
|
|
52
|
+
uri.query = URI.encode_www_form(params) unless params.empty?
|
|
53
|
+
|
|
54
|
+
req = build_request(method, uri)
|
|
55
|
+
req["Authorization"] = "Bearer #{@token}"
|
|
56
|
+
req["Accept"] = "application/json"
|
|
57
|
+
|
|
58
|
+
if body
|
|
59
|
+
req["Content-Type"] = "application/json"
|
|
60
|
+
req.body = body.to_json
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
response = connection.request(req)
|
|
64
|
+
handle_response(response)
|
|
65
|
+
rescue Net::OpenTimeout, Net::ReadTimeout, SocketError, Errno::ECONNREFUSED => e
|
|
66
|
+
raise NetworkError, "Network error: #{e.message}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def build_request(method, uri)
|
|
70
|
+
case method
|
|
71
|
+
when :get then Net::HTTP::Get.new(uri)
|
|
72
|
+
when :post then Net::HTTP::Post.new(uri)
|
|
73
|
+
when :put then Net::HTTP::Put.new(uri)
|
|
74
|
+
when :patch then Net::HTTP::Patch.new(uri)
|
|
75
|
+
when :delete then Net::HTTP::Delete.new(uri)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def handle_response(response)
|
|
80
|
+
status = response.code.to_i
|
|
81
|
+
parsed_body = parse_body(response)
|
|
82
|
+
|
|
83
|
+
case status
|
|
84
|
+
when 200..299
|
|
85
|
+
# Follow Location header on 201 to fetch the created resource
|
|
86
|
+
if status == 201 && parsed_body.nil? && response["location"]
|
|
87
|
+
location = response["location"].sub(/\.json$/, "")
|
|
88
|
+
return get(location)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
Response.new(body: parsed_body, headers: response.to_hash, status: status)
|
|
92
|
+
when 301, 302
|
|
93
|
+
raise AuthError.new("Redirected to #{response["location"]} — endpoint may require session auth",
|
|
94
|
+
status: status, body: parsed_body)
|
|
95
|
+
when 401
|
|
96
|
+
raise AuthError.new("Authentication failed (401)", status: 401, body: parsed_body)
|
|
97
|
+
when 403
|
|
98
|
+
raise ForbiddenError.new("Forbidden (403)", status: 403, body: parsed_body)
|
|
99
|
+
when 404
|
|
100
|
+
raise NotFoundError.new("Not found (404)", status: 404, body: parsed_body)
|
|
101
|
+
when 422
|
|
102
|
+
raise ValidationError.new(parse_error(response), status: 422, body: parsed_body)
|
|
103
|
+
when 429
|
|
104
|
+
raise RateLimitError.new("Rate limited (429)", status: 429, body: parsed_body)
|
|
105
|
+
else
|
|
106
|
+
raise ServerError.new("HTTP #{response.code}: #{response.body}", status: status, body: parsed_body)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def parse_body(response)
|
|
111
|
+
body = response.body&.strip
|
|
112
|
+
body.nil? || body.empty? ? nil : JSON.parse(body)
|
|
113
|
+
rescue JSON::ParserError
|
|
114
|
+
nil
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def parse_error(response)
|
|
118
|
+
data = JSON.parse(response.body)
|
|
119
|
+
data["error"] || data["errors"]&.join(", ") || response.body
|
|
120
|
+
rescue JSON::ParserError
|
|
121
|
+
response.body
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
data/lib/fizzy/errors.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fizzy
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
attr_reader :status, :body
|
|
6
|
+
|
|
7
|
+
def initialize(message = nil, status: nil, body: nil)
|
|
8
|
+
super(message)
|
|
9
|
+
@status = status
|
|
10
|
+
@body = body
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class AuthError < Error; end
|
|
15
|
+
class NotFoundError < Error; end
|
|
16
|
+
class ValidationError < Error; end
|
|
17
|
+
class ForbiddenError < Error; end
|
|
18
|
+
class ServerError < Error; end
|
|
19
|
+
class RateLimitError < Error; end
|
|
20
|
+
class NetworkError < Error; end
|
|
21
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fizzy
|
|
4
|
+
class Formatter
|
|
5
|
+
def self.table(rows, headers:, io: $stdout)
|
|
6
|
+
return if rows.empty?
|
|
7
|
+
|
|
8
|
+
all_rows = [headers] + rows
|
|
9
|
+
widths = headers.each_index.map do |i|
|
|
10
|
+
all_rows.map { |r| r[i].to_s.length }.max
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
io.puts headers.each_with_index.map { |h, i| h.to_s.ljust(widths[i]) }.join(" ")
|
|
14
|
+
io.puts widths.map { |w| "-" * w }.join(" ")
|
|
15
|
+
rows.each do |row|
|
|
16
|
+
io.puts row.each_with_index.map { |c, i| c.to_s.ljust(widths[i]) }.join(" ")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.json(data, io: $stdout)
|
|
21
|
+
io.puts JSON.pretty_generate(data)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.detail(pairs, io: $stdout)
|
|
25
|
+
width = pairs.map { |k, _| k.to_s.length }.max
|
|
26
|
+
pairs.each do |key, value|
|
|
27
|
+
io.puts "#{key.to_s.rjust(width)} #{value}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.truncate(str, max)
|
|
32
|
+
return "" unless str
|
|
33
|
+
|
|
34
|
+
str.length > max ? "#{str[0...(max - 1)]}…" : str
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fizzy
|
|
4
|
+
class Paginator
|
|
5
|
+
def initialize(client)
|
|
6
|
+
@client = client
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def all(path, params: {})
|
|
10
|
+
items = []
|
|
11
|
+
each_page(path, params: params) { |page| items.concat(Array(page)) }
|
|
12
|
+
items
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def each_page(path, params: {})
|
|
16
|
+
current_path = path
|
|
17
|
+
current_params = params
|
|
18
|
+
|
|
19
|
+
loop do
|
|
20
|
+
response = @client.get(current_path, params: current_params)
|
|
21
|
+
yield response.body if block_given?
|
|
22
|
+
|
|
23
|
+
next_path = parse_next_link(response.headers)
|
|
24
|
+
break unless next_path
|
|
25
|
+
|
|
26
|
+
current_path = next_path
|
|
27
|
+
current_params = {}
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def parse_next_link(headers)
|
|
34
|
+
link = headers["link"]&.first
|
|
35
|
+
return unless link
|
|
36
|
+
|
|
37
|
+
match = link.match(/<([^>]+)>;\s*rel="next"/)
|
|
38
|
+
return unless match
|
|
39
|
+
|
|
40
|
+
URI(match[1]).request_uri
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/fizzy.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
require_relative "fizzy/version"
|
|
8
|
+
require_relative "fizzy/errors"
|
|
9
|
+
require_relative "fizzy/auth"
|
|
10
|
+
require_relative "fizzy/client"
|
|
11
|
+
require_relative "fizzy/paginator"
|
|
12
|
+
require_relative "fizzy/formatter"
|
|
13
|
+
require_relative "fizzy/cli"
|
metadata
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: fizzy-cli
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- David Paluy
|
|
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: thor
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.5'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.5'
|
|
26
|
+
description: Command-line client for Fizzy project management. Manage boards, cards,
|
|
27
|
+
columns, steps, comments, and more from the terminal.
|
|
28
|
+
email:
|
|
29
|
+
- dpaluy@gmail.com
|
|
30
|
+
executables:
|
|
31
|
+
- fizzy
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files:
|
|
34
|
+
- CHANGELOG.md
|
|
35
|
+
- LICENSE.txt
|
|
36
|
+
- README.md
|
|
37
|
+
files:
|
|
38
|
+
- CHANGELOG.md
|
|
39
|
+
- LICENSE.txt
|
|
40
|
+
- README.md
|
|
41
|
+
- bin/fizzy
|
|
42
|
+
- lib/fizzy.rb
|
|
43
|
+
- lib/fizzy/auth.rb
|
|
44
|
+
- lib/fizzy/cli.rb
|
|
45
|
+
- lib/fizzy/cli/auth.rb
|
|
46
|
+
- lib/fizzy/cli/base.rb
|
|
47
|
+
- lib/fizzy/cli/boards.rb
|
|
48
|
+
- lib/fizzy/cli/cards.rb
|
|
49
|
+
- lib/fizzy/cli/columns.rb
|
|
50
|
+
- lib/fizzy/cli/comments.rb
|
|
51
|
+
- lib/fizzy/cli/notifications.rb
|
|
52
|
+
- lib/fizzy/cli/pins.rb
|
|
53
|
+
- lib/fizzy/cli/reactions.rb
|
|
54
|
+
- lib/fizzy/cli/steps.rb
|
|
55
|
+
- lib/fizzy/cli/tags.rb
|
|
56
|
+
- lib/fizzy/cli/users.rb
|
|
57
|
+
- lib/fizzy/client.rb
|
|
58
|
+
- lib/fizzy/errors.rb
|
|
59
|
+
- lib/fizzy/formatter.rb
|
|
60
|
+
- lib/fizzy/paginator.rb
|
|
61
|
+
- lib/fizzy/version.rb
|
|
62
|
+
homepage: https://github.com/dpaluy/fizzy-cli
|
|
63
|
+
licenses:
|
|
64
|
+
- MIT
|
|
65
|
+
metadata:
|
|
66
|
+
rubygems_mfa_required: 'true'
|
|
67
|
+
homepage_uri: https://github.com/dpaluy/fizzy-cli
|
|
68
|
+
source_code_uri: https://github.com/dpaluy/fizzy-cli
|
|
69
|
+
changelog_uri: https://github.com/dpaluy/fizzy-cli/blob/master/CHANGELOG.md
|
|
70
|
+
bug_tracker_uri: https://github.com/dpaluy/fizzy-cli/issues
|
|
71
|
+
rdoc_options: []
|
|
72
|
+
require_paths:
|
|
73
|
+
- lib
|
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
75
|
+
requirements:
|
|
76
|
+
- - ">="
|
|
77
|
+
- !ruby/object:Gem::Version
|
|
78
|
+
version: '3.2'
|
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
80
|
+
requirements:
|
|
81
|
+
- - ">="
|
|
82
|
+
- !ruby/object:Gem::Version
|
|
83
|
+
version: '0'
|
|
84
|
+
requirements: []
|
|
85
|
+
rubygems_version: 4.0.3
|
|
86
|
+
specification_version: 4
|
|
87
|
+
summary: CLI for Fizzy project management
|
|
88
|
+
test_files: []
|