togglv8 0.2.0 → 1.0.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.
- checksums.yaml +4 -4
- data/README.md +4 -0
- data/lib/togglv8/time_entries.rb +5 -3
- data/lib/togglv8/users.rb +11 -1
- data/lib/togglv8/version.rb +1 -1
- data/lib/togglv8.rb +30 -34
- data/spec/lib/togglv8/time_entries_spec.rb +12 -8
- data/spec/lib/togglv8_spec.rb +26 -5
- data/spec/spec_helper.rb +2 -7
- data/spec/togglv8_spec_helper.rb +28 -24
- data/togglv8.gemspec +1 -0
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7b2c12ae09772a1296677907a4e684d3ad5cfde5
|
4
|
+
data.tar.gz: 5afd4390ec69c38f3a8795e55b9e38a14f96ada7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 96b68a6420a5751a764a8bb54400be952281958d1325b86581ff39d5faa176807cd235a94795e5eb3802228e7f35bdc12bdda2c0b94b89fd07afbbb4b8844e38
|
7
|
+
data.tar.gz: 2be636c2115bc9be8bb66ebcdedcf8f46be8897d40256deccf96e57af5c7078f5e83a975e58ea133f8ee599b2c263c49fb48cbc2b7ed321952af5c6470860a4c
|
data/README.md
CHANGED
@@ -45,6 +45,10 @@ toggl_api.create_time_entry({description: "Workspace time entry",
|
|
45
45
|
|
46
46
|
See specs for more examples.
|
47
47
|
|
48
|
+
**Note:** Requests are rate-limited. See [Toggl API docs](https://github.com/toggl/toggl_api_docs#the-api-format):
|
49
|
+
|
50
|
+
> For rate limiting we have implemented a Leaky bucket. When a limit has been hit the request will get a HTTP 429 response and it's the task of the client to sleep/wait until bucket is empty. Limits will and can change during time, but a safe window will be 1 request per second. Limiting is applied per api token per IP, meaning two users from the same IP will get their rate allocated separately.
|
51
|
+
|
48
52
|
## Documentation
|
49
53
|
|
50
54
|
Run `rdoc` to generate documentation. Open `doc/index.html` in your browser.
|
data/lib/togglv8/time_entries.rb
CHANGED
@@ -73,10 +73,12 @@ module TogglV8
|
|
73
73
|
return formatted_ts.sub('+00:00', 'Z')
|
74
74
|
end
|
75
75
|
|
76
|
-
def get_time_entries(
|
76
|
+
def get_time_entries(dates = {})
|
77
|
+
start_date = dates[:start_date]
|
78
|
+
end_date = dates[:end_date]
|
77
79
|
params = []
|
78
|
-
params.push("start_date=#{iso8601(
|
79
|
-
params.push("end_date=#{iso8601(
|
80
|
+
params.push("start_date=#{iso8601(start_date)}") unless start_date.nil?
|
81
|
+
params.push("end_date=#{iso8601(end_date)}") unless end_date.nil?
|
80
82
|
get "time_entries%s" % [params.empty? ? "" : "?#{params.join('&')}"]
|
81
83
|
end
|
82
84
|
|
data/lib/togglv8/users.rb
CHANGED
@@ -33,7 +33,17 @@ module TogglV8
|
|
33
33
|
|
34
34
|
def my_projects(user=nil)
|
35
35
|
user = me(all=true) if user.nil?
|
36
|
-
user['projects']
|
36
|
+
return {} unless user['projects']
|
37
|
+
projects = user['projects']
|
38
|
+
projects.delete_if { |p| p['server_deleted_at'] }
|
39
|
+
end
|
40
|
+
|
41
|
+
# TODO: Test my_deleted_projects
|
42
|
+
def my_deleted_projects(user=nil)
|
43
|
+
user = me(all=true) if user.nil?
|
44
|
+
return {} unless user['projects']
|
45
|
+
projects = user['projects']
|
46
|
+
projects.keep_if { |p| p['server_deleted_at'] }
|
37
47
|
end
|
38
48
|
|
39
49
|
def my_tags(user=nil)
|
data/lib/togglv8/version.rb
CHANGED
data/lib/togglv8.rb
CHANGED
@@ -20,6 +20,8 @@ Oj.default_options = { mode: :compat }
|
|
20
20
|
|
21
21
|
module TogglV8
|
22
22
|
TOGGL_API_URL = 'https://www.toggl.com/api/'
|
23
|
+
DELAY_SEC = 1
|
24
|
+
MAX_RETRIES = 3
|
23
25
|
|
24
26
|
class API
|
25
27
|
TOGGL_API_V8_URL = TOGGL_API_URL + 'v8/'
|
@@ -66,7 +68,6 @@ module TogglV8
|
|
66
68
|
end
|
67
69
|
end
|
68
70
|
|
69
|
-
|
70
71
|
def requireParams(params, fields=[])
|
71
72
|
raise ArgumentError, 'params is not a Hash' unless params.is_a? Hash
|
72
73
|
return if fields.empty?
|
@@ -77,61 +78,56 @@ module TogglV8
|
|
77
78
|
raise ArgumentError, errors.join(', ') if !errors.empty?
|
78
79
|
end
|
79
80
|
|
81
|
+
def _call_api(procs)
|
82
|
+
@logger.debug(procs[:debug_output].call)
|
83
|
+
full_resp = nil
|
84
|
+
i = 0
|
85
|
+
loop do
|
86
|
+
i += 1
|
87
|
+
full_resp = procs[:api_call].call
|
88
|
+
@logger.ap(full_resp.env, :debug)
|
89
|
+
break if full_resp.status != 429 || i >= MAX_RETRIES
|
90
|
+
sleep(DELAY_SEC)
|
91
|
+
end
|
80
92
|
|
81
|
-
|
82
|
-
@logger.debug("GET #{resource}")
|
83
|
-
full_resp = self.conn.get(resource)
|
84
|
-
# @logger.ap(full_resp.env, :debug)
|
85
|
-
|
86
|
-
raise 'Too many requests in a given amount of time.' if full_resp.status == 429
|
93
|
+
raise "HTTP Status: #{full_resp.status}" unless full_resp.status.between?(200,299)
|
87
94
|
raise Oj.dump(full_resp.env) unless full_resp.success?
|
88
95
|
return {} if full_resp.body.nil? || full_resp.body == 'null'
|
89
96
|
|
90
|
-
|
97
|
+
full_resp
|
98
|
+
end
|
91
99
|
|
100
|
+
def get(resource)
|
101
|
+
full_resp = _call_api(debug_output: lambda { "GET #{resource}" },
|
102
|
+
api_call: lambda { self.conn.get(resource) } )
|
103
|
+
return {} if full_resp == {}
|
104
|
+
resp = Oj.load(full_resp.body)
|
92
105
|
return resp['data'] if resp.respond_to?(:has_key?) && resp.has_key?('data')
|
93
106
|
resp
|
94
107
|
end
|
95
108
|
|
96
109
|
def post(resource, data='')
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
raise 'Too many requests in a given amount of time.' if full_resp.status == 429
|
102
|
-
raise Oj.dump(full_resp.env) unless full_resp.success?
|
103
|
-
return {} if full_resp.body.nil? || full_resp.body == 'null'
|
104
|
-
|
110
|
+
full_resp = _call_api(debug_output: lambda { "POST #{resource} / #{data}" },
|
111
|
+
api_call: lambda { self.conn.post(resource, Oj.dump(data)) } )
|
112
|
+
return {} if full_resp == {}
|
105
113
|
resp = Oj.load(full_resp.body)
|
106
114
|
resp['data']
|
107
115
|
end
|
108
116
|
|
109
117
|
def put(resource, data='')
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
raise 'Too many requests in a given amount of time.' if full_resp.status == 429
|
115
|
-
raise Oj.dump(full_resp.env) unless full_resp.success?
|
116
|
-
return {} if full_resp.body.nil? || full_resp.body == 'null'
|
117
|
-
|
118
|
+
full_resp = _call_api(debug_output: lambda { "PUT #{resource} / #{data}" },
|
119
|
+
api_call: lambda { self.conn.put(resource, Oj.dump(data)) } )
|
120
|
+
return {} if full_resp == {}
|
118
121
|
resp = Oj.load(full_resp.body)
|
119
122
|
resp['data']
|
120
123
|
end
|
121
124
|
|
122
125
|
def delete(resource)
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
raise 'Too many requests in a given amount of time.' if full_resp.status == 429
|
128
|
-
|
129
|
-
raise Oj.dump(full_resp.env) unless full_resp.success?
|
130
|
-
return {} if full_resp.body.nil? || full_resp.body == 'null'
|
131
|
-
|
126
|
+
full_resp = _call_api(debug_output: lambda { "DELETE #{resource}" },
|
127
|
+
api_call: lambda { self.conn.delete(resource) } )
|
128
|
+
return {} if full_resp == {}
|
132
129
|
full_resp.body
|
133
130
|
end
|
134
131
|
|
135
132
|
end
|
136
|
-
|
137
133
|
end
|
@@ -83,6 +83,10 @@ describe 'Time Entries' do
|
|
83
83
|
}
|
84
84
|
@now = DateTime.now
|
85
85
|
|
86
|
+
start = { 'start' => @toggl.iso8601(@now - 9) }
|
87
|
+
@time_entry_nine_days_ago = @toggl.create_time_entry(time_entry_info.merge(start))
|
88
|
+
@nine_days_ago_id = @time_entry_nine_days_ago['id']
|
89
|
+
|
86
90
|
start = { 'start' => @toggl.iso8601(@now - 7) }
|
87
91
|
@time_entry_last_week = @toggl.create_time_entry(time_entry_info.merge(start))
|
88
92
|
@last_week_id = @time_entry_last_week['id']
|
@@ -107,23 +111,23 @@ describe 'Time Entries' do
|
|
107
111
|
expect(ids).to eq [ @last_week_id, @now_id ]
|
108
112
|
end
|
109
113
|
|
110
|
-
it 'gets time entries after
|
111
|
-
ids = @toggl.get_time_entries(
|
114
|
+
it 'gets time entries after start_date (up till now)' do
|
115
|
+
ids = @toggl.get_time_entries({start_date: @now - 1}).map { |t| t['id']}
|
112
116
|
expect(ids).to eq [ @now_id ]
|
113
117
|
end
|
114
118
|
|
115
|
-
it 'gets time entries
|
116
|
-
ids = @toggl.get_time_entries(
|
117
|
-
expect(ids).to
|
119
|
+
it 'gets time entries between 9 days ago and end_date' do
|
120
|
+
ids = @toggl.get_time_entries({end_date: @now + 1}).map { |t| t['id']}
|
121
|
+
expect(ids).to eq [ @last_week_id, @now_id ]
|
118
122
|
end
|
119
123
|
|
120
|
-
it 'gets time entries between
|
121
|
-
ids = @toggl.get_time_entries(
|
124
|
+
it 'gets time entries between start_date and end_date' do
|
125
|
+
ids = @toggl.get_time_entries({start_date: @now - 1, end_date: @now + 1}).map { |t| t['id']}
|
122
126
|
expect(ids).to eq [ @now_id ]
|
123
127
|
end
|
124
128
|
|
125
129
|
it 'gets time entries in the future' do
|
126
|
-
ids = @toggl.get_time_entries(
|
130
|
+
ids = @toggl.get_time_entries({start_date: @now - 1, end_date: @now + 8}).map { |t| t['id']}
|
127
131
|
expect(ids).to eq [ @now_id, @next_week_id ]
|
128
132
|
end
|
129
133
|
end
|
data/spec/lib/togglv8_spec.rb
CHANGED
@@ -1,10 +1,6 @@
|
|
1
1
|
require 'fileutils'
|
2
2
|
|
3
3
|
describe 'TogglV8::API' do
|
4
|
-
before :each do
|
5
|
-
sleep(Testing::DELAY_SEC)
|
6
|
-
end
|
7
|
-
|
8
4
|
it 'initializes with api_token' do
|
9
5
|
toggl = TogglV8::API.new(Testing::API_TOKEN)
|
10
6
|
me = toggl.me
|
@@ -23,7 +19,7 @@ describe 'TogglV8::API' do
|
|
23
19
|
|
24
20
|
it 'does not initialize with bogus api_token' do
|
25
21
|
toggl = TogglV8::API.new('4880nqor1orr9n241sn08070q33oq49s')
|
26
|
-
expect { toggl.me }
|
22
|
+
expect { toggl.me }.to raise_error(RuntimeError, "HTTP Status: 403")
|
27
23
|
end
|
28
24
|
|
29
25
|
context '.toggl file' do
|
@@ -54,6 +50,31 @@ describe 'TogglV8::API' do
|
|
54
50
|
it 'raises error if .toggl file is missing' do
|
55
51
|
expect{ toggl = TogglV8::API.new }.to raise_error(RuntimeError)
|
56
52
|
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'handles errors' do
|
56
|
+
before :all do
|
57
|
+
@toggl = TogglV8::API.new(Testing::API_TOKEN)
|
58
|
+
Response = Struct.new(:env, :status, :success?, :body)
|
59
|
+
end
|
57
60
|
|
61
|
+
it 'surfaces an HTTP Status Code in case of error' do
|
62
|
+
expect(@toggl.conn).to receive(:get).once.and_return(
|
63
|
+
Response.new('', 400, false, nil))
|
64
|
+
expect { @toggl.me }.to raise_error(RuntimeError, "HTTP Status: 400")
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'retries a request up to 3 times if a 429 is received' do
|
68
|
+
expect(@toggl.conn).to receive(:get).exactly(3).times.and_return(
|
69
|
+
Response.new('', 429, false, nil))
|
70
|
+
expect { @toggl.me }.to raise_error(RuntimeError, "HTTP Status: 429")
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'retries a request after 429' do
|
74
|
+
expect(@toggl.conn).to receive(:get).twice.and_return(
|
75
|
+
Response.new('', 429, false, nil),
|
76
|
+
Response.new('', 200, true, nil))
|
77
|
+
expect(@toggl.me).to eq({}) # response is {} in this case because body is nil
|
78
|
+
end
|
58
79
|
end
|
59
80
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -74,12 +74,8 @@ RSpec.configure do |config|
|
|
74
74
|
end
|
75
75
|
|
76
76
|
config.before(:suite) do
|
77
|
-
|
78
|
-
|
79
|
-
end
|
80
|
-
|
81
|
-
config.before(:each) do
|
82
|
-
sleep(Testing::DELAY_SEC)
|
77
|
+
toggl = TogglV8::API.new(Testing::API_TOKEN)
|
78
|
+
TogglV8SpecHelper.setUp(toggl)
|
83
79
|
end
|
84
80
|
|
85
81
|
# The settings below are suggested to provide a good initial experience
|
@@ -136,5 +132,4 @@ class Testing
|
|
136
132
|
API_TOKEN = '4880adbe1bee9a241fa08070d33bd49f'
|
137
133
|
USERNAME = 'togglv8@mailinator.com'
|
138
134
|
PASSWORD = 'togglv8'
|
139
|
-
DELAY_SEC = 1
|
140
135
|
end
|
data/spec/togglv8_spec_helper.rb
CHANGED
@@ -3,68 +3,72 @@ require 'logger'
|
|
3
3
|
|
4
4
|
class TogglV8SpecHelper
|
5
5
|
@logger = Logger.new(STDOUT)
|
6
|
+
@logger.level = Logger::WARN
|
6
7
|
|
7
|
-
def self.setUp()
|
8
|
-
toggl =
|
9
|
-
user = toggl.me(all=true)
|
10
|
-
default_workspace_id = user['default_wid']
|
8
|
+
def self.setUp(toggl)
|
9
|
+
@toggl = toggl
|
10
|
+
user = @toggl.me(all=true)
|
11
|
+
@default_workspace_id = user['default_wid']
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
delete_all_tags(toggl)
|
15
|
-
delete_all_time_entries(toggl)
|
16
|
-
delete_all_workspaces(toggl)
|
13
|
+
delete_all_projects(@toggl)
|
14
|
+
delete_all_clients(@toggl)
|
15
|
+
delete_all_tags(@toggl)
|
16
|
+
delete_all_time_entries(@toggl)
|
17
|
+
delete_all_workspaces(@toggl)
|
17
18
|
end
|
18
19
|
|
19
20
|
def self.delete_all_clients(toggl)
|
20
|
-
clients = toggl.my_clients
|
21
|
+
clients = @toggl.my_clients
|
21
22
|
unless clients.nil?
|
22
23
|
client_ids ||= clients.map { |c| c['id'] }
|
24
|
+
@logger.debug("Deleting #{client_ids.length} clients")
|
23
25
|
client_ids.each do |c_id|
|
24
|
-
toggl.delete_client(c_id)
|
26
|
+
@toggl.delete_client(c_id)
|
25
27
|
end
|
26
28
|
end
|
27
29
|
end
|
28
30
|
|
29
31
|
def self.delete_all_projects(toggl)
|
30
|
-
projects = toggl.
|
32
|
+
projects = @toggl.projects(@default_workspace_id)
|
31
33
|
unless projects.nil?
|
32
34
|
project_ids ||= projects.map { |p| p['id'] }
|
33
|
-
project_ids.
|
34
|
-
|
35
|
-
|
35
|
+
@logger.debug("Deleting #{project_ids.length} projects")
|
36
|
+
return unless project_ids.length > 0
|
37
|
+
@toggl.delete_projects(project_ids)
|
36
38
|
end
|
37
39
|
end
|
38
40
|
|
39
41
|
def self.delete_all_tags(toggl)
|
40
|
-
tags = toggl.my_tags
|
41
|
-
|
42
|
+
tags = @toggl.my_tags
|
43
|
+
unless tags.nil?
|
42
44
|
tag_ids ||= tags.map { |t| t['id'] }
|
45
|
+
@logger.debug("Deleting #{tag_ids.length} tags")
|
43
46
|
tag_ids.each do |t_id|
|
44
|
-
toggl.delete_tag(t_id)
|
47
|
+
@toggl.delete_tag(t_id)
|
45
48
|
end
|
46
49
|
end
|
47
50
|
end
|
48
51
|
|
49
52
|
def self.delete_all_time_entries(toggl)
|
50
|
-
time_entries = toggl.my_time_entries
|
53
|
+
time_entries = @toggl.my_time_entries
|
51
54
|
unless time_entries.nil?
|
52
55
|
time_entry_ids ||= time_entries.map { |t| t['id'] }
|
56
|
+
@logger.debug("Deleting #{time_entry_ids.length} time_entries")
|
53
57
|
time_entry_ids.each do |t_id|
|
54
|
-
toggl.delete_time_entry(t_id)
|
58
|
+
@toggl.delete_time_entry(t_id)
|
55
59
|
end
|
56
60
|
end
|
57
61
|
end
|
58
62
|
|
59
63
|
def self.delete_all_workspaces(toggl)
|
60
|
-
user = toggl.me(all=true)
|
61
|
-
workspaces = toggl.my_workspaces(user)
|
64
|
+
user = @toggl.me(all=true)
|
65
|
+
workspaces = @toggl.my_workspaces(user)
|
62
66
|
unless workspaces.nil?
|
63
67
|
workspace_ids ||= workspaces.map { |w| w['id'] }
|
64
68
|
workspace_ids.delete(user['default_wid'])
|
69
|
+
@logger.debug("Leaving #{workspace_ids.length} workspaces")
|
65
70
|
workspace_ids.each do |w_id|
|
66
|
-
@
|
67
|
-
toggl.leave_workspace(w_id)
|
71
|
+
@toggl.leave_workspace(w_id)
|
68
72
|
end
|
69
73
|
end
|
70
74
|
end
|
data/togglv8.gemspec
CHANGED
@@ -22,6 +22,7 @@ Gem::Specification.new do |spec|
|
|
22
22
|
spec.add_development_dependency "bundler"
|
23
23
|
spec.add_development_dependency "rake"
|
24
24
|
spec.add_development_dependency "rspec"
|
25
|
+
spec.add_development_dependency "rspec-mocks"
|
25
26
|
spec.add_development_dependency "awesome_print"
|
26
27
|
spec.add_development_dependency "fivemat"
|
27
28
|
spec.add_development_dependency "simplecov"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: togglv8
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tom Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-12-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec-mocks
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
70
|
name: awesome_print
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|