trellodon 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bf1b8715b6cbad2c1bd3ab4fce72198a442380cf4f2d7bb39c55928ef72b032f
4
- data.tar.gz: 2caa2a3a24fe463cc1cb730a3aad68928323ec0fea04fea8e3a0f10ebf051ea7
3
+ metadata.gz: 9de6b483c5d9dd4a4dc5a669a4ce6d3ba56cac3575e9d9174ae8603e7f6adf9a
4
+ data.tar.gz: 834fbc2e3c2da99e0b69d5989da18d476976d6e4912077579d04898c24ab6f7b
5
5
  SHA512:
6
- metadata.gz: b50b3279f7a9e787b2f2e718109902a9cda88729f7d7b330aa5db9db944f913af33d6f26fab7b1f6c4d2866a1241d62d99c4dbe989ea468499bcaaa4908a153f
7
- data.tar.gz: 52d86ebd0b43ad0b064facbfb66332623422b2c688eff732188b7ea2ce8baba2842a331a941e1aafb410f114c31aef3ec80f7588b7f6a07808070019f9eb12ba
6
+ metadata.gz: ced2b722b7a3f8536a0fbf958b3e3dfdb9e71921d7658c31f69c1b817b0cc88b4d25d5ccd7c94dd7db7d97e0109fceba574c96c51a2ed7910f74645225f51442
7
+ data.tar.gz: 99b59ce531022d3f341d0c97306bb24cec800d5bfd6590b7342b16bc8c138bab853a8007203b382f5ccedd94cb060dd0f459a28502b394e687ab3ce57086934d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
- ## [Unreleased]
1
+ # Change log
2
+
3
+ ## [master]
4
+
5
+ ## [0.2.0] - 2022-04-12
6
+
7
+ - Store Trello API creds locally for avoid asking it before execution ([@fargelus][]).
8
+ - Download attachments, human names in comments, rename index.md to README.md ([@rinasergeeva][]).
9
+ - Added minimal project README.md ([@rinasergeeva][], [@fargelus][]).
10
+ - --sync param for switch to `Schedulers::Inline` in CLI ([@fargelus][]).
11
+ - Concurrent scheduler with customizable thread pool size ([@fargelus][]).
12
+ - Checklists support ([@rinasergeeva][]).
2
13
 
3
14
  ## [0.1.0] - 2022-04-07
4
15
 
5
16
  - Initial release
17
+
18
+ [@fargelus]: https://github.com/fargelus
19
+ [@rinasergeeva]: https://github.com/rinasergeeva
data/README.md CHANGED
@@ -1,3 +1,90 @@
1
- # trellodon
1
+ [![Build](https://github.com/evilmartians/trellodon/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/evilmartians/trellodon/actions/workflows/main.yml)
2
+ [![Gem Version](https://badge.fury.io/rb/trellodon.svg)](https://rubygems.org/gems/trellodon)
2
3
 
3
- Dump Trello boards to your file system
4
+ # Trellodon
5
+
6
+ A Ruby tool to export a Trello board and convert it into a set of folders and markdown files, corresponding to lists and cards on the board. For each card, you’ll get the basic details such as:
7
+
8
+ * Name
9
+ * Description
10
+ * Labels
11
+ * Comments
12
+ * Attachments
13
+
14
+ <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
15
+
16
+ ## Installation
17
+
18
+ ```
19
+ gem install trellodon
20
+ ```
21
+
22
+ ## Prerequisites
23
+
24
+ Trellodon needs two secret codes to download your boards:
25
+
26
+ * Trello API key
27
+ * Authentication token
28
+
29
+ To generate them, go to the [trello.com/app-key](https://trello.com/app-key) page and follow the instructions.
30
+
31
+ ## Usage
32
+
33
+ ```sh
34
+ Usage: trellodon dump
35
+ Options:
36
+ --board VALUE Board URL or ID
37
+ --out VALUE Destination folder path
38
+ -v, --version Print current version
39
+ -h, --help Print help
40
+ ```
41
+
42
+ ### Detailed example
43
+
44
+ Suppose we have a Trello board called `projects` with lists `Brainstorm`, `TODO`, `DOING`, `DONE`. Lists contains own cards.<br>
45
+ All Trello boards have its own id this id requires Trellodon to fetch board via Trello API.
46
+ You can find this id in two ways:
47
+
48
+ * In URL `https://trello.com/b/{id}/projects` your Trello board id will be placed as `{id}` in example;
49
+ * Put `.json` in your board URL like this `https://trello.com/b/{id}/projects.json` id field in json output is what you need;
50
+
51
+ You can also paste whole board URL and Trellodon will parse it correctly.<br>
52
+ After launch Trellodon creates follow output in specified folder:
53
+
54
+ ```sh
55
+ projects/
56
+ Brainstorm/
57
+ first_card_title/
58
+ README.md
59
+ attachments/
60
+ image.png
61
+ TODO/
62
+ first_card_title/
63
+ README.md
64
+ attachments/
65
+ report.docx
66
+ ...
67
+ ```
68
+
69
+ Each card has follow output format:
70
+
71
+ ```md
72
+ ---
73
+ title: card_title
74
+ last_updated_at: 2022-03-16 16:28:39 UTC
75
+ labels: Test
76
+ ---
77
+
78
+ # card_title
79
+
80
+ ## Description
81
+ Some card description
82
+
83
+ ## Comments
84
+ ** John Doe @doe at 2022-03-16 16:28:39 UTC**
85
+
86
+ ## Attachments
87
+ ### Image.png
88
+ **date**: 2022-03-16
89
+ **url**: https://trello.com/1/cards/#{card_id}/attachments/#{attachment.id}/download/image.png
90
+ ```
@@ -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 { |_1| 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 { |_1| 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,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "trellodon/schedulers/base"
4
+ require "concurrent"
5
+
6
+ module Trellodon
7
+ module Schedulers
8
+ class Concurrent < Base
9
+ using RubyNext
10
+
11
+ MAX_THREADS = 50
12
+
13
+ def post(&block)
14
+ ::Concurrent::Future.execute(executor: executor, &block)
15
+ end
16
+
17
+ private def executor
18
+ ::Concurrent::FixedThreadPool.new(MAX_THREADS)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "trello"
4
-
5
4
  require "trellodon/entities"
6
5
 
7
6
  module Trellodon
@@ -12,6 +11,7 @@ module Trellodon
12
11
  developer_public_key: api_key,
13
12
  member_token: api_token
14
13
  )
14
+ @members = {}
15
15
  end
16
16
 
17
17
  def fetch_board(board_id)
@@ -42,6 +42,7 @@ module Trellodon
42
42
  list_id: card.list_id,
43
43
  comments: comments_from(card),
44
44
  attachments: attachments_from(card),
45
+ checklists: checklists_from(card),
45
46
  last_activity_date: card.last_activity_date
46
47
  )
47
48
  end
@@ -57,15 +58,10 @@ module Trellodon
57
58
  block.call
58
59
  rescue Trello::Error => err
59
60
  raise unless err.status_code == 429
60
-
61
61
  attempt += 1
62
-
63
62
  cooldown = 2**attempt + rand(2**attempt) - 2**(attempt - 1)
64
-
65
63
  logger.warn "API limit exceeded, cool down for #{cooldown}s"
66
-
67
64
  sleep cooldown
68
-
69
65
  retry
70
66
  end
71
67
  end
@@ -75,7 +71,7 @@ module Trellodon
75
71
  Comment.new(
76
72
  text: comment.data["text"],
77
73
  date: comment.date,
78
- creator_id: comment.creator_id
74
+ creator: member_from(comment)
79
75
  )
80
76
  end
81
77
  end
@@ -86,6 +82,10 @@ module Trellodon
86
82
  file_name: attach.file_name,
87
83
  mime_type: attach.mime_type,
88
84
  bytes: attach.bytes,
85
+ date: attach.date,
86
+ name: attach.name,
87
+ is_upload: attach.is_upload,
88
+ headers: attach.is_upload ? {"Authorization" => "OAuth oauth_consumer_key=\"#{@client.configuration.developer_public_key}\", oauth_token=\"#{@client.configuration.member_token}\""} : {},
89
89
  url: attach.url
90
90
  )
91
91
  end
@@ -100,5 +100,36 @@ module Trellodon
100
100
  )
101
101
  end
102
102
  end
103
+
104
+ def checklists_from(card)
105
+ card.checklists.map do |checklist|
106
+ Checklist.new(
107
+ id: checklist.id,
108
+ name: checklist.name,
109
+ items: checklist_items_from(checklist)
110
+ )
111
+ end
112
+ end
113
+
114
+ def checklist_items_from(checklist)
115
+ checklist.items.map do |item|
116
+ ChecklistItem.new(
117
+ id: item.id,
118
+ name: item.name,
119
+ state: item.state
120
+ )
121
+ end
122
+ end
123
+
124
+ def member_from(comment)
125
+ return @members[comment.creator_id] if @members.has_key? comment.creator_id
126
+
127
+ member = @client.find(:members, comment.creator_id)
128
+ @members[comment.creator_id] = Member.new(
129
+ id: member.id,
130
+ full_name: member.full_name,
131
+ username: member.username
132
+ )
133
+ end
103
134
  end
104
135
  end
data/lib/trellodon/cli.rb CHANGED
@@ -1,36 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "trellodon"
4
-
5
4
  require "tty-prompt"
5
+ require "fileutils"
6
6
 
7
7
  module Trellodon
8
8
  class CLI
9
- class Prompt
10
- def initialize
11
- @prompt = TTY::Prompt.new
12
- end
9
+ class Prompt < TTY::Prompt
10
+ include Singleton
13
11
 
14
12
  def ask_api_key
15
- @prompt.mask(
13
+ mask(
16
14
  "Provide your Developer API Key (see https://trello.com/app-key):"
17
15
  ) { |q| q.required true }
18
16
  end
19
17
 
20
18
  def ask_api_token
21
- @prompt.mask(
19
+ mask(
22
20
  "Provide your API token:"
23
21
  ) { |q| q.required true }
24
22
  end
25
23
 
26
24
  def ask_board
27
- @prompt.ask("Which board would you like to dump? (URL or ID)") do |q|
25
+ ask("Which board would you like to dump? (URL or ID)") do |q|
28
26
  q.required true
29
27
  end
30
28
  end
31
29
 
32
30
  def ask_folder
33
- @prompt.ask("Destination folder?", default: "./")
31
+ ask("Destination folder?", default: "./")
34
32
  end
35
33
  end
36
34
 
@@ -44,10 +42,16 @@ module Trellodon
44
42
  out: "Destination folder path"
45
43
  )
46
44
 
47
- extend_options do |opts, _|
48
- opts.banner = "Usage: trellodon dump\n"\
45
+ extend_options do |opts, config|
46
+ opts.banner = "Usage: trellodon dump \e[34m[options]\e[0m\n"\
49
47
  "Options:\n"
50
48
 
49
+ opts.on("--sync", "Process API requests synchronously")
50
+ opts.on("--concurrency VALUE", "Amount of processing threads (default: 50)")
51
+ opts.on("--clear-auth", "Remove saved api credentials") do
52
+ config.reload(api_token: nil, api_key: nil)
53
+ end
54
+
51
55
  opts.on_tail("-v", "--version", "Print current version") do
52
56
  puts Trellodon::VERSION
53
57
  exit
@@ -59,8 +63,25 @@ module Trellodon
59
63
  end
60
64
  end
61
65
 
66
+ def resolve_config_path(_, _)
67
+ config_path
68
+ end
69
+
70
+ def config_path
71
+ File.join(Dir.home, ".config", "#{config_name}.yml")
72
+ end
73
+
74
+ def update_config_file!
75
+ dir = File.dirname(config_path)
76
+ FileUtils.mkdir_p(dir) unless File.exist?(dir)
77
+
78
+ yml = "api_token: #{api_token}\n"\
79
+ "api_key: #{api_key}"
80
+ File.write(config_path, yml)
81
+ end
82
+
62
83
  private def prompt
63
- @prompt ||= Prompt.new
84
+ Prompt.instance
64
85
  end
65
86
 
66
87
  def api_key
@@ -82,34 +103,66 @@ module Trellodon
82
103
 
83
104
  def initialize
84
105
  @config = Config.new
106
+ @scheduler = Schedulers::Concurrent.new
85
107
  end
86
108
 
87
109
  def run
88
110
  check_command!
89
111
 
112
+ need_to_fill_config = config.to_h[:api_key].nil?
113
+
90
114
  executor = Trellodon::APIExecutor.new(
91
115
  api_key: config.api_key,
92
116
  api_token: config.api_token,
93
117
  formatter: Trellodon::Formatters::Markdown.new(output_dir: config.out),
94
- scheduler: Schedulers::Threaded.new
118
+ scheduler: scheduler
95
119
  )
96
120
  executor.download(config.board)
121
+
122
+ ask_save_credentials if need_to_fill_config
97
123
  end
98
124
 
99
125
  private
100
126
 
101
- attr_reader :config
127
+ attr_reader :config, :scheduler
128
+
129
+ def prompt
130
+ Prompt.instance
131
+ end
102
132
 
103
133
  def check_command!
104
- command, = config.option_parser.permute!(ARGV)
134
+ params = {}
135
+ command, = config.option_parser.parse!(ARGV, into: params)
105
136
  unless command
106
137
  puts config.option_parser.help
107
138
  exit
108
139
  end
109
140
 
110
- return if command == "dump"
141
+ if command == "dump"
142
+ change_scheduler(params) if params.key?(:sync) || params.key?(:concurrency)
143
+ return
144
+ end
111
145
 
112
146
  raise "Unknown command: #{command}. Available commands: dump"
113
147
  end
148
+
149
+ def ask_save_credentials
150
+ answer = prompt.yes?("Would you like to save api creds so next time trellodon won't asking it?", default: true)
151
+ config.update_config_file! if answer
152
+ end
153
+
154
+ def change_scheduler(params)
155
+ if params.key?(:sync)
156
+ @scheduler = Schedulers::Inline.new
157
+ return
158
+ end
159
+
160
+ return unless params[:concurrency]
161
+
162
+ threads = params[:concurrency].to_i
163
+ raise "Value of concurrency option must be integer and greater than zero" unless threads.positive?
164
+
165
+ @scheduler = Schedulers::Concurrent.new(threads)
166
+ end
114
167
  end
115
168
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Trellodon
4
- Attachment = Struct.new("Attachment", :file_name, :mime_type, :bytes, :url, keyword_init: true)
4
+ Attachment = Struct.new("Attachment", :file_name, :mime_type, :bytes, :date, :name, :is_upload, :headers, :url, keyword_init: true)
5
5
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Trellodon
4
- Card = Struct.new("Card", :id, :short_id, :name, :desc, :list_id, :labels, :comments, :attachments, :last_activity_date, keyword_init: true)
4
+ Card = Struct.new("Card", :id, :short_id, :name, :desc, :list_id, :labels, :comments, :attachments, :checklists, :last_activity_date, keyword_init: true)
5
5
  end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trellodon
4
+ Checklist = Struct.new("Checklist", :id, :name, :items, keyword_init: true)
5
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trellodon
4
+ ChecklistItem = Struct.new("ChecklistItem", :id, :name, :state, keyword_init: true) do
5
+ def checked?
6
+ state == "complete"
7
+ end
8
+ end
9
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Trellodon
4
- Comment = Struct.new("Comment", :text, :date, :creator_id, keyword_init: true)
4
+ Comment = Struct.new("Comment", :text, :date, :creator, keyword_init: true)
5
5
  end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trellodon
4
+ Member = Struct.new("Member", :id, :full_name, :username, keyword_init: true)
5
+ end
@@ -5,3 +5,6 @@ require "trellodon/entities/card"
5
5
  require "trellodon/entities/list"
6
6
  require "trellodon/entities/comment"
7
7
  require "trellodon/entities/attachment"
8
+ require "trellodon/entities/checklist"
9
+ require "trellodon/entities/checklist_item"
10
+ require "trellodon/entities/member"
@@ -9,13 +9,16 @@ module Trellodon
9
9
  class Markdown < Base
10
10
  attr_reader :output_dir
11
11
 
12
+ MD_FILENAME = "README.md"
13
+ ATTACHMENTS_DIRNAME = "attachments"
14
+
12
15
  def initialize(output_dir:, **opts)
13
16
  super(**opts)
14
17
  @output_dir = output_dir
15
18
  end
16
19
 
17
20
  def card_added(card)
18
- card_mdfile = File.join(card_path(card), "index.md")
21
+ card_mdfile = File.join(card_path(card), MD_FILENAME)
19
22
  raise "File #{card_mdfile} already exists" if File.exist?(card_mdfile)
20
23
 
21
24
  File.write(card_mdfile, format_card(card))
@@ -29,7 +32,7 @@ module Trellodon
29
32
  private
30
33
 
31
34
  def card_path(card)
32
- raise "Board is undefined" if @board.nil?
35
+ raise "Board is undefined" unless @board
33
36
  raise "List id is undefined" if card.list_id.nil? || card.list_id.empty?
34
37
  list = @board.get_list(card.list_id)
35
38
  raise "List #{card.list_id} is not found" if list.nil?
@@ -43,17 +46,16 @@ module Trellodon
43
46
  end
44
47
 
45
48
  def card_attachments_path(card)
46
- attachments_dir = File.join(card_path(card), "attachments")
49
+ attachments_dir = File.join(card_path(card), ATTACHMENTS_DIRNAME)
47
50
  FileUtils.mkdir_p(attachments_dir) unless File.directory?(attachments_dir)
48
51
  attachments_dir
49
52
  end
50
53
 
51
54
  def download_attachments(card)
52
- return # FIXME: 401 Unauthorized ???
53
- return if card.attachments.nil? || card.attachments.empty? # rubocop:disable Lint/UnreachableCode
55
+ return if card.attachments.nil? || card.attachments.empty?
54
56
  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
+ card.attachments.select(&:is_upload).each do |att|
58
+ IO.copy_stream(URI.parse(att.url).open(att.headers), File.join(attachments_path, att.file_name))
57
59
  end
58
60
  end
59
61
 
@@ -61,7 +63,9 @@ module Trellodon
61
63
  create_card_header(card) +
62
64
  create_card_title(card) +
63
65
  create_card_description(card) +
64
- create_card_comments(card)
66
+ create_card_checklists(card) +
67
+ create_card_comments(card) +
68
+ create_card_attachments(card)
65
69
  end
66
70
 
67
71
  def create_card_header(card)
@@ -85,13 +89,35 @@ module Trellodon
85
89
  <<~EOS
86
90
 
87
91
  ## Description
92
+
88
93
  #{card.desc}
89
94
  EOS
90
95
  end
91
96
 
92
97
  def create_card_labels(card)
93
98
  return "" if card.labels.nil? || card.labels.empty?
94
- card.labels.map { |label| "\n - " + label }.reduce(:+)
99
+ card.labels.map { |label| "\n - " + label }.join
100
+ end
101
+
102
+ def create_card_checklists(card)
103
+ return "" if card.checklists.nil? || card.checklists.empty?
104
+ <<~EOS
105
+
106
+ ## Checklists
107
+ #{card.checklists.map { |checklist| create_card_checklist(checklist) }.join}
108
+ EOS
109
+ end
110
+
111
+ def create_card_checklist(checklist)
112
+ <<~EOS
113
+
114
+ ### #{checklist.name}
115
+ #{checklist.items&.map { |item| create_card_checklist_item(item) }&.join}
116
+ EOS
117
+ end
118
+
119
+ def create_card_checklist_item(item)
120
+ "\n- [#{item.checked? ? "x" : " "}] #{item.name}"
95
121
  end
96
122
 
97
123
  def create_card_comments(card)
@@ -99,17 +125,42 @@ module Trellodon
99
125
  <<~EOS
100
126
 
101
127
  ## Comments
102
- #{card.comments.map { |comment| create_card_comment(comment) }.reduce(:+)}
128
+ #{card.comments.map { |comment| create_card_comment(comment) }.join}
103
129
  EOS
104
130
  end
105
131
 
106
132
  def create_card_comment(comment)
107
133
  <<~EOS
108
134
 
109
- **#{comment.creator_id} @ #{comment.date}**
135
+ **#{comment.creator.full_name} (@#{comment.creator.username}) at #{comment.date}**
110
136
  #{comment.text}
111
137
  EOS
112
138
  end
139
+
140
+ def create_card_attachments(card)
141
+ return "" if card.attachments.nil? || card.attachments.empty?
142
+ <<~EOS
143
+
144
+ ## Attachments
145
+ #{card.attachments.map { |attachment| create_card_attachment(attachment) }.join}
146
+ EOS
147
+ end
148
+
149
+ def create_card_attachment(attachment)
150
+ <<~EOS
151
+
152
+ ### #{attachment.name}
153
+
154
+ **date**: #{attachment.date}
155
+ **url**: #{attachment.url}
156
+ **local copy**: #{create_attachment_local_link(attachment)}
157
+ EOS
158
+ end
159
+
160
+ def create_attachment_local_link(attachment)
161
+ return "-" unless attachment.is_upload
162
+ "[#{attachment.file_name}](#{File.join("./", ATTACHMENTS_DIRNAME, attachment.file_name)})"
163
+ end
113
164
  end
114
165
  end
115
166
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "trellodon/schedulers/base"
4
+ require "concurrent"
5
+
6
+ module Trellodon
7
+ module Schedulers
8
+ class Concurrent < Base
9
+ using RubyNext
10
+
11
+ DEFAULT_MAX_THREADS = 50
12
+
13
+ attr_reader :max_threads
14
+
15
+ def initialize(threads_count = DEFAULT_MAX_THREADS)
16
+ @max_threads = threads_count
17
+ end
18
+
19
+ def post(&block)
20
+ ::Concurrent::Future.execute(executor:, &block)
21
+ end
22
+
23
+ private
24
+
25
+ def executor
26
+ ::Concurrent::FixedThreadPool.new(max_threads)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -3,3 +3,4 @@
3
3
  require "trellodon/schedulers/base"
4
4
  require "trellodon/schedulers/inline"
5
5
  require "trellodon/schedulers/threaded"
6
+ require "trellodon/schedulers/concurrent"
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "trello"
4
+ require "logger"
5
+
6
+ module Trello
7
+ # Nullify Trello client API logger
8
+ def self.logger
9
+ Logger.new(nil)
10
+ end
11
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Trellodon
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/trellodon.rb CHANGED
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "ruby-next"
4
+
5
+ require "ruby-next/language/setup"
6
+ RubyNext::Language.setup_gem_load_path(transpile: true)
7
+
3
8
  require "trellodon/version"
4
9
  require "trellodon/config"
5
10
  require "trellodon/api_executor"
metadata CHANGED
@@ -1,15 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trellodon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - fargelus
8
8
  - rinasergeeva
9
+ - palkan
9
10
  autorequire:
10
11
  bindir: bin
11
12
  cert_chain: []
12
- date: 2022-04-08 00:00:00.000000000 Z
13
+ date: 2022-04-12 00:00:00.000000000 Z
13
14
  dependencies:
14
15
  - !ruby/object:Gem::Dependency
15
16
  name: anyway_config
@@ -53,12 +54,27 @@ dependencies:
53
54
  - - ">="
54
55
  - !ruby/object:Gem::Version
55
56
  version: '0'
57
+ - !ruby/object:Gem::Dependency
58
+ name: concurrent-ruby
59
+ requirement: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ type: :runtime
65
+ prerelease: false
66
+ version_requirements: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
56
71
  description: "\n The main purpose of Trellodon is to make it possible to backup
57
72
  Trello boards to file system in a human-readable form (e.g., Markdown files).\n
58
73
  \ "
59
74
  email:
60
75
  - ddraudred@gmail.com
61
76
  - catherine.sergeeva@gmail.com
77
+ - dementiev.vm@gmail.com
62
78
  executables:
63
79
  - trellodon
64
80
  extensions: []
@@ -70,6 +86,8 @@ files:
70
86
  - bin/console
71
87
  - bin/setup
72
88
  - bin/trellodon
89
+ - lib/.rbnext/2.7/trellodon/api_executor.rb
90
+ - lib/.rbnext/3.1/trellodon/schedulers/concurrent.rb
73
91
  - lib/trellodon.rb
74
92
  - lib/trellodon/api_client.rb
75
93
  - lib/trellodon/api_executor.rb
@@ -79,15 +97,20 @@ files:
79
97
  - lib/trellodon/entities/attachment.rb
80
98
  - lib/trellodon/entities/board.rb
81
99
  - lib/trellodon/entities/card.rb
100
+ - lib/trellodon/entities/checklist.rb
101
+ - lib/trellodon/entities/checklist_item.rb
82
102
  - lib/trellodon/entities/comment.rb
83
103
  - lib/trellodon/entities/list.rb
104
+ - lib/trellodon/entities/member.rb
84
105
  - lib/trellodon/formatters.rb
85
106
  - lib/trellodon/formatters/base.rb
86
107
  - lib/trellodon/formatters/markdown.rb
87
108
  - lib/trellodon/schedulers.rb
88
109
  - lib/trellodon/schedulers/base.rb
110
+ - lib/trellodon/schedulers/concurrent.rb
89
111
  - lib/trellodon/schedulers/inline.rb
90
112
  - lib/trellodon/schedulers/threaded.rb
113
+ - lib/trellodon/trello.rb
91
114
  - lib/trellodon/version.rb
92
115
  - sig/trellodon.rbs
93
116
  homepage: https://github.com/evilmartians/trellodon