trellodon 0.1.0 → 0.2.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 +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
|
+
[](https://github.com/evilmartians/trellodon/actions/workflows/main.yml)
|
2
|
+
[](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
|