checkoff 0.5.4 → 0.9.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.
@@ -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