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.
@@ -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
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fizzy
4
+ VERSION = "0.1.0"
5
+ 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: []