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 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