togglv8 1.1.0 → 1.2.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: 04a79328e6a07a58324e23cc86e3886bbb502d18
4
- data.tar.gz: 5d3ef179fe12cda05f519915fb746ef0a8080fae
3
+ metadata.gz: 4c535170f2f4008596f53dd97645d5946d62740e
4
+ data.tar.gz: f007be73994ef07272e1100e4f2046dc22d69e3a
5
5
  SHA512:
6
- metadata.gz: a3cdb34425ded4ff9496cb14948ea39d539db802cf07c664a29295d6b55a403034c98a4814470fa0f3e80126eed09d828d6e3f3308f022c8fa437839bff499eb
7
- data.tar.gz: cec3ad2de4a51bcc5578ce64d8ea45abeadee7b8f43b7136059d959d64bde5811f08be4693b031d39f8388513114be74e7a793e93a5a3926227fde5ccf9b4470
6
+ metadata.gz: 082469da76c56138a7bee169c205874a150384959b516b65633e4672288b0962fbfbccb1909ec2ad6a176bf076516d898b6a9fe3bc3bad52ac8048c2240a8d9a
7
+ data.tar.gz: 64463ae5e379fee5e870be1c84e4a76d0b30138634f002371cd0fd08898cf6aa9bc0530287a540ffe1a4380dcc0c51214810c9acf614aa0802d1aa43f976cbbb
@@ -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
- **Note:** Currently togglv8 only includes calls to [Toggl API](https://github.com/toggl/toggl_api_docs/blob/master/toggl_api.md), not the [Reports API](https://github.com/toggl/toggl_api_docs/blob/master/reports.md)
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' => "Workspace time entry",
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' => "2015-08-18T01:13:40.000Z",
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. (The default is `true`.) This code snippet demonstrates the debug output.
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
 
@@ -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
@@ -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
@@ -1,144 +1,9 @@
1
- require 'faraday'
2
- require 'oj'
1
+ require_relative 'togglv8/version'
3
2
 
4
- require 'logger'
5
- require 'awesome_print' # for debug output
3
+ require_relative 'togglv8/connection'
6
4
 
7
- require_relative 'togglv8/clients'
8
- require_relative 'togglv8/dashboard'
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
+
@@ -1,4 +1,4 @@
1
1
  module TogglV8
2
2
  # :section:
3
- VERSION = "1.1.0"
3
+ VERSION = "1.2.0"
4
4
  end
@@ -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
@@ -1,6 +1,6 @@
1
1
  require 'fileutils'
2
2
 
3
- describe 'TogglV8::API' do
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
- Response.new('', 400, false, nil))
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
- Response.new('', 429, false, nil))
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
- Response.new('', 429, false, nil),
76
- Response.new('', 200, true, nil))
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
@@ -1,7 +1,7 @@
1
1
  require "simplecov"
2
2
  require "coveralls"
3
3
 
4
- SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
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
 
@@ -1,9 +1,7 @@
1
1
  require_relative '../lib/togglv8'
2
- require 'logger'
3
2
 
4
3
  class TogglV8SpecHelper
5
- @logger = Logger.new(STDOUT)
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
- @logger.debug("Deleting #{client_ids.length} clients")
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
- @logger.debug("Deleting #{project_ids.length} projects")
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
- @logger.debug("Deleting #{tag_ids.length} tags")
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
- @logger.debug("Deleting #{time_entry_ids.length} time_entries")
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
- @logger.debug("Leaving #{workspace_ids.length} workspaces")
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.1.0
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-02-23 00:00:00.000000000 Z
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