Class: TodoistRestClient

Inherits:
Object
  • Object
show all
Defined in:
lib/todoist_rest_client.rb,
lib/todoist_rest_client/version.rb

Constant Summary collapse

DAYS =
%i[sun mon tue wed thu fri sat].freeze
BASE =
{
  rest: {v1: "https://api.todoist.com/api/v1/", legacy: "https://api.todoist.com/rest/v2/"},
  sync: {v1: "https://api.todoist.com/api/v1/sync", legacy: "https://api.todoist.com/sync/v9/sync"}
}.freeze
AUTO_SCHED_LABELS =
["auto_schedule"]
VERSION =
"0.2.0"

Class Attribute Summary collapse

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(bearer_token) ⇒ TodoistRestClient

Returns a new instance of TodoistRestClient.



43
44
45
46
47
48
49
50
# File 'lib/todoist_rest_client.rb', line 43

def initialize(bearer_token)
  @auth = "Bearer #{bearer_token}"
  @responses = []
  @req = RestRequestor.new
  @paged_reqs = []
  @request_history = []
  @stats_paginated = []
end

Class Attribute Details

.loggerObject



35
36
37
# File 'lib/todoist_rest_client.rb', line 35

def logger
  @logger ||= Logger.new($stdout)
end

Instance Attribute Details

#jsw_skip_pryObject

Returns the value of attribute jsw_skip_pry.



40
41
42
# File 'lib/todoist_rest_client.rb', line 40

def jsw_skip_pry
  @jsw_skip_pry
end

#paged_reqsObject

Returns the value of attribute paged_reqs.



40
41
42
# File 'lib/todoist_rest_client.rb', line 40

def paged_reqs
  @paged_reqs
end

#reqObject

Returns the value of attribute req.



40
41
42
# File 'lib/todoist_rest_client.rb', line 40

def req
  @req
end

#request_historyObject (readonly)

Returns the value of attribute request_history.



41
42
43
# File 'lib/todoist_rest_client.rb', line 41

def request_history
  @request_history
end

#responsesObject (readonly)

Returns the value of attribute responses.



41
42
43
# File 'lib/todoist_rest_client.rb', line 41

def responses
  @responses
end

#stats_paginatedObject (readonly)

Returns the value of attribute stats_paginated.



41
42
43
# File 'lib/todoist_rest_client.rb', line 41

def stats_paginated
  @stats_paginated
end

Instance Method Details

#add_comment(task_id, content) ⇒ Object



101
102
103
# File 'lib/todoist_rest_client.rb', line 101

def add_comment(task_id, content)
  api_rest_request(:comments, :post, j_params: {task_id:, content:})
end

#add_due_date(item_id, project_id) ⇒ Object



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/todoist_rest_client.rb', line 85

def add_due_date(item_id, project_id)
  proj_root_name = projects.find { |p| p.id == project_id }&.root_name
  rul = rules.find { |r| r[:name] == proj_root_name }
  due_str = if rul
    next_t_date = (next_task_date(rul) || Todoist.now.hour < 16) ? Todoist.today : Todoist.today + 1
    if rul[:repeat_str].present?
      "#{rul[:repeat_str]} starting #{next_t_date}"
    else
      next_t_date
    end
  else
    (Todoist.now.hour < 16) ? Todoist.today : Todoist.today + 1
  end
  update_task(item_id, due_string: due_str, labels: AUTO_SCHED_LABELS)
end

#api_compare(path, verb = :get, q_params: {}, j_params: {}, retries: 0, **kwparams) ⇒ Object



52
53
54
55
56
# File 'lib/todoist_rest_client.rb', line 52

def api_compare(path, verb = :get, q_params: {}, j_params: {}, retries: 0, **kwparams)
  r_legacy = api_rest_request(path, verb, q_params:, j_params:, retries:, version: :legacy, **kwparams)
  r_v1 = api_rest_request(path, verb, q_params:, j_params:, retries:, version: :v1, **kwparams)
  {new: r_v1, legacy: r_legacy}
end

#api_rest_request(path, verb = :get, q_params: {}, j_params: {}, retries: 0, version: :v1, **kwparams) ⇒ Object



73
74
75
76
77
78
79
80
81
82
83
# File 'lib/todoist_rest_client.rb', line 73

def api_rest_request(path, verb = :get, q_params: {}, j_params: {}, retries: 0, version: :v1, **kwparams)
  q_params.each { |k, v| q_params.delete(k) if v.blank? }
  if ["get", :get].include?(verb) && q_params["limit"].nil? && q_params[:limit].nil?
    q_params[:limit] = 200
  end
  req_path = "#{BASE[:rest][version]}#{path}"
  @request_history << {path: req_path, verb:, q_params:, j_params: j_params.merge!(kwparams)}
  resp = req.request(req_path, verb, q_params:, j_params: j_params.merge!(kwparams), auth: @auth)
  responses << resp
  resp
end

#api_sync_request(path = nil, verb = :post, f_hash: {}, commands: [], retries: 0, version: :v1) ⇒ Object

SYNC METHODS



407
408
409
410
# File 'lib/todoist_rest_client.rb', line 407

def api_sync_request(path = nil, verb = :post, f_hash: {}, commands: [], retries: 0, version: :v1)
  f_hash[:commands] = commands.to_json if commands.present?
  req.request("#{BASE[:sync][version]}#{path}".freeze, verb, f_hash:, auth: @auth)
end

#build_comment(cmt) ⇒ Object



483
484
485
486
# File 'lib/todoist_rest_client.rb', line 483

def build_comment(cmt)
  cmt[:file_attachment] = Todoist::FileAttachment[cmt.delete("file_attachment") || cmt.delete("attachment") || {}]
  Todoist::Comment[cmt]
end

#build_struct(struct, hashed_vals) ⇒ Object



479
480
481
# File 'lib/todoist_rest_client.rb', line 479

def build_struct(struct, hashed_vals)
  struct[hashed_vals]
end

#build_task(tsk) ⇒ Object

p_rivate



474
475
476
477
# File 'lib/todoist_rest_client.rb', line 474

def build_task(tsk)
  tsk[:project_name] = projects.find { |p| p.id == tsk["project_id"] }&.name
  Todoist::Task[tsk]
end

#close_task(idd = nil, id: nil) ⇒ Object



105
106
107
108
109
110
111
112
# File 'lib/todoist_rest_client.rb', line 105

def close_task(idd = nil, id: nil)
  id ||= idd
  raise ArgumentError, "You must supply id as an unamed argument or as a named argument" if id.nil?
  api_rest_request("tasks/#{id}/close", :post)
  true
rescue RestRequestor::StandardError
  false
end

#comments(project_id: nil, task_id: nil) ⇒ Object

Raises:

  • (ArgumentError)


114
115
116
117
118
119
120
121
122
# File 'lib/todoist_rest_client.rb', line 114

def comments(project_id: nil, task_id: nil)
  raise ArgumentError, "project_id or task_id must be supplied" if project_id.nil? && task_id.nil?
  path = if project_id.present?
    "comments?project_id=#{project_id}"
  else
    "comments?task_id=#{task_id}"
  end
  api_request(path).map { |c| build_comment(c) }
end

#create(item, **kwparams) ⇒ Object



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/todoist_rest_client.rb', line 124

def create(item, **kwparams)
  path = item.to_s.pluralize
  required = []
  case item
  when :task
    required += %i[content]
  when :section
    required += %i[name project_id]
  when :comment
    required += %i[task_id content]
    kwparams[:task_id] ||= kwparams.delete(:id)
  when :project_comment
    required += %i[project_id content]
    kwparams[:project_id_id] ||= kwparams.delete(:id)
    path = "comments"
    rtn_obj = Todoist::Comment
  when :label, :project
    required += %i[name]
  else
    raise ArgumentError, "Unable to create item for #{item}"
  end
  unless (required - kwparams.keys).empty?
    raise ArgumentError, "You must supply #{required} to create #{item.to_s.pluralize}"
  end
  api_rtn = api_rest_request(path, :post, j_params: kwparams)
  rtn_obj = (rtn_obj || "Todoist::#{item.to_s.camelcase}".constantize)[api_rtn]
  case item
  when :task
    (@tasks || []) << rtn_obj
  when :project
    (@projects || []) << rtn_obj
  end
  rtn_obj
end

#debugObject



468
469
470
# File 'lib/todoist_rest_client.rb', line 468

def debug
  binding.pry if !@jsw_skip_pry # standard:disable Lint/Debugger
end

#delete_duplicates(content, save_ids: [], incl: :all) ⇒ Object



412
413
414
415
416
417
418
419
420
421
# File 'lib/todoist_rest_client.rb', line 412

def delete_duplicates(content, save_ids: [], incl: :all)
  dups = tasks(incl).select { |t| !save_ids.include?(t.id) && t.content == content }.sort_by { |t| t.added_at }
  commands = dups[1..].map { |d| {type: :item_delete, uuid: SecureRandom.uuid, args: {id: d.id}} }
  responses = []
  while commands.any?
    batch = commands.shift(100)
    responses << api_sync_request(commands: batch)
  end
  responses
end

#delete_task(idd = nil, id: nil) ⇒ Object

Raises:

  • (ArgumentError)


159
160
161
162
163
# File 'lib/todoist_rest_client.rb', line 159

def delete_task(idd = nil, id: nil)
  id ||= idd
  raise ArgumentError, "You must supply id as an unamed argument or as a named argument" if id.nil?
  api_rest_request("tasks/#{id}", :delete)
end

#edit_rulesObject



165
166
167
168
169
170
171
172
173
174
175
# File 'lib/todoist_rest_client.rb', line 165

def edit_rules
  dflts = {days: [0, 1, 2, 3, 4, 5, 6], look_ahead: 35, repeat_str: "every 35 days", global?: false, repeater?: false}
  rules.each do |rule|
    upd = true
    rule[:obj] = dflts.merge(rule[:obj])
    rule[:obj].delete(:repeater?)
    if upd
      update_task(rule[:t_id], description: rule[:obj].to_json)
    end
  end
end

#fix_date(tsk) ⇒ Object



177
178
179
180
181
182
# File 'lib/todoist_rest_client.rb', line 177

def fix_date(tsk)
  mm = tsk.due.date.match(/^(.*T.*)T.*/)
  if mm
    update_task(tsk.id, due_date: mm[1], due_string: tsk.due.string)
  end
end

#labels(force: false) ⇒ Object



184
185
186
187
188
189
190
# File 'lib/todoist_rest_client.rb', line 184

def labels(force: false)
  if !force && defined?(@labels)
    @labels
  else
    @labels = paginated_request("labels").map { |l| Todoist::Label[l] }
  end
end

#load_sample_tasks(filename = nil) ⇒ Object



192
193
194
195
196
197
198
199
200
# File 'lib/todoist_rest_client.rb', line 192

def load_sample_tasks(filename = nil)
  filename ||= "data/sample_tasks.yml"
  if @loaded&.dig(filename)
    return @loaded[filename]
  end
  @loaded ||= {}
  permitted_classes = [Todoist::Task, Todoist::Due, Symbol, Time, Date]
  @loaded[filename] ||= YAML.load_file(filename, permitted_classes:, aliases: true)
end

#masterObject



423
424
425
# File 'lib/todoist_rest_client.rb', line 423

def master
  sync.items.select { |i| i.content.match?(/Master\s*$/) }
end

#move_item(item_id, **kwargs) ⇒ Object



427
428
429
# File 'lib/todoist_rest_client.rb', line 427

def move_item(item_id, **kwargs)
  api_sync_request(commands: [{type: "item_move", uuid: SecureRandom.uuid, args: {id: item_id}.merge(kwargs)}])
end

#multi_move(items, **kwargs) ⇒ Object



431
432
433
434
435
436
437
438
439
440
# File 'lib/todoist_rest_client.rb', line 431

def multi_move(items, **kwargs)
  resp = []
  while items.any?
    commands = []
    99.times do
      commands << {type: "item_move", uuid: SecureRandom.uuid, args: {id: items.pop}.merge(kwargs)}
    end
    resp << api_sync_request(commands:)
  end
end

#next_project_id(root_name, create_proj: true, max_tasks: 299) ⇒ Object



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/todoist_rest_client.rb', line 202

def next_project_id(root_name, create_proj: true, max_tasks: 299)
  root_name = root_name.to_s.downcase.tr("_", " ")
  if root_name == "inbox"
    return projects.find { |p| p.root_name == "inbox" }&.id
  end
  np_id = project_tasks.find { |pt| pt[:root_name] == root_name && pt[:tasks] < max_tasks }&.dig(:id)
  return np_id if np_id.present? || !create_proj

  next_proj_num = projects.select { |p| p[:root_name] == root_name }.map { |sp| sp.p_num }.max + 1
  root_p = projects.find { |pt| pt[:root_name] == root_name && pt[:p_num] == 1 }
  np = if root_p
    create(:project, name: "#{root_name.titleize} #{next_proj_num}", color: root_p.color)
  else
    create(:project, name: "#{root_name.titleize} #{next_proj_num}")
  end
  projects << np
  np.id
end

#next_task_date(options = {}) ⇒ Object



221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
# File 'lib/todoist_rest_client.rb', line 221

def next_task_date(options = {})
  if options[:due_str].present?
    if options[:due_str] == "today"
      if Todoist.now.hour > 15
        return (Todoist.today + 1).strftime("%Y-%m-%d")
      else
        return Todoist.today.strftime("%Y-%m-%d")
      end
    else
      return options[:due_str]
    end
  end
  t_per_day = tasks_per_day(options.slice(:days_out, :days, :root_name))
  if options[:min_tasks_per_day].present?
    next_date = t_per_day.find { |t| t[:tasks] < options[:min_tasks_per_day] }&.dig(:date)
    return next_date if next_date.present?
  end
  min_tasks = t_per_day.map { |tpd| tpd[:tasks] }.min
  if options[:max_tasks_per_day].present? && min_tasks >= options[:max_tasks_per_day]
    return nil
  end
  t_per_day.find { |t| t[:tasks] == min_tasks }&.dig(:date)
end

#no_due_date(status = nil) ⇒ Object



245
246
247
248
# File 'lib/todoist_rest_client.rb', line 245

def no_due_date(status = nil)
  status ||= self.status
  status[:items].select { |i| i.dig("due", "date").nil? && i.parent_id.nil? }
end

#paginated_request(path, verb = :get, q_params: {}, j_params: {}, retries: 0, **kwparams) ⇒ Object



58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/todoist_rest_client.rb', line 58

def paginated_request(path, verb = :get, q_params: {}, j_params: {}, retries: 0, **kwparams)
  q_params[:limit] ||= 200
  results = []
  loop do
    req = api_rest_request(path, verb, q_params:, j_params:, retries:, version: :v1, **kwparams)
    q_params[:cursor] = req["next_cursor"]
    @paged_reqs << req
    puts "Path: #{path}, (#{results.count} + #{req["results"].count}), Next Cursor: [#{req["next_cursor"]}]"
    results += req["results"]
    @stats_paginated << {path:, count: results.count, cursor: req["next_cursor"], last_id: results.last&.dig("id")}
    break if req["next_cursor"].blank?
  end
  results
end

#project_id(search_name) ⇒ Object



258
259
260
261
262
# File 'lib/todoist_rest_client.rb', line 258

def project_id(search_name)
  search_name = search_name.to_s.downcase
  proj = projects.find { |p| p.name.downcase == search_name } || projects.find { |p| p.root_name == search_name }
  proj&.id
end

#project_name(proj_id, force: false) ⇒ Object



264
265
266
267
# File 'lib/todoist_rest_client.rb', line 264

def project_name(proj_id, force: false)
  proj_id = proj_id.to_s
  projects(force:).find { |p| p.id == proj_id }&.dig(:name)
end

#project_root_name(proj_id, force: false) ⇒ Object



269
270
271
272
# File 'lib/todoist_rest_client.rb', line 269

def project_root_name(proj_id, force: false)
  proj_id = proj_id.to_s
  projects(force:).find { |p| p.id == proj_id }&.dig(:name_parts, 0)
end

#project_rules(project_id: nil, root_name: nil) ⇒ Object



274
275
276
277
278
279
280
281
282
# File 'lib/todoist_rest_client.rb', line 274

def project_rules(project_id: nil, root_name: nil)
  if project_id.nil?
    raise ArgumentError, "You must provide project_id OR root_name" if root_name.nil?
    project_id = projects.find { |p| p.root_name == root_name }&.id
  end
  rul = tasks.find { |t| t.labels.include?("project_rules") && t.project_id == project_id }
  return nil if rul.nil?
  JSON.parse(rul.description)
end

#project_tasksObject



284
285
286
287
288
# File 'lib/todoist_rest_client.rb', line 284

def project_tasks
  projects.map do |proj|
    {root_name: proj.root_name, p_num: proj.p_num, id: proj.id, tasks: tasks(:all).count { |tsk| tsk.project_id == proj.id }}
  end
end

#projects(force: false) ⇒ Object



250
251
252
253
254
255
256
# File 'lib/todoist_rest_client.rb', line 250

def projects(force: false)
  if !force && defined?(@projects)
    @projects
  else
    @projects = paginated_request("projects").map { |p| Todoist::Project[p] }
  end
end

#recurring_tasks(include_annual: false) ⇒ Object



290
291
292
# File 'lib/todoist_rest_client.rb', line 290

def recurring_tasks(include_annual: false)
  tasks.select { |t| t.due.is_recurring }
end

#remove_section(item, holding_project_id:) ⇒ Object

The Sync API has no direct way to unset a section; moving the item out of its project and back in is what clears the section assignment.



444
445
446
447
448
449
# File 'lib/todoist_rest_client.rb', line 444

def remove_section(item, holding_project_id:)
  api_sync_request(commands: [
    {type: "item_move", uuid: SecureRandom.uuid, args: {id: item.id, project_id: holding_project_id}},
    {type: "item_move", uuid: SecureRandom.uuid, args: {id: item.id, project_id: item.project_id}}
  ])
end

#reopen_task(idd = nil, id: nil) ⇒ Object

Raises:

  • (ArgumentError)


294
295
296
297
298
# File 'lib/todoist_rest_client.rb', line 294

def reopen_task(idd = nil, id: nil)
  id ||= idd
  raise ArgumentError, "You must supply id as an unamed argument or as a named argument" if id.nil?
  api_rest_request("tasks/#{id}/reopen", :post)
end

#rulesObject



300
301
302
303
304
# File 'lib/todoist_rest_client.rb', line 300

def rules
  tasks.select { |t| t.labels.include?("project_rules") }.map do |rul|
    JSON.parse(rul.description).deep_symbolize_keys.merge({t_id: rul.id, t_name: rul.content})
  end
end

#sections(project_id = nil, force: false) ⇒ Object



306
307
308
309
310
311
312
313
314
315
# File 'lib/todoist_rest_client.rb', line 306

def sections(project_id = nil, force: false)
  if force || !defined?(@sections)
    @sections = paginated_request("sections").map { |s| Todoist::Section[s] }
  end
  if project_id.nil?
    @sections
  else
    @sections.select { |s| s.project_id == project_id.to_s }
  end
end

#single_task(idd = nil, id: nil, raw: false) ⇒ Object

Raises:

  • (ArgumentError)


317
318
319
320
321
# File 'lib/todoist_rest_client.rb', line 317

def single_task(idd = nil, id: nil, raw: false)
  id ||= idd
  raise ArgumentError, "You must supply id as an unamed argument or as a named argument" if id.nil?
  build_task(api_rest_request("tasks/#{id}"))
end

#sync(sync_token = "*", force: false, resource_types: nil, version: :v1) ⇒ Object



451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
# File 'lib/todoist_rest_client.rb', line 451

def sync(sync_token = "*", force: false, resource_types: nil, version: :v1)
  resource_types = if resource_types.nil?
    %i[filters labels locations projects sync_token stats items comments sections]
  else
    [resource_types].flatten
  end
  if resource_types.include?(:comments)
    resource_types.delete(:comments)
    resource_types << :notes
  end
  if !force && defined?(@sync)
    @sync
  else
    api_sync_request(f_hash: {sync_token:, resource_types: resource_types.to_json}, version:)
  end
end

#task_comments(task_id, raw: false) ⇒ Object



323
324
325
326
327
328
329
330
# File 'lib/todoist_rest_client.rb', line 323

def task_comments(task_id, raw: false)
  resp = api_rest_request(:comments, q_params: {task_id:})
  if raw
    resp
  else
    resp["results"].map { |r| Todoist::Comment[r] }
  end
end


332
333
334
335
336
# File 'lib/todoist_rest_client.rb', line 332

def task_from_link(link, raw: false)
  resp = api_rest_request("tasks/#{link.split("-")[-1]}")
  return resp if raw
  build_task(resp)
end

#tasks(incl = :dated, subtasks: nil, force: false) ⇒ Object



338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
# File 'lib/todoist_rest_client.rb', line 338

def tasks(incl = :dated, subtasks: nil, force: false)
  tsks = if !force && defined?(@tasks)
    case incl
    when :dated then @tasks.select { |t| !t.due.date.nil? }
    when :all then @tasks
    when :undated then @tasks.select { |t| t.due.date.nil? }
    end
  else
    @tasks = paginated_request("tasks/").map { |t| build_task(t) }
    tasks(incl)
  end
  case subtasks
  when nil then tsks
  when true then tsks.select { |t| t.parent_id.present? }
  when false then tsks.select { |t| t.parent_id.blank? }
  end
end

#tasks_by(criteria, value, include_subtasks: false) ⇒ Object



356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
# File 'lib/todoist_rest_client.rb', line 356

def tasks_by(criteria, value, include_subtasks: false)
  case criteria.to_s.downcase
  when "project"
    project = projects.find { |p| p["name"].downcase == value.to_s.downcase }
    return if project.nil?
    selected_tasks = tasks(:dated).select { |t| t["project_id"] == project["id"] }
  when "root_name"
    proj_ids = projects.select { |p| p.root_name == value.to_s.downcase }.map { |sp| sp.id }
    selected_tasks = tasks(:dated).select { |t| proj_ids.include?(t["project_id"]) }
  when "project_id"
    selected_tasks = tasks(:dated).select { |t| t["project_id"] == value.to_s }
  else
    return
  end
  selected_tasks.select! { |t| t["parent_id"].nil? } unless include_subtasks
  selected_tasks
end

#tasks_per_day(options = {}) ⇒ Object



374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
# File 'lib/todoist_rest_client.rb', line 374

def tasks_per_day(options = {})
  days_out = options[:days_out] || 35
  days = options[:days] || [*0..6]
  cur_tasks = if options[:root_name].present?
    tasks_by(:root_name, options[:root_name])
  else
    tasks
  end
  tasks_per_day = (1..days_out).map { |num| {date: (Date.today + num).to_s, tasks: [], wday: (Date.today + num).wday} }
  sorted = cur_tasks&.select { |t| t.due.days_until.between?(1, days_out + 1) }
    &.sort_by { |t| t.due.date }
    &.group_by { |t| t.due.date_obj.to_date.to_s }
  tasks_per_day.each { |task_d| task_d[:tasks] += sorted[task_d[:date]] || [] }
  tasks_per_day.map! { |t| {date: t[:date], tasks: t[:tasks].count, wday: t[:wday]} }
  tasks_per_day.select { |t| days.include?(t[:wday]) }
end

#test_task(content: nil, due_string: "today") ⇒ Object



391
392
393
394
# File 'lib/todoist_rest_client.rb', line 391

def test_task(content: nil, due_string: "today")
  content = "#{content || "Test Task"} - #{Date.today.strftime("%A")}"
  create(:task, content:, due_string:)
end

#update_comment(comment_id, content) ⇒ Object



396
397
398
# File 'lib/todoist_rest_client.rb', line 396

def update_comment(comment_id, content)
  api_rest_request("comments/#{comment_id}", :post, j_params: {content:})
end

#update_task(task_id, **params) ⇒ Object

Raises:

  • (ArgumentError)


400
401
402
403
# File 'lib/todoist_rest_client.rb', line 400

def update_task(task_id, **params)
  raise ArgumentError, "You must provide at least one key / value pair to update a task" if params.empty?
  api_rest_request("tasks/#{task_id}", :post, j_params: params)
end