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 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
@@ -0,0 +1,5 @@
1
+ export TOGGL_API_TOKEN='API_TOKEN_GOES_HERE'
2
+ export TOGGL_WORKSPACE_ID='8675309'
3
+ export TOGGL_BASIC_AUTH='BASIC AUTH CRED GOES HERE TO FILTER OUT OF VCR CASSETTES'
4
+ export TOGGL_EMAIL='test@example.net'
5
+ export TOGGL_ORGANIZATION_ID='9035768'
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
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
+ [![Ruby](https://github.com/mcordell/toggl_rb/actions/workflows/ruby.yml/badge.svg)](https://github.com/mcordell/toggl_rb/actions/workflows/ruby.yml)
4
+ [![Coverage Status](https://coveralls.io/repos/github/mcordell/toggl_rb/badge.svg)](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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TogglRb
4
+ class QueryParams < Params
5
+ end
6
+ 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TogglRb
4
+ VERSION = "0.1.0"
5
+ 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
@@ -0,0 +1,4 @@
1
+ module TogglRb
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
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: []