checkoff 0.10.0 → 0.11.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +4 -3
- data/.envrc +1 -1
- data/.gitignore +1 -0
- data/Gemfile.lock +17 -8
- data/Makefile +7 -3
- data/checkoff.gemspec +6 -1
- data/coverage/.last_run.json +2 -2
- data/exe/checkoff +1 -2
- data/fix.sh +42 -7
- data/lib/checkoff/cli.rb +98 -85
- data/lib/checkoff/config_loader.rb +22 -16
- data/lib/checkoff/projects.rb +31 -27
- data/lib/checkoff/sections.rb +65 -54
- data/lib/checkoff/subtasks.rb +14 -11
- data/lib/checkoff/tasks.rb +17 -8
- data/lib/checkoff/version.rb +2 -1
- data/lib/checkoff/workspaces.rb +8 -4
- data/rakelib/{ci.rake → citest.rake} +1 -1
- data/rakelib/clear_metrics.rake +10 -1
- metadata +34 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0f9082aa251f94c438c29019a88db01de1732564c237bb3dff8d4aef6ce8eea6
|
4
|
+
data.tar.gz: 90cc397c4381839ddc1ba55c0096e84e7baf735653d2aa46267a1e52dd684603
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 21438c7ac67f3e782a1fee1c7adae43baa13df126904668dc6784cfab27b57d4abea67380af34560ff834e6b6a00a1fcd50ce12f171781991c273cc8bd9ce10e
|
7
|
+
data.tar.gz: 863441bc08bd83be385bb8b5806ad37a720a421cdfab44efcb9c31e528b649ee11a6e0d392401e4fb2931ff4c6f4a7934d2b652df4fb200c8286bc758c9ad7ba
|
data/.circleci/config.yml
CHANGED
@@ -19,7 +19,7 @@ commands:
|
|
19
19
|
export PATH="${HOME}/.pyenv/bin:${PATH}"
|
20
20
|
export PATH="${HOME}/.rbenv/bin:${HOME}/.rbenv/shims:${PATH}"
|
21
21
|
export PATH="${HOME}/project/node_modules/.bin:${PATH}"
|
22
|
-
eval "$(pyenv init
|
22
|
+
eval "$(pyenv init --path)"
|
23
23
|
eval "$(pyenv virtualenv-init -)"
|
24
24
|
eval "$(rbenv init -)"
|
25
25
|
export BUNDLE_PATH=vendor/bundle
|
@@ -97,11 +97,12 @@ jobs:
|
|
97
97
|
steps:
|
98
98
|
- set_up_environment
|
99
99
|
- run_with_languages:
|
100
|
-
label:
|
100
|
+
label: Test
|
101
101
|
command: |
|
102
|
-
|
102
|
+
bundle exec rake citest
|
103
103
|
# https://github.com/bluelabsio/records-mover/blob/master/Makefile#L25
|
104
104
|
git status --porcelain coverage/.last_run.json
|
105
|
+
git diff coverage/.last_run.json
|
105
106
|
test -z "$(git status --porcelain coverage/.last_run.json)"
|
106
107
|
|
107
108
|
workflows:
|
data/.envrc
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
PATH_add bin
|
2
|
-
eval $(with-op op-env sh -e ASANA__PERSONAL_ACCESS_TOKEN)
|
2
|
+
eval $(with-op op-env sh -e ASANA__PERSONAL_ACCESS_TOKEN -e ASANA__DEFAULT_ASSIGNEE_GID)
|
data/.gitignore
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
checkoff (0.
|
4
|
+
checkoff (0.11.0)
|
5
5
|
activesupport
|
6
6
|
asana (> 0.10.0)
|
7
7
|
cache_method
|
8
8
|
dalli
|
9
|
+
gli
|
9
10
|
|
10
11
|
GEM
|
11
12
|
remote: https://rubygems.org/
|
@@ -27,15 +28,19 @@ GEM
|
|
27
28
|
cache_method (0.2.7)
|
28
29
|
cache (>= 0.2.1)
|
29
30
|
childprocess (4.0.0)
|
30
|
-
concurrent-ruby (1.1.
|
31
|
+
concurrent-ruby (1.1.9)
|
31
32
|
dalli (2.7.11)
|
32
|
-
docile (1.
|
33
|
-
faraday (1.4.
|
33
|
+
docile (1.4.0)
|
34
|
+
faraday (1.4.2)
|
35
|
+
faraday-em_http (~> 1.0)
|
36
|
+
faraday-em_synchrony (~> 1.0)
|
34
37
|
faraday-excon (~> 1.1)
|
35
38
|
faraday-net_http (~> 1.0)
|
36
39
|
faraday-net_http_persistent (~> 1.1)
|
37
40
|
multipart-post (>= 1.2, < 3)
|
38
41
|
ruby2_keywords (>= 0.0.4)
|
42
|
+
faraday-em_http (1.0.0)
|
43
|
+
faraday-em_synchrony (1.0.0)
|
39
44
|
faraday-excon (1.1.0)
|
40
45
|
faraday-net_http (1.0.1)
|
41
46
|
faraday-net_http_persistent (1.1.0)
|
@@ -44,6 +49,7 @@ GEM
|
|
44
49
|
faraday_middleware-multi_json (0.0.6)
|
45
50
|
faraday_middleware
|
46
51
|
multi_json
|
52
|
+
gli (2.20.0)
|
47
53
|
i18n (1.8.10)
|
48
54
|
concurrent-ruby (~> 1.0)
|
49
55
|
imagen (0.1.8)
|
@@ -62,9 +68,10 @@ GEM
|
|
62
68
|
multi_json (~> 1.3)
|
63
69
|
multi_xml (~> 0.5)
|
64
70
|
rack (>= 1.2, < 3)
|
65
|
-
overcommit (0.
|
71
|
+
overcommit (0.58.0)
|
66
72
|
childprocess (>= 0.6.3, < 5)
|
67
73
|
iniparse (~> 1.4)
|
74
|
+
rexml (~> 3.2)
|
68
75
|
parallel (1.20.1)
|
69
76
|
parser (3.0.1.1)
|
70
77
|
ast (~> 2.4.1)
|
@@ -73,7 +80,7 @@ GEM
|
|
73
80
|
rake (13.0.3)
|
74
81
|
regexp_parser (2.1.1)
|
75
82
|
rexml (3.2.5)
|
76
|
-
rubocop (1.
|
83
|
+
rubocop (1.15.0)
|
77
84
|
parallel (~> 1.10)
|
78
85
|
parser (>= 3.0.0.0)
|
79
86
|
rainbow (>= 2.2.2, < 4.0)
|
@@ -96,6 +103,7 @@ GEM
|
|
96
103
|
simplecov-html (~> 0.11)
|
97
104
|
simplecov_json_formatter (~> 0.1)
|
98
105
|
simplecov-html (0.12.3)
|
106
|
+
simplecov-lcov (0.8.0)
|
99
107
|
simplecov_json_formatter (0.1.3)
|
100
108
|
tzinfo (2.0.4)
|
101
109
|
concurrent-ruby (~> 1.0)
|
@@ -118,13 +126,14 @@ DEPENDENCIES
|
|
118
126
|
checkoff!
|
119
127
|
minitest-profile
|
120
128
|
mocha
|
121
|
-
overcommit
|
129
|
+
overcommit (>= 0.58.0)
|
122
130
|
rake (~> 13.0)
|
123
131
|
rubocop
|
124
132
|
rubocop-minitest
|
125
133
|
rubocop-rake
|
126
134
|
simplecov
|
135
|
+
simplecov-lcov
|
127
136
|
undercover
|
128
137
|
|
129
138
|
BUNDLED WITH
|
130
|
-
2.2.
|
139
|
+
2.2.19
|
data/Makefile
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
.PHONY: clean test help quality localtest
|
1
|
+
.PHONY: clean test help quality localtest
|
2
2
|
.DEFAULT_GOAL := default
|
3
3
|
|
4
4
|
define PRINT_HELP_PYSCRIPT
|
@@ -32,7 +32,10 @@ Gemfile.lock.installed: Gemfile.lock
|
|
32
32
|
|
33
33
|
bundle_install: Gemfile.lock.installed ## Install Ruby dependencies
|
34
34
|
|
35
|
-
|
35
|
+
clear_metrics: ## remove or reset result artifacts created by tests and quality tools
|
36
|
+
bundle exec rake clear_metrics
|
37
|
+
|
38
|
+
clean: clear_metrics ## remove all built artifacts
|
36
39
|
|
37
40
|
quality: ## run precommit quality checks
|
38
41
|
bundle exec overcommit --run
|
@@ -40,7 +43,8 @@ quality: ## run precommit quality checks
|
|
40
43
|
test: ## Run lower-level tests
|
41
44
|
@bundle exec rake test
|
42
45
|
|
43
|
-
localtest:
|
46
|
+
localtest: ## run default local actions
|
47
|
+
@bundle exec rake localtest
|
44
48
|
|
45
49
|
update_from_cookiecutter: ## Bring in changes from template project used to create this repo
|
46
50
|
bundle exec overcommit --uninstall
|
data/checkoff.gemspec
CHANGED
@@ -26,16 +26,21 @@ Gem::Specification.new do |spec|
|
|
26
26
|
spec.add_runtime_dependency 'asana', '>0.10.0'
|
27
27
|
spec.add_runtime_dependency 'cache_method'
|
28
28
|
spec.add_runtime_dependency 'dalli'
|
29
|
+
spec.add_runtime_dependency 'gli'
|
29
30
|
|
30
31
|
spec.add_development_dependency 'bump'
|
31
32
|
spec.add_development_dependency 'bundler'
|
32
33
|
spec.add_development_dependency 'minitest-profile'
|
33
34
|
spec.add_development_dependency 'mocha'
|
34
|
-
|
35
|
+
# 0.58.0 and 0.57.0 don't seem super compatible with signatures, and
|
36
|
+
# magit doesn't seem to want to use the bundled version at the moment,
|
37
|
+
# so let's favor the more recent version...
|
38
|
+
spec.add_development_dependency 'overcommit', ['>=0.58.0']
|
35
39
|
spec.add_development_dependency 'rake', '~> 13.0'
|
36
40
|
spec.add_development_dependency 'rubocop'
|
37
41
|
spec.add_development_dependency 'rubocop-minitest'
|
38
42
|
spec.add_development_dependency 'rubocop-rake'
|
39
43
|
spec.add_development_dependency 'simplecov'
|
44
|
+
spec.add_development_dependency 'simplecov-lcov'
|
40
45
|
spec.add_development_dependency 'undercover'
|
41
46
|
end
|
data/coverage/.last_run.json
CHANGED
data/exe/checkoff
CHANGED
data/fix.sh
CHANGED
@@ -2,6 +2,16 @@
|
|
2
2
|
|
3
3
|
set -o pipefail
|
4
4
|
|
5
|
+
apt_upgraded=0
|
6
|
+
|
7
|
+
update_apt() {
|
8
|
+
if [ "${apt_upgraded}" = 0 ]
|
9
|
+
then
|
10
|
+
sudo apt-get update -y
|
11
|
+
apt_upgraded=1
|
12
|
+
fi
|
13
|
+
}
|
14
|
+
|
5
15
|
install_rbenv() {
|
6
16
|
if [ "$(uname)" == "Darwin" ]
|
7
17
|
then
|
@@ -77,6 +87,8 @@ ensure_dev_library() {
|
|
77
87
|
|
78
88
|
ensure_ruby_build_requirements() {
|
79
89
|
ensure_dev_library readline/readline.h readline libreadline-dev
|
90
|
+
ensure_dev_library zlib.h zlib zlib1g-dev
|
91
|
+
ensure_dev_library openssl/ssl.h openssl libssl-dev
|
80
92
|
}
|
81
93
|
|
82
94
|
# You can find out which feature versions are still supported / have
|
@@ -92,7 +104,17 @@ ensure_ruby_versions() {
|
|
92
104
|
|
93
105
|
for ver in $ruby_versions
|
94
106
|
do
|
95
|
-
|
107
|
+
# These CFLAGS can be retired once 2.6.7 is no longer needed :
|
108
|
+
#
|
109
|
+
# https://github.com/rbenv/ruby-build/issues/1747
|
110
|
+
# https://github.com/rbenv/ruby-build/issues/1489
|
111
|
+
# https://bugs.ruby-lang.org/issues/17777
|
112
|
+
if [ "${ver}" == 2.6.7 ]
|
113
|
+
then
|
114
|
+
CFLAGS="-Wno-error=implicit-function-declaration" rbenv install -s "${ver}"
|
115
|
+
else
|
116
|
+
rbenv install -s "${ver}"
|
117
|
+
fi
|
96
118
|
done
|
97
119
|
}
|
98
120
|
|
@@ -101,7 +123,17 @@ ensure_bundle() {
|
|
101
123
|
#
|
102
124
|
# https://app.circleci.com/pipelines/github/apiology/source_finder/21/workflows/88db659f-a4f4-4751-abc0-46f5929d8e58/jobs/107
|
103
125
|
set_rbenv_env_variables
|
104
|
-
bundle --version >/dev/null 2>&1 || gem install bundler
|
126
|
+
bundle --version >/dev/null 2>&1 || gem install --no-document bundler
|
127
|
+
bundler_version=$(bundle --version | cut -d ' ' -f3)
|
128
|
+
bundler_version_major=$(cut -d. -f1 <<< "${bundler_version}")
|
129
|
+
bundler_version_minor=$(cut -d. -f2 <<< "${bundler_version}")
|
130
|
+
# Version 2.1 of bundler seems to have some issues with nokogiri:
|
131
|
+
#
|
132
|
+
# https://app.asana.com/0/1107901397356088/1199504270687298
|
133
|
+
if [ "${bundler_version_major}" == 2 ] && [ "${bundler_version_minor}" -lt 2 ]
|
134
|
+
then
|
135
|
+
gem install --no-document bundler
|
136
|
+
fi
|
105
137
|
make bundle_install
|
106
138
|
# https://bundler.io/v2.0/bundle_lock.html#SUPPORTING-OTHER-PLATFORMS
|
107
139
|
#
|
@@ -138,8 +170,9 @@ WARNING: seems you still have not added 'pyenv' to the load path.
|
|
138
170
|
# Load pyenv automatically by adding
|
139
171
|
# the following to ~/.bashrc:
|
140
172
|
|
141
|
-
export
|
142
|
-
|
173
|
+
export PYENV_ROOT="${HOME}/.pyenv"
|
174
|
+
export PATH="${PYENV_ROOT}/bin:$PATH"
|
175
|
+
eval "$(pyenv init --path)"
|
143
176
|
eval "$(pyenv virtualenv-init -)"
|
144
177
|
EOF
|
145
178
|
fi
|
@@ -153,8 +186,9 @@ set_pyenv_env_variables() {
|
|
153
186
|
#
|
154
187
|
# https://app.circleci.com/pipelines/github/apiology/cookiecutter-pypackage/15/workflows/10506069-7662-46bd-b915-2992db3f795b/jobs/15
|
155
188
|
set +u
|
156
|
-
export
|
157
|
-
|
189
|
+
export PYENV_ROOT="${HOME}/.pyenv"
|
190
|
+
export PATH="${PYENV_ROOT}/bin:$PATH"
|
191
|
+
eval "$(pyenv init --path)"
|
158
192
|
eval "$(pyenv virtualenv-init -)"
|
159
193
|
set -u
|
160
194
|
}
|
@@ -179,7 +213,7 @@ install_package() {
|
|
179
213
|
HOMEBREW_NO_AUTO_UPDATE=1 brew install "${homebrew_package}"
|
180
214
|
elif type apt-get >/dev/null 2>&1
|
181
215
|
then
|
182
|
-
|
216
|
+
update_apt
|
183
217
|
sudo apt-get install -y "${apt_package}"
|
184
218
|
else
|
185
219
|
>&2 echo "Teach me how to install packages on this plaform"
|
@@ -192,6 +226,7 @@ ensure_python_build_requirements() {
|
|
192
226
|
ensure_dev_library bzlib.h bzip2 libbz2-dev
|
193
227
|
ensure_dev_library openssl/ssl.h openssl libssl-dev
|
194
228
|
ensure_dev_library ffi.h libffi libffi-dev
|
229
|
+
ensure_dev_library sqlite3.h sqlite3 libsqlite3-dev
|
195
230
|
}
|
196
231
|
|
197
232
|
# You can find out which feature versions are still supported / have
|
data/lib/checkoff/cli.rb
CHANGED
@@ -4,6 +4,7 @@
|
|
4
4
|
|
5
5
|
require 'ostruct'
|
6
6
|
require 'dalli'
|
7
|
+
require 'gli'
|
7
8
|
require 'cache_method'
|
8
9
|
require_relative 'workspaces'
|
9
10
|
require_relative 'projects'
|
@@ -11,41 +12,44 @@ require_relative 'tasks'
|
|
11
12
|
require_relative 'sections'
|
12
13
|
|
13
14
|
module Checkoff
|
14
|
-
#
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
def initialize(config: Checkoff::ConfigLoader.load(:asana),
|
20
|
-
workspaces: Checkoff::Workspaces.new(config: config),
|
15
|
+
# CLI subcommand that shows tasks in JSON form
|
16
|
+
class ViewSubcommand
|
17
|
+
def initialize(workspace_name, project_name, section_name,
|
18
|
+
task_name,
|
19
|
+
config: Checkoff::ConfigLoader.load(:asana),
|
21
20
|
projects: Checkoff::Projects.new(config: config),
|
22
21
|
sections: Checkoff::Sections.new(config: config,
|
23
22
|
projects: projects),
|
24
|
-
tasks: Checkoff::Tasks.new(config: config
|
25
|
-
|
26
|
-
|
27
|
-
@
|
28
|
-
@
|
23
|
+
tasks: Checkoff::Tasks.new(config: config,
|
24
|
+
sections: sections),
|
25
|
+
stderr: $stderr)
|
26
|
+
@workspace_name = workspace_name
|
27
|
+
@stderr = stderr
|
28
|
+
validate_and_assign_project_name(project_name)
|
29
|
+
@section_name = section_name
|
30
|
+
@task_name = task_name
|
29
31
|
@sections = sections
|
30
32
|
@tasks = tasks
|
31
|
-
@kernel = kernel
|
32
|
-
@stderr = stderr
|
33
33
|
end
|
34
34
|
|
35
|
-
def
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
task_out[:due] = task.due_at
|
35
|
+
def run
|
36
|
+
if section_name.nil?
|
37
|
+
run_on_project(workspace_name, project_name)
|
38
|
+
elsif task_name.nil?
|
39
|
+
run_on_section(workspace_name, project_name, section_name)
|
40
|
+
else
|
41
|
+
run_on_task(workspace_name, project_name, section_name, task_name)
|
43
42
|
end
|
44
|
-
task_out
|
45
43
|
end
|
46
44
|
|
47
|
-
|
48
|
-
|
45
|
+
private
|
46
|
+
|
47
|
+
def validate_and_assign_project_name(project_name)
|
48
|
+
@project_name = if project_name.start_with? ':'
|
49
|
+
project_name[1..].to_sym
|
50
|
+
else
|
51
|
+
project_name
|
52
|
+
end
|
49
53
|
end
|
50
54
|
|
51
55
|
def run_on_project(workspace, project)
|
@@ -63,80 +67,89 @@ module Checkoff
|
|
63
67
|
tasks_to_hash(tasks).to_json
|
64
68
|
end
|
65
69
|
|
66
|
-
def
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
+
def run_on_task(workspace, project, section, task_name)
|
71
|
+
section = nil if section == ''
|
72
|
+
task = tasks.task(workspace, project, task_name, section_name: section)
|
73
|
+
task_to_hash(task).to_json
|
70
74
|
end
|
71
75
|
|
72
|
-
def
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
76
|
+
def task_to_hash(task)
|
77
|
+
task_out = {
|
78
|
+
name: task.name,
|
79
|
+
}
|
80
|
+
if task.due_on
|
81
|
+
task_out[:due] = task.due_on
|
82
|
+
elsif task.due_at
|
83
|
+
task_out[:due] = task.due_at
|
84
|
+
end
|
85
|
+
task_out
|
77
86
|
end
|
78
87
|
|
79
|
-
def
|
80
|
-
|
81
|
-
subargs.project = args[2]
|
82
|
-
subargs.section = args[3]
|
88
|
+
def tasks_to_hash(tasks)
|
89
|
+
tasks.map { |task| task_to_hash(task) }
|
83
90
|
end
|
84
91
|
|
85
|
-
|
86
|
-
|
87
|
-
subargs.task_name = args[2]
|
88
|
-
end
|
92
|
+
attr_reader :workspace_name, :project_name, :section_name, :task_name, :sections, :tasks, :stderr
|
93
|
+
end
|
89
94
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
raise
|
101
|
-
end
|
102
|
-
[mode, subargs]
|
95
|
+
# CLI subcommand that creates a task
|
96
|
+
class QuickaddSubcommand
|
97
|
+
def initialize(workspace_name, task_name,
|
98
|
+
config: Checkoff::ConfigLoader.load(:asana),
|
99
|
+
workspaces: Checkoff::Workspaces.new(config: config),
|
100
|
+
tasks: Checkoff::Tasks.new(config: config))
|
101
|
+
@workspace_name = workspace_name
|
102
|
+
@task_name = task_name
|
103
|
+
@workspaces = workspaces
|
104
|
+
@tasks = tasks
|
103
105
|
end
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
stderr.puts " #{$PROGRAM_NAME} quickadd workspace task_name"
|
110
|
-
stderr.puts
|
111
|
-
stderr.puts "'project' can be set to a project name, or :my_tasks, " \
|
112
|
-
":my_tasks_upcoming, :my_tasks_new, or :my_tasks_today"
|
106
|
+
|
107
|
+
def run
|
108
|
+
workspace = @workspaces.workspace_by_name(workspace_name)
|
109
|
+
@tasks.add_task(task_name,
|
110
|
+
workspace_gid: workspace.gid)
|
113
111
|
end
|
114
112
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
113
|
+
private
|
114
|
+
|
115
|
+
attr_reader :workspace_name, :task_name
|
116
|
+
end
|
117
|
+
|
118
|
+
# Provide ability for CLI to pull Asana items
|
119
|
+
class CheckoffGLIApp
|
120
|
+
extend GLI::App
|
121
|
+
|
122
|
+
program_desc 'Command-line client for Asana (unofficial)'
|
123
|
+
|
124
|
+
subcommand_option_handling :normal
|
125
|
+
arguments :strict
|
126
|
+
|
127
|
+
desc 'Add a short task to Asana'
|
128
|
+
arg 'workspace'
|
129
|
+
arg 'task_name'
|
130
|
+
command :quickadd do |c|
|
131
|
+
c.action do |_global_options, _options, args|
|
132
|
+
workspace_name = args.fetch(0)
|
133
|
+
task_name = args.fetch(1)
|
134
|
+
|
135
|
+
QuickaddSubcommand.new(workspace_name, task_name).run
|
125
136
|
end
|
126
137
|
end
|
127
138
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
139
|
+
desc 'Output representation of Asana tasks'
|
140
|
+
arg 'workspace'
|
141
|
+
arg 'project'
|
142
|
+
arg 'section', :optional
|
143
|
+
arg 'task_name', :optional
|
144
|
+
command :view do |c|
|
145
|
+
c.action do |_global_options, _options, args|
|
146
|
+
workspace_name = args.fetch(0)
|
147
|
+
project_name = args.fetch(1)
|
148
|
+
section_name = args[2]
|
149
|
+
task_name = args[3]
|
150
|
+
|
151
|
+
puts ViewSubcommand.new(workspace_name, project_name, section_name, task_name).run
|
138
152
|
end
|
139
153
|
end
|
140
154
|
end
|
141
|
-
# rubocop:enable Metrics/ClassLength
|
142
155
|
end
|
@@ -13,10 +13,6 @@ module Checkoff
|
|
13
13
|
@yaml_filename = yaml_filename
|
14
14
|
end
|
15
15
|
|
16
|
-
def envvar_name(key)
|
17
|
-
"#{@envvar_prefix}__#{key.upcase}"
|
18
|
-
end
|
19
|
-
|
20
16
|
def [](key)
|
21
17
|
config_value = @config[key]
|
22
18
|
return config_value unless config_value.nil?
|
@@ -31,25 +27,35 @@ module Checkoff
|
|
31
27
|
raise KeyError,
|
32
28
|
"Please configure either the #{key} key in #{@yaml_filename} or set #{envvar_name(key)}"
|
33
29
|
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def envvar_name(key)
|
34
|
+
"#{@envvar_prefix}__#{key.upcase}"
|
35
|
+
end
|
34
36
|
end
|
35
37
|
|
36
38
|
# Load configuration file
|
37
39
|
class ConfigLoader
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
40
|
+
class << self
|
41
|
+
def load(sym)
|
42
|
+
yaml_result = load_yaml_file(sym)
|
43
|
+
EnvFallbackConfigLoader.new(yaml_result, sym, yaml_filename(sym))
|
44
|
+
end
|
42
45
|
|
43
|
-
|
44
|
-
filename = yaml_filename(sym)
|
45
|
-
return {} unless File.exist?(filename)
|
46
|
+
private
|
46
47
|
|
47
|
-
|
48
|
-
|
48
|
+
def load_yaml_file(sym)
|
49
|
+
filename = yaml_filename(sym)
|
50
|
+
return {} unless File.exist?(filename)
|
51
|
+
|
52
|
+
YAML.load_file(filename).with_indifferent_access
|
53
|
+
end
|
49
54
|
|
50
|
-
|
51
|
-
|
52
|
-
|
55
|
+
def yaml_filename(sym)
|
56
|
+
file = "#{sym}.yml"
|
57
|
+
File.expand_path("~/.#{file}")
|
58
|
+
end
|
53
59
|
end
|
54
60
|
end
|
55
61
|
end
|
data/lib/checkoff/projects.rb
CHANGED
@@ -22,8 +22,6 @@ module Checkoff
|
|
22
22
|
LONG_CACHE_TIME = MINUTE * 15
|
23
23
|
SHORT_CACHE_TIME = MINUTE * 5
|
24
24
|
|
25
|
-
# XXX: Move low-level functions private
|
26
|
-
|
27
25
|
def initialize(config: Checkoff::ConfigLoader.load(:asana),
|
28
26
|
asana_client: Asana::Client,
|
29
27
|
workspaces: Checkoff::Workspaces.new(config: config))
|
@@ -32,10 +30,41 @@ module Checkoff
|
|
32
30
|
@workspaces = workspaces
|
33
31
|
end
|
34
32
|
|
33
|
+
# Returns Asana Ruby API Client object
|
35
34
|
def client
|
36
35
|
@workspaces.client
|
37
36
|
end
|
38
37
|
|
38
|
+
# pulls an Asana API project class given a name
|
39
|
+
def project(workspace_name, project_name)
|
40
|
+
if project_name.is_a?(Symbol) && project_name.to_s.start_with?('my_tasks')
|
41
|
+
my_tasks(workspace_name)
|
42
|
+
else
|
43
|
+
projects = projects_by_workspace_name(workspace_name)
|
44
|
+
projects.find do |project|
|
45
|
+
project.name == project_name
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
cache_method :project, LONG_CACHE_TIME
|
50
|
+
|
51
|
+
# find uncompleted tasks in a list
|
52
|
+
def active_tasks(tasks)
|
53
|
+
tasks.select { |task| task.completed_at.nil? }
|
54
|
+
end
|
55
|
+
|
56
|
+
# pull task objects from a named project
|
57
|
+
def tasks_from_project(project, only_uncompleted: true, extra_fields: [])
|
58
|
+
options = task_options
|
59
|
+
options[:completed_since] = '9999-12-01' if only_uncompleted
|
60
|
+
options[:project] = project.gid
|
61
|
+
options[:options][:fields] += extra_fields
|
62
|
+
client.tasks.find_all(**options).to_a
|
63
|
+
end
|
64
|
+
cache_method :tasks_from_project, SHORT_CACHE_TIME
|
65
|
+
|
66
|
+
private
|
67
|
+
|
39
68
|
def projects
|
40
69
|
client.projects
|
41
70
|
end
|
@@ -56,22 +85,6 @@ module Checkoff
|
|
56
85
|
projects.find_by_id(gid)
|
57
86
|
end
|
58
87
|
|
59
|
-
def project(workspace_name, project_name)
|
60
|
-
if project_name.is_a?(Symbol) && project_name.to_s.start_with?('my_tasks')
|
61
|
-
my_tasks(workspace_name)
|
62
|
-
else
|
63
|
-
projects = projects_by_workspace_name(workspace_name)
|
64
|
-
projects.find do |project|
|
65
|
-
project.name == project_name
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
69
|
-
cache_method :project, LONG_CACHE_TIME
|
70
|
-
|
71
|
-
def active_tasks(tasks)
|
72
|
-
tasks.select { |task| task.completed_at.nil? }
|
73
|
-
end
|
74
|
-
|
75
88
|
def task_options
|
76
89
|
{
|
77
90
|
per_page: 100,
|
@@ -81,14 +94,5 @@ module Checkoff
|
|
81
94
|
},
|
82
95
|
}
|
83
96
|
end
|
84
|
-
|
85
|
-
def tasks_from_project(project, only_uncompleted: true, extra_fields: [])
|
86
|
-
options = task_options
|
87
|
-
options[:completed_since] = '9999-12-01' if only_uncompleted
|
88
|
-
options[:project] = project.gid
|
89
|
-
options[:options][:fields] += extra_fields
|
90
|
-
client.tasks.find_all(**options).to_a
|
91
|
-
end
|
92
|
-
cache_method :tasks_from_project, SHORT_CACHE_TIME
|
93
97
|
end
|
94
98
|
end
|
data/lib/checkoff/sections.rb
CHANGED
@@ -23,6 +23,71 @@ module Checkoff
|
|
23
23
|
@time = time
|
24
24
|
end
|
25
25
|
|
26
|
+
# Given a list of tasks, pull a Hash of tasks with section name -> task list
|
27
|
+
def by_section(tasks, project_gid)
|
28
|
+
by_section = {}
|
29
|
+
tasks.each do |task|
|
30
|
+
file_task_by_section(by_section, task, project_gid)
|
31
|
+
end
|
32
|
+
by_section
|
33
|
+
end
|
34
|
+
cache_method :by_section, LONG_CACHE_TIME
|
35
|
+
|
36
|
+
# Given a project object, pull all tasks, then provide a Hash of
|
37
|
+
# tasks with section name -> task list of the uncompleted tasks
|
38
|
+
def tasks_by_section_for_project(project)
|
39
|
+
raw_tasks = projects.tasks_from_project(project)
|
40
|
+
active_tasks = projects.active_tasks(raw_tasks)
|
41
|
+
by_section(active_tasks, project.gid)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Given a workspace name and project name, then provide a Hash of
|
45
|
+
# tasks with section name -> task list of the uncompleted tasks
|
46
|
+
def tasks_by_section(workspace_name, project_name)
|
47
|
+
project = project_or_raise(workspace_name, project_name)
|
48
|
+
case project_name
|
49
|
+
when :my_tasks, :my_tasks_new, :my_tasks_today, :my_tasks_upcoming
|
50
|
+
user_task_list_by_section(workspace_name, project_name)
|
51
|
+
else
|
52
|
+
tasks_by_section_for_project(project)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
cache_method :tasks_by_section, SHORT_CACHE_TIME
|
56
|
+
|
57
|
+
# XXX: Rename to section_tasks
|
58
|
+
#
|
59
|
+
# Pulls task objects from a specified section
|
60
|
+
def tasks(workspace_name, project_name, section_name)
|
61
|
+
tasks_by_section(workspace_name, project_name).fetch(section_name, [])
|
62
|
+
end
|
63
|
+
cache_method :tasks, SHORT_CACHE_TIME
|
64
|
+
|
65
|
+
# Pulls just names of tasks from a given section.
|
66
|
+
def section_task_names(workspace_name, project_name, section_name)
|
67
|
+
tasks = tasks(workspace_name, project_name, section_name)
|
68
|
+
if tasks.nil?
|
69
|
+
by_section = tasks_by_section(workspace_name, project_name)
|
70
|
+
desc = "#{workspace_name} | #{project_name} | #{section_name}"
|
71
|
+
raise "Could not find task names for #{desc}. " \
|
72
|
+
"Valid sections: #{by_section.keys}"
|
73
|
+
end
|
74
|
+
tasks.map(&:name)
|
75
|
+
end
|
76
|
+
cache_method :section_task_names, SHORT_CACHE_TIME
|
77
|
+
|
78
|
+
# returns if a task's due field is at or before the current date/time
|
79
|
+
def task_due?(task)
|
80
|
+
if task.due_at
|
81
|
+
Time.parse(task.due_at) <= time.now
|
82
|
+
elsif task.due_on
|
83
|
+
Date.parse(task.due_on) <= time.today
|
84
|
+
else
|
85
|
+
true # set a due date if you don't want to do this now
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
26
91
|
def_delegators :@projects, :client
|
27
92
|
|
28
93
|
def file_task_by_section(by_section, task, project_gid)
|
@@ -35,27 +100,12 @@ module Checkoff
|
|
35
100
|
by_section[current_section] << task
|
36
101
|
end
|
37
102
|
|
38
|
-
def by_section(tasks, project_gid)
|
39
|
-
by_section = {}
|
40
|
-
tasks.each do |task|
|
41
|
-
file_task_by_section(by_section, task, project_gid)
|
42
|
-
end
|
43
|
-
by_section
|
44
|
-
end
|
45
|
-
cache_method :by_section, LONG_CACHE_TIME
|
46
|
-
|
47
103
|
def legacy_tasks_by_section_for_project(project)
|
48
104
|
raw_tasks = projects.tasks_from_project(project)
|
49
105
|
active_tasks = projects.active_tasks(raw_tasks)
|
50
106
|
legacy_by_section(active_tasks)
|
51
107
|
end
|
52
108
|
|
53
|
-
def tasks_by_section_for_project(project)
|
54
|
-
raw_tasks = projects.tasks_from_project(project)
|
55
|
-
active_tasks = projects.active_tasks(raw_tasks)
|
56
|
-
by_section(active_tasks, project.gid)
|
57
|
-
end
|
58
|
-
|
59
109
|
def legacy_file_task_by_section(current_section, by_section, task)
|
60
110
|
if task.name =~ /:$/
|
61
111
|
current_section = task.name
|
@@ -120,35 +170,6 @@ module Checkoff
|
|
120
170
|
end
|
121
171
|
end
|
122
172
|
|
123
|
-
def tasks_by_section(workspace_name, project_name)
|
124
|
-
project = project_or_raise(workspace_name, project_name)
|
125
|
-
case project_name
|
126
|
-
when :my_tasks, :my_tasks_new, :my_tasks_today, :my_tasks_upcoming
|
127
|
-
user_task_list_by_section(workspace_name, project_name)
|
128
|
-
else
|
129
|
-
tasks_by_section_for_project(project)
|
130
|
-
end
|
131
|
-
end
|
132
|
-
cache_method :tasks_by_section, SHORT_CACHE_TIME
|
133
|
-
|
134
|
-
# XXX: Rename to section_tasks
|
135
|
-
def tasks(workspace_name, project_name, section_name)
|
136
|
-
tasks_by_section(workspace_name, project_name).fetch(section_name, [])
|
137
|
-
end
|
138
|
-
cache_method :tasks, SHORT_CACHE_TIME
|
139
|
-
|
140
|
-
def section_task_names(workspace_name, project_name, section_name)
|
141
|
-
tasks = tasks(workspace_name, project_name, section_name)
|
142
|
-
if tasks.nil?
|
143
|
-
by_section = tasks_by_section(workspace_name, project_name)
|
144
|
-
desc = "#{workspace_name} | #{project_name} | #{section_name}"
|
145
|
-
raise "Could not find task names for #{desc}. " \
|
146
|
-
"Valid sections: #{by_section.keys}"
|
147
|
-
end
|
148
|
-
tasks.map(&:name)
|
149
|
-
end
|
150
|
-
cache_method :section_task_names, SHORT_CACHE_TIME
|
151
|
-
|
152
173
|
def user_task_list_migrated_to_real_sections?(workspace_name)
|
153
174
|
workspace = workspaces.workspace_by_name(workspace_name)
|
154
175
|
result = client.user_task_lists.get_user_task_list_for_user(user_gid: 'me',
|
@@ -156,16 +177,6 @@ module Checkoff
|
|
156
177
|
result.migration_status != 'not_migrated'
|
157
178
|
end
|
158
179
|
|
159
|
-
def task_due?(task)
|
160
|
-
if task.due_at
|
161
|
-
Time.parse(task.due_at) <= time.now
|
162
|
-
elsif task.due_on
|
163
|
-
Date.parse(task.due_on) <= time.today
|
164
|
-
else
|
165
|
-
true # set a due date if you don't want to do this now
|
166
|
-
end
|
167
|
-
end
|
168
|
-
|
169
180
|
def project_task_names(workspace_name, project_name)
|
170
181
|
by_section = tasks_by_section(workspace_name, project_name)
|
171
182
|
by_section.flat_map do |section_name, tasks|
|
data/lib/checkoff/subtasks.rb
CHANGED
@@ -11,17 +11,7 @@ module Checkoff
|
|
11
11
|
|
12
12
|
extend Forwardable
|
13
13
|
|
14
|
-
|
15
|
-
if task.name =~ /:$/
|
16
|
-
current_section = task.name
|
17
|
-
by_section[current_section] = []
|
18
|
-
else
|
19
|
-
by_section[current_section] ||= []
|
20
|
-
by_section[current_section] << task
|
21
|
-
end
|
22
|
-
[current_section, by_section]
|
23
|
-
end
|
24
|
-
|
14
|
+
# pulls a Hash of subtasks broken out by section
|
25
15
|
def by_section(tasks)
|
26
16
|
current_section = nil
|
27
17
|
by_section = {}
|
@@ -37,5 +27,18 @@ module Checkoff
|
|
37
27
|
task.subtasks(projects.task_options)
|
38
28
|
end
|
39
29
|
cache_method :raw_subtasks, LONG_CACHE_TIME
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def file_task_by_section(current_section, by_section, task)
|
34
|
+
if task.name =~ /:$/
|
35
|
+
current_section = task.name
|
36
|
+
by_section[current_section] = []
|
37
|
+
else
|
38
|
+
by_section[current_section] ||= []
|
39
|
+
by_section[current_section] << task
|
40
|
+
end
|
41
|
+
[current_section, by_section]
|
42
|
+
end
|
40
43
|
end
|
41
44
|
end
|
data/lib/checkoff/tasks.rb
CHANGED
@@ -5,7 +5,7 @@
|
|
5
5
|
require_relative 'sections'
|
6
6
|
|
7
7
|
module Checkoff
|
8
|
-
# Pull
|
8
|
+
# Pull tasks from Asana
|
9
9
|
class Tasks
|
10
10
|
MINUTE = 60
|
11
11
|
HOUR = MINUTE * 60
|
@@ -22,6 +22,22 @@ module Checkoff
|
|
22
22
|
@asana_task = asana_task
|
23
23
|
end
|
24
24
|
|
25
|
+
# Pull a specific task by name
|
26
|
+
def task(workspace_name, project_name, task_name,
|
27
|
+
section_name: :unspecified,
|
28
|
+
only_uncompleted: true)
|
29
|
+
project = projects.project(workspace_name, project_name)
|
30
|
+
tasks = if section_name == :unspecified
|
31
|
+
projects.tasks_from_project(project,
|
32
|
+
only_uncompleted: only_uncompleted)
|
33
|
+
else
|
34
|
+
@sections.tasks(workspace_name, project_name, section_name)
|
35
|
+
end
|
36
|
+
tasks.find { |task| task.name == task_name }
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
25
41
|
def client
|
26
42
|
@sections.client
|
27
43
|
end
|
@@ -30,13 +46,6 @@ module Checkoff
|
|
30
46
|
@projects ||= @sections.projects
|
31
47
|
end
|
32
48
|
|
33
|
-
def task(workspace_name, project_name, task_name, only_uncompleted: true)
|
34
|
-
project = projects.project(workspace_name, project_name)
|
35
|
-
tasks = projects.tasks_from_project(project,
|
36
|
-
only_uncompleted: only_uncompleted)
|
37
|
-
tasks.find { |task| task.name == task_name }
|
38
|
-
end
|
39
|
-
|
40
49
|
def tasks_minus_sections(tasks)
|
41
50
|
@sections.by_section(tasks).values.flatten
|
42
51
|
end
|
data/lib/checkoff/version.rb
CHANGED
data/lib/checkoff/workspaces.rb
CHANGED
@@ -9,6 +9,7 @@ module Checkoff
|
|
9
9
|
@asana_client = asana_client
|
10
10
|
end
|
11
11
|
|
12
|
+
# Returns Asana Ruby API Client object
|
12
13
|
def client
|
13
14
|
@client ||= @asana_client.new do |c|
|
14
15
|
c.authentication :access_token, @config.fetch(:personal_access_token)
|
@@ -17,14 +18,17 @@ module Checkoff
|
|
17
18
|
end
|
18
19
|
end
|
19
20
|
|
20
|
-
|
21
|
-
@config.fetch(:default_workspace_gid)
|
22
|
-
end
|
23
|
-
|
21
|
+
# Pulls an Asana workspace object
|
24
22
|
def workspace_by_name(workspace_name)
|
25
23
|
client.workspaces.find_all.find do |workspace|
|
26
24
|
workspace.name == workspace_name
|
27
25
|
end || raise("Could not find workspace named [#{workspace_name}]")
|
28
26
|
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def default_workspace_gid
|
31
|
+
@config.fetch(:default_workspace_gid)
|
32
|
+
end
|
29
33
|
end
|
30
34
|
end
|
data/rakelib/clear_metrics.rake
CHANGED
@@ -1,8 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
desc '
|
3
|
+
desc 'Ensure that any locally ratcheted coverage metrics are cleared back ' \
|
4
|
+
'to git baseline'
|
4
5
|
task :clear_metrics do |_t|
|
5
6
|
ret =
|
6
7
|
system('git checkout coverage/.last_run.json')
|
7
8
|
raise unless ret
|
9
|
+
|
10
|
+
# Without this old lines which are removed are still counted,
|
11
|
+
# leading to inconsistent coverage percentages between runs.
|
12
|
+
#
|
13
|
+
# need to save coverage/.last_run.json
|
14
|
+
ret =
|
15
|
+
system('rm -fr coverage/assets coverage/.*.json.lock coverage/lcov/* coverage/index.html coverage/.resultset.json')
|
16
|
+
raise unless ret
|
8
17
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: checkoff
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.11.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vince Broz
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-06-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: gli
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
84
|
name: bump
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -128,14 +142,14 @@ dependencies:
|
|
128
142
|
requirements:
|
129
143
|
- - ">="
|
130
144
|
- !ruby/object:Gem::Version
|
131
|
-
version:
|
145
|
+
version: 0.58.0
|
132
146
|
type: :development
|
133
147
|
prerelease: false
|
134
148
|
version_requirements: !ruby/object:Gem::Requirement
|
135
149
|
requirements:
|
136
150
|
- - ">="
|
137
151
|
- !ruby/object:Gem::Version
|
138
|
-
version:
|
152
|
+
version: 0.58.0
|
139
153
|
- !ruby/object:Gem::Dependency
|
140
154
|
name: rake
|
141
155
|
requirement: !ruby/object:Gem::Requirement
|
@@ -206,6 +220,20 @@ dependencies:
|
|
206
220
|
- - ">="
|
207
221
|
- !ruby/object:Gem::Version
|
208
222
|
version: '0'
|
223
|
+
- !ruby/object:Gem::Dependency
|
224
|
+
name: simplecov-lcov
|
225
|
+
requirement: !ruby/object:Gem::Requirement
|
226
|
+
requirements:
|
227
|
+
- - ">="
|
228
|
+
- !ruby/object:Gem::Version
|
229
|
+
version: '0'
|
230
|
+
type: :development
|
231
|
+
prerelease: false
|
232
|
+
version_requirements: !ruby/object:Gem::Requirement
|
233
|
+
requirements:
|
234
|
+
- - ">="
|
235
|
+
- !ruby/object:Gem::Version
|
236
|
+
version: '0'
|
209
237
|
- !ruby/object:Gem::Dependency
|
210
238
|
name: undercover
|
211
239
|
requirement: !ruby/object:Gem::Requirement
|
@@ -281,7 +309,7 @@ files:
|
|
281
309
|
- metrics/rubocop_high_water_mark
|
282
310
|
- metrics/scalastyle_high_water_mark
|
283
311
|
- metrics/shellcheck_high_water_mark
|
284
|
-
- rakelib/
|
312
|
+
- rakelib/citest.rake
|
285
313
|
- rakelib/clear_metrics.rake
|
286
314
|
- rakelib/default.rake
|
287
315
|
- rakelib/gem_tasks.rake
|
@@ -310,7 +338,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
310
338
|
- !ruby/object:Gem::Version
|
311
339
|
version: '0'
|
312
340
|
requirements: []
|
313
|
-
rubygems_version: 3.1
|
341
|
+
rubygems_version: 3.0.3.1
|
314
342
|
signing_key:
|
315
343
|
specification_version: 4
|
316
344
|
summary: Command-line and gem client for Asana (unofficial)
|