togglv8 0.2.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a10237427eb728d8536f6276c84e0e83a051695c
4
- data.tar.gz: bb81c9c5d1c9a2f1d17b943eefdc1682565da2eb
3
+ metadata.gz: 7b2c12ae09772a1296677907a4e684d3ad5cfde5
4
+ data.tar.gz: 5afd4390ec69c38f3a8795e55b9e38a14f96ada7
5
5
  SHA512:
6
- metadata.gz: 944c4db8f611b45f210a53aba3ad3de9b5ea2c1237d982e4f06fe24637c0e5c6caaacf96ad936d59255d7bc8cb3e03e76a6d66851fbd33a205105f1d9e5cd23b
7
- data.tar.gz: a5b4cdc752d55441db098753d4f578d68f7888118686dad90f1fbf79991fab5ffc80f4998ed0d07df81234176077f1511300cb472ab78e41c1123717e0dedb13
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.
@@ -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(start_timestamp=nil, end_timestamp=nil)
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(start_timestamp)}") if !start_timestamp.nil?
79
- params.push("end_date=#{iso8601(end_timestamp)}") if !end_timestamp.nil?
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)
@@ -1,4 +1,4 @@
1
1
  module TogglV8
2
2
  # :section:
3
- VERSION = "0.2.0"
3
+ VERSION = "1.0.0"
4
4
  end
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
- def get(resource)
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
- resp = Oj.load(full_resp.body)
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
- @logger.debug("POST #{resource} / #{data}")
98
- full_resp = self.conn.post(resource, Oj.dump(data))
99
- # @logger.ap(full_resp.env, :debug)
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
- @logger.debug("PUT #{resource} / #{data}")
111
- full_resp = self.conn.put(resource, Oj.dump(data))
112
- # @logger.ap(full_resp.env, :debug)
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
- @logger.debug("DELETE #{resource}")
124
- full_resp = self.conn.delete(resource)
125
- # @logger.ap(full_resp.env, :debug)
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 start timestamp (up till now)' do
111
- ids = @toggl.get_time_entries(start_timestamp = @now - 1).map { |t| t['id']}
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 before end timestamp' do
116
- ids = @toggl.get_time_entries(end_timestamp = @now + 1).map { |t| t['id']}
117
- expect(ids).to be_empty
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 start and end timestamps' do
121
- ids = @toggl.get_time_entries(start_timestamp = @now - 1, end_timestamp = @now + 1).map { |t| t['id']}
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(start_timestamp = @now - 1, end_timestamp = @now + 8).map { |t| t['id']}
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
@@ -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 } .to raise_error(RuntimeError)
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
- TogglV8SpecHelper.setUp()
78
- puts "NOTE: Delaying #{Testing::DELAY_SEC} second(s) between specs to avoid failure due to '429 Too Many Requests'"
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
@@ -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 = TogglV8::API.new(Testing::API_TOKEN)
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
- delete_all_clients(toggl)
13
- delete_all_projects(toggl)
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.my_projects
32
+ projects = @toggl.projects(@default_workspace_id)
31
33
  unless projects.nil?
32
34
  project_ids ||= projects.map { |p| p['id'] }
33
- project_ids.each do |p_id|
34
- toggl.delete_project(p_id)
35
- end
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
- unless tags.nil?
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
- @logger.debug(w_id)
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.2.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-08-22 00:00:00.000000000 Z
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