gyunyu 0.1.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.
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +26 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +35 -0
- data/Rakefile +46 -0
- data/VERSION +1 -0
- data/bin/gyunyu +11 -0
- data/gyunyu.gemspec +92 -0
- data/lib/gyunyu.rb +9 -0
- data/lib/gyunyu/app.rb +4 -0
- data/lib/gyunyu/command.rb +21 -0
- data/lib/gyunyu/command/export/app.rb +150 -0
- data/lib/gyunyu/command/export/format/csv.rb +27 -0
- data/lib/gyunyu/command/export/format/json.rb +16 -0
- data/lib/gyunyu/command/export/format/yaml.rb +16 -0
- data/lib/gyunyu/command/export/option.rb +63 -0
- data/lib/gyunyu/expander.rb +40 -0
- data/lib/gyunyu/token.rb +89 -0
- data/spec/app/.gitkeep +0 -0
- data/spec/app/command/export/option_spec.rb +137 -0
- data/spec/app/command_spec.rb +62 -0
- data/spec/app/expander_spec.rb +140 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/token/token_spec.rb +87 -0
- data/tasks/git.rake +7 -0
- data/tasks/spec.rake +11 -0
- data/tmp/.gitkeep +0 -0
- metadata +178 -0
@@ -0,0 +1,63 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require 'optparse'
|
3
|
+
|
4
|
+
module Gyunyu
|
5
|
+
module Command
|
6
|
+
module Export
|
7
|
+
class Option
|
8
|
+
class FormatNotFound < StandardError; end
|
9
|
+
|
10
|
+
FIELD_SEP = ','
|
11
|
+
|
12
|
+
def initialize( argv = [] )
|
13
|
+
@lists = []
|
14
|
+
@filter = nil
|
15
|
+
@fields = []
|
16
|
+
@format = :csv
|
17
|
+
|
18
|
+
parser.parse( argv )
|
19
|
+
end
|
20
|
+
attr_reader :lists, :filter, :fields, :format
|
21
|
+
|
22
|
+
def fields
|
23
|
+
if @fields.size > 0
|
24
|
+
@fields
|
25
|
+
else
|
26
|
+
%w( task_id name due )
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def parser
|
31
|
+
OptionParser.new do |opt|
|
32
|
+
opt.on('-l', '--list LIST') { |list|
|
33
|
+
if !@lists.include?( list )
|
34
|
+
@lists << list
|
35
|
+
end
|
36
|
+
}
|
37
|
+
opt.on('-f', '--filter FILTER') { |filter|
|
38
|
+
@filter = filter if filter.size > 0
|
39
|
+
}
|
40
|
+
opt.on('-d', '--field FIELD') { |field|
|
41
|
+
if field.include?( FIELD_SEP )
|
42
|
+
@fields = field.split( FIELD_SEP )
|
43
|
+
elsif !@fields.include?( field )
|
44
|
+
@fields << field
|
45
|
+
end
|
46
|
+
}
|
47
|
+
opt.on('-o', '--format FORMAT') { |format|
|
48
|
+
format.downcase!
|
49
|
+
formats = Format.constants.map { |e|
|
50
|
+
e.to_s.downcase
|
51
|
+
}
|
52
|
+
if formats.include?( format )
|
53
|
+
@format = format
|
54
|
+
else
|
55
|
+
raise FormatNotFound
|
56
|
+
end
|
57
|
+
}
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Gyunyu
|
2
|
+
module Expander
|
3
|
+
class << self
|
4
|
+
#
|
5
|
+
# [param] Array or Hash taskseries
|
6
|
+
# [return] Array
|
7
|
+
#
|
8
|
+
def taskseries( list )
|
9
|
+
expanded = []
|
10
|
+
|
11
|
+
if list.is_a?( Hash ) and list.has_key?( 'taskseries' )
|
12
|
+
list = list['taskseries']
|
13
|
+
end
|
14
|
+
|
15
|
+
list.each { |series|
|
16
|
+
expanded += tasks( series )
|
17
|
+
}
|
18
|
+
|
19
|
+
expanded
|
20
|
+
end
|
21
|
+
|
22
|
+
#
|
23
|
+
# [param] Hash taskseries
|
24
|
+
# [return] Array
|
25
|
+
#
|
26
|
+
def tasks( taskseries )
|
27
|
+
task = Marshal.load( Marshal.dump( taskseries ) )
|
28
|
+
task.delete('id')
|
29
|
+
task.delete('task')
|
30
|
+
task['taskseries_id'] = taskseries['id']
|
31
|
+
|
32
|
+
taskseries['task'].map { |t|
|
33
|
+
t['task_id'] = t['id']
|
34
|
+
t.delete( 'id' )
|
35
|
+
task.merge( t )
|
36
|
+
}
|
37
|
+
end
|
38
|
+
end # of << self
|
39
|
+
end
|
40
|
+
end
|
data/lib/gyunyu/token.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Gyunyu
|
4
|
+
class Token
|
5
|
+
API_KEY = '0af285d87a0798cdd4751acfb02fc058'
|
6
|
+
SHARED_SECRET = 'f0611feb04a04e1d'
|
7
|
+
|
8
|
+
include ::FileUtils
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
RTM::API.init( API_KEY, SHARED_SECRET )
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.get
|
15
|
+
@self = self.new
|
16
|
+
token = @self.load
|
17
|
+
|
18
|
+
if ( !token )
|
19
|
+
token = @self.request
|
20
|
+
@self.save( token )
|
21
|
+
end
|
22
|
+
|
23
|
+
RTM::API.token = token
|
24
|
+
token
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.purge
|
28
|
+
RTM::API.token = nil
|
29
|
+
self.new.instance_eval {
|
30
|
+
while ( File.exist?( magazine ) )
|
31
|
+
rm( magazine )
|
32
|
+
end
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def save( token )
|
37
|
+
if ( !File.exist?( magazine ) )
|
38
|
+
open( magazine, 'w', 0600 ) { }
|
39
|
+
end
|
40
|
+
|
41
|
+
open( magazine, 'r+b', 0600 ) { |f|
|
42
|
+
f.flock( File::LOCK_EX )
|
43
|
+
f.truncate(0)
|
44
|
+
f.print token
|
45
|
+
f.flock( File::LOCK_UN )
|
46
|
+
}
|
47
|
+
token
|
48
|
+
end
|
49
|
+
|
50
|
+
def load
|
51
|
+
token = nil
|
52
|
+
|
53
|
+
if ( File.exist?( magazine ) )
|
54
|
+
open( magazine, 'r+b' ) { |f|
|
55
|
+
# TODO WARN if permission is too loose
|
56
|
+
f.flock( File::LOCK_SH )
|
57
|
+
token = f.read
|
58
|
+
f.flock( File::LOCK_UN )
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
token
|
63
|
+
end
|
64
|
+
|
65
|
+
def magazine
|
66
|
+
env || dotfile
|
67
|
+
end
|
68
|
+
|
69
|
+
def env
|
70
|
+
ENV['RTMSTAT_TOKEN']
|
71
|
+
end
|
72
|
+
|
73
|
+
def dotfile
|
74
|
+
File.join( ENV['HOME'], '.gyunyu' )
|
75
|
+
end
|
76
|
+
|
77
|
+
def request
|
78
|
+
frob = RTM::Auth::GetFrob.new.invoke
|
79
|
+
url = RTM::API.get_auth_url('read', frob)
|
80
|
+
puts "open RTM with browser and accept API KEY."
|
81
|
+
puts "and hit enter."
|
82
|
+
system("open '#{url}'")
|
83
|
+
STDIN.gets
|
84
|
+
|
85
|
+
res = RTM::Auth::GetToken.new(frob).invoke
|
86
|
+
res[:token]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/spec/app/.gitkeep
ADDED
File without changes
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require File.dirname(__FILE__) + '/../../../spec_helper'
|
3
|
+
|
4
|
+
describe Gyunyu::Command::Export::Option do
|
5
|
+
describe 'parser' do
|
6
|
+
subject {
|
7
|
+
Gyunyu::Command::Export::Option.new.parser
|
8
|
+
}
|
9
|
+
it {
|
10
|
+
subject.class.should == OptionParser
|
11
|
+
}
|
12
|
+
|
13
|
+
describe 'lists' do
|
14
|
+
context 'no lists' do
|
15
|
+
subject {
|
16
|
+
Gyunyu::Command::Export::Option.new.lists
|
17
|
+
}
|
18
|
+
it {
|
19
|
+
should == []
|
20
|
+
}
|
21
|
+
end
|
22
|
+
context 'one list' do
|
23
|
+
subject {
|
24
|
+
Gyunyu::Command::Export::Option.new( %w( -l 仕事 ) ).lists
|
25
|
+
}
|
26
|
+
it {
|
27
|
+
should == %w( 仕事 )
|
28
|
+
}
|
29
|
+
end
|
30
|
+
context 'two lists' do
|
31
|
+
subject {
|
32
|
+
Gyunyu::Command::Export::Option.new( %w( -l 仕事 -l 勉強 ) ).lists
|
33
|
+
}
|
34
|
+
it {
|
35
|
+
should == %w( 仕事 勉強 )
|
36
|
+
}
|
37
|
+
end
|
38
|
+
context 'twice same list' do
|
39
|
+
subject {
|
40
|
+
Gyunyu::Command::Export::Option.new( %w( -l 仕事 -l 仕事 ) ).lists
|
41
|
+
}
|
42
|
+
it {
|
43
|
+
should == %w( 仕事 )
|
44
|
+
}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe 'filter' do
|
49
|
+
context 'no filter' do
|
50
|
+
subject {
|
51
|
+
Gyunyu::Command::Export::Option.new( %w() ).filter
|
52
|
+
}
|
53
|
+
it {
|
54
|
+
should be_nil
|
55
|
+
}
|
56
|
+
end
|
57
|
+
context 'one filter' do
|
58
|
+
subject {
|
59
|
+
Gyunyu::Command::Export::Option.new( %w( -f filter ) ).filter
|
60
|
+
}
|
61
|
+
it {
|
62
|
+
should == 'filter'
|
63
|
+
}
|
64
|
+
end
|
65
|
+
context 'multiple filters' do
|
66
|
+
subject {
|
67
|
+
Gyunyu::Command::Export::Option.new( %W( -f first\ one -f last\ one ) ).filter
|
68
|
+
}
|
69
|
+
it {
|
70
|
+
should == 'last one'
|
71
|
+
}
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe 'fields' do
|
76
|
+
context 'no fields' do
|
77
|
+
subject {
|
78
|
+
Gyunyu::Command::Export::Option.new().fields
|
79
|
+
}
|
80
|
+
it {
|
81
|
+
should == %w( task_id name due )
|
82
|
+
}
|
83
|
+
end
|
84
|
+
context 'specified fields' do
|
85
|
+
subject {
|
86
|
+
Gyunyu::Command::Export::Option.new( %w( -d id ) ).fields
|
87
|
+
}
|
88
|
+
it {
|
89
|
+
should == %w( id )
|
90
|
+
}
|
91
|
+
end
|
92
|
+
context 'duplicated fields' do
|
93
|
+
subject {
|
94
|
+
Gyunyu::Command::Export::Option.new( %w( -d id -d name -d id ) ).fields
|
95
|
+
}
|
96
|
+
it {
|
97
|
+
should == %w( id name )
|
98
|
+
}
|
99
|
+
end
|
100
|
+
context 'include #{FIELD_SEP}' do
|
101
|
+
subject {
|
102
|
+
Gyunyu::Command::Export::Option.new( %w( -d id,name,taskseries_id,due ) ).fields
|
103
|
+
}
|
104
|
+
it 'bulk definition' do
|
105
|
+
should == %w( id name taskseries_id due )
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
describe 'format' do
|
111
|
+
context 'no format' do
|
112
|
+
subject {
|
113
|
+
Gyunyu::Command::Export::Option.new().format
|
114
|
+
}
|
115
|
+
it {
|
116
|
+
should == :csv
|
117
|
+
}
|
118
|
+
end
|
119
|
+
context 'correct format' do
|
120
|
+
subject {
|
121
|
+
Gyunyu::Command::Export::Option.new( %w( -o json ) ).format
|
122
|
+
}
|
123
|
+
it {
|
124
|
+
should == 'json'
|
125
|
+
}
|
126
|
+
end
|
127
|
+
context 'wrong format' do
|
128
|
+
subject {
|
129
|
+
Gyunyu::Command::Export::Option.new( %w( -o atom ) ).format
|
130
|
+
}
|
131
|
+
it {
|
132
|
+
lambda { subject }.should raise_error( Gyunyu::Command::Export::Option::FormatNotFound )
|
133
|
+
}
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
2
|
+
|
3
|
+
describe Gyunyu::Command do
|
4
|
+
describe 'commands' do
|
5
|
+
let(:commands) {
|
6
|
+
Gyunyu::Command.commands
|
7
|
+
}
|
8
|
+
describe 'array' do
|
9
|
+
it {
|
10
|
+
commands.size.should > 0
|
11
|
+
}
|
12
|
+
end
|
13
|
+
describe 'array of strings' do
|
14
|
+
it 'array of strings' do
|
15
|
+
commands.map { |e| e.should.class == String }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe 'dispatch' do
|
21
|
+
context 'correct command' do
|
22
|
+
before {
|
23
|
+
stub.instance_of(Gyunyu::Command::Export::App).run {
|
24
|
+
"`export' dispatched"
|
25
|
+
}
|
26
|
+
}
|
27
|
+
subject {
|
28
|
+
Gyunyu::Command.dispatch( 'export' )
|
29
|
+
}
|
30
|
+
it {
|
31
|
+
should == "`export' dispatched"
|
32
|
+
}
|
33
|
+
end
|
34
|
+
context 'wrong command' do
|
35
|
+
subject {
|
36
|
+
Gyunyu::Command.dispatch( 'foo' )
|
37
|
+
}
|
38
|
+
it {
|
39
|
+
lambda {subject}.should raise_error(Gyunyu::Command::NotFound)
|
40
|
+
}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe 'app_const' do
|
45
|
+
context 'correct app' do
|
46
|
+
subject {
|
47
|
+
Gyunyu::Command.app_const( 'export' )
|
48
|
+
}
|
49
|
+
it {
|
50
|
+
should == Gyunyu::Command::Export::App
|
51
|
+
}
|
52
|
+
end
|
53
|
+
context 'wrong app' do
|
54
|
+
subject {
|
55
|
+
Gyunyu::Command.app_const( 'foo' )
|
56
|
+
}
|
57
|
+
it {
|
58
|
+
lambda {subject}.should raise_error( NameError )
|
59
|
+
}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require File.dirname(__FILE__) + '/../spec_helper'
|
3
|
+
|
4
|
+
describe Gyunyu::Expander do
|
5
|
+
describe 'taskseries' do
|
6
|
+
shared_examples_for :taskseries do
|
7
|
+
it 'array of hash' do
|
8
|
+
subject.class.should == Array
|
9
|
+
subject.each { |e|
|
10
|
+
e.class.should == Hash
|
11
|
+
}
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
context 'array given' do
|
16
|
+
it_should_behave_like :taskseries do
|
17
|
+
subject {
|
18
|
+
Gyunyu::Expander.taskseries( task_raw_data.first['taskseries'] )
|
19
|
+
}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
context 'hash given' do
|
23
|
+
it_should_behave_like :taskseries do
|
24
|
+
subject {
|
25
|
+
Gyunyu::Expander.taskseries( task_raw_data.first )
|
26
|
+
}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe 'tasks' do
|
32
|
+
subject {
|
33
|
+
Gyunyu::Expander.tasks(task_raw_data.first['taskseries'].first)
|
34
|
+
}
|
35
|
+
it {
|
36
|
+
should == [
|
37
|
+
{ "taskseries_id"=>"118872028",
|
38
|
+
"created"=>"2011-05-28T01:04:49Z",
|
39
|
+
"modified"=>"2011-10-10T22:00:53Z",
|
40
|
+
"name"=>"jsのlintについて",
|
41
|
+
"source"=>"js",
|
42
|
+
"url"=>"",
|
43
|
+
"location_id"=>"",
|
44
|
+
"tags"=>[{"tag"=>["research", "writing"]}],
|
45
|
+
"participants"=>[{}],
|
46
|
+
"notes"=>
|
47
|
+
[{"note"=>
|
48
|
+
[{"id"=>"26111105",
|
49
|
+
"created"=>"2011-10-10T22:00:53Z",
|
50
|
+
"modified"=>"2011-10-10T22:00:53Z",
|
51
|
+
"title"=>"",
|
52
|
+
"content"=>"意図が分かんないな、このメモ。結局何をしたかった?"}]}],
|
53
|
+
"task_id"=>"181820465",
|
54
|
+
"due"=>"",
|
55
|
+
"has_due_time"=>"0",
|
56
|
+
"added"=>"2011-05-28T01:04:49Z",
|
57
|
+
"completed"=>"",
|
58
|
+
"deleted"=>"",
|
59
|
+
"priority"=>"1",
|
60
|
+
"postponed"=>"0",
|
61
|
+
"estimate"=>""}]
|
62
|
+
}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def task_raw_data
|
67
|
+
# [{'id' => list id,
|
68
|
+
# 'taskseries' =>
|
69
|
+
# []
|
70
|
+
# }]
|
71
|
+
[{"id"=>"135243",
|
72
|
+
"taskseries"=>
|
73
|
+
[{"id"=>"118872028",
|
74
|
+
"created"=>"2011-05-28T01:04:49Z",
|
75
|
+
"modified"=>"2011-10-10T22:00:53Z",
|
76
|
+
"name"=>"jsのlintについて",
|
77
|
+
"source"=>"js",
|
78
|
+
"url"=>"",
|
79
|
+
"location_id"=>"",
|
80
|
+
"tags"=>[{"tag"=>["research", "writing"]}],
|
81
|
+
"participants"=>[{}],
|
82
|
+
"notes"=>
|
83
|
+
[{"note"=>
|
84
|
+
[{"id"=>"26111105",
|
85
|
+
"created"=>"2011-10-10T22:00:53Z",
|
86
|
+
"modified"=>"2011-10-10T22:00:53Z",
|
87
|
+
"title"=>"",
|
88
|
+
"content"=>"意図が分かんないな、このメモ。結局何をしたかった?"}]}],
|
89
|
+
"task"=>
|
90
|
+
[{"id"=>"181820465",
|
91
|
+
"due"=>"",
|
92
|
+
"has_due_time"=>"0",
|
93
|
+
"added"=>"2011-05-28T01:04:49Z",
|
94
|
+
"completed"=>"",
|
95
|
+
"deleted"=>"",
|
96
|
+
"priority"=>"1",
|
97
|
+
"postponed"=>"0",
|
98
|
+
"estimate"=>""}]},
|
99
|
+
{"id"=>"46116936",
|
100
|
+
"created"=>"2009-08-01T08:35:30Z",
|
101
|
+
"modified"=>"2010-05-10T03:18:43Z",
|
102
|
+
"name"=>"あの本読む",
|
103
|
+
"source"=>"js",
|
104
|
+
"url"=>"",
|
105
|
+
"location_id"=>"",
|
106
|
+
"tags"=>[{"tag"=>["reading"]}],
|
107
|
+
"participants"=>[{}],
|
108
|
+
"notes"=>[{}],
|
109
|
+
"task"=>
|
110
|
+
[{"id"=>"65881131",
|
111
|
+
"due"=>"",
|
112
|
+
"has_due_time"=>"0",
|
113
|
+
"added"=>"2009-08-01T08:35:30Z",
|
114
|
+
"completed"=>"",
|
115
|
+
"deleted"=>"",
|
116
|
+
"priority"=>"N",
|
117
|
+
"postponed"=>"0",
|
118
|
+
"estimate"=>"1時間"}]},
|
119
|
+
{"id"=>"115609158",
|
120
|
+
"created"=>"2011-05-02T10:13:47Z",
|
121
|
+
"modified"=>"2011-05-07T12:26:43Z",
|
122
|
+
"name"=>"この本読む",
|
123
|
+
"source"=>"js",
|
124
|
+
"url"=>"",
|
125
|
+
"location_id"=>"",
|
126
|
+
"tags"=>[{"tag"=>["reading"]}],
|
127
|
+
"participants"=>[{}],
|
128
|
+
"notes"=>[{}],
|
129
|
+
"task"=>
|
130
|
+
[{"id"=>"176273444",
|
131
|
+
"due"=>"",
|
132
|
+
"has_due_time"=>"0",
|
133
|
+
"added"=>"2011-05-02T10:13:47Z",
|
134
|
+
"completed"=>"",
|
135
|
+
"deleted"=>"",
|
136
|
+
"priority"=>"1",
|
137
|
+
"postponed"=>"0",
|
138
|
+
"estimate"=>"8h"}]}
|
139
|
+
]}]
|
140
|
+
end
|