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