toggl_rb 0.1.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 +7 -0
- data/.env_sample +5 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/LICENSE +7 -0
- data/README.md +34 -0
- data/Rakefile +12 -0
- data/lib/toggl_rb/client.rb +38 -0
- data/lib/toggl_rb/config.rb +29 -0
- data/lib/toggl_rb/core/groups.rb +32 -0
- data/lib/toggl_rb/core/me.rb +44 -0
- data/lib/toggl_rb/core/projects.rb +80 -0
- data/lib/toggl_rb/core/time_entries.rb +65 -0
- data/lib/toggl_rb/core/users.rb +32 -0
- data/lib/toggl_rb/core.rb +15 -0
- data/lib/toggl_rb/endpoint_dsl.rb +92 -0
- data/lib/toggl_rb/json_patch.rb +41 -0
- data/lib/toggl_rb/param.rb +25 -0
- data/lib/toggl_rb/params.rb +45 -0
- data/lib/toggl_rb/query_params.rb +6 -0
- data/lib/toggl_rb/reports/detailed.rb +81 -0
- data/lib/toggl_rb/reports/summary.rb +68 -0
- data/lib/toggl_rb/reports.rb +14 -0
- data/lib/toggl_rb/response.rb +26 -0
- data/lib/toggl_rb/version.rb +5 -0
- data/lib/toggl_rb.rb +34 -0
- data/sig/toggl_rb.rbs +4 -0
- metadata +88 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a9e314bac612a688395f89e9050e3205e2bbd563074efa11682805f76e02612e
|
4
|
+
data.tar.gz: 18ff60baf2d6e43bce98857d0014cbb3c614dc53ea9868bd627ca602f9ae2e52
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c35729adaf2528f5157936018741056e25eefd9b01db858809eda6e3f2cc2b720115bd5a630328caadd467ec16bebbc3f875a7a0e33ce5ccd11f4b2fdd663ca7
|
7
|
+
data.tar.gz: 40e30a18d467111c32dc544f5cfcf37c2c4cab895c26cc721dd63a7d49803f0ec7b50bae06165b1aead82e686e1cfd8044697886a934f771fb3234a7a90b460e
|
data/.env_sample
ADDED
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.0.3
|
data/LICENSE
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright (c) 2024 Michael Cordell (https://mikecordell.com)
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# TogglRb
|
2
|
+
|
3
|
+
[](https://github.com/mcordell/toggl_rb/actions/workflows/ruby.yml)
|
4
|
+
[](https://coveralls.io/github/mcordell/toggl_rb)
|
5
|
+
|
6
|
+
TODO: Delete this and the text below, and describe your gem
|
7
|
+
|
8
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/toggl_rb`. To experiment with that code, run `bin/console` for an interactive prompt.
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
|
13
|
+
|
14
|
+
Install the gem and add to the application's Gemfile by executing:
|
15
|
+
|
16
|
+
$ bundle add UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
|
17
|
+
|
18
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
19
|
+
|
20
|
+
$ gem install UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
|
21
|
+
|
22
|
+
## Usage
|
23
|
+
|
24
|
+
TODO: Write usage instructions here
|
25
|
+
|
26
|
+
## Development
|
27
|
+
|
28
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
29
|
+
|
30
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
31
|
+
|
32
|
+
## Contributing
|
33
|
+
|
34
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/mcordell/toggl_rb.
|
data/Rakefile
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "faraday"
|
4
|
+
|
5
|
+
module TogglRb
|
6
|
+
# Config class stores global configuration for the TogglRb gem
|
7
|
+
class Client
|
8
|
+
attr_reader :core_connection, :reports_connection
|
9
|
+
|
10
|
+
URLS = {
|
11
|
+
core: { url: "https://api.track.toggl.com/api/v9" },
|
12
|
+
reports: { url: "https://api.track.toggl.com/reports/api/v3/" }
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@core_connection = Faraday.new(URLS.dig(:core, :url)).tap do |fc|
|
17
|
+
fc.set_basic_auth(config.username, config.password) if config.basic_auth?
|
18
|
+
fc.set_basic_auth(config.api_token, TogglRb::Config::API_TOKEN_PASSWORD) if config.api_token?
|
19
|
+
fc.headers["Content-Type"] = "application/json"
|
20
|
+
fc.headers["Accept"] = "application/json"
|
21
|
+
end
|
22
|
+
|
23
|
+
@reports_connection = Faraday.new(URLS.dig(:reports, :url)).tap do |fc|
|
24
|
+
fc.set_basic_auth(config.username, config.password) if config.basic_auth?
|
25
|
+
fc.set_basic_auth(config.api_token, TogglRb::Config::API_TOKEN_PASSWORD) if config.api_token?
|
26
|
+
fc.headers["Content-Type"] = "application/json"
|
27
|
+
fc.headers["Accept"] = "application/json"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# @return [TogglRb::Config]
|
34
|
+
def config
|
35
|
+
TogglRb.config
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TogglRb
|
4
|
+
# Config class stores global configuration for the TogglRb gem
|
5
|
+
class Config
|
6
|
+
attr_accessor :username, :password, :api_token
|
7
|
+
attr_writer :debug_logging
|
8
|
+
|
9
|
+
API_TOKEN_PASSWORD = "api_token"
|
10
|
+
|
11
|
+
# Whether username and password are set within this config in order to use basic auth. Basic auth is necessary for
|
12
|
+
# reports API.
|
13
|
+
# @return [Boolean] whether username and password exist
|
14
|
+
def basic_auth?
|
15
|
+
!!(username && password)
|
16
|
+
end
|
17
|
+
|
18
|
+
def api_token?
|
19
|
+
!!api_token
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [Boolean] whether to log debug messages
|
23
|
+
def debug_logging
|
24
|
+
!!@debug_logging || ENV.fetch("TOGGL_RB_DEBUG_LOG", nil) || false
|
25
|
+
end
|
26
|
+
|
27
|
+
alias debug_logging? debug_logging
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TogglRb
|
4
|
+
module Core
|
5
|
+
class Groups
|
6
|
+
include EndpointDSL
|
7
|
+
|
8
|
+
request_method :get
|
9
|
+
request_path "organizations/%<organization_id>s/workspaces/%<workspace_id>s/groups"
|
10
|
+
def list(organization_id:, workspace_id:)
|
11
|
+
resource_path = format(request_path, workspace_id: workspace_id, organization_id: organization_id)
|
12
|
+
|
13
|
+
send_request(request_method, resource_path).body_json
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def send_request(request_method, resource_path, body = nil)
|
19
|
+
if body.nil?
|
20
|
+
TogglRb::Response.new(connection.send(request_method, resource_path))
|
21
|
+
else
|
22
|
+
params = body.to_json unless body.is_a?(String)
|
23
|
+
TogglRb::Response.new(connection.send(request_method, resource_path, params))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def connection
|
28
|
+
TogglRb::Core.connection
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module TogglRb
|
6
|
+
module Core
|
7
|
+
class Me
|
8
|
+
include EndpointDSL
|
9
|
+
|
10
|
+
request_method :get
|
11
|
+
request_path "me"
|
12
|
+
def get(with_related_data: false)
|
13
|
+
resource_path = with_related_data ? "#{request_path}?with_related_data=true" : request_path
|
14
|
+
send_request(request_method, resource_path).body_json
|
15
|
+
end
|
16
|
+
|
17
|
+
request_method :get
|
18
|
+
request_path "me/workspaces"
|
19
|
+
query_param :since, Date,
|
20
|
+
description: "Retrieve workspaces created/modified/deleted since this date using UNIX timestamp, " \
|
21
|
+
"including the dates a workspace member got added, removed or updated in the workspace."
|
22
|
+
def workspaces(_params = {})
|
23
|
+
# TODO: implement since query_param
|
24
|
+
send_request(request_method, request_path).body_json
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def send_request(request_method, resource_path, body = nil)
|
30
|
+
rsp = if request_method == :get
|
31
|
+
connection.get(resource_path)
|
32
|
+
else
|
33
|
+
params = body.to_json unless body.is_a?(String)
|
34
|
+
connection.send(request_method, resource_path, params.to_json)
|
35
|
+
end
|
36
|
+
TogglRb::Response.new(rsp)
|
37
|
+
end
|
38
|
+
|
39
|
+
def connection
|
40
|
+
TogglRb::Core.connection
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TogglRb
|
4
|
+
module Core
|
5
|
+
class Projects
|
6
|
+
include TogglRb::EndpointDSL
|
7
|
+
|
8
|
+
request_method :get
|
9
|
+
request_path "workspaces/%<workspace_id>s/projects"
|
10
|
+
query_param :active, "Boolean"
|
11
|
+
query_param :since, "Boolean"
|
12
|
+
query_param :active, "boolean", description: "active"
|
13
|
+
query_param :since, Integer,
|
14
|
+
description: "Retrieve projects created/modified/deleted since this date using UNIX timestamp."
|
15
|
+
query_param :billable, "boolean", description: "billable"
|
16
|
+
query_param :user_ids, "array", description: "user_ids"
|
17
|
+
query_param :client_ids, "array", description: "client_ids"
|
18
|
+
query_param :group_ids, "array", description: "group_ids"
|
19
|
+
query_param :statuses, "array", description: "statuses"
|
20
|
+
query_param :name, String, description: "name"
|
21
|
+
query_param :page, Integer, description: "page"
|
22
|
+
query_param :sort_field, String, description: "sort_field"
|
23
|
+
query_param :sort_order, String, description: "sort_order"
|
24
|
+
query_param :only_templates, "boolean", description: "only_templates"
|
25
|
+
query_param :per_page, Integer, description: "Number of items per page, default 151. Cannot exceed 200."
|
26
|
+
def search(workspace_id, query_params = {})
|
27
|
+
params = build_query_params(query_params)
|
28
|
+
resource_path = format(request_path, workspace_id: workspace_id)
|
29
|
+
response = connection.get(resource_path) do |request|
|
30
|
+
request.params = params.request_params
|
31
|
+
end
|
32
|
+
Response.new(response)
|
33
|
+
end
|
34
|
+
|
35
|
+
request_method :post
|
36
|
+
request_path "workspaces/%<workspace_id>s/projects"
|
37
|
+
param :active, "Boolean", description: "Whether the project is active or archived."
|
38
|
+
param :auto_estimates, "Boolean", optional: true, description: "Whether estimates are based on task hours."
|
39
|
+
param :billable, "Boolean", optional: true, description: "Whether the project is billable."
|
40
|
+
param :cid, Integer, description: "Client ID (legacy)."
|
41
|
+
param :client_id, Integer, optional: true, description: "Client ID."
|
42
|
+
param :client_name, String, optional: true, description: "Client name."
|
43
|
+
param :color, String, description: "Project color."
|
44
|
+
param :currency, String, optional: true, description: "Project currency."
|
45
|
+
param :end_date, String, description: "End date of the project timeframe."
|
46
|
+
param :estimated_hours, Integer, optional: true, description: "Estimated hours for the project."
|
47
|
+
param :fixed_fee, "Number", optional: true, description: "Project fixed fee."
|
48
|
+
param :is_private, "Boolean", description: "Whether the project is private."
|
49
|
+
param :name, String, description: "Project name."
|
50
|
+
param :rate, "Number", optional: true, description: "Hourly rate for the project."
|
51
|
+
param :rate_change_mode, String, optional: true,
|
52
|
+
description: "Rate change mode (start-today, override-current, override-all)."
|
53
|
+
param :recurring, "Boolean", optional: true, description: "Whether the project is recurring."
|
54
|
+
# nested_param :recurring_parameters do
|
55
|
+
# param :custom_period, Integer, description: "Custom period for recurring setting."
|
56
|
+
# param :period, String, description: "Recurring period, e.g., 'monthly'."
|
57
|
+
# param :project_start_date, String, description: "Start date for the recurring project."
|
58
|
+
# end
|
59
|
+
param :start_date, String, description: "Start date of the project timeframe."
|
60
|
+
param :template, "Boolean", optional: true, description: "Whether the project is a template."
|
61
|
+
param :template_id, "Integer", optional: true, description: "Template ID for the project."
|
62
|
+
def create(workspace_id, project_attributes)
|
63
|
+
resource_path = format(request_path, workspace_id: workspace_id)
|
64
|
+
|
65
|
+
send_request(request_method, resource_path, project_attributes).body_json
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def send_request(request_method, resource_path, body)
|
71
|
+
params = body.to_json unless body.is_a?(String)
|
72
|
+
TogglRb::Response.new(connection.send(request_method, resource_path, params))
|
73
|
+
end
|
74
|
+
|
75
|
+
def connection
|
76
|
+
TogglRb::Core.connection
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TogglRb
|
4
|
+
module Core
|
5
|
+
class TimeEntries
|
6
|
+
include EndpointDSL
|
7
|
+
|
8
|
+
request_method :post
|
9
|
+
request_path "workspaces/%<workspace_id>s/time_entries"
|
10
|
+
param :billable, "Boolean", optional: true, default: false,
|
11
|
+
description: "Whether the time entry is marked as billable."
|
12
|
+
param :created_with, String, required: true,
|
13
|
+
description: "Identifies the service/application used to create it."
|
14
|
+
param :description, String, optional: true, description: "Time entry description."
|
15
|
+
param :duration, Integer, required: true, description: "Duration in seconds; negative for running entries."
|
16
|
+
param :pid, Integer, optional: true, description: "Project ID (legacy field)."
|
17
|
+
param :project_id, Integer, optional: true, description: "Project ID."
|
18
|
+
param :shared_with_user_ids, "Array<Integer>", optional: true,
|
19
|
+
description: "User IDs to share this time entry with."
|
20
|
+
param :start, Time, required: true, description: "Start time in UTC. Format: 2006-01-02T15:04:05Z."
|
21
|
+
param :start_date, Date, optional: true,
|
22
|
+
description: "Takes precedence over the date part of 'start'. Format: 2006-11-07."
|
23
|
+
param :stop, Time, optional: true, description: "Stop time in UTC."
|
24
|
+
param :tag_action, String, optional: true, description: "Used when updating; can be 'add' or 'delete'."
|
25
|
+
param :tag_ids, "Array<Integer>", optional: true, description: "Tag IDs to add/remove."
|
26
|
+
param :tags, "Array<String>", optional: true,
|
27
|
+
description: "Tag names to add/remove; creates tag if it doesn't exist."
|
28
|
+
param :task_id, Integer, optional: true, description: "Task ID."
|
29
|
+
param :user_id, Integer, optional: true, description: "Time Entry creator ID; uses requester ID if omitted."
|
30
|
+
param :workspace_id, Integer, required: true, description: "Workspace ID."
|
31
|
+
# NOTE: tid is a legacy field for Task ID.
|
32
|
+
# Note: uid is a legacy field for Time Entry creator ID.
|
33
|
+
# NOTE: duronly is deprecated and can be ignored.
|
34
|
+
# NOTE: wid is a legacy param for Workspace ID which will be populated with the passed argument
|
35
|
+
def create(workspace_id, time_entry_attributes)
|
36
|
+
time_entry_attributes[:wid] = workspace_id.to_i
|
37
|
+
resource_path = format(request_path, workspace_id: workspace_id)
|
38
|
+
|
39
|
+
send_request(request_method, resource_path, time_entry_attributes).body_json
|
40
|
+
end
|
41
|
+
|
42
|
+
request_method :patch
|
43
|
+
request_path "workspaces/%<workspace_id>s/time_entries/%<time_entry_ids>s"
|
44
|
+
# @param workspace_id [String|Integer] the workspace ID
|
45
|
+
# @param time_entries [Array<Integer>] the IDs of the task entries to update
|
46
|
+
# @param operation [TogglRb::JSONPatch] the operation to apply to the time entries
|
47
|
+
def patch(workspace_id, time_entries, operation)
|
48
|
+
resource_path = format(request_path, workspace_id: workspace_id, time_entry_ids: time_entries.join(","))
|
49
|
+
|
50
|
+
send_request(request_method, resource_path, operation).body_json
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def send_request(request_method, resource_path, body)
|
56
|
+
params = body.to_json unless body.is_a?(String)
|
57
|
+
TogglRb::Response.new(connection.send(request_method, resource_path, params))
|
58
|
+
end
|
59
|
+
|
60
|
+
def connection
|
61
|
+
TogglRb::Core.connection
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TogglRb
|
4
|
+
module Core
|
5
|
+
class Users
|
6
|
+
include EndpointDSL
|
7
|
+
|
8
|
+
request_method :get
|
9
|
+
request_path "workspaces/%<workspace_id>s/users"
|
10
|
+
def list(workspace_id)
|
11
|
+
resource_path = format(request_path, workspace_id: workspace_id)
|
12
|
+
|
13
|
+
send_request(request_method, resource_path, nil).body_json
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def send_request(request_method, resource_path, body)
|
19
|
+
if body.nil?
|
20
|
+
TogglRb::Response.new(connection.send(request_method, resource_path))
|
21
|
+
else
|
22
|
+
params = body.to_json unless body.is_a?(String)
|
23
|
+
TogglRb::Response.new(connection.send(request_method, resource_path, params))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def connection
|
28
|
+
TogglRb::Core.connection
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TogglRb
|
4
|
+
module Core
|
5
|
+
require_relative "core/groups"
|
6
|
+
require_relative "core/projects"
|
7
|
+
require_relative "core/time_entries"
|
8
|
+
require_relative "core/users"
|
9
|
+
require_relative "core/me"
|
10
|
+
|
11
|
+
def self.connection
|
12
|
+
TogglRb.client.core_connection
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TogglRb
|
4
|
+
module EndpointDSL
|
5
|
+
def self.included(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
ALLOWED_METHOD_TYPES = %i[post get put patch].freeze
|
11
|
+
ALLOWED_PRETTY = ALLOWED_METHOD_TYPES.map { |s| ":#{s}" }.join(", ").freeze
|
12
|
+
|
13
|
+
def method_added(method_name)
|
14
|
+
return if @adding_method || !@current_path
|
15
|
+
|
16
|
+
param_definitions[method_name] = @current_params || {}
|
17
|
+
method_request_paths[method_name] = @current_path
|
18
|
+
method_request_methods[method_name] = @current_method_type
|
19
|
+
method_query_params[method_name] = @current_query_params || {}
|
20
|
+
@current_params = nil # Reset for the next method
|
21
|
+
@current_path = nil
|
22
|
+
@current_method_type = nil
|
23
|
+
super
|
24
|
+
end
|
25
|
+
|
26
|
+
def request_path(path)
|
27
|
+
@current_path = path
|
28
|
+
end
|
29
|
+
|
30
|
+
def request_method(method_type)
|
31
|
+
method_type = method_type.to_sym
|
32
|
+
return @current_method_type = method_type if ALLOWED_METHOD_TYPES.include?(method_type)
|
33
|
+
|
34
|
+
raise ArgumentError, "method type: #{method_type} is not one of #{ALLOWED_PRETTY}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def param_definitions
|
38
|
+
@param_definitions ||= {}
|
39
|
+
end
|
40
|
+
|
41
|
+
def method_query_params
|
42
|
+
@method_query_params ||= {}
|
43
|
+
end
|
44
|
+
|
45
|
+
def method_request_paths
|
46
|
+
@method_request_paths ||= {}
|
47
|
+
end
|
48
|
+
|
49
|
+
def method_request_methods
|
50
|
+
@method_request_methods ||= {}
|
51
|
+
end
|
52
|
+
|
53
|
+
def param(name, type, *other_args)
|
54
|
+
@current_params ||= {}
|
55
|
+
@current_params[name] = { type: type, other_args: other_args }
|
56
|
+
end
|
57
|
+
|
58
|
+
def query_param(name, type, *other_args)
|
59
|
+
@current_query_params ||= {}
|
60
|
+
@current_query_params[name] = { type: type, other_args: other_args }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def params_for_method(method_name)
|
65
|
+
self.class.param_definitions[method_name] || {}
|
66
|
+
end
|
67
|
+
|
68
|
+
def build_params(request_params)
|
69
|
+
caller_info = caller_locations(1, 1).first
|
70
|
+
Params.build(params_for_method(caller_info.label.to_sym), request_params)
|
71
|
+
end
|
72
|
+
|
73
|
+
def build_query_params(request_params)
|
74
|
+
caller_info = caller_locations(1, 1).first
|
75
|
+
QueryParams.build(query_params_for_method(caller_info.label.to_sym), request_params)
|
76
|
+
end
|
77
|
+
|
78
|
+
def request_path
|
79
|
+
caller_info = caller_locations(1, 1).first
|
80
|
+
self.class.method_request_paths.fetch(caller_info.label.to_sym)
|
81
|
+
end
|
82
|
+
|
83
|
+
def request_method
|
84
|
+
caller_info = caller_locations(1, 1).first
|
85
|
+
self.class.method_request_methods.fetch(caller_info.label.to_sym)
|
86
|
+
end
|
87
|
+
|
88
|
+
def query_params_for_method(method_name)
|
89
|
+
self.class.method_query_params[method_name] || {}
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TogglRb
|
4
|
+
class JSONPatch
|
5
|
+
# frozen_string_literal: true
|
6
|
+
class Operation
|
7
|
+
attr_accessor(:type, :path, :value)
|
8
|
+
|
9
|
+
def initialize(type, path, value)
|
10
|
+
@type = type
|
11
|
+
@path = path
|
12
|
+
@value = value
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_h
|
16
|
+
{ "op" => type, "path" => path, "value" => value }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Replace < Operation
|
21
|
+
def initialize(path = nil, value = nil)
|
22
|
+
super("replace", path, value)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_reader :operations
|
27
|
+
|
28
|
+
def initialize(operations = [])
|
29
|
+
@operations = operations
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_json(*_args)
|
33
|
+
operations.map(&:to_h).to_json
|
34
|
+
end
|
35
|
+
|
36
|
+
def replace(path, value)
|
37
|
+
operations.push(Replace.new(path, value))
|
38
|
+
self
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TogglRb
|
4
|
+
class Param
|
5
|
+
attr_reader :name, :type, :other_attributes
|
6
|
+
|
7
|
+
def self.build(name, definition)
|
8
|
+
new(
|
9
|
+
name,
|
10
|
+
definition.fetch(:type),
|
11
|
+
definition.fetch(:other_args, [])
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(name, type, other_attributes = [])
|
16
|
+
@name = name
|
17
|
+
@type = type
|
18
|
+
@other_attributes = other_attributes
|
19
|
+
end
|
20
|
+
|
21
|
+
def required?
|
22
|
+
@other_attributes.include?(:required)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TogglRb
|
4
|
+
class Params
|
5
|
+
attr_accessor(:request_params, :first_row_number)
|
6
|
+
attr_reader :definition
|
7
|
+
|
8
|
+
def self.build(params_definition, request_params)
|
9
|
+
new(params_definition).tap { |p| p.request_params = request_params }
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(params_definition = {})
|
13
|
+
@definition = params_definition.to_h do |name, definition|
|
14
|
+
p = Param.build(name, definition)
|
15
|
+
[p.name, p]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def validate_required!
|
20
|
+
missing = required_parameters.reject { |p| request_params.key?(p.name) }
|
21
|
+
return if missing.empty?
|
22
|
+
|
23
|
+
missing = missing.count == 1 ? "#{missing.first.name} param" : "#{missing.map(&:name).join(", ")} params"
|
24
|
+
|
25
|
+
raise ArgumentError, "#{missing} must be provided"
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_json(*_args)
|
29
|
+
processed_params.to_json
|
30
|
+
end
|
31
|
+
|
32
|
+
def processed_params
|
33
|
+
return request_params unless @first_row_number
|
34
|
+
|
35
|
+
request_params[:first_row_number] = first_row_number.to_i
|
36
|
+
request_params
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def required_parameters
|
42
|
+
@definition.values.select(&:required?)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module TogglRb
|
6
|
+
module Reports
|
7
|
+
class Detailed
|
8
|
+
include TogglRb::EndpointDSL
|
9
|
+
# @param [String|Integer] workspace_id the toggl workspace id we are searching
|
10
|
+
# @param [Hash] params the options for searching for time entries
|
11
|
+
# @option params [Boolean] :grouped Whether time entries should be grouped, optional, default
|
12
|
+
# false.
|
13
|
+
# @option params [Boolean] :hide_amounts Whether amounts should be hidden, optional, default false.
|
14
|
+
# @option params [Integer] :max_duration_seconds Max duration seconds, optional, filtering attribute.
|
15
|
+
# Time Audit only, should be greater than MinDurationSeconds.
|
16
|
+
# @option params [Integer] :min_duration_seconds Min duration seconds, optional, filtering attribute.
|
17
|
+
# Time Audit only, should be less than MaxDurationSeconds.
|
18
|
+
# @option params [String] :order_by Order by field, optional, default "date". Can
|
19
|
+
# be "date", "user", "duration", "description" or
|
20
|
+
# "last_update".
|
21
|
+
# @option params [String] :order_dir Order direction, optional. Can be ASC or DESC.
|
22
|
+
# @option params [Integer] :page_size PageSize defines the number of items per page, optional,
|
23
|
+
# default 50.
|
24
|
+
# @option params [Array<String>] :postedFields -
|
25
|
+
# @option params [Array<Integer>] :project_ids Project IDs, optional, filtering attribute. To filter
|
26
|
+
# records with no projects, use [null].
|
27
|
+
# @option params [Integer] :rounding Whether time should be rounded, optional, default from
|
28
|
+
# workspace settings.
|
29
|
+
# @option params [Integer] :rounding_minutes Rounding minutes value, optional, default from workspace
|
30
|
+
# settings. Should be 0, 1, 5, 6, 10, 12, 15, 30, 60 or 240.
|
31
|
+
# @option params [String] :start_time -
|
32
|
+
# @option params [Array<Integer>] :tag_ids Tag IDs, optional, filtering attribute. To filter records
|
33
|
+
# with no tags, use [null].
|
34
|
+
# @option params [Array<Integer>] :task_ids Task IDs, optional, filtering attribute. To filter records
|
35
|
+
# with no tasks, use [null].
|
36
|
+
# @option params [Array<Integer>] :time_entry_ids TimeEntryIDs filters by time entries. This was added to
|
37
|
+
# support retro-compatibility with reports v2.
|
38
|
+
# @option params [Array<Integer>] :user_ids User IDs, optional, filtering attribute.
|
39
|
+
request_path "workspace/%<workspace_id>s/search/time_entries"
|
40
|
+
request_method :post
|
41
|
+
param :start_date, Date, :required, description: "should be less than end_date"
|
42
|
+
param :end_date, Date, description: "should be greater than start_date"
|
43
|
+
param :description, String, description: "filter by description"
|
44
|
+
param :billable, "Boolean", description: "Whether the time entry is set to visible (premium feature)"
|
45
|
+
param :client_ids, "IntegerArray",
|
46
|
+
description: "filter by client ids. Use [nil] to filter records with no clients"
|
47
|
+
param :group_ids, "IntegerArray", description: "filter by group ids"
|
48
|
+
def search_time_entries(workspace_id, params = {})
|
49
|
+
request_options = params.delete(:request_options) || {}
|
50
|
+
params_object = build_params(params)
|
51
|
+
params_object.validate_required!
|
52
|
+
resource_path = format(request_path, workspace_id: workspace_id)
|
53
|
+
response = send_request(request_method, resource_path, params_object)
|
54
|
+
get_all = request_options.fetch(:get_all, false)
|
55
|
+
|
56
|
+
return response.body_json unless get_all
|
57
|
+
|
58
|
+
all_tasks = response.body_json
|
59
|
+
|
60
|
+
while response.more?
|
61
|
+
params_object.first_row_number = response.next_row_number
|
62
|
+
response = send_request(request_method, resource_path, params_object)
|
63
|
+
all_tasks += response.body_json
|
64
|
+
end
|
65
|
+
|
66
|
+
all_tasks
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def send_request(request_method, resource_path, body)
|
72
|
+
params = body.to_json unless body.is_a?(String)
|
73
|
+
TogglRb::Response.new(connection.send(request_method, resource_path, params))
|
74
|
+
end
|
75
|
+
|
76
|
+
def connection
|
77
|
+
TogglRb::Reports.connection
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TogglRb
|
4
|
+
module Reports
|
5
|
+
class Summary
|
6
|
+
include TogglRb::EndpointDSL
|
7
|
+
|
8
|
+
request_path "workspace/%<workspace_id>s/summary/time_entries"
|
9
|
+
request_method :post
|
10
|
+
param :billable, "Boolean", optional: true, description: "Whether the time entry is set as billable."
|
11
|
+
param :client_ids, "Array<Integer>", optional: true,
|
12
|
+
description: "Client IDs, use [null] to filter records with no clients."
|
13
|
+
param :description, String, optional: true, description: "Description of the filter."
|
14
|
+
param :distinguish_rates, "Boolean", optional: true, default: false,
|
15
|
+
description: "Create new subgroups for each rate."
|
16
|
+
param :end_date, String,
|
17
|
+
description: "End date, format example time.DateOnly, must be greater than start date."
|
18
|
+
param :group_ids, "Array<Integer>", optional: true, description: "Group IDs for filtering."
|
19
|
+
param :grouping, String, optional: true, description: "Grouping option."
|
20
|
+
param :include_time_entry_ids, "Boolean", optional: true, default: false,
|
21
|
+
description: "Include time entry IDs in results."
|
22
|
+
param :max_duration_seconds, Integer, optional: true, description: "Max duration in seconds."
|
23
|
+
param :min_duration_seconds, Integer, optional: true, description: "Min duration in seconds."
|
24
|
+
param :project_ids, "Array<Integer>", optional: true, description: "Project IDs, use [null] for no projects."
|
25
|
+
param :rounding, Integer, optional: true, description: "Rounding method, default from workspace settings."
|
26
|
+
param :rounding_minutes, Integer, optional: true,
|
27
|
+
description: "Rounding minutes, should be specific values."
|
28
|
+
param :start_date, String, required: true,
|
29
|
+
description: "Start date, format example time.DateOnly, less than end date."
|
30
|
+
param :sub_grouping, String, optional: true, description: "SubGrouping option."
|
31
|
+
param :tag_ids, "Array<Integer>", optional: true, description: "Tag IDs, use [null] for no tags."
|
32
|
+
param :task_ids, "Array<Integer>", optional: true, description: "Task IDs, use [null] for no tasks."
|
33
|
+
param :time_entry_ids, "Array<Integer>", description: "Filter by time entries, for compatibility."
|
34
|
+
param :user_ids, "Array<Integer>", optional: true, description: "User IDs for filtering."
|
35
|
+
def search_time_entries(workspace_id, params = {})
|
36
|
+
params_object = build_params(params)
|
37
|
+
params_object.validate_required!
|
38
|
+
resource_path = format(request_path, workspace_id: workspace_id)
|
39
|
+
response = send_request(request_method, resource_path, params_object)
|
40
|
+
request_options = params.fetch(:request_options, {})
|
41
|
+
get_all = request_options.fetch(:get_all, false)
|
42
|
+
|
43
|
+
return response.body_json unless get_all
|
44
|
+
|
45
|
+
all_tasks = response.body_json
|
46
|
+
|
47
|
+
while response.more?
|
48
|
+
params_object.first_row_number = response.next_row_number
|
49
|
+
response = send_request(request_method, resource_path, params_object)
|
50
|
+
all_tasks += response.body_json
|
51
|
+
end
|
52
|
+
|
53
|
+
all_tasks
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def send_request(request_method, resource_path, body)
|
59
|
+
params = body.to_json unless body.is_a?(String)
|
60
|
+
TogglRb::Response.new(connection.send(request_method, resource_path, params))
|
61
|
+
end
|
62
|
+
|
63
|
+
def connection
|
64
|
+
TogglRb::Reports.connection
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TogglRb
|
4
|
+
# @class Reports provides functionality for leveraging Toggl Data Structure to generate customized reports via the
|
5
|
+
# reports from the Toggl API.
|
6
|
+
module Reports
|
7
|
+
require_relative "reports/detailed"
|
8
|
+
require_relative "reports/summary"
|
9
|
+
|
10
|
+
def self.connection
|
11
|
+
TogglRb.client.reports_connection
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TogglRb
|
4
|
+
class Response
|
5
|
+
extend Forwardable
|
6
|
+
NEXT_ROW_NUMBER_HEADER = "x-next-row-number"
|
7
|
+
|
8
|
+
def initialize(farady_response)
|
9
|
+
@farady_response = farady_response
|
10
|
+
end
|
11
|
+
|
12
|
+
def body_json
|
13
|
+
@body_json ||= JSON.parse(@farady_response.body)
|
14
|
+
end
|
15
|
+
|
16
|
+
def more?
|
17
|
+
!!next_row_number
|
18
|
+
end
|
19
|
+
|
20
|
+
def next_row_number
|
21
|
+
@farady_response.headers[NEXT_ROW_NUMBER_HEADER]
|
22
|
+
end
|
23
|
+
|
24
|
+
def_delegators :@farady_response, :status
|
25
|
+
end
|
26
|
+
end
|
data/lib/toggl_rb.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
require_relative "toggl_rb/response"
|
5
|
+
require_relative "toggl_rb/client"
|
6
|
+
require_relative "toggl_rb/config"
|
7
|
+
require_relative "toggl_rb/param"
|
8
|
+
require_relative "toggl_rb/params"
|
9
|
+
require_relative "toggl_rb/query_params"
|
10
|
+
require_relative "toggl_rb/endpoint_dsl"
|
11
|
+
require_relative "toggl_rb/reports"
|
12
|
+
require_relative "toggl_rb/core"
|
13
|
+
require_relative "toggl_rb/version"
|
14
|
+
require_relative "toggl_rb/json_patch"
|
15
|
+
|
16
|
+
module TogglRb
|
17
|
+
class Error < StandardError; end
|
18
|
+
|
19
|
+
def self.debug_logging?
|
20
|
+
config.debug_logging?
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.config
|
24
|
+
@config ||= Config.new
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.client
|
28
|
+
@client ||= Client.new
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.operation(operations = [])
|
32
|
+
JSONPatch.new(operations)
|
33
|
+
end
|
34
|
+
end
|
data/sig/toggl_rb.rbs
ADDED
metadata
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: toggl_rb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Michael Cordell
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-04-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: faraday
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.9'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.9'
|
27
|
+
description: A toggl rb API client supporting V9 version of the main API and V3 of
|
28
|
+
the reporting API
|
29
|
+
email:
|
30
|
+
- mike@mikecordell.com
|
31
|
+
executables: []
|
32
|
+
extensions: []
|
33
|
+
extra_rdoc_files: []
|
34
|
+
files:
|
35
|
+
- ".env_sample"
|
36
|
+
- ".rspec"
|
37
|
+
- ".ruby-version"
|
38
|
+
- LICENSE
|
39
|
+
- README.md
|
40
|
+
- Rakefile
|
41
|
+
- lib/toggl_rb.rb
|
42
|
+
- lib/toggl_rb/client.rb
|
43
|
+
- lib/toggl_rb/config.rb
|
44
|
+
- lib/toggl_rb/core.rb
|
45
|
+
- lib/toggl_rb/core/groups.rb
|
46
|
+
- lib/toggl_rb/core/me.rb
|
47
|
+
- lib/toggl_rb/core/projects.rb
|
48
|
+
- lib/toggl_rb/core/time_entries.rb
|
49
|
+
- lib/toggl_rb/core/users.rb
|
50
|
+
- lib/toggl_rb/endpoint_dsl.rb
|
51
|
+
- lib/toggl_rb/json_patch.rb
|
52
|
+
- lib/toggl_rb/param.rb
|
53
|
+
- lib/toggl_rb/params.rb
|
54
|
+
- lib/toggl_rb/query_params.rb
|
55
|
+
- lib/toggl_rb/reports.rb
|
56
|
+
- lib/toggl_rb/reports/detailed.rb
|
57
|
+
- lib/toggl_rb/reports/summary.rb
|
58
|
+
- lib/toggl_rb/response.rb
|
59
|
+
- lib/toggl_rb/version.rb
|
60
|
+
- sig/toggl_rb.rbs
|
61
|
+
homepage: https://github.com/mcordell/toggl_rb
|
62
|
+
licenses:
|
63
|
+
- MIT
|
64
|
+
metadata:
|
65
|
+
homepage_uri: https://github.com/mcordell/toggl_rb
|
66
|
+
source_code_uri: https://github.com/mcordell/toggl_rb
|
67
|
+
changelog_uri: https://github.com/mcordell/toggl_rb/blob/master/CHANGELOG.md
|
68
|
+
rubygems_mfa_required: 'true'
|
69
|
+
post_install_message:
|
70
|
+
rdoc_options: []
|
71
|
+
require_paths:
|
72
|
+
- lib
|
73
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 3.0.0
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
requirements: []
|
84
|
+
rubygems_version: 3.4.17
|
85
|
+
signing_key:
|
86
|
+
specification_version: 4
|
87
|
+
summary: A toggl rb API client supporting V9
|
88
|
+
test_files: []
|