asana-client 0.0.1
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.
- data/bin/asana +6 -0
- data/lib/asana-client.rb +381 -0
- metadata +78 -0
data/bin/asana
ADDED
data/lib/asana-client.rb
ADDED
@@ -0,0 +1,381 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
##
|
4
|
+
# Asana API library and command-line client
|
5
|
+
# Tommy MacWilliam <tmacwilliam@cs.harvard.edu>
|
6
|
+
#
|
7
|
+
#
|
8
|
+
|
9
|
+
require "rubygems"
|
10
|
+
require "JSON"
|
11
|
+
require "net/https"
|
12
|
+
require "yaml"
|
13
|
+
require "chronic"
|
14
|
+
|
15
|
+
module Asana
|
16
|
+
|
17
|
+
API_URL = "https://app.asana.com/api/1.0/"
|
18
|
+
|
19
|
+
# initialize config values
|
20
|
+
def Asana.init
|
21
|
+
begin
|
22
|
+
@@config = YAML.load_file File.expand_path "~/.asana-client"
|
23
|
+
rescue
|
24
|
+
abort "Configuration file could not be found.\nSee https://github.com/tmac721/asana-client for installation instructions."
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# parse argumens
|
29
|
+
def Asana.parse(args)
|
30
|
+
# no arguments given
|
31
|
+
if args.empty?
|
32
|
+
abort "Nothing to do here."
|
33
|
+
end
|
34
|
+
|
35
|
+
# concatenate array into a string
|
36
|
+
string = args.join " "
|
37
|
+
|
38
|
+
# finish n: complete the task with id n
|
39
|
+
if string =~ /^finish (\d+)$/
|
40
|
+
Asana::Task.finish $1
|
41
|
+
puts "Task completed!"
|
42
|
+
exit
|
43
|
+
end
|
44
|
+
|
45
|
+
# workspace: display tasks in that workspace
|
46
|
+
if string =~ /^(\w+)$/
|
47
|
+
# get corresponding workspace object
|
48
|
+
workspace = Asana::Workspace.find $1
|
49
|
+
abort "Workspace not found!" unless workspace
|
50
|
+
|
51
|
+
# display all tasks in workspace
|
52
|
+
puts workspace.tasks unless workspace.tasks.empty?
|
53
|
+
exit
|
54
|
+
end
|
55
|
+
|
56
|
+
# workspace/project: display tasks in that project
|
57
|
+
if string =~ /^(\w+)\/(\w+)$/
|
58
|
+
# get corresponding workspace
|
59
|
+
workspace = Asana::Workspace.find $1
|
60
|
+
abort "Workspace not found!" unless workspace
|
61
|
+
|
62
|
+
# get corresponding project
|
63
|
+
project = Asana::Project.find workspace, $2
|
64
|
+
abort "Project not found!" unless project
|
65
|
+
|
66
|
+
# display all tasks in project
|
67
|
+
puts project.tasks unless project.tasks.empty?
|
68
|
+
exit
|
69
|
+
end
|
70
|
+
|
71
|
+
# extract assignee, if any
|
72
|
+
assignee = nil
|
73
|
+
args.each do |word|
|
74
|
+
if word =~ /^@(\w+)$/
|
75
|
+
assignee = word[1..-1]
|
76
|
+
args.delete word
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# extract due date, if any
|
81
|
+
due = Chronic.parse(args.reverse[0..1].reverse.join(" "))
|
82
|
+
if !due.nil? && due.to_s =~ /(\d+)-(\d+)-(\d+)/
|
83
|
+
# penultimate word is part of the date or a stop word, so remove it
|
84
|
+
if Chronic.parse(args.reverse[1]) || ["due", "on", "for"].include?(args.reverse[1].downcase)
|
85
|
+
args.pop
|
86
|
+
end
|
87
|
+
|
88
|
+
# extract date from datetime and remove date from task name
|
89
|
+
args.pop
|
90
|
+
due = "#{$1}-#{$2}-#{$3}"
|
91
|
+
end
|
92
|
+
|
93
|
+
# reset string, because we modifed argv
|
94
|
+
string = args.join " "
|
95
|
+
|
96
|
+
# workspace task name: create task in that workspace
|
97
|
+
if string =~ /^(\w+) ([\w ]+)/
|
98
|
+
# get corresponding workspace
|
99
|
+
workspace = Asana::Workspace.find $1
|
100
|
+
|
101
|
+
# create task in workspace
|
102
|
+
Asana::Task.create workspace, $2, assignee, due
|
103
|
+
puts "Task created in #{workspace.name}!"
|
104
|
+
exit
|
105
|
+
end
|
106
|
+
|
107
|
+
# workspace/project task name: create task in that workspace
|
108
|
+
if string =~ /^(\w+)\/(\w+) ([\w ]+)/
|
109
|
+
# get corresponding workspace
|
110
|
+
workspace = Asana::Workspace.find $1
|
111
|
+
|
112
|
+
# create task in workspace
|
113
|
+
task = Asana::Task.create workspace, $3, assignee, due
|
114
|
+
|
115
|
+
# get corresponding project
|
116
|
+
project = Asana::Project.find workspace, $2
|
117
|
+
abort "Project not found!" unless project
|
118
|
+
|
119
|
+
# add task to project
|
120
|
+
Asana.post "tasks/#{task['data']['id']}/addProject", { "project" => project.id }
|
121
|
+
puts "Task created in #{workspace.name}/#{project.name}!"
|
122
|
+
exit
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# perform a GET request and return the response body as an object
|
127
|
+
def Asana.get(url)
|
128
|
+
return Asana.http_request(Net::HTTP::Get, url, nil, nil)
|
129
|
+
end
|
130
|
+
|
131
|
+
# perform a PUT request and return the response body as an object
|
132
|
+
def Asana.put(url, data, query = nil)
|
133
|
+
return Asana.http_request(Net::HTTP::Put, url, data, query)
|
134
|
+
end
|
135
|
+
|
136
|
+
# perform a POST request and return the response body as an object
|
137
|
+
def Asana.post(url, data, query = nil)
|
138
|
+
return Asana.http_request(Net::HTTP::Post, url, data, query)
|
139
|
+
end
|
140
|
+
|
141
|
+
# perform an HTTP request to the Asana API
|
142
|
+
def Asana.http_request(type, url, data, query)
|
143
|
+
# set up http object
|
144
|
+
uri = URI.parse API_URL + url
|
145
|
+
http = Net::HTTP.new uri.host, uri.port
|
146
|
+
http.use_ssl = true
|
147
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
148
|
+
|
149
|
+
# all requests are json
|
150
|
+
header = {
|
151
|
+
"Content-Type" => "application/json"
|
152
|
+
}
|
153
|
+
|
154
|
+
# make request
|
155
|
+
req = type.new("#{uri.path}?#{uri.query}", header)
|
156
|
+
req.basic_auth @@config["api_key"], ''
|
157
|
+
if req.respond_to?(:set_form_data) && !data.nil?
|
158
|
+
req.set_form_data data
|
159
|
+
end
|
160
|
+
res = http.start { |http| http.request req }
|
161
|
+
|
162
|
+
# return request object
|
163
|
+
return JSON.parse(res.body)
|
164
|
+
end
|
165
|
+
|
166
|
+
# get all of the users workspaces
|
167
|
+
def Asana.workspaces
|
168
|
+
spaces = self.get "workspaces"
|
169
|
+
list = []
|
170
|
+
|
171
|
+
# convert array to hash indexed on workspace name
|
172
|
+
spaces["data"].each do |space|
|
173
|
+
list.push Workspace.new :id => space["id"], :name => space["name"]
|
174
|
+
end
|
175
|
+
|
176
|
+
list
|
177
|
+
end
|
178
|
+
|
179
|
+
class Project
|
180
|
+
attr_accessor :id, :name, :workspace
|
181
|
+
|
182
|
+
def initialize(hash)
|
183
|
+
self.id = hash[:id] || 0
|
184
|
+
self.name = hash[:name] || ""
|
185
|
+
self.workspace = hash[:workspace] || nil
|
186
|
+
end
|
187
|
+
|
188
|
+
# search for a project within a workspace
|
189
|
+
def self.find(workspace, name)
|
190
|
+
# if given string for workspace, convert to object
|
191
|
+
if workspace.is_a? String
|
192
|
+
workspace = Asana::Workspace.find workspace
|
193
|
+
end
|
194
|
+
|
195
|
+
# check if any workspace contains the given name, and return first hit
|
196
|
+
name.downcase!
|
197
|
+
if workspace
|
198
|
+
workspace.projects.each do |project|
|
199
|
+
if project.name.downcase.include? name
|
200
|
+
return project
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
nil
|
206
|
+
end
|
207
|
+
|
208
|
+
# get all tasks associated with the current project
|
209
|
+
def tasks
|
210
|
+
task_objects = Asana.get "tasks?workspace=#{workspace.id}&project=#{self.id}"
|
211
|
+
list = []
|
212
|
+
|
213
|
+
task_objects["data"].each do |task|
|
214
|
+
list.push Task.new :id => task["id"], :name => task["name"],
|
215
|
+
:workspace => self.workspace, :project => self
|
216
|
+
end
|
217
|
+
|
218
|
+
list
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
class Task
|
223
|
+
attr_accessor :id, :name, :workspace, :project
|
224
|
+
|
225
|
+
def initialize(hash)
|
226
|
+
self.id = hash[:id] || 0
|
227
|
+
self.name = hash[:name] || ""
|
228
|
+
self.workspace = hash[:workspace] || nil
|
229
|
+
self.project = hash[:project] || nil
|
230
|
+
end
|
231
|
+
|
232
|
+
# create a new task on the server
|
233
|
+
def self.create(workspace, name, assignee = nil, due = nil)
|
234
|
+
# if given string for workspace, convert to object
|
235
|
+
if workspace.is_a? String
|
236
|
+
workspace = Asana::Workspace.find workspace
|
237
|
+
end
|
238
|
+
abort "Workspace not found" unless workspace
|
239
|
+
|
240
|
+
# if assignee was given, get user
|
241
|
+
if !assignee.nil?
|
242
|
+
assignee = Asana::User.find workspace, assignee
|
243
|
+
abort "Assignee not found" unless assignee
|
244
|
+
end
|
245
|
+
|
246
|
+
# add task to workspace
|
247
|
+
params = {
|
248
|
+
"workspace" => workspace.id,
|
249
|
+
"name" => name,
|
250
|
+
"assignee" => (assignee.nil?) ? "me" : assignee.id
|
251
|
+
}
|
252
|
+
|
253
|
+
# attach due date if given
|
254
|
+
if !due.nil?
|
255
|
+
params["due_on"] = due
|
256
|
+
end
|
257
|
+
|
258
|
+
# add task to workspace
|
259
|
+
Asana.post "tasks", params
|
260
|
+
end
|
261
|
+
|
262
|
+
# comment on a task
|
263
|
+
def self.comment(id, text)
|
264
|
+
Asana.post "tasks/#{id}/stories", { "text" => text }
|
265
|
+
end
|
266
|
+
|
267
|
+
# comment on the current task
|
268
|
+
def comment(text)
|
269
|
+
self.comment(self.id, text)
|
270
|
+
end
|
271
|
+
|
272
|
+
# finish a task
|
273
|
+
def self.finish(id)
|
274
|
+
Asana.put "tasks/#{id}", { "completed" => true }
|
275
|
+
end
|
276
|
+
|
277
|
+
# finish the current task
|
278
|
+
def finish
|
279
|
+
self.finish(self.id)
|
280
|
+
end
|
281
|
+
|
282
|
+
def to_s
|
283
|
+
"(#{self.id}) #{self.name}"
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
class User
|
288
|
+
attr_accessor :id, :name
|
289
|
+
|
290
|
+
def initialize(hash)
|
291
|
+
self.id = hash[:id] || 0
|
292
|
+
self.name = hash[:name] || ""
|
293
|
+
end
|
294
|
+
|
295
|
+
def self.find(workspace, name)
|
296
|
+
# if given string for workspace, convert to object
|
297
|
+
if workspace.is_a? String
|
298
|
+
workspace = Asana::Workspace.find workspace
|
299
|
+
end
|
300
|
+
|
301
|
+
# check if any workspace contains the given name, and return first hit
|
302
|
+
name.downcase!
|
303
|
+
workspace.users.each do |user|
|
304
|
+
if user.name.downcase.include? name
|
305
|
+
return user
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
nil
|
310
|
+
end
|
311
|
+
|
312
|
+
def to_s
|
313
|
+
self.name
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
class Workspace
|
318
|
+
attr_accessor :id, :name
|
319
|
+
|
320
|
+
def initialize(hash)
|
321
|
+
self.id = hash[:id] || 0
|
322
|
+
self.name = hash[:name] || ""
|
323
|
+
end
|
324
|
+
|
325
|
+
# search a workspace by name
|
326
|
+
def self.find(name)
|
327
|
+
# check if any workspace contains the given name, and return first hit
|
328
|
+
name.downcase!
|
329
|
+
Asana.workspaces.each do |workspace|
|
330
|
+
if workspace.name.downcase.include? name
|
331
|
+
return workspace
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
nil
|
336
|
+
end
|
337
|
+
|
338
|
+
# get all projects associated with a workspace
|
339
|
+
def projects
|
340
|
+
project_objects = Asana.get "projects?workspace=#{self.id}"
|
341
|
+
list = []
|
342
|
+
|
343
|
+
project_objects["data"].each do |project|
|
344
|
+
list.push Project.new :id => project["id"], :name => project["name"], :workspace => self
|
345
|
+
end
|
346
|
+
|
347
|
+
list
|
348
|
+
end
|
349
|
+
|
350
|
+
# get tasks assigned to me within this workspace
|
351
|
+
def tasks
|
352
|
+
task_objects = Asana.get "tasks?workspace=#{self.id}&assignee=me"
|
353
|
+
list = []
|
354
|
+
|
355
|
+
task_objects["data"].each do |task|
|
356
|
+
list.push Task.new :id => task["id"], :name => task["name"],
|
357
|
+
:workspace => self
|
358
|
+
end
|
359
|
+
|
360
|
+
list
|
361
|
+
end
|
362
|
+
|
363
|
+
# get all users in the workspace
|
364
|
+
def users
|
365
|
+
user_objects = Asana.get "workspaces/#{self.id}/users"
|
366
|
+
list = []
|
367
|
+
|
368
|
+
user_objects["data"].each do |user|
|
369
|
+
list.push User.new :id => user["id"], :name => user["name"]
|
370
|
+
end
|
371
|
+
|
372
|
+
list
|
373
|
+
end
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
|
378
|
+
if __FILE__ == $0
|
379
|
+
Asana.init
|
380
|
+
Asana.parse ARGV
|
381
|
+
end
|
metadata
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: asana-client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.1
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Tommy MacWilliam
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2012-04-25 00:00:00 -04:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: chronic
|
18
|
+
prerelease: false
|
19
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
|
+
none: false
|
21
|
+
requirements:
|
22
|
+
- - ">="
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: 0.6.7
|
25
|
+
type: :runtime
|
26
|
+
version_requirements: *id001
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: json
|
29
|
+
prerelease: false
|
30
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
31
|
+
none: false
|
32
|
+
requirements:
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: 1.6.6
|
36
|
+
type: :runtime
|
37
|
+
version_requirements: *id002
|
38
|
+
description: Command-line client and library for browsing, creating, and completing Asana tasks.
|
39
|
+
email: tmacwilliam@cs.harvard.edu
|
40
|
+
executables:
|
41
|
+
- asana
|
42
|
+
extensions: []
|
43
|
+
|
44
|
+
extra_rdoc_files: []
|
45
|
+
|
46
|
+
files:
|
47
|
+
- lib/asana-client.rb
|
48
|
+
- bin/asana
|
49
|
+
has_rdoc: true
|
50
|
+
homepage: https://github.com/tmac721/asana-client
|
51
|
+
licenses: []
|
52
|
+
|
53
|
+
post_install_message:
|
54
|
+
rdoc_options: []
|
55
|
+
|
56
|
+
require_paths:
|
57
|
+
- lib
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
59
|
+
none: false
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: "0"
|
64
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: "0"
|
70
|
+
requirements: []
|
71
|
+
|
72
|
+
rubyforge_project:
|
73
|
+
rubygems_version: 1.6.2
|
74
|
+
signing_key:
|
75
|
+
specification_version: 3
|
76
|
+
summary: Ruby client for Asana's REST API
|
77
|
+
test_files: []
|
78
|
+
|