checkoff 0.5.4 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "result": {
3
- "covered_percent": 83.74
3
+ "covered_percent": 89.61
4
4
  }
5
5
  }
@@ -0,0 +1,13 @@
1
+ {
2
+ "_template": "https://github.com/apiology/cookiecutter-gem",
3
+ "email": "vince@broz.cc",
4
+ "full_name": "Vince Broz",
5
+ "github_username": "apiology",
6
+ "module_name": "Checkoff",
7
+ "open_source_license": "MIT license",
8
+ "project_name": "Checkoff",
9
+ "project_short_description": "Command-line and gem client for Asana (unofficial)",
10
+ "project_slug": "checkoff",
11
+ "type_of_github_repo": "public",
12
+ "version": "0.8.0"
13
+ }
data/fix.sh ADDED
@@ -0,0 +1,274 @@
1
+ #!/bin/bash -eu
2
+
3
+ set -o pipefail
4
+
5
+ install_rbenv() {
6
+ if [ "$(uname)" == "Darwin" ]
7
+ then
8
+ HOMEBREW_NO_AUTO_UPDATE=1 brew install rbenv || true
9
+ if ! type rbenv 2>/dev/null
10
+ then
11
+ # https://github.com/pyenv/pyenv-installer/blob/master/bin/pyenv-installer
12
+ >&2 cat <<EOF
13
+ WARNING: seems you still have not added 'rbenv' to the load path.
14
+
15
+ # Load rbenv automatically by adding
16
+ # the following to ~/.bashrc:
17
+
18
+ export PATH="$HOME/.rbenv/bin:$PATH"
19
+ eval "$(rbenv init -)"
20
+ EOF
21
+ fi
22
+ else
23
+ git clone https://github.com/rbenv/rbenv.git ~/.rbenv
24
+ fi
25
+ }
26
+
27
+ set_rbenv_env_variables() {
28
+ export PATH="${HOME}/.rbenv/bin:$PATH"
29
+ eval "$(rbenv init -)"
30
+ }
31
+
32
+ install_ruby_build() {
33
+ if [ "$(uname)" == "Darwin" ]
34
+ then
35
+ HOMEBREW_NO_AUTO_UPDATE=1 brew install ruby-build || true
36
+ else
37
+ mkdir -p "$(rbenv root)"/plugins
38
+ git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build
39
+ fi
40
+ }
41
+
42
+ ensure_ruby_build() {
43
+ if ! type ruby-build >/dev/null 2>&1 && ! [ -d "${HOME}/.rbenv/plugins/ruby-build" ]
44
+ then
45
+ install_ruby_build
46
+ fi
47
+ }
48
+
49
+ ensure_rbenv() {
50
+ if ! type rbenv >/dev/null 2>&1 && ! [ -f "${HOME}/.rbenv/bin/rbenv" ]
51
+ then
52
+ install_rbenv
53
+ fi
54
+
55
+ set_rbenv_env_variables
56
+
57
+ ensure_ruby_build
58
+ }
59
+
60
+ latest_ruby_version() {
61
+ major_minor=${1}
62
+ rbenv install --list 2>/dev/null | grep "^${major_minor}."
63
+ }
64
+
65
+ ensure_dev_library() {
66
+ header_file_name=${1:?header file name}
67
+ homebrew_package=${2:?homebrew package}
68
+ apt_package=${3:-${homebrew_package}}
69
+ if ! [ -f /usr/include/"${header_file_name}" ] && \
70
+ ! [ -f /usr/include/x86_64-linux-gnu/"${header_file_name}" ] && \
71
+ ! [ -f /usr/local/include/"${header_file_name}" ] && \
72
+ ! [ -f /usr/local/opt/"${homebrew_package}"/include/"${header_file_name}" ]
73
+ then
74
+ install_package "${homebrew_package}" "${apt_package}"
75
+ fi
76
+ }
77
+
78
+ ensure_ruby_build_requirements() {
79
+ ensure_dev_library readline/readline.h readline libreadline-dev
80
+ }
81
+
82
+ # You can find out which feature versions are still supported / have
83
+ # been release here: https://www.ruby-lang.org/en/downloads/
84
+ ensure_ruby_versions() {
85
+ # You can find out which feature versions are still supported / have
86
+ # been release here: https://www.python.org/downloads/
87
+ ruby_versions="$(latest_ruby_version 2.6)"
88
+
89
+ echo "Latest Ruby versions: ${ruby_versions}"
90
+
91
+ ensure_ruby_build_requirements
92
+
93
+ for ver in $ruby_versions
94
+ do
95
+ rbenv install -s "${ver}"
96
+ done
97
+ }
98
+
99
+ ensure_bundle() {
100
+ # Not sure why this is needed a second time, but it seems to be?
101
+ #
102
+ # https://app.circleci.com/pipelines/github/apiology/source_finder/21/workflows/88db659f-a4f4-4751-abc0-46f5929d8e58/jobs/107
103
+ set_rbenv_env_variables
104
+ bundle --version >/dev/null 2>&1 || gem install bundler
105
+ bundle install
106
+ # https://bundler.io/v2.0/bundle_lock.html#SUPPORTING-OTHER-PLATFORMS
107
+ #
108
+ # "If you want your bundle to support platforms other than the one
109
+ # you're running locally, you can run bundle lock --add-platform
110
+ # PLATFORM to add PLATFORM to the lockfile, force bundler to
111
+ # re-resolve and consider the new platform when picking gems, all
112
+ # without needing to have a machine that matches PLATFORM handy to
113
+ # install those platform-specific gems on.'
114
+ bundle lock --add-platform x86_64-darwin-20 x86_64-linux
115
+ }
116
+
117
+ set_ruby_local_version() {
118
+ latest_ruby_version="$(cut -d' ' -f1 <<< "${ruby_versions}")"
119
+ echo "${latest_ruby_version}" > .ruby-version
120
+ }
121
+
122
+ latest_python_version() {
123
+ major_minor=${1}
124
+ # https://stackoverflow.com/questions/369758/how-to-trim-whitespace-from-a-bash-variable
125
+ pyenv install --list | grep "^ ${major_minor}." | grep -v -- -dev | tail -1 | xargs
126
+ }
127
+
128
+ install_pyenv() {
129
+ if [ "$(uname)" == "Darwin" ]
130
+ then
131
+ HOMEBREW_NO_AUTO_UPDATE=1 brew install pyenv || true
132
+ if ! type pyenv 2>/dev/null
133
+ then
134
+ # https://github.com/pyenv/pyenv-installer/blob/master/bin/pyenv-installer
135
+ >&2 cat <<EOF
136
+ WARNING: seems you still have not added 'pyenv' to the load path.
137
+
138
+ # Load pyenv automatically by adding
139
+ # the following to ~/.bashrc:
140
+
141
+ export PATH="$HOME/.pyenv/bin:$PATH"
142
+ eval "$(pyenv init -)"
143
+ eval "$(pyenv virtualenv-init -)"
144
+ EOF
145
+ fi
146
+ else
147
+ curl https://pyenv.run | bash
148
+ fi
149
+ }
150
+
151
+ set_pyenv_env_variables() {
152
+ # looks like pyenv scripts aren't -u clean:
153
+ #
154
+ # https://app.circleci.com/pipelines/github/apiology/cookiecutter-pypackage/15/workflows/10506069-7662-46bd-b915-2992db3f795b/jobs/15
155
+ set +u
156
+ export PATH="${HOME}/.pyenv/bin:$PATH"
157
+ eval "$(pyenv init -)"
158
+ eval "$(pyenv virtualenv-init -)"
159
+ set -u
160
+ }
161
+
162
+ ensure_pyenv() {
163
+ if ! type pyenv >/dev/null 2>&1 && ! [ -f "${HOME}/.pyenv/bin/pyenv" ]
164
+ then
165
+ install_pyenv
166
+ fi
167
+
168
+ if ! type pyenv >/dev/null 2>&1
169
+ then
170
+ set_pyenv_env_variables
171
+ fi
172
+ }
173
+
174
+ install_package() {
175
+ homebrew_package=${1:?homebrew package}
176
+ apt_package=${2:-${homebrew_package}}
177
+ if [ "$(uname)" == "Darwin" ]
178
+ then
179
+ HOMEBREW_NO_AUTO_UPDATE=1 brew install "${homebrew_package}"
180
+ elif type apt-get >/dev/null 2>&1
181
+ then
182
+ sudo apt-get update -y
183
+ sudo apt-get install -y "${apt_package}"
184
+ else
185
+ >&2 echo "Teach me how to install packages on this plaform"
186
+ exit 1
187
+ fi
188
+ }
189
+
190
+ ensure_python_build_requirements() {
191
+ ensure_dev_library zlib.h zlib zlib1g-dev
192
+ ensure_dev_library bzlib.h bzip2 libbz2-dev
193
+ ensure_dev_library openssl/ssl.h openssl libssl-dev
194
+ ensure_dev_library ffi.h libffi libffi-dev
195
+ }
196
+
197
+ # You can find out which feature versions are still supported / have
198
+ # been release here: https://www.python.org/downloads/
199
+ ensure_python_versions() {
200
+ # You can find out which feature versions are still supported / have
201
+ # been release here: https://www.python.org/downloads/
202
+ python_versions="$(latest_python_version 3.9)"
203
+
204
+ echo "Latest Python versions: ${python_versions}"
205
+
206
+ ensure_python_build_requirements
207
+
208
+ for ver in $python_versions
209
+ do
210
+ if [ "$(uname)" == Darwin ]
211
+ then
212
+ pyenv_install() {
213
+ CFLAGS="-I/usr/local/opt/zlib/include -I/usr/local/opt/bzip2/include" LDFLAGS="-L/usr/local/opt/zlib/lib -L/usr/local/opt/bzip2/lib" pyenv install --skip-existing "$@"
214
+ }
215
+
216
+ major_minor="$(cut -d. -f1-2 <<<"${ver}")"
217
+ if [ "${major_minor}" == 3.6 ]
218
+ then
219
+ pyenv_install --patch "${ver}" < <(curl -sSL https://github.com/python/cpython/commit/8ea6353.patch\?full_index=1)
220
+ else
221
+ pyenv_install "${ver}"
222
+ fi
223
+ else
224
+ pyenv install -s "${ver}"
225
+ fi
226
+ done
227
+ }
228
+
229
+ ensure_pyenv_virtualenvs() {
230
+ latest_python_version="$(cut -d' ' -f1 <<< "${python_versions}")"
231
+ virtualenv_name="checkoff-${latest_python_version}"
232
+ pyenv virtualenv "${latest_python_version}" "${virtualenv_name}" || true
233
+ # You can use this for your global stuff!
234
+ pyenv virtualenv "${latest_python_version}" mylibs || true
235
+ # shellcheck disable=SC2086
236
+ pyenv local "${virtualenv_name}" ${python_versions} mylibs
237
+ }
238
+
239
+ ensure_pip() {
240
+ # Make sure we have a pip with the 20.3 resolver, and after the
241
+ # initial bugfix release
242
+ pip install 'pip>=20.3.1'
243
+ }
244
+
245
+ ensure_python_requirements() {
246
+ pip install -r requirements_dev.txt
247
+ }
248
+
249
+ ensure_shellcheck() {
250
+ if ! type shellcheck >/dev/null 2>&1
251
+ then
252
+ install_package shellcheck
253
+ fi
254
+ }
255
+
256
+ ensure_rbenv
257
+
258
+ ensure_ruby_versions
259
+
260
+ set_ruby_local_version
261
+
262
+ ensure_bundle
263
+
264
+ ensure_pyenv
265
+
266
+ ensure_python_versions
267
+
268
+ ensure_pyenv_virtualenvs
269
+
270
+ ensure_pip
271
+
272
+ ensure_python_requirements
273
+
274
+ ensure_shellcheck
data/lib/checkoff/cli.rb CHANGED
@@ -12,6 +12,7 @@ require_relative 'sections'
12
12
 
13
13
  module Checkoff
14
14
  # Provide ability for CLI to pull Asana items
15
+ # rubocop:disable Metrics/ClassLength
15
16
  class CLI
16
17
  attr_reader :sections, :stderr
17
18
 
@@ -86,6 +87,7 @@ module Checkoff
86
87
  subargs.task_name = args[2]
87
88
  end
88
89
 
90
+ # rubocop:disable Metrics/MethodLength
89
91
  def parse_args(args)
90
92
  mode = args[0]
91
93
  subargs = OpenStruct.new
@@ -99,6 +101,7 @@ module Checkoff
99
101
  end
100
102
  [mode, subargs]
101
103
  end
104
+ # rubocop:enable Metrics/MethodLength
102
105
 
103
106
  def output_help
104
107
  stderr.puts 'View tasks:'
@@ -110,6 +113,10 @@ module Checkoff
110
113
  end
111
114
 
112
115
  def view(workspace_name, project_name, section_name)
116
+ if project_name.nil?
117
+ stderr.puts 'Please specify a project name'
118
+ exit(1)
119
+ end
113
120
  project_name = project_name[1..-1].to_sym if project_name.start_with? ':'
114
121
  if section_name.nil?
115
122
  run_on_project(workspace_name, project_name)
@@ -131,4 +138,5 @@ module Checkoff
131
138
  end
132
139
  end
133
140
  end
141
+ # rubocop:enable Metrics/ClassLength
134
142
  end
@@ -4,12 +4,52 @@ require 'yaml'
4
4
  require 'active_support/core_ext/hash'
5
5
 
6
6
  module Checkoff
7
+ # Use the provided config from a YAML file, and fall back to env
8
+ # variable if it's not populated for a key'
9
+ class EnvFallbackConfigLoader
10
+ def initialize(config, sym, yaml_filename)
11
+ @config = config
12
+ @envvar_prefix = sym.upcase
13
+ @yaml_filename = yaml_filename
14
+ end
15
+
16
+ def envvar_name(key)
17
+ "#{@envvar_prefix}__#{key.upcase}"
18
+ end
19
+
20
+ def [](key)
21
+ config_value = @config[key]
22
+ return config_value unless config_value.nil?
23
+
24
+ ENV[envvar_name(key)]
25
+ end
26
+
27
+ def fetch(key)
28
+ out = self[key]
29
+ return out unless out.nil?
30
+
31
+ raise KeyError,
32
+ "Please configure either the #{key} key in #{@yaml_filename} or set #{envvar_name(key)}"
33
+ end
34
+ end
35
+
7
36
  # Load configuration file
8
37
  class ConfigLoader
9
- def self.load(sym)
38
+ def self.yaml_filename(sym)
10
39
  file = "#{sym}.yml"
11
- YAML.load_file(File.expand_path("~/.#{file}"))
12
- .with_indifferent_access
40
+ File.expand_path("~/.#{file}")
41
+ end
42
+
43
+ def self.load_yaml_file(sym)
44
+ filename = yaml_filename(sym)
45
+ return {} unless File.exist?(filename)
46
+
47
+ YAML.load_file(filename).with_indifferent_access
48
+ end
49
+
50
+ def self.load(sym)
51
+ yaml_result = load_yaml_file(sym)
52
+ EnvFallbackConfigLoader.new(yaml_result, sym, yaml_filename(sym))
13
53
  end
14
54
  end
15
55
  end
@@ -49,10 +49,10 @@ module Checkoff
49
49
  end
50
50
 
51
51
  def my_tasks(workspace_name)
52
- my_tasks = @config[:my_tasks]
53
- gid = @config[:my_tasks][workspace_name] unless my_tasks.nil?
54
- raise "Please define [:my_tasks][#{workspace_name}] in config file" if my_tasks.nil? || gid.nil?
55
-
52
+ workspace = @workspaces.workspace_by_name(workspace_name)
53
+ result = client.user_task_lists.get_user_task_list_for_user(user_gid: 'me',
54
+ workspace: workspace.gid)
55
+ gid = result.gid
56
56
  projects.find_by_id(gid)
57
57
  end
58
58
 
@@ -77,7 +77,7 @@ module Checkoff
77
77
  per_page: 100,
78
78
  options: {
79
79
  fields: %w[name completed_at due_at due_on assignee_status tags
80
- memberships.project.gid memberships.section.name],
80
+ memberships.project.gid memberships.section.name dependencies],
81
81
  },
82
82
  }
83
83
  end
@@ -4,6 +4,7 @@ require 'forwardable'
4
4
 
5
5
  module Checkoff
6
6
  # Query different sections of Asana projects
7
+ # rubocop:disable Metrics/ClassLength
7
8
  class Sections
8
9
  MINUTE = 60
9
10
  LONG_CACHE_TIME = MINUTE * 15
@@ -11,12 +12,14 @@ module Checkoff
11
12
 
12
13
  extend Forwardable
13
14
 
14
- attr_reader :projects, :time
15
+ attr_reader :projects, :workspaces, :time
15
16
 
16
17
  def initialize(config: Checkoff::ConfigLoader.load(:asana),
17
18
  projects: Checkoff::Projects.new(config: config),
19
+ workspaces: Checkoff::Workspaces.new(config: config),
18
20
  time: Time)
19
21
  @projects = projects
22
+ @workspaces = workspaces
20
23
  @time = time
21
24
  end
22
25
 
@@ -26,7 +29,7 @@ module Checkoff
26
29
  membership = task.memberships.find { |m| m['project']['gid'] == project_gid }
27
30
  raise "Could not find task in project_gid #{project_gid}: #{task}" if membership.nil?
28
31
 
29
- current_section = membership.section.name
32
+ current_section = membership['section']['name']
30
33
  current_section = nil if current_section == '(no section)'
31
34
  by_section[current_section] ||= []
32
35
  by_section[current_section] << task
@@ -41,6 +44,12 @@ module Checkoff
41
44
  end
42
45
  cache_method :by_section, LONG_CACHE_TIME
43
46
 
47
+ def legacy_tasks_by_section_for_project(project)
48
+ raw_tasks = projects.tasks_from_project(project)
49
+ active_tasks = projects.active_tasks(raw_tasks)
50
+ legacy_by_section(active_tasks)
51
+ end
52
+
44
53
  def tasks_by_section_for_project(project)
45
54
  raw_tasks = projects.tasks_from_project(project)
46
55
  active_tasks = projects.active_tasks(raw_tasks)
@@ -68,12 +77,12 @@ module Checkoff
68
77
  by_section
69
78
  end
70
79
 
71
- def tasks_by_section_for_project_and_assignee_status(project,
72
- assignee_status)
80
+ def legacy_tasks_by_section_for_project_and_assignee_status(project,
81
+ assignee_status)
73
82
  raw_tasks = projects.tasks_from_project(project)
74
83
  by_assignee_status =
75
84
  projects.active_tasks(raw_tasks)
76
- .group_by(&:assignee_status)
85
+ .group_by(&:assignee_status)
77
86
  active_tasks = by_assignee_status[assignee_status]
78
87
  legacy_by_section(active_tasks)
79
88
  end
@@ -87,20 +96,48 @@ module Checkoff
87
96
  project
88
97
  end
89
98
 
99
+ def verify_legacy_user_task_list!(workspace_name)
100
+ return unless user_task_list_migrated_to_real_sections?(workspace_name)
101
+
102
+ raise NotImplementedError, 'Section-based user task lists not yet supported'
103
+ end
104
+
105
+ ASSIGNEE_STATUS_BY_PROJECT_NAME = {
106
+ my_tasks_new: 'inbox',
107
+ my_tasks_today: 'today',
108
+ my_tasks_upcoming: 'upcoming',
109
+ }.freeze
110
+
111
+ def user_task_list_by_section(workspace_name, project_name)
112
+ verify_legacy_user_task_list!(workspace_name)
113
+
114
+ project = projects.project(workspace_name, project_name)
115
+ if project_name == :my_tasks
116
+ legacy_tasks_by_section_for_project(project)
117
+ else
118
+ legacy_tasks_by_section_for_project_and_assignee_status(project,
119
+ ASSIGNEE_STATUS_BY_PROJECT_NAME.fetch(project_name))
120
+ end
121
+ end
122
+
123
+ # rubocop:disable Metrics/MethodLength
90
124
  def tasks_by_section(workspace_name, project_name)
91
125
  project = project_or_raise(workspace_name, project_name)
92
126
  case project_name
127
+ when :my_tasks
128
+ user_task_list_by_section(workspace_name, project_name)
93
129
  when :my_tasks_new
94
- tasks_by_section_for_project_and_assignee_status(project, 'inbox')
130
+ user_task_list_by_section(workspace_name, project_name)
95
131
  when :my_tasks_today
96
- tasks_by_section_for_project_and_assignee_status(project, 'today')
132
+ user_task_list_by_section(workspace_name, project_name)
97
133
  when :my_tasks_upcoming
98
- tasks_by_section_for_project_and_assignee_status(project, 'upcoming')
134
+ user_task_list_by_section(workspace_name, project_name)
99
135
  else
100
136
  tasks_by_section_for_project(project)
101
137
  end
102
138
  end
103
139
  cache_method :tasks_by_section, SHORT_CACHE_TIME
140
+ # rubocop:enable Metrics/MethodLength
104
141
 
105
142
  # XXX: Rename to section_tasks
106
143
  def tasks(workspace_name, project_name, section_name)
@@ -120,6 +157,13 @@ module Checkoff
120
157
  end
121
158
  cache_method :section_task_names, SHORT_CACHE_TIME
122
159
 
160
+ def user_task_list_migrated_to_real_sections?(workspace_name)
161
+ workspace = workspaces.workspace_by_name(workspace_name)
162
+ result = client.user_task_lists.get_user_task_list_for_user(user_gid: 'me',
163
+ workspace: workspace.gid)
164
+ result.migration_status != 'not_migrated'
165
+ end
166
+
123
167
  def task_due?(task)
124
168
  if task.due_at
125
169
  Time.parse(task.due_at) <= time.now
@@ -143,4 +187,5 @@ module Checkoff
143
187
  end
144
188
  cache_method :project_task_names, SHORT_CACHE_TIME
145
189
  end
190
+ # rubocop:enable Metrics/ClassLength
146
191
  end