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 +4 -4
- data/CHANGELOG.md +15 -1
- data/README.md +89 -2
- data/lib/.rbnext/2.7/trellodon/api_executor.rb +68 -0
- data/lib/.rbnext/3.1/trellodon/schedulers/concurrent.rb +22 -0
- data/lib/trellodon/api_client.rb +38 -7
- data/lib/trellodon/cli.rb +69 -16
- data/lib/trellodon/entities/attachment.rb +1 -1
- data/lib/trellodon/entities/card.rb +1 -1
- data/lib/trellodon/entities/checklist.rb +5 -0
- data/lib/trellodon/entities/checklist_item.rb +9 -0
- data/lib/trellodon/entities/comment.rb +1 -1
- data/lib/trellodon/entities/member.rb +5 -0
- data/lib/trellodon/entities.rb +3 -0
- data/lib/trellodon/formatters/markdown.rb +62 -11
- data/lib/trellodon/schedulers/concurrent.rb +30 -0
- data/lib/trellodon/schedulers.rb +1 -0
- data/lib/trellodon/trello.rb +11 -0
- data/lib/trellodon/version.rb +1 -1
- data/lib/trellodon.rb +5 -0
- metadata +25 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9de6b483c5d9dd4a4dc5a669a4ce6d3ba56cac3575e9d9174ae8603e7f6adf9a
|
4
|
+
data.tar.gz: 834fbc2e3c2da99e0b69d5989da18d476976d6e4912077579d04898c24ab6f7b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ced2b722b7a3f8536a0fbf958b3e3dfdb9e71921d7658c31f69c1b817b0cc88b4d25d5ccd7c94dd7db7d97e0109fceba574c96c51a2ed7910f74645225f51442
|
7
|
+
data.tar.gz: 99b59ce531022d3f341d0c97306bb24cec800d5bfd6590b7342b16bc8c138bab853a8007203b382f5ccedd94cb060dd0f459a28502b394e687ab3ce57086934d
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,19 @@
|
|
1
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/trellodon/api_client.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
11
|
-
@prompt = TTY::Prompt.new
|
12
|
-
end
|
9
|
+
class Prompt < TTY::Prompt
|
10
|
+
include Singleton
|
13
11
|
|
14
12
|
def ask_api_key
|
15
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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:
|
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
|
-
|
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
|
-
|
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
|
data/lib/trellodon/entities.rb
CHANGED
@@ -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),
|
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"
|
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),
|
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
|
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
|
-
|
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 }.
|
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) }.
|
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.
|
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
|
data/lib/trellodon/schedulers.rb
CHANGED
data/lib/trellodon/version.rb
CHANGED
data/lib/trellodon.rb
CHANGED
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.
|
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-
|
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
|