trellodon 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: bf1b8715b6cbad2c1bd3ab4fce72198a442380cf4f2d7bb39c55928ef72b032f
4
+ data.tar.gz: 2caa2a3a24fe463cc1cb730a3aad68928323ec0fea04fea8e3a0f10ebf051ea7
5
+ SHA512:
6
+ metadata.gz: b50b3279f7a9e787b2f2e718109902a9cda88729f7d7b330aa5db9db944f913af33d6f26fab7b1f6c4d2866a1241d62d99c4dbe989ea468499bcaaa4908a153f
7
+ data.tar.gz: 52d86ebd0b43ad0b064facbfb66332623422b2c688eff732188b7ea2ce8baba2842a331a941e1aafb410f114c31aef3ec80f7588b7f6a07808070019f9eb12ba
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2022-04-07
4
+
5
+ - Initial release
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Evil Martians
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # trellodon
2
+
3
+ Dump Trello boards to your file system
data/bin/console ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "trellodon"
6
+ require "irb"
7
+
8
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
data/bin/trellodon ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "trellodon/cli"
5
+
6
+ Trellodon::CLI.new.run
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "trello"
4
+
5
+ require "trellodon/entities"
6
+
7
+ module Trellodon
8
+ class APIClient
9
+ def initialize(api_key:, api_token:, logger: Config.logger)
10
+ @logger = logger
11
+ @client = Trello::Client.new(
12
+ developer_public_key: api_key,
13
+ member_token: api_token
14
+ )
15
+ end
16
+
17
+ def fetch_board(board_id)
18
+ retrying do
19
+ board = @client.find(:boards, board_id)
20
+
21
+ Board.new(
22
+ id: board.id,
23
+ short_id: board.short_url.chars.last(8).join,
24
+ name: board.name,
25
+ lists: lists_from(board),
26
+ card_ids: @client.find_many(Trello::Card, "/boards/#{board_id}/cards", fields: "id").map(&:id),
27
+ last_activity_date: board.last_activity_date
28
+ )
29
+ end
30
+ end
31
+
32
+ def fetch_card(card_id)
33
+ retrying do
34
+ card = @client.find(:cards, card_id)
35
+
36
+ Card.new(
37
+ id: card.id,
38
+ short_id: card.short_url.chars.last(8).join,
39
+ name: card.name,
40
+ desc: card.desc,
41
+ labels: card.labels.map(&:name),
42
+ list_id: card.list_id,
43
+ comments: comments_from(card),
44
+ attachments: attachments_from(card),
45
+ last_activity_date: card.last_activity_date
46
+ )
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ attr_reader :logger
53
+
54
+ def retrying(&block)
55
+ attempt = 0
56
+ begin
57
+ block.call
58
+ rescue Trello::Error => err
59
+ raise unless err.status_code == 429
60
+
61
+ attempt += 1
62
+
63
+ cooldown = 2**attempt + rand(2**attempt) - 2**(attempt - 1)
64
+
65
+ logger.warn "API limit exceeded, cool down for #{cooldown}s"
66
+
67
+ sleep cooldown
68
+
69
+ retry
70
+ end
71
+ end
72
+
73
+ def comments_from(card)
74
+ card.comments.map do |comment|
75
+ Comment.new(
76
+ text: comment.data["text"],
77
+ date: comment.date,
78
+ creator_id: comment.creator_id
79
+ )
80
+ end
81
+ end
82
+
83
+ def attachments_from(card)
84
+ card.attachments.map do |attach|
85
+ Attachment.new(
86
+ file_name: attach.file_name,
87
+ mime_type: attach.mime_type,
88
+ bytes: attach.bytes,
89
+ url: attach.url
90
+ )
91
+ end
92
+ end
93
+
94
+ def lists_from(board)
95
+ board.lists.map do |list|
96
+ List.new(
97
+ id: list.id,
98
+ name: list.name,
99
+ short_id: board.short_url.chars.last(8).join
100
+ )
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "trellodon/api_client"
4
+ require "trellodon/config"
5
+
6
+ module Trellodon
7
+ class APIExecutor
8
+ BOARD_ID_REGEX = /\/b\/([^\/]+)\//
9
+ private_constant :BOARD_ID_REGEX
10
+
11
+ def initialize(api_key:, api_token:, scheduler:, formatter:, logger: Config.logger)
12
+ @api_key = api_key
13
+ @api_token = api_token
14
+ @formatter = formatter
15
+ @logger = logger
16
+ @scheduler = scheduler
17
+ check_credentials!
18
+
19
+ @api_client = APIClient.new(api_key: @api_key, api_token: @api_token)
20
+ end
21
+
22
+ def download(board_pointer)
23
+ extract_board_id(board_pointer)
24
+
25
+ startup_time = Time.now
26
+ logger.debug "Fetching board 🚀️️"
27
+ board = scheduler.post do
28
+ api_client.fetch_board(board_id).tap { formatter.board_added(_1) }
29
+ end.value
30
+
31
+ logger.debug "Fetching cards in board with comments and attachments 🐢"
32
+ board.card_ids.map do |card_id|
33
+ scheduler.post do
34
+ api_client.fetch_card(card_id).tap { formatter.card_added(_1) }
35
+ end
36
+ end.map(&:value)
37
+
38
+ formatter.finish
39
+ logger.debug "All Trello API requests finished in #{(Time.now - startup_time).to_i} seconds ⌛"
40
+ end
41
+
42
+ private
43
+
44
+ attr_reader :api_client, :board_id, :logger, :scheduler, :formatter
45
+
46
+ def check_credentials!
47
+ return if @api_key.to_s.size.positive? && @api_token.to_s.size.positive?
48
+
49
+ raise ArgumentError, "Missing credentials. Please fill out both api_key, api_token first."
50
+ end
51
+
52
+ def extract_board_id(board_pointer)
53
+ @board_id = if URI::DEFAULT_PARSER.make_regexp.match?(board_pointer)
54
+ parse_board_url(board_pointer)
55
+ else
56
+ board_pointer
57
+ end
58
+ end
59
+
60
+ def parse_board_url(board_url)
61
+ rel_path = URI.parse(board_url).request_uri
62
+ match_data = rel_path.match(BOARD_ID_REGEX)
63
+ raise ArgumentError, "Wrong trello board url" if match_data.nil? || match_data.size != 2
64
+
65
+ match_data[1]
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "trellodon"
4
+
5
+ require "tty-prompt"
6
+
7
+ module Trellodon
8
+ class CLI
9
+ class Prompt
10
+ def initialize
11
+ @prompt = TTY::Prompt.new
12
+ end
13
+
14
+ def ask_api_key
15
+ @prompt.mask(
16
+ "Provide your Developer API Key (see https://trello.com/app-key):"
17
+ ) { |q| q.required true }
18
+ end
19
+
20
+ def ask_api_token
21
+ @prompt.mask(
22
+ "Provide your API token:"
23
+ ) { |q| q.required true }
24
+ end
25
+
26
+ def ask_board
27
+ @prompt.ask("Which board would you like to dump? (URL or ID)") do |q|
28
+ q.required true
29
+ end
30
+ end
31
+
32
+ def ask_folder
33
+ @prompt.ask("Destination folder?", default: "./")
34
+ end
35
+ end
36
+
37
+ class Config < Trellodon::Config
38
+ attr_config :board, :out
39
+
40
+ ignore_options :api_key, :api_token
41
+
42
+ describe_options(
43
+ board: "Board URL or ID",
44
+ out: "Destination folder path"
45
+ )
46
+
47
+ extend_options do |opts, _|
48
+ opts.banner = "Usage: trellodon dump\n"\
49
+ "Options:\n"
50
+
51
+ opts.on_tail("-v", "--version", "Print current version") do
52
+ puts Trellodon::VERSION
53
+ exit
54
+ end
55
+
56
+ opts.on_tail("-h", "--help", "Print help") do
57
+ puts opts
58
+ exit
59
+ end
60
+ end
61
+
62
+ private def prompt
63
+ @prompt ||= Prompt.new
64
+ end
65
+
66
+ def api_key
67
+ super || (self.api_key = prompt.ask_api_key)
68
+ end
69
+
70
+ def api_token
71
+ super || (self.api_token = prompt.ask_api_token)
72
+ end
73
+
74
+ def board
75
+ super || (self.board = prompt.ask_board)
76
+ end
77
+
78
+ def out
79
+ super || (self.out = prompt.ask_folder)
80
+ end
81
+ end
82
+
83
+ def initialize
84
+ @config = Config.new
85
+ end
86
+
87
+ def run
88
+ check_command!
89
+
90
+ executor = Trellodon::APIExecutor.new(
91
+ api_key: config.api_key,
92
+ api_token: config.api_token,
93
+ formatter: Trellodon::Formatters::Markdown.new(output_dir: config.out),
94
+ scheduler: Schedulers::Threaded.new
95
+ )
96
+ executor.download(config.board)
97
+ end
98
+
99
+ private
100
+
101
+ attr_reader :config
102
+
103
+ def check_command!
104
+ command, = config.option_parser.permute!(ARGV)
105
+ unless command
106
+ puts config.option_parser.help
107
+ exit
108
+ end
109
+
110
+ return if command == "dump"
111
+
112
+ raise "Unknown command: #{command}. Available commands: dump"
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "anyway_config"
4
+ require "logger"
5
+
6
+ module Trellodon
7
+ class Config < Anyway::Config
8
+ config_name :trellodon
9
+
10
+ attr_config :api_token, :api_key
11
+
12
+ def self.logger
13
+ Logger.new($stdout)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trellodon
4
+ Attachment = Struct.new("Attachment", :file_name, :mime_type, :bytes, :url, keyword_init: true)
5
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trellodon
4
+ Board = Struct.new("Board", :id, :short_id, :name, :card_ids, :lists, :last_activity_date, keyword_init: true) do
5
+ def get_list(list_id)
6
+ return nil if lists.nil?
7
+ lists.find { |list| list.id == list_id }
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trellodon
4
+ Card = Struct.new("Card", :id, :short_id, :name, :desc, :list_id, :labels, :comments, :attachments, :last_activity_date, keyword_init: true)
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trellodon
4
+ Comment = Struct.new("Comment", :text, :date, :creator_id, keyword_init: true)
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trellodon
4
+ List = Struct.new("List", :id, :short_id, :name, keyword_init: true)
5
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "trellodon/entities/board"
4
+ require "trellodon/entities/card"
5
+ require "trellodon/entities/list"
6
+ require "trellodon/entities/comment"
7
+ require "trellodon/entities/attachment"
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trellodon
4
+ module Formatters
5
+ class Base
6
+ attr_reader :board, :logger
7
+
8
+ def initialize(logger: Config.logger)
9
+ @logger = logger
10
+ end
11
+
12
+ def board_added(board)
13
+ @board = board
14
+ end
15
+
16
+ def card_added(card)
17
+ raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
18
+ end
19
+
20
+ def finish
21
+ raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open-uri"
4
+ require "fileutils"
5
+ require "trellodon/formatters/base"
6
+
7
+ module Trellodon
8
+ module Formatters
9
+ class Markdown < Base
10
+ attr_reader :output_dir
11
+
12
+ def initialize(output_dir:, **opts)
13
+ super(**opts)
14
+ @output_dir = output_dir
15
+ end
16
+
17
+ def card_added(card)
18
+ card_mdfile = File.join(card_path(card), "index.md")
19
+ raise "File #{card_mdfile} already exists" if File.exist?(card_mdfile)
20
+
21
+ File.write(card_mdfile, format_card(card))
22
+ download_attachments(card)
23
+ end
24
+
25
+ def finish
26
+ logger.info "Markdown dump is here: #{@output_dir}"
27
+ end
28
+
29
+ private
30
+
31
+ def card_path(card)
32
+ raise "Board is undefined" if @board.nil?
33
+ raise "List id is undefined" if card.list_id.nil? || card.list_id.empty?
34
+ list = @board.get_list(card.list_id)
35
+ raise "List #{card.list_id} is not found" if list.nil?
36
+
37
+ card_dir = File.join(@output_dir,
38
+ "#{board.name} [#{board.short_id}]",
39
+ "#{list.name} [#{list.short_id}]",
40
+ "#{card.name} [#{card.short_id}]")
41
+ FileUtils.mkdir_p(card_dir) unless File.directory?(card_dir)
42
+ card_dir
43
+ end
44
+
45
+ def card_attachments_path(card)
46
+ attachments_dir = File.join(card_path(card), "attachments")
47
+ FileUtils.mkdir_p(attachments_dir) unless File.directory?(attachments_dir)
48
+ attachments_dir
49
+ end
50
+
51
+ def download_attachments(card)
52
+ return # FIXME: 401 Unauthorized ???
53
+ return if card.attachments.nil? || card.attachments.empty? # rubocop:disable Lint/UnreachableCode
54
+ attachments_path = card_attachments_path(card)
55
+ card.attachments.each do |att|
56
+ IO.copy_stream(URI.parse(att.url).open, File.join(attachments_path, att.file_name))
57
+ end
58
+ end
59
+
60
+ def format_card(card)
61
+ create_card_header(card) +
62
+ create_card_title(card) +
63
+ create_card_description(card) +
64
+ create_card_comments(card)
65
+ end
66
+
67
+ def create_card_header(card)
68
+ <<~EOS
69
+ ---
70
+ title: #{card.name}
71
+ last_updated_at: #{card.last_activity_date}
72
+ labels: #{create_card_labels(card)}
73
+ ---
74
+ EOS
75
+ end
76
+
77
+ def create_card_title(card)
78
+ <<~EOS
79
+
80
+ # #{card.name}
81
+ EOS
82
+ end
83
+
84
+ def create_card_description(card)
85
+ <<~EOS
86
+
87
+ ## Description
88
+ #{card.desc}
89
+ EOS
90
+ end
91
+
92
+ def create_card_labels(card)
93
+ return "" if card.labels.nil? || card.labels.empty?
94
+ card.labels.map { |label| "\n - " + label }.reduce(:+)
95
+ end
96
+
97
+ def create_card_comments(card)
98
+ return "" if card.comments.nil? || card.comments.empty?
99
+ <<~EOS
100
+
101
+ ## Comments
102
+ #{card.comments.map { |comment| create_card_comment(comment) }.reduce(:+)}
103
+ EOS
104
+ end
105
+
106
+ def create_card_comment(comment)
107
+ <<~EOS
108
+
109
+ **#{comment.creator_id} @ #{comment.date}**
110
+ #{comment.text}
111
+ EOS
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "trellodon/formatters/base"
4
+ require "trellodon/formatters/markdown"
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trellodon
4
+ module Schedulers
5
+ class Base
6
+ def post
7
+ raise NotImplementedError
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "trellodon/schedulers/base"
4
+
5
+ module Trellodon
6
+ module Schedulers
7
+ class Inline < Base
8
+ Result = Struct.new(:value)
9
+
10
+ def post(&block)
11
+ Result.new(block.call)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "trellodon/schedulers/base"
4
+
5
+ module Trellodon
6
+ module Schedulers
7
+ class Threaded < Base
8
+ def post(&block)
9
+ Thread.new(&block)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "trellodon/schedulers/base"
4
+ require "trellodon/schedulers/inline"
5
+ require "trellodon/schedulers/threaded"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trellodon
4
+ VERSION = "0.1.0"
5
+ end
data/lib/trellodon.rb ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "trellodon/version"
4
+ require "trellodon/config"
5
+ require "trellodon/api_executor"
6
+
7
+ require "trellodon/entities"
8
+ require "trellodon/schedulers"
9
+ require "trellodon/formatters"
data/sig/trellodon.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Trellodon
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: trellodon
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - fargelus
8
+ - rinasergeeva
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2022-04-08 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: anyway_config
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: ruby-trello
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: tty-prompt
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ description: "\n The main purpose of Trellodon is to make it possible to backup
57
+ Trello boards to file system in a human-readable form (e.g., Markdown files).\n
58
+ \ "
59
+ email:
60
+ - ddraudred@gmail.com
61
+ - catherine.sergeeva@gmail.com
62
+ executables:
63
+ - trellodon
64
+ extensions: []
65
+ extra_rdoc_files: []
66
+ files:
67
+ - CHANGELOG.md
68
+ - LICENSE
69
+ - README.md
70
+ - bin/console
71
+ - bin/setup
72
+ - bin/trellodon
73
+ - lib/trellodon.rb
74
+ - lib/trellodon/api_client.rb
75
+ - lib/trellodon/api_executor.rb
76
+ - lib/trellodon/cli.rb
77
+ - lib/trellodon/config.rb
78
+ - lib/trellodon/entities.rb
79
+ - lib/trellodon/entities/attachment.rb
80
+ - lib/trellodon/entities/board.rb
81
+ - lib/trellodon/entities/card.rb
82
+ - lib/trellodon/entities/comment.rb
83
+ - lib/trellodon/entities/list.rb
84
+ - lib/trellodon/formatters.rb
85
+ - lib/trellodon/formatters/base.rb
86
+ - lib/trellodon/formatters/markdown.rb
87
+ - lib/trellodon/schedulers.rb
88
+ - lib/trellodon/schedulers/base.rb
89
+ - lib/trellodon/schedulers/inline.rb
90
+ - lib/trellodon/schedulers/threaded.rb
91
+ - lib/trellodon/version.rb
92
+ - sig/trellodon.rbs
93
+ homepage: https://github.com/evilmartians/trellodon
94
+ licenses: []
95
+ metadata:
96
+ homepage_uri: https://github.com/evilmartians/trellodon
97
+ source_code_uri: https://github.com/evilmartians/trellodon
98
+ changelog_uri: https://github.com/evilmartians/trellodon/CHANGELOG.md
99
+ post_install_message:
100
+ rdoc_options: []
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: 2.6.0
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ requirements: []
114
+ rubygems_version: 3.3.7
115
+ signing_key:
116
+ specification_version: 4
117
+ summary: 'Trellodon: dump Trello boards to your file system'
118
+ test_files: []