togglv8 1.1.0 → 1.2.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/CHANGELOG.md +92 -0
- data/README.md +69 -6
- data/lib/logging.rb +36 -0
- data/lib/reportsv2.rb +148 -0
- data/lib/togglv8.rb +4 -139
- data/lib/togglv8/connection.rb +118 -0
- data/lib/togglv8/togglv8.rb +40 -0
- data/lib/togglv8/version.rb +1 -1
- data/spec/lib/reportsv2_spec.rb +198 -0
- data/spec/lib/togglv8_spec.rb +5 -6
- data/spec/spec_helper.rb +18 -2
- data/spec/togglv8_spec_helper.rb +6 -8
- metadata +9 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4c535170f2f4008596f53dd97645d5946d62740e
|
|
4
|
+
data.tar.gz: f007be73994ef07272e1100e4f2046dc22d69e3a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 082469da76c56138a7bee169c205874a150384959b516b65633e4672288b0962fbfbccb1909ec2ad6a176bf076516d898b6a9fe3bc3bad52ac8048c2240a8d9a
|
|
7
|
+
data.tar.gz: 64463ae5e379fee5e870be1c84e4a76d0b30138634f002371cd0fd08898cf6aa9bc0530287a540ffe1a4380dcc0c51214810c9acf614aa0802d1aa43f976cbbb
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Change Log
|
|
2
|
+
|
|
3
|
+
Notable changes are documented here following conventions outlined at [Keep a CHANGELOG](http://keepachangelog.com/).
|
|
4
|
+
|
|
5
|
+
Changes that are not intended to affect usage (e.g. documentation, specs, removal of dead code, etc.) are generally not documented here.
|
|
6
|
+
|
|
7
|
+
Version numbers are meant to adhere to [Semantic Versioning](http://semver.org/).
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## [1.2.0] - 2016-07-24
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
* Add support for [Toggl Reports API v2](https://github.com/toggl/toggl_api_docs/blob/master/reports.md).
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## [1.1.0] - 2016-02-22
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
* Add `tags(workspace_id)`.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
## [1.0.5] - 2016-02-22
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
* Add specs for encoding of ISO8601 times with + UTC offset. (See [1.0.4](#104---2016-01-22))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
## [1.0.4] - 2016-01-22
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
* Manually encode `+` to `%2B` before every API call. (Fixes #11)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## [1.0.3] - 2016-01-22
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
* Add `debug()` method to enable debugging output including full API response.
|
|
41
|
+
|
|
42
|
+
## [1.0.2] - 2015-12-12
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
* Require params 'tags' and 'tag_action' in `update_time_entries_tags()`.
|
|
46
|
+
|
|
47
|
+
## [1.0.1] - 2015-12-10
|
|
48
|
+
### Fixed
|
|
49
|
+
|
|
50
|
+
* Fix Toggl API call in `get_project_tasks()`. (Fixes #5)
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
|
|
54
|
+
* Add `my_tasks()`.
|
|
55
|
+
* Add null checks to various methods.
|
|
56
|
+
|
|
57
|
+
### Changed
|
|
58
|
+
|
|
59
|
+
* Require params 'name' and 'pid' in `create_task()`.
|
|
60
|
+
|
|
61
|
+
## [1.0.0] - 2015-12-06
|
|
62
|
+
### Added
|
|
63
|
+
|
|
64
|
+
* Add `my_deleted_projects()`.
|
|
65
|
+
|
|
66
|
+
### Changed
|
|
67
|
+
|
|
68
|
+
* Exclude deleted projects from `my_projects()` results.
|
|
69
|
+
* Change `get_time_entries()` parameters.
|
|
70
|
+
- old: `start_timestamp=nil, end_timestamp=nil`
|
|
71
|
+
- new: `dates = {}`
|
|
72
|
+
* Raise RuntimeError w/ HTTP Status code if request is not successful.
|
|
73
|
+
* Handle 429 (Too Many Requests) by pausing for 1 second and retrying up to 3 times.
|
|
74
|
+
- API calls are limited to 1/sec due to toggl.com limits
|
|
75
|
+
* Refactor duplication out of GET/POST/PUT/DELETE API calls.
|
|
76
|
+
|
|
77
|
+
## [0.2.0] - 2015-08-21
|
|
78
|
+
### Added
|
|
79
|
+
|
|
80
|
+
* Add Ruby interface to most functions of [Toggl V8 API](https://github.com/toggl/toggl_api_docs/blob/master/toggl_api.md) (as of 2015-08-21).
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
[Unreleased]: https://github.com/kanet77/togglv8/compare/v1.2.0...HEAD
|
|
84
|
+
[1.2.0]: https://github.com/kanet77/togglv8/compare/v1.1.0...v1.2.0
|
|
85
|
+
[1.1.0]: https://github.com/kanet77/togglv8/compare/v1.0.5...v1.1.0
|
|
86
|
+
[1.0.5]: https://github.com/kanet77/togglv8/compare/v1.0.4...v1.0.5
|
|
87
|
+
[1.0.4]: https://github.com/kanet77/togglv8/compare/v1.0.3...v1.0.4
|
|
88
|
+
[1.0.3]: https://github.com/kanet77/togglv8/compare/v1.0.2...v1.0.3
|
|
89
|
+
[1.0.2]: https://github.com/kanet77/togglv8/compare/v1.0.1...v1.0.2
|
|
90
|
+
[1.0.1]: https://github.com/kanet77/togglv8/compare/v1.0.0...v1.0.1
|
|
91
|
+
[1.0.0]: https://github.com/kanet77/togglv8/compare/v0.2.0...v1.0.0
|
|
92
|
+
[0.2.0]: https://github.com/kanet77/togglv8/compare/a1d5cc5...v0.2.0
|
data/README.md
CHANGED
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
|
|
8
8
|
[togglv8](/) is a Ruby Wrapper for [Toggl API v8](https://github.com/toggl/toggl_api_docs). It is designed to mirror the Toggl API as closely as possible.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
togglv8 supports both [Toggl API](https://github.com/toggl/toggl_api_docs/blob/master/toggl_api.md) and [Reports API](https://github.com/toggl/toggl_api_docs/blob/master/reports.md)
|
|
11
|
+
|
|
12
|
+
## Change Log
|
|
13
|
+
|
|
14
|
+
See [CHANGELOG](CHANGELOG.md) for a summary of notable changes in each version.
|
|
11
15
|
|
|
12
16
|
## Installation
|
|
13
17
|
|
|
@@ -25,24 +29,77 @@ Or install it yourself as:
|
|
|
25
29
|
|
|
26
30
|
$ gem install togglv8
|
|
27
31
|
|
|
32
|
+
## Initialization
|
|
33
|
+
|
|
34
|
+
### TogglV8::API
|
|
35
|
+
|
|
36
|
+
TogglV8::API communicates with [Toggl API v8](https://github.com/toggl/toggl_api_docs/blob/master/toggl_api.md) and can be initialized in one of three ways.
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
TogglV8::API.new # reads API token from file ~/.toggl
|
|
40
|
+
TogglV8::API.new(api_token) # explicit API token
|
|
41
|
+
TogglV8::API.new(username, password) # username & password
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### TogglV8::ReportsV2
|
|
45
|
+
|
|
46
|
+
TogglV8::ReportsV2 communicates with [Toggl Reports API v2](https://github.com/toggl/toggl_api_docs/blob/master/reports.md) and can be initialized in one of three ways. Toggl.com requires authentication with an API token for Reports API v2.
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
TogglV8::ReportsV2.new # reads API token from file ~/.toggl
|
|
50
|
+
TogglV8::ReportsV2.new(toggl_api_file: toggl_file) # reads API token from toggl_file
|
|
51
|
+
TogglV8::ReportsV2.new(api_token: api_token) # explicit API token
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Note:** `workspace_id` must be set in order to generate reports.
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
toggl = TogglV8::API.new
|
|
58
|
+
reports = TogglV8::ReportsV2.new
|
|
59
|
+
reports.workspace_id = toggl.workspaces.first['id']
|
|
60
|
+
```
|
|
61
|
+
|
|
28
62
|
## Usage
|
|
29
63
|
|
|
30
|
-
This short example shows one way to create a time entry for the first workspace of the user identified by `<API_TOKEN
|
|
64
|
+
This short example shows one way to create a time entry for the first workspace of the user identified by `<API_TOKEN>`. It then generates various reports containing that time entry.
|
|
31
65
|
|
|
32
66
|
```ruby
|
|
33
67
|
require 'togglv8'
|
|
68
|
+
require 'json'
|
|
34
69
|
|
|
35
70
|
toggl_api = TogglV8::API.new(<API_TOKEN>)
|
|
36
71
|
user = toggl_api.me(all=true)
|
|
37
72
|
workspaces = toggl_api.my_workspaces(user)
|
|
38
73
|
workspace_id = workspaces.first['id']
|
|
39
|
-
toggl_api.create_time_entry({
|
|
40
|
-
'description' => "
|
|
74
|
+
time_entry = toggl_api.create_time_entry({
|
|
75
|
+
'description' => "My awesome workspace time entry",
|
|
41
76
|
'wid' => workspace_id,
|
|
42
77
|
'duration' => 1200,
|
|
43
|
-
'start' =>
|
|
78
|
+
'start' => toggl_api.iso8601((Time.now - 3600).to_datetime),
|
|
44
79
|
'created_with' => "My awesome Ruby application"
|
|
45
80
|
})
|
|
81
|
+
|
|
82
|
+
begin
|
|
83
|
+
reports = TogglV8::ReportsV2.new(api_token: <API_TOKEN>)
|
|
84
|
+
begin
|
|
85
|
+
reports.summary
|
|
86
|
+
rescue Exception => e
|
|
87
|
+
puts e.message # workspace_id is required
|
|
88
|
+
end
|
|
89
|
+
reports.workspace_id = workspace_id
|
|
90
|
+
summary = reports.summary
|
|
91
|
+
puts "Generating summary JSON..."
|
|
92
|
+
puts JSON.pretty_generate(summary)
|
|
93
|
+
puts "Generating summary PDF..."
|
|
94
|
+
reports.write_summary('toggl_summary.pdf')
|
|
95
|
+
puts "Generating weekly CSV..."
|
|
96
|
+
reports.write_weekly('toggl_weekly.csv')
|
|
97
|
+
puts "Generating details XLS..."
|
|
98
|
+
reports.write_details('toggl_details.xls')
|
|
99
|
+
# Note: toggl.com does not generate Weekly XLS report (as of 2016-07-24)
|
|
100
|
+
ensure
|
|
101
|
+
toggl_api.delete_time_entry(time_entry['id'])
|
|
102
|
+
end
|
|
46
103
|
```
|
|
47
104
|
|
|
48
105
|
See specs for more examples.
|
|
@@ -53,7 +110,7 @@ See specs for more examples.
|
|
|
53
110
|
|
|
54
111
|
## Debugging
|
|
55
112
|
|
|
56
|
-
The `TogglV8::API#debug` method determines if debug output is printed to STDOUT.
|
|
113
|
+
The `TogglV8::API#debug` method determines if debug output is printed to STDOUT. This code snippet demonstrates the debug output.
|
|
57
114
|
|
|
58
115
|
```ruby
|
|
59
116
|
require 'togglv8'
|
|
@@ -79,6 +136,12 @@ Also available on [DocumentUp](https://documentup.com/kanet77/togglv8)
|
|
|
79
136
|
|
|
80
137
|
## Acknowledgements
|
|
81
138
|
|
|
139
|
+
- Thanks to the following contributors (in alphabetical order):
|
|
140
|
+
* [archonic](https://github.com/archonic) ([fork](https://github.com/archonic/togglv8))
|
|
141
|
+
* [ddiatmb](https://github.com/ddiatmb) ([fork](https://github.com/ddiatmb/togglv8))
|
|
142
|
+
* [itaymendel](https://github.com/itaymendel)
|
|
143
|
+
* [ppawlikmb](https://github.com/ppawlikmb) ([fork](https://github.com/ppawlikmb/togglv8))
|
|
144
|
+
* [worldsmithroy](https://github.com/worldsmithroy) ([fork](https://github.com/worldsmithroy/togglv8))
|
|
82
145
|
- Thanks to [Koen Van der Auwera](https://github.com/atog) for the [Ruby Wrapper for Toggl API v6](https://github.com/atog/toggl)
|
|
83
146
|
- Thanks to the Toggl team for exposing the API.
|
|
84
147
|
|
data/lib/logging.rb
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require 'logger'
|
|
2
|
+
require 'awesome_print' # for debug output
|
|
3
|
+
|
|
4
|
+
# From http://stackoverflow.com/questions/917566/ruby-share-logger-instance-among-module-classes
|
|
5
|
+
module Logging
|
|
6
|
+
class << self
|
|
7
|
+
def logger
|
|
8
|
+
@logger ||= Logger.new($stdout)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def logger=(logger)
|
|
12
|
+
@logger = logger
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Addition
|
|
17
|
+
def self.included(base)
|
|
18
|
+
class << base
|
|
19
|
+
def logger
|
|
20
|
+
Logging.logger
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def logger
|
|
26
|
+
Logging.logger
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def debug(debug=true)
|
|
30
|
+
if debug
|
|
31
|
+
logger.level = Logger::DEBUG
|
|
32
|
+
else
|
|
33
|
+
logger.level = Logger::WARN
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/reportsv2.rb
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
module TogglV8
|
|
2
|
+
TOGGL_REPORTS_URL = 'https://toggl.com/reports/api/'
|
|
3
|
+
|
|
4
|
+
class ReportsV2
|
|
5
|
+
include TogglV8::Connection
|
|
6
|
+
|
|
7
|
+
REPORTS_V2_URL = TOGGL_REPORTS_URL + 'v2/'
|
|
8
|
+
|
|
9
|
+
attr_reader :conn
|
|
10
|
+
|
|
11
|
+
attr_accessor :workspace_id
|
|
12
|
+
|
|
13
|
+
def initialize(opts={})
|
|
14
|
+
debug(false)
|
|
15
|
+
|
|
16
|
+
@user_agent = 'togglv8'
|
|
17
|
+
|
|
18
|
+
username = opts[:api_token]
|
|
19
|
+
if username.nil?
|
|
20
|
+
toggl_api_file = opts[:toggl_api_file] || File.join(Dir.home, TOGGL_FILE)
|
|
21
|
+
if FileTest.exist?(toggl_api_file) then
|
|
22
|
+
username = IO.read(toggl_api_file)
|
|
23
|
+
else
|
|
24
|
+
raise "Expecting\n" +
|
|
25
|
+
" 1) api_token in file #{toggl_api_file}, or\n" +
|
|
26
|
+
" 2) parameter: (api_token), or\n" +
|
|
27
|
+
"\n\tSee https://github.com/toggl/toggl_api_docs/blob/master/reports.md#authentication"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
@conn = TogglV8::Connection.open(username, API_TOKEN, REPORTS_V2_URL, opts)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
##
|
|
35
|
+
# ---------
|
|
36
|
+
# :section: Reports
|
|
37
|
+
#
|
|
38
|
+
# The following parameters and filters can be used in all of the reports
|
|
39
|
+
#
|
|
40
|
+
# user_agent : the name of this application so Toggl can get in touch
|
|
41
|
+
# (string, *required*)
|
|
42
|
+
# workspace_id : The workspace whose data you want to access.
|
|
43
|
+
# (integer, *required*)
|
|
44
|
+
# since : ISO 8601 date (YYYY-MM-DD), by default until - 6 days.
|
|
45
|
+
# (string)
|
|
46
|
+
# until : ISO 8601 date (YYYY-MM-DD), by default today
|
|
47
|
+
# (string)
|
|
48
|
+
# billable : possible values: yes/no/both, default both
|
|
49
|
+
# client_ids : client ids separated by a comma, 0 if you want to filter out time entries without a client
|
|
50
|
+
# project_ids : project ids separated by a comma, 0 if you want to filter out time entries without a project
|
|
51
|
+
# user_ids : user ids separated by a comma
|
|
52
|
+
# members_of_group_ids : group ids separated by a comma. This limits provided user_ids to the provided group members
|
|
53
|
+
# or_members_of_group_ids : group ids separated by a comma. This extends provided user_ids with the provided group members
|
|
54
|
+
# tag_ids : tag ids separated by a comma, 0 if you want to filter out time entries without a tag
|
|
55
|
+
# task_ids : task ids separated by a comma, 0 if you want to filter out time entries without a task
|
|
56
|
+
# time_entry_ids : time entry ids separated by a comma
|
|
57
|
+
# description : time entry description
|
|
58
|
+
# (string)
|
|
59
|
+
# without_description : filters out the time entries which do not have a description ('(no description)')
|
|
60
|
+
# (true/false)
|
|
61
|
+
# order_field : date/description/duration/user in detailed reports
|
|
62
|
+
# title/duration/amount in summary reports
|
|
63
|
+
# title/day1/day2/day3/day4/day5/day6/day7/week_total in weekly report
|
|
64
|
+
# order_desc : on for descending and off for ascending order
|
|
65
|
+
# (on/off)
|
|
66
|
+
# distinct_rates : on/off, default off
|
|
67
|
+
# rounding : on/off, default off, rounds time according to workspace settings
|
|
68
|
+
# display_hours : decimal/minutes, display hours with minutes or as a decimal number, default minutes
|
|
69
|
+
#
|
|
70
|
+
# NB! Maximum date span (until - since) is one year.
|
|
71
|
+
|
|
72
|
+
# extension can be one of ['.pdf', '.csv', '.xls']. Possibly others?
|
|
73
|
+
def report(type, extension, opts)
|
|
74
|
+
raise "workspace_id is required" if @workspace_id.nil?
|
|
75
|
+
get "#{type}#{extension}", {
|
|
76
|
+
'user_agent': @user_agent,
|
|
77
|
+
'workspace_id': @workspace_id,
|
|
78
|
+
}.merge(opts)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def weekly(extension='', opts={})
|
|
82
|
+
report('weekly', extension, opts)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def details(extension='', opts={})
|
|
86
|
+
report('details', extension, opts)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def summary(extension='', opts={})
|
|
90
|
+
report('summary', extension, opts)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def write_report(filename)
|
|
94
|
+
extension = File.extname(filename)
|
|
95
|
+
report = yield(extension)
|
|
96
|
+
File.open(filename, "wb") do |file|
|
|
97
|
+
file.write(report)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def write_weekly(filename, opts={})
|
|
102
|
+
write_report(filename) do |extension|
|
|
103
|
+
weekly(extension, opts)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def write_details(filename, opts={})
|
|
108
|
+
write_report(filename) do |extension|
|
|
109
|
+
details(extension, opts)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def write_summary(filename, opts={})
|
|
114
|
+
write_report(filename) do |extension|
|
|
115
|
+
summary(extension, opts)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def revision
|
|
120
|
+
get "revision"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def index
|
|
124
|
+
get "index"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def project(opts={})
|
|
128
|
+
raise "workspace_id is required" if @workspace_id.nil?
|
|
129
|
+
get "project", {
|
|
130
|
+
'user_agent': @user_agent,
|
|
131
|
+
'workspace_id': @workspace_id,
|
|
132
|
+
}.merge(opts)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def env
|
|
136
|
+
get "env"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def error400
|
|
140
|
+
get "error400"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def error500
|
|
144
|
+
get "error500"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
end
|
|
148
|
+
end
|
data/lib/togglv8.rb
CHANGED
|
@@ -1,144 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
require 'oj'
|
|
1
|
+
require_relative 'togglv8/version'
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
require 'awesome_print' # for debug output
|
|
3
|
+
require_relative 'togglv8/connection'
|
|
6
4
|
|
|
7
|
-
require_relative 'togglv8/
|
|
8
|
-
require_relative '
|
|
9
|
-
require_relative 'togglv8/project_users'
|
|
10
|
-
require_relative 'togglv8/projects'
|
|
11
|
-
require_relative 'togglv8/tags'
|
|
12
|
-
require_relative 'togglv8/tasks'
|
|
13
|
-
require_relative 'togglv8/time_entries'
|
|
14
|
-
require_relative 'togglv8/users'
|
|
15
|
-
require_relative 'togglv8/version'
|
|
16
|
-
require_relative 'togglv8/workspaces'
|
|
5
|
+
require_relative 'togglv8/togglv8'
|
|
6
|
+
require_relative 'reportsv2'
|
|
17
7
|
|
|
18
8
|
# :mode => :compat will convert symbols to strings
|
|
19
9
|
Oj.default_options = { :mode => :compat }
|
|
20
|
-
|
|
21
|
-
module TogglV8
|
|
22
|
-
TOGGL_API_URL = 'https://www.toggl.com/api/'
|
|
23
|
-
DELAY_SEC = 1
|
|
24
|
-
MAX_RETRIES = 3
|
|
25
|
-
|
|
26
|
-
class API
|
|
27
|
-
TOGGL_API_V8_URL = TOGGL_API_URL + 'v8/'
|
|
28
|
-
API_TOKEN = 'api_token'
|
|
29
|
-
TOGGL_FILE = '.toggl'
|
|
30
|
-
|
|
31
|
-
attr_reader :conn
|
|
32
|
-
|
|
33
|
-
def initialize(username=nil, password=API_TOKEN, opts={})
|
|
34
|
-
@logger = Logger.new(STDOUT)
|
|
35
|
-
@logger.level = Logger::WARN
|
|
36
|
-
|
|
37
|
-
if username.nil? && password == API_TOKEN
|
|
38
|
-
toggl_api_file = File.join(Dir.home, TOGGL_FILE)
|
|
39
|
-
if FileTest.exist?(toggl_api_file) then
|
|
40
|
-
username = IO.read(toggl_api_file)
|
|
41
|
-
else
|
|
42
|
-
raise "Expecting\n" +
|
|
43
|
-
" 1) api_token in file #{toggl_api_file}, or\n" +
|
|
44
|
-
" 2) parameter: (api_token), or\n" +
|
|
45
|
-
" 3) parameters: (username, password).\n" +
|
|
46
|
-
"\n\tSee https://github.com/toggl/toggl_api_docs/blob/master/chapters/authentication.md"
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
@conn = TogglV8::API.connection(username, password, opts)
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def debug(debug=true)
|
|
54
|
-
if debug
|
|
55
|
-
@logger.level = Logger::DEBUG
|
|
56
|
-
else
|
|
57
|
-
@logger.level = Logger::WARN
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
#---------#
|
|
62
|
-
# Private #
|
|
63
|
-
#---------#
|
|
64
|
-
|
|
65
|
-
private
|
|
66
|
-
|
|
67
|
-
attr_writer :conn
|
|
68
|
-
|
|
69
|
-
def self.connection(username, password, opts={})
|
|
70
|
-
Faraday.new(:url => TOGGL_API_V8_URL, :ssl => {:verify => true}) do |faraday|
|
|
71
|
-
faraday.request :url_encoded
|
|
72
|
-
faraday.response :logger, Logger.new('faraday.log') if opts[:log]
|
|
73
|
-
faraday.adapter Faraday.default_adapter
|
|
74
|
-
faraday.headers = { "Content-Type" => "application/json" }
|
|
75
|
-
faraday.basic_auth username, password
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def requireParams(params, fields=[])
|
|
80
|
-
raise ArgumentError, 'params is not a Hash' unless params.is_a? Hash
|
|
81
|
-
return if fields.empty?
|
|
82
|
-
errors = []
|
|
83
|
-
for f in fields
|
|
84
|
-
errors.push("params[#{f}] is required") unless params.has_key?(f)
|
|
85
|
-
end
|
|
86
|
-
raise ArgumentError, errors.join(', ') if !errors.empty?
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def _call_api(procs)
|
|
90
|
-
@logger.debug(procs[:debug_output].call)
|
|
91
|
-
full_resp = nil
|
|
92
|
-
i = 0
|
|
93
|
-
loop do
|
|
94
|
-
i += 1
|
|
95
|
-
full_resp = procs[:api_call].call
|
|
96
|
-
@logger.ap(full_resp.env, :debug)
|
|
97
|
-
break if full_resp.status != 429 || i >= MAX_RETRIES
|
|
98
|
-
sleep(DELAY_SEC)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
raise "HTTP Status: #{full_resp.status}" unless full_resp.success?
|
|
102
|
-
return {} if full_resp.body.nil? || full_resp.body == 'null'
|
|
103
|
-
|
|
104
|
-
full_resp
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def get(resource)
|
|
108
|
-
resource.gsub!('+', '%2B')
|
|
109
|
-
full_resp = _call_api(debug_output: lambda { "GET #{resource}" },
|
|
110
|
-
api_call: lambda { self.conn.get(resource) } )
|
|
111
|
-
return {} if full_resp == {}
|
|
112
|
-
resp = Oj.load(full_resp.body)
|
|
113
|
-
return resp['data'] if resp.respond_to?(:has_key?) && resp.has_key?('data')
|
|
114
|
-
resp
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def post(resource, data='')
|
|
118
|
-
resource.gsub!('+', '%2B')
|
|
119
|
-
full_resp = _call_api(debug_output: lambda { "POST #{resource} / #{data}" },
|
|
120
|
-
api_call: lambda { self.conn.post(resource, Oj.dump(data)) } )
|
|
121
|
-
return {} if full_resp == {}
|
|
122
|
-
resp = Oj.load(full_resp.body)
|
|
123
|
-
resp['data']
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def put(resource, data='')
|
|
127
|
-
resource.gsub!('+', '%2B')
|
|
128
|
-
full_resp = _call_api(debug_output: lambda { "PUT #{resource} / #{data}" },
|
|
129
|
-
api_call: lambda { self.conn.put(resource, Oj.dump(data)) } )
|
|
130
|
-
return {} if full_resp == {}
|
|
131
|
-
resp = Oj.load(full_resp.body)
|
|
132
|
-
resp['data']
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def delete(resource)
|
|
136
|
-
resource.gsub!('+', '%2B')
|
|
137
|
-
full_resp = _call_api(debug_output: lambda { "DELETE #{resource}" },
|
|
138
|
-
api_call: lambda { self.conn.delete(resource) } )
|
|
139
|
-
return {} if full_resp == {}
|
|
140
|
-
full_resp.body
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
end
|
|
144
|
-
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
require 'faraday'
|
|
2
|
+
require 'oj'
|
|
3
|
+
|
|
4
|
+
require_relative '../logging'
|
|
5
|
+
|
|
6
|
+
module TogglV8
|
|
7
|
+
module Connection
|
|
8
|
+
include Logging
|
|
9
|
+
|
|
10
|
+
DELAY_SEC = 1
|
|
11
|
+
MAX_RETRIES = 3
|
|
12
|
+
|
|
13
|
+
API_TOKEN = 'api_token'
|
|
14
|
+
TOGGL_FILE = '.toggl'
|
|
15
|
+
|
|
16
|
+
def self.qualify(username, password)
|
|
17
|
+
if username.nil? && password == API_TOKEN
|
|
18
|
+
toggl_api_file = File.join(Dir.home, TOGGL_FILE)
|
|
19
|
+
|
|
20
|
+
# logger.debug("toggl_api_file = #{toggl_api_file}")
|
|
21
|
+
if FileTest.exist?(toggl_api_file) then
|
|
22
|
+
username = IO.read(toggl_api_file)
|
|
23
|
+
else
|
|
24
|
+
raise "Expecting\n" +
|
|
25
|
+
" 1) api_token in file #{toggl_api_file}, or\n" +
|
|
26
|
+
" 2) parameter: (api_token), or\n" +
|
|
27
|
+
" 3) parameters: (username, password).\n" +
|
|
28
|
+
"\n\tSee https://github.com/toggl/toggl_api_docs/blob/master/chapters/authentication.md"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
return username, password
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.open(username=nil, password=API_TOKEN, url=nil, opts={})
|
|
35
|
+
username, password = qualify(username, password)
|
|
36
|
+
raise 'Missing URL' if url.nil?
|
|
37
|
+
|
|
38
|
+
Faraday.new(:url => url, :ssl => {:verify => true}) do |faraday|
|
|
39
|
+
faraday.request :url_encoded
|
|
40
|
+
faraday.response :logger, Logger.new('faraday.log') if opts[:log]
|
|
41
|
+
faraday.adapter Faraday.default_adapter
|
|
42
|
+
faraday.headers = { "Content-Type" => "application/json" }
|
|
43
|
+
faraday.basic_auth username, password
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def requireParams(params, fields=[])
|
|
48
|
+
raise ArgumentError, 'params is not a Hash' unless params.is_a? Hash
|
|
49
|
+
return if fields.empty?
|
|
50
|
+
errors = []
|
|
51
|
+
for f in fields
|
|
52
|
+
errors.push("params[#{f}] is required") unless params.has_key?(f)
|
|
53
|
+
end
|
|
54
|
+
raise ArgumentError, errors.join(', ') if !errors.empty?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def _call_api(procs)
|
|
58
|
+
# logger.debug(procs[:debug_output].call)
|
|
59
|
+
full_resp = nil
|
|
60
|
+
i = 0
|
|
61
|
+
loop do
|
|
62
|
+
i += 1
|
|
63
|
+
full_resp = procs[:api_call].call
|
|
64
|
+
logger.ap(full_resp.env, :debug)
|
|
65
|
+
break if full_resp.status != 429 || i >= MAX_RETRIES
|
|
66
|
+
sleep(DELAY_SEC)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
raise full_resp.headers['warning'] if full_resp.headers['warning']
|
|
70
|
+
raise "HTTP Status: #{full_resp.status}" unless full_resp.success?
|
|
71
|
+
return {} if full_resp.body.nil? || full_resp.body == 'null'
|
|
72
|
+
|
|
73
|
+
full_resp
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def get(resource, params={})
|
|
77
|
+
query_params = params.map { |k,v| "#{k}=#{v}" }.join('&')
|
|
78
|
+
resource += "?#{query_params}" unless query_params.empty?
|
|
79
|
+
resource.gsub!('+', '%2B')
|
|
80
|
+
full_resp = _call_api(debug_output: lambda { "GET #{resource}" },
|
|
81
|
+
api_call: lambda { self.conn.get(resource) } )
|
|
82
|
+
return {} if full_resp == {}
|
|
83
|
+
begin
|
|
84
|
+
resp = Oj.load(full_resp.body)
|
|
85
|
+
return resp['data'] if resp.respond_to?(:has_key?) && resp.has_key?('data')
|
|
86
|
+
return resp
|
|
87
|
+
rescue Oj::ParseError
|
|
88
|
+
return full_resp.body
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def post(resource, data='')
|
|
93
|
+
resource.gsub!('+', '%2B')
|
|
94
|
+
full_resp = _call_api(debug_output: lambda { "POST #{resource} / #{data}" },
|
|
95
|
+
api_call: lambda { self.conn.post(resource, Oj.dump(data)) } )
|
|
96
|
+
return {} if full_resp == {}
|
|
97
|
+
resp = Oj.load(full_resp.body)
|
|
98
|
+
resp['data']
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def put(resource, data='')
|
|
102
|
+
resource.gsub!('+', '%2B')
|
|
103
|
+
full_resp = _call_api(debug_output: lambda { "PUT #{resource} / #{data}" },
|
|
104
|
+
api_call: lambda { self.conn.put(resource, Oj.dump(data)) } )
|
|
105
|
+
return {} if full_resp == {}
|
|
106
|
+
resp = Oj.load(full_resp.body)
|
|
107
|
+
resp['data']
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def delete(resource)
|
|
111
|
+
resource.gsub!('+', '%2B')
|
|
112
|
+
full_resp = _call_api(debug_output: lambda { "DELETE #{resource}" },
|
|
113
|
+
api_call: lambda { self.conn.delete(resource) } )
|
|
114
|
+
return {} if full_resp == {}
|
|
115
|
+
full_resp.body
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require_relative 'clients'
|
|
2
|
+
require_relative 'dashboard'
|
|
3
|
+
require_relative 'project_users'
|
|
4
|
+
require_relative 'projects'
|
|
5
|
+
require_relative 'tags'
|
|
6
|
+
require_relative 'tasks'
|
|
7
|
+
require_relative 'time_entries'
|
|
8
|
+
require_relative 'users'
|
|
9
|
+
require_relative 'version'
|
|
10
|
+
require_relative 'workspaces'
|
|
11
|
+
|
|
12
|
+
module TogglV8
|
|
13
|
+
TOGGL_API_URL = 'https://www.toggl.com/api/'
|
|
14
|
+
|
|
15
|
+
class API
|
|
16
|
+
include TogglV8::Connection
|
|
17
|
+
|
|
18
|
+
TOGGL_API_V8_URL = TOGGL_API_URL + 'v8/'
|
|
19
|
+
|
|
20
|
+
attr_reader :conn
|
|
21
|
+
|
|
22
|
+
def initialize(username=nil, password=API_TOKEN, opts={})
|
|
23
|
+
debug(false)
|
|
24
|
+
@conn = TogglV8::Connection.open(username, password,
|
|
25
|
+
TOGGL_API_V8_URL, opts)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
data/lib/togglv8/version.rb
CHANGED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
require 'fileutils'
|
|
2
|
+
|
|
3
|
+
describe 'ReportsV2' do
|
|
4
|
+
it 'initializes with api_token' do
|
|
5
|
+
reports = TogglV8::ReportsV2.new(api_token: Testing::API_TOKEN)
|
|
6
|
+
env = reports.env
|
|
7
|
+
expect(env).to_not be nil
|
|
8
|
+
expect(env['user']['api_token']).to eq Testing::API_TOKEN
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'does not initialize with bogus api_token' do
|
|
12
|
+
reports = TogglV8::ReportsV2.new(api_token: '4880nqor1orr9n241sn08070q33oq49s')
|
|
13
|
+
expect { reports.env }.to raise_error(RuntimeError, "HTTP Status: 401")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
context '.toggl file' do
|
|
17
|
+
before :each do
|
|
18
|
+
@home = File.join(Dir.pwd, "tmp")
|
|
19
|
+
Dir.mkdir(@home)
|
|
20
|
+
|
|
21
|
+
@original_home = Dir.home
|
|
22
|
+
ENV['HOME'] = @home
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
after :each do
|
|
26
|
+
FileUtils.rm_rf(@home)
|
|
27
|
+
ENV['HOME'] = @original_home
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'initializes with .toggl file' do
|
|
31
|
+
toggl_file = File.join(@home, '.toggl')
|
|
32
|
+
File.open(toggl_file, 'w') { |file| file.write(Testing::API_TOKEN) }
|
|
33
|
+
|
|
34
|
+
reports = TogglV8::ReportsV2.new
|
|
35
|
+
env = reports.env
|
|
36
|
+
expect(env).to_not be nil
|
|
37
|
+
expect(env['user']['api_token']).to eq Testing::API_TOKEN
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'initializes with custom toggl file' do
|
|
41
|
+
toggl_file = File.join(@home, 'my_toggl')
|
|
42
|
+
File.open(toggl_file, 'w') { |file| file.write(Testing::API_TOKEN) }
|
|
43
|
+
|
|
44
|
+
reports = TogglV8::ReportsV2.new({toggl_api_file: toggl_file})
|
|
45
|
+
env = reports.env
|
|
46
|
+
expect(env).to_not be nil
|
|
47
|
+
expect(env['user']['api_token']).to eq Testing::API_TOKEN
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'raises error if .toggl file is missing' do
|
|
51
|
+
expect{ reports = TogglV8::ReportsV2.new }.to raise_error(RuntimeError)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
context 'handles errors' do
|
|
56
|
+
before :all do
|
|
57
|
+
@reports = TogglV8::ReportsV2.new(api_token: Testing::API_TOKEN)
|
|
58
|
+
@reports.workspace_id = @workspace_id
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'surfaces a Warning HTTP header in case of error' do
|
|
62
|
+
# https://github.com/toggl/toggl_api_docs/blob/master/reports.md#failed-requests
|
|
63
|
+
expect { @reports.error400 }.to raise_error(RuntimeError,
|
|
64
|
+
"This URL is intended only for testing")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'retries a request up to 3 times if a 429 is received' do
|
|
68
|
+
expect(@reports.conn).to receive(:get).exactly(3).times.and_return(
|
|
69
|
+
MockResponse.new(429, {}, 'body'))
|
|
70
|
+
expect { @reports.env }.to raise_error(RuntimeError, "HTTP Status: 429")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'retries a request after 429' do
|
|
74
|
+
expect(@reports.conn).to receive(:get).twice.and_return(
|
|
75
|
+
MockResponse.new(429, {}, 'body'),
|
|
76
|
+
MockResponse.new(200, {}, 'rev1.2.3'))
|
|
77
|
+
expect(@reports.revision).to eq('rev1.2.3')
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
context 'revision' do
|
|
82
|
+
it 'has not changed' do
|
|
83
|
+
reports = TogglV8::ReportsV2.new(api_token: Testing::API_TOKEN)
|
|
84
|
+
reports.workspace_id = @workspace_id
|
|
85
|
+
expect(reports.revision).to eq("0.0.38\n-8a007ca")
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
context 'blank reports' do
|
|
90
|
+
before :all do
|
|
91
|
+
@toggl = TogglV8::API.new(Testing::API_TOKEN)
|
|
92
|
+
@workspaces = @toggl.workspaces
|
|
93
|
+
@workspace_id = @workspaces.first['id']
|
|
94
|
+
@reports = TogglV8::ReportsV2.new(api_token: Testing::API_TOKEN)
|
|
95
|
+
@reports.workspace_id = @workspace_id
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it 'summary' do
|
|
99
|
+
expect(@reports.summary).to eq []
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'weekly' do
|
|
103
|
+
expect(@reports.weekly).to eq []
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it 'details' do
|
|
107
|
+
expect(@reports.details).to eq []
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
context 'reports' do
|
|
112
|
+
before :all do
|
|
113
|
+
@toggl = TogglV8::API.new(Testing::API_TOKEN)
|
|
114
|
+
@workspaces = @toggl.workspaces
|
|
115
|
+
@workspace_id = @workspaces.first['id']
|
|
116
|
+
time_entry_info = {
|
|
117
|
+
'wid' => @workspace_id,
|
|
118
|
+
'start' => @toggl.iso8601(DateTime.now),
|
|
119
|
+
'duration' => 77
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@time_entry = @toggl.create_time_entry(time_entry_info)
|
|
123
|
+
|
|
124
|
+
@reports = TogglV8::ReportsV2.new(api_token: Testing::API_TOKEN)
|
|
125
|
+
@reports.workspace_id = @workspace_id
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
after :all do
|
|
129
|
+
@toggl.delete_time_entry(@time_entry['id'])
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it 'summary' do
|
|
133
|
+
summary = @reports.summary
|
|
134
|
+
expect(summary.length).to eq 1
|
|
135
|
+
expect(summary.first['time']).to eq 77000
|
|
136
|
+
expect(summary.first['items'].length).to eq 1
|
|
137
|
+
expect(summary.first['items'].first['time']).to eq 77000
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it 'weekly' do
|
|
141
|
+
weekly = @reports.weekly
|
|
142
|
+
expect(weekly.length).to eq 1
|
|
143
|
+
expect(weekly.first['details'].first['title']['user']).to eq 'togglv8'
|
|
144
|
+
expect(weekly.first['totals'][7]).to eq 77000
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it 'details' do
|
|
148
|
+
details = @reports.details
|
|
149
|
+
expect(details.length).to eq 1
|
|
150
|
+
expect(details.first['user']).to eq 'togglv8'
|
|
151
|
+
expect(details.first['dur']).to eq 77000
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
context 'CSV reports' do
|
|
156
|
+
before :all do
|
|
157
|
+
@toggl = TogglV8::API.new(Testing::API_TOKEN)
|
|
158
|
+
@workspaces = @toggl.workspaces
|
|
159
|
+
@workspace_id = @workspaces.first['id']
|
|
160
|
+
time_entry_info = {
|
|
161
|
+
'wid' => @workspace_id,
|
|
162
|
+
'start' => @toggl.iso8601(DateTime.now),
|
|
163
|
+
'duration' => 77
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
@time_entry = @toggl.create_time_entry(time_entry_info)
|
|
167
|
+
|
|
168
|
+
@reports = TogglV8::ReportsV2.new(api_token: Testing::API_TOKEN)
|
|
169
|
+
@reports.workspace_id = @workspace_id
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
after :all do
|
|
173
|
+
@toggl.delete_time_entry(@time_entry['id'])
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
it 'summary' do
|
|
177
|
+
summary = @reports.summary
|
|
178
|
+
expect(summary.length).to eq 1
|
|
179
|
+
expect(summary.first['time']).to eq 77000
|
|
180
|
+
expect(summary.first['items'].length).to eq 1
|
|
181
|
+
expect(summary.first['items'].first['time']).to eq 77000
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it 'weekly' do
|
|
185
|
+
weekly = @reports.weekly
|
|
186
|
+
expect(weekly.length).to eq 1
|
|
187
|
+
expect(weekly.first['details'].first['title']['user']).to eq 'togglv8'
|
|
188
|
+
expect(weekly.first['totals'][7]).to eq 77000
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
it 'details' do
|
|
192
|
+
details = @reports.details
|
|
193
|
+
expect(details.length).to eq 1
|
|
194
|
+
expect(details.first['user']).to eq 'togglv8'
|
|
195
|
+
expect(details.first['dur']).to eq 77000
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
data/spec/lib/togglv8_spec.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
require 'fileutils'
|
|
2
2
|
|
|
3
|
-
describe 'TogglV8
|
|
3
|
+
describe 'TogglV8' do
|
|
4
4
|
it 'initializes with api_token' do
|
|
5
5
|
toggl = TogglV8::API.new(Testing::API_TOKEN)
|
|
6
6
|
me = toggl.me
|
|
@@ -55,25 +55,24 @@ describe 'TogglV8::API' do
|
|
|
55
55
|
context 'handles errors' do
|
|
56
56
|
before :all do
|
|
57
57
|
@toggl = TogglV8::API.new(Testing::API_TOKEN)
|
|
58
|
-
Response = Struct.new(:env, :status, :success?, :body)
|
|
59
58
|
end
|
|
60
59
|
|
|
61
60
|
it 'surfaces an HTTP Status Code in case of error' do
|
|
62
61
|
expect(@toggl.conn).to receive(:get).once.and_return(
|
|
63
|
-
|
|
62
|
+
MockResponse.new(400, {}, 'body'))
|
|
64
63
|
expect { @toggl.me }.to raise_error(RuntimeError, "HTTP Status: 400")
|
|
65
64
|
end
|
|
66
65
|
|
|
67
66
|
it 'retries a request up to 3 times if a 429 is received' do
|
|
68
67
|
expect(@toggl.conn).to receive(:get).exactly(3).times.and_return(
|
|
69
|
-
|
|
68
|
+
MockResponse.new(429, {}, 'body'))
|
|
70
69
|
expect { @toggl.me }.to raise_error(RuntimeError, "HTTP Status: 429")
|
|
71
70
|
end
|
|
72
71
|
|
|
73
72
|
it 'retries a request after 429' do
|
|
74
73
|
expect(@toggl.conn).to receive(:get).twice.and_return(
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
MockResponse.new(429, {}, 'body'),
|
|
75
|
+
MockResponse.new(200, {}, nil))
|
|
77
76
|
expect(@toggl.me).to eq({}) # response is {} in this case because body is nil
|
|
78
77
|
end
|
|
79
78
|
end
|
data/spec/spec_helper.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
require "simplecov"
|
|
2
2
|
require "coveralls"
|
|
3
3
|
|
|
4
|
-
SimpleCov.
|
|
4
|
+
SimpleCov.formatters = [
|
|
5
5
|
SimpleCov::Formatter::HTMLFormatter,
|
|
6
6
|
Coveralls::SimpleCov::Formatter
|
|
7
7
|
]
|
|
@@ -32,7 +32,23 @@ RSpec.configure do |config|
|
|
|
32
32
|
|
|
33
33
|
config.before(:suite) do
|
|
34
34
|
toggl = TogglV8::API.new(Testing::API_TOKEN)
|
|
35
|
-
TogglV8SpecHelper.setUp(toggl)
|
|
35
|
+
TogglV8SpecHelper.setUp(toggl) # start tests from known state
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class MockResponse
|
|
40
|
+
# https://github.com/lostisland/faraday/blob/master/lib/faraday/response.rb
|
|
41
|
+
|
|
42
|
+
attr_accessor :status, :headers, :body, :env
|
|
43
|
+
|
|
44
|
+
def initialize(status, headers, body)
|
|
45
|
+
@status = status
|
|
46
|
+
@headers = headers
|
|
47
|
+
@body = body
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def success?
|
|
51
|
+
@status == 200
|
|
36
52
|
end
|
|
37
53
|
end
|
|
38
54
|
|
data/spec/togglv8_spec_helper.rb
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
require_relative '../lib/togglv8'
|
|
2
|
-
require 'logger'
|
|
3
2
|
|
|
4
3
|
class TogglV8SpecHelper
|
|
5
|
-
|
|
6
|
-
@logger.level = Logger::WARN
|
|
4
|
+
include Logging
|
|
7
5
|
|
|
8
6
|
def self.setUp(toggl)
|
|
9
7
|
user = toggl.me(all=true)
|
|
@@ -20,7 +18,7 @@ class TogglV8SpecHelper
|
|
|
20
18
|
clients = toggl.my_clients
|
|
21
19
|
unless clients.nil?
|
|
22
20
|
client_ids ||= clients.map { |c| c['id'] }
|
|
23
|
-
|
|
21
|
+
# logger.debug("Deleting #{client_ids.length} clients")
|
|
24
22
|
client_ids.each do |c_id|
|
|
25
23
|
toggl.delete_client(c_id)
|
|
26
24
|
end
|
|
@@ -31,7 +29,7 @@ class TogglV8SpecHelper
|
|
|
31
29
|
projects = toggl.projects(@default_workspace_id)
|
|
32
30
|
unless projects.nil?
|
|
33
31
|
project_ids ||= projects.map { |p| p['id'] }
|
|
34
|
-
|
|
32
|
+
# logger.debug("Deleting #{project_ids.length} projects")
|
|
35
33
|
return unless project_ids.length > 0
|
|
36
34
|
toggl.delete_projects(project_ids)
|
|
37
35
|
end
|
|
@@ -41,7 +39,7 @@ class TogglV8SpecHelper
|
|
|
41
39
|
tags = toggl.my_tags
|
|
42
40
|
unless tags.nil?
|
|
43
41
|
tag_ids ||= tags.map { |t| t['id'] }
|
|
44
|
-
|
|
42
|
+
# logger.debug("Deleting #{tag_ids.length} tags")
|
|
45
43
|
tag_ids.each do |t_id|
|
|
46
44
|
toggl.delete_tag(t_id)
|
|
47
45
|
end
|
|
@@ -53,7 +51,7 @@ class TogglV8SpecHelper
|
|
|
53
51
|
{ :start_date => DateTime.now - 30, :end_date => DateTime.now + 30 } )
|
|
54
52
|
unless time_entries.nil?
|
|
55
53
|
time_entry_ids ||= time_entries.map { |t| t['id'] }
|
|
56
|
-
|
|
54
|
+
# logger.debug("Deleting #{time_entry_ids.length} time_entries")
|
|
57
55
|
time_entry_ids.each do |t_id|
|
|
58
56
|
toggl.delete_time_entry(t_id)
|
|
59
57
|
end
|
|
@@ -66,7 +64,7 @@ class TogglV8SpecHelper
|
|
|
66
64
|
unless workspaces.nil?
|
|
67
65
|
workspace_ids ||= workspaces.map { |w| w['id'] }
|
|
68
66
|
workspace_ids.delete(user['default_wid'])
|
|
69
|
-
|
|
67
|
+
# logger.debug("Leaving #{workspace_ids.length} workspaces")
|
|
70
68
|
workspace_ids.each do |w_id|
|
|
71
69
|
toggl.leave_workspace(w_id)
|
|
72
70
|
end
|
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: 1.
|
|
4
|
+
version: 1.2.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: 2016-
|
|
11
|
+
date: 2016-07-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -176,21 +176,27 @@ files:
|
|
|
176
176
|
- ".rdoc_options"
|
|
177
177
|
- ".rspec"
|
|
178
178
|
- ".travis.yml"
|
|
179
|
+
- CHANGELOG.md
|
|
179
180
|
- Gemfile
|
|
180
181
|
- LICENSE.txt
|
|
181
182
|
- README.md
|
|
182
183
|
- Rakefile
|
|
184
|
+
- lib/logging.rb
|
|
185
|
+
- lib/reportsv2.rb
|
|
183
186
|
- lib/togglv8.rb
|
|
184
187
|
- lib/togglv8/clients.rb
|
|
188
|
+
- lib/togglv8/connection.rb
|
|
185
189
|
- lib/togglv8/dashboard.rb
|
|
186
190
|
- lib/togglv8/project_users.rb
|
|
187
191
|
- lib/togglv8/projects.rb
|
|
188
192
|
- lib/togglv8/tags.rb
|
|
189
193
|
- lib/togglv8/tasks.rb
|
|
190
194
|
- lib/togglv8/time_entries.rb
|
|
195
|
+
- lib/togglv8/togglv8.rb
|
|
191
196
|
- lib/togglv8/users.rb
|
|
192
197
|
- lib/togglv8/version.rb
|
|
193
198
|
- lib/togglv8/workspaces.rb
|
|
199
|
+
- spec/lib/reportsv2_spec.rb
|
|
194
200
|
- spec/lib/togglv8/clients_spec.rb
|
|
195
201
|
- spec/lib/togglv8/dashboard_spec.rb
|
|
196
202
|
- spec/lib/togglv8/projects_spec.rb
|
|
@@ -228,6 +234,7 @@ signing_key:
|
|
|
228
234
|
specification_version: 4
|
|
229
235
|
summary: Toggl v8 API wrapper (See https://github.com/toggl/toggl_api_docs)
|
|
230
236
|
test_files:
|
|
237
|
+
- spec/lib/reportsv2_spec.rb
|
|
231
238
|
- spec/lib/togglv8/clients_spec.rb
|
|
232
239
|
- spec/lib/togglv8/dashboard_spec.rb
|
|
233
240
|
- spec/lib/togglv8/projects_spec.rb
|