From 9e2e50edfafdd3dcbdc7d90179573ffa7b5b37d6 Mon Sep 17 00:00:00 2001 From: tk Date: Tue, 18 Aug 2015 03:17:37 -0500 Subject: [PATCH] Convert to gem. --- .gitignore | 5 +- .rspec | 1 + Gemfile | 7 +- Gemfile.lock | 52 ++++-- LICENSE | 21 --- LICENSE.txt | 22 +++ README.md | 55 +++++- Rakefile | 2 + VERSION | 1 - atest.rb | 22 --- lib/togglv8.rb | 285 ++++++++++++++++++++++++++++ lib/togglv8/users.rb | 29 +++ lib/togglv8/version.rb | 5 + spec/lib/togglv8_spec.rb | 46 +++++ spec/spec_helper.rb | 90 +++++++++ test.rb | 145 --------------- togglV8.rb | 392 --------------------------------------- togglv8.gemspec | 27 +++ 18 files changed, 595 insertions(+), 612 deletions(-) create mode 100644 .rspec delete mode 100644 LICENSE create mode 100644 LICENSE.txt create mode 100644 Rakefile delete mode 100644 VERSION delete mode 100755 atest.rb create mode 100644 lib/togglv8.rb create mode 100644 lib/togglv8/users.rb create mode 100644 lib/togglv8/version.rb create mode 100644 spec/lib/togglv8_spec.rb create mode 100644 spec/spec_helper.rb delete mode 100755 test.rb delete mode 100755 togglV8.rb create mode 100644 togglv8.gemspec diff --git a/.gitignore b/.gitignore index 8aa59729b..a5f3fb3d2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ -faraday.log -toggl_entities.txt -me.json \ No newline at end of file +/doc/ +/pkg/ \ No newline at end of file diff --git a/.rspec b/.rspec new file mode 100644 index 000000000..c99d2e739 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/Gemfile b/Gemfile index 3d908681d..b6fadf328 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,4 @@ source 'https://rubygems.org' -gem 'faraday', '~> 0.8.7' -gem 'awesome_print', '~> 1.1.0' -gem 'json', '~> 1.8.0' -gem 'logger', '~> 1.2.8' -gem 'jazor', '~> 0.1.8' \ No newline at end of file +# Specify your gem's dependencies in togglv8.gemspec +gemspec diff --git a/Gemfile.lock b/Gemfile.lock index 8318ab11a..9264c5636 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,25 +1,43 @@ +PATH + remote: . + specs: + togglv8 (0.2.0) + faraday (~> 0.9) + oj (~> 2.12) + GEM remote: https://rubygems.org/ specs: - awesome_print (1.1.0) - faraday (0.8.7) - multipart-post (~> 1.1) - jazor (0.1.8) - json - term-ansicolor - json (1.8.0) - logger (1.2.8) - multipart-post (1.2.0) - term-ansicolor (1.2.1) - tins (~> 0.8) - tins (0.8.0) + awesome_print (1.6.1) + diff-lcs (1.2.5) + faraday (0.9.1) + multipart-post (>= 1.2, < 3) + multipart-post (2.0.0) + oj (2.12.12) + rake (10.4.2) + rspec (3.3.0) + rspec-core (~> 3.3.0) + rspec-expectations (~> 3.3.0) + rspec-mocks (~> 3.3.0) + rspec-core (3.3.2) + rspec-support (~> 3.3.0) + rspec-expectations (3.3.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.3.0) + rspec-mocks (3.3.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.3.0) + rspec-support (3.3.0) PLATFORMS ruby DEPENDENCIES - awesome_print (~> 1.1.0) - faraday (~> 0.8.7) - jazor (~> 0.1.8) - json (~> 1.8.0) - logger (~> 1.2.8) + awesome_print (~> 1.6) + bundler (~> 1.7) + rake (~> 10.4) + rspec (~> 3.3) + togglv8! + +BUNDLED WITH + 1.10.6 diff --git a/LICENSE b/LICENSE deleted file mode 100644 index d9a66ccc3..000000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2013 Tom Kane - -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: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -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. \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 000000000..d7e7deca6 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,22 @@ +Copyright (c) 2013-2015 Tom Kane + +MIT License + +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: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +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. diff --git a/README.md b/README.md index 86379be9f..03ed02773 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,60 @@ # Toggl API v8 + [Toggl](http://www.toggl.com) is a time tracking tool. [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. **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) -# Usage -- See [test.rb](test.rb) and [atest.rb](atest.rb) for examples of calling the Toggl API with Ruby. -- See [API calls.md](API calls.md) for examples of calling the Toggl API with resty (like curl) on the command line. +## Installation + +Add this line to your application's Gemfile: + +```ruby +gem 'togglv8' +``` + +And then execute: + + $ bundle + +Or install it yourself as: + + $ gem install togglv8 + +## Usage + +This short example shows one way to create a time entry for the first workspace of the user identified by ``: + +```ruby +require 'togglv8' + +toggl = Toggl::V8.new() +user = toggl.me(all=true) +workspaces = toggl.my_workspaces(user) +workspace_id = workspaces.first['id'] +toggl.create_time_entry({description: "Workspace time entry", + wid: workspace_id, + duration: 1200, + start: "2015-08-18T01:13:40.000Z", + created_with: "My awesome Ruby application"}) +``` + +See specs for more examples. + +## Acknowledgements -# Acknowledgements - Thanks to [Koen Van der Auwera](https://github.com/atog) for the [Ruby Wrapper for Toggl API v6](https://github.com/atog/toggl) - Thanks to the Toggl team for exposing the API. -# License -Copyright (c) 2013 Tom Kane. Released under the [MIT License](http://opensource.org/licenses/mit-license.php). See [LICENSE](LICENSE) for details. \ No newline at end of file +## Contributing + +1. Fork it ( https://github.com/[my-github-username]/togglv8/fork ) +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Add some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create a new Pull Request + +## License + +Copyright (c) 2013-2015 Tom Kane. Released under the [MIT License](http://opensource.org/licenses/mit-license.php). See [LICENSE.txt](LICENSE.txt) for details. \ No newline at end of file diff --git a/Rakefile b/Rakefile new file mode 100644 index 000000000..809eb5616 --- /dev/null +++ b/Rakefile @@ -0,0 +1,2 @@ +require "bundler/gem_tasks" + diff --git a/VERSION b/VERSION deleted file mode 100644 index 446ba66e7..000000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.1.4 \ No newline at end of file diff --git a/atest.rb b/atest.rb deleted file mode 100755 index 62e3b79a1..000000000 --- a/atest.rb +++ /dev/null @@ -1,22 +0,0 @@ -#! /usr/bin/env rvm ruby-1.9.3-head do ruby -# encoding: utf-8 - -require_relative 'togglV8' -require 'time' - -tog = Toggl.new - -if __FILE__ == $0 - te = tog.get_time_entries(Time.new(2012, 1, 1)) - te.each do |t| - puts "#{t['wid']} #{t['description']} #{t['id']}" - end - - tp = {} - ts = tog.tasks(282224) - ts.each do |t| - tp[t['id']] = t['name'] - # puts t - end - ap tp -end \ No newline at end of file diff --git a/lib/togglv8.rb b/lib/togglv8.rb new file mode 100644 index 000000000..a0cb93f58 --- /dev/null +++ b/lib/togglv8.rb @@ -0,0 +1,285 @@ +require 'faraday' +require 'logger' +require 'oj' +require 'awesome_print' # for debug output + +require_relative 'togglv8/version' +require_relative 'togglv8/users' + +# :compat mode will convert symbols to strings +Oj.default_options = { mode: :compat } + +module Toggl + TOGGL_API_URL = 'https://www.toggl.com/api/' + + class V8 + TOGGL_API_V8_URL = TOGGL_API_URL + 'v8/' + API_TOKEN = 'api_token' + + attr_accessor :conn + + def initialize(username=nil, password=API_TOKEN, opts={}) + if username.nil? && password == API_TOKEN + toggl_api_file = ENV['HOME']+'/.toggl' + if FileTest.exist?(toggl_api_file) then + username = IO.read(toggl_api_file) + else + raise SystemCallError, + "\tExpecting 1) api_token in file ~/.toggl, or 2) (api_token), or 3) (username, password).\n" + + "\tSee https://github.com/toggl/toggl_api_docs/blob/master/chapters/authentication.md" + end + end + + @conn = Toggl::V8.connection(username, password, opts) + end + + def self.connection(username, password, opts={}) + Faraday.new(url: TOGGL_API_V8_URL, ssl: {verify: true}) do |faraday| + faraday.request :url_encoded + faraday.response :logger, Logger.new('faraday.log') if opts[:log] + faraday.adapter Faraday.default_adapter + faraday.headers = { "Content-Type" => "application/json" } + faraday.basic_auth username, password + end + end + + def debug_on(debug=true) + puts "debugging is %s" % [debug ? "ON" : "OFF"] + @debug = debug + end + + def checkParams(params, fields=[]) + raise ArgumentError, 'params is not a Hash' unless params.is_a? Hash + return if fields.empty? + errors = [] + for f in fields + errors.push("params[#{f}] is required") unless params.has_key?(f) + end + raise ArgumentError, errors.join(', ') if !errors.empty? + end + + +#----------# +#--- Me ---# +#----------# + + def me(all=nil) + # TODO: Reconcile this with get_client_projects + res = get "me%s" % [all.nil? ? "" : "?with_related_data=#{all}"] + end + + def my_clients(user) + user['projects'] + end + + def my_projects(user) + user['projects'] + end + + def my_tags(user) + user['tags'] + end + + def my_time_entries(user) + user['time_entries'] + end + + def my_workspaces(user) + user['workspaces'] + end + + +#---------------# +#--- Clients ---# +#---------------# + +# name : The name of the client (string, required, unique in workspace) +# wid : workspace ID, where the client will be used (integer, required) +# notes : Notes for the client (string, not required) +# hrate : The hourly rate for this client (float, not required, available only for pro workspaces) +# cur : The name of the client's currency (string, not required, available only for pro workspaces) +# at : timestamp that is sent in the response, indicates the time client was last updated + + def create_client(params) + checkParams(params, [:name, :wid]) + post "clients", {client: params} + end + + def get_client(client_id) + get "clients/#{client_id}" + end + + def update_client(client_id, params) + put "clients/#{client_id}", {client: params} + end + + def delete_client(client_id) + delete "clients/#{client_id}" + end + + def get_client_projects(client_id, params={}) + active = params.has_key?(:active) ? "?active=#{params[:active]}" : "" + get "clients/#{client_id}/projects#{active}" + end + + +#--------------------# +#--- Time entries ---# +#--------------------# +# +# https://github.com/toggl/toggl_api_docs/blob/master/chapters/time_entries.md +# +# description : (string, strongly suggested to be used) +# wid : workspace ID (integer, required if pid or tid not supplied) +# pid : project ID (integer, not required) +# tid : task ID (integer, not required) +# billable : (boolean, not required, default false, available for pro workspaces) +# start : time entry start time (string, required, ISO 8601 date and time) +# stop : time entry stop time (string, not required, ISO 8601 date and time) +# duration : time entry duration in seconds. If the time entry is currently running, +# the duration attribute contains a negative value, +# denoting the start of the time entry in seconds since epoch (Jan 1 1970). +# The correct duration can be calculated as current_time + duration, +# where current_time is the current time in seconds since epoch. (integer, required) +# created_with : the name of your client app (string, required) +# tags : a list of tag names (array of strings, not required) +# duronly : should Toggl show the start and stop time of this time entry? (boolean, not required) +# at : timestamp that is sent in the response, indicates the time item was last updated + + def create_time_entry(params) + checkParams(params, [:description, :start, :duration, :created_with]) + if !params.has_key?(:wid) and !params.has_key?(:pid) and !params.has_key?(:tid) then + raise ArgumentError, "one of params['wid'], params['pid'], params['tid'] is required" + end + post "time_entries", {time_entry: params} + end + + def start_time_entry(params) + if !params.has_key?(:wid) and !params.has_key?(:pid) and !params.has_key?(:tid) then + raise ArgumentError, "one of params['wid'], params['pid'], params['tid'] is required" + end + post "time_entries/start", {time_entry: params} + end + + def stop_time_entry(time_entry_id) + put "time_entries/#{time_entry_id}/stop", {} + end + + def get_time_entry(time_entry_id) + get "time_entries/#{time_entry_id}" + end + + def update_time_entry(time_entry_id, params) + put "time_entries/#{time_entry_id}", {time_entry: params} + end + + def delete_time_entry(time_entry_id) + delete "time_entries/#{time_entry_id}" + end + + def iso8601(date) + return nil if date.nil? + if date.is_a?(Time) or date.is_a?(Date) + iso = date.iso8601 + elsif date.is_a?(String) + iso = DateTime.parse(date).iso8601 + else + raise ArgumentError, "Can't convert #{date.class} to ISO-8601 Date/Time" + end + return Faraday::Utils.escape(iso) + end + + def get_time_entries(start_date=nil, end_date=nil) + params = [] + params.push("start_date=#{iso8601(start_date)}") if !start_date.nil? + params.push("end_date=#{iso8601(end_date)}") if !end_date.nil? + get "time_entries%s" % [params.empty? ? "" : "?#{params.join('&')}"] + end + + +#------------------# +#--- Workspaces ---# +#------------------# + +# name : (string, required) +# premium : If it's a pro workspace or not. Shows if someone is paying for the workspace or not (boolean, not required) +# at : timestamp that is sent in the response, indicates the time item was last updated + + def workspaces + get "workspaces" + end + + def clients(workspace=nil) + if workspace.nil? + get "clients" + else + get "workspaces/#{workspace}/clients" + end + end + + def projects(workspace, params={}) + active = params.has_key?(:active) ? "?active=#{params[:active]}" : "" + get "workspaces/#{workspace}/projects#{active}" + end + + def users(workspace) + get "workspaces/#{workspace}/users" + end + + def tasks(workspace, params={}) + active = params.has_key?(:active) ? "?active=#{params[:active]}" : "" + get "workspaces/#{workspace}/tasks#{active}" + end + +#---------------# +#--- Private ---# +#---------------# + + private + + def get(resource) + puts " ----------- " if @debug + puts "GET #{resource}" if @debug + full_res = self.conn.get(resource) + ap full_res.env if @debug + res = Oj.load(full_res.env[:body]) + res.is_a?(Array) || res['data'].nil? ? res : res['data'] + end + + def post(resource, data='') + puts " ----------- " if @debug + puts "POST #{resource} / #{data}" if @debug + full_res = self.conn.post(resource, Oj.dump(data)) + ap full_res if @debug + ap full_res.env if @debug + if (200 == full_res.env[:status]) then + res = Oj.load(full_res.env[:body]) + res['data'].nil? ? res : res['data'] + return res + else + msg = "POST #{full_res.env[:url]} (status: #{full_res.env[:status]})" + msg += "\n\tERROR: #{full_res.env[:body]}" + raise msg + end + end + + def put(resource, data='') + puts " ----------- " if @debug + puts "PUT #{resource} / #{Oj.dump(data)}" if @debug + full_res = self.conn.put(resource, Oj.dump(data)) + ap full_res.env if @debug + res = Oj.load(full_res.env[:body]) + res['data'].nil? ? res : res['data'] + end + + def delete(resource) + puts " ----------- " if @debug + puts "DELETE #{resource}" if @debug + full_res = self.conn.delete(resource) + ap full_res.env if @debug + (200 == full_res.env[:status]) ? "" : eval(full_res.env[:body]) + end + + end + +end diff --git a/lib/togglv8/users.rb b/lib/togglv8/users.rb new file mode 100644 index 000000000..ac53e0d30 --- /dev/null +++ b/lib/togglv8/users.rb @@ -0,0 +1,29 @@ +module Toggl + class V8 + + def me(all=nil) + # TODO: Reconcile this with get_client_projects + res = get "me%s" % [all.nil? ? "" : "?with_related_data=#{all}"] + end + + def my_clients(user) + user['projects'] + end + + def my_projects(user) + user['projects'] + end + + def my_tags(user) + user['tags'] + end + + def my_time_entries(user) + user['time_entries'] + end + + def my_workspaces(user) + user['workspaces'] + end + end +end \ No newline at end of file diff --git a/lib/togglv8/version.rb b/lib/togglv8/version.rb new file mode 100644 index 000000000..5613af988 --- /dev/null +++ b/lib/togglv8/version.rb @@ -0,0 +1,5 @@ +module Toggl + class V8 + VERSION = "0.2.0" + end +end diff --git a/spec/lib/togglv8_spec.rb b/spec/lib/togglv8_spec.rb new file mode 100644 index 000000000..c75c576cc --- /dev/null +++ b/spec/lib/togglv8_spec.rb @@ -0,0 +1,46 @@ +require_relative '../../lib/togglv8' +require 'oj' + +describe Toggl do + before :all do + @toggl = Toggl::V8.new('4880adbe1bee9a241fa08070d33bd49f') + @user = @toggl.me(all=true) + end + + it 'can return /me' do + expect(@user).to_not be_nil + expect(@user['id']).to eq 1820939 + expect(@user['fullname']).to eq 'togglv8' + expect(@user['default_wid']).to eq 1060392 + expect(@user['image_url']).to eq 'https://assets.toggl.com/avatars/a5d106126b6bed8df283e708af0828ee.png' + expect(@user['timezone']).to eq 'Etc/UTC' + expect(@user['workspaces'].length).to eq 1 + expect(@user['workspaces'].first['name']).to eq "togglv8's workspace" + end + + it 'can return /my_clients' do + my_clients = @toggl.my_clients(@user) + expect(my_clients).to be nil + end + + it 'can return /my_projects' do + my_projects = @toggl.my_projects(@user) + expect(my_projects).to be nil + end + + it 'can return /my_tags' do + my_tags = @toggl.my_tags(@user) + expect(my_tags).to be nil + end + + it 'can return /my_time_entries' do + my_time_entries = @toggl.my_time_entries(@user) + expect(my_time_entries).to be nil + end + + it 'can return /my_workspaces' do + my_workspaces = @toggl.my_workspaces(@user) + expect(my_workspaces.length).to eq 1 + end + +end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 000000000..140de7600 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,90 @@ +# This file was generated by the `rspec --init` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause this +# file to always be loaded, without a need to explicitly require it in any files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need it. +# +# The `.rspec` file also contains a few flags that are not defaults but that +# users commonly want. +# +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + expectations.syntax = :expect + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # These two settings work together to allow you to limit a spec run + # to individual examples or groups you care about by tagging them with + # `:focus` metadata. When nothing is tagged with `:focus`, all examples + # get run. + config.filter_run :focus + config.run_all_when_everything_filtered = true + + # Limits the available syntax to the non-monkey patched syntax that is recommended. + # For more details, see: + # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax + # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching + config.disable_monkey_patching! + + # This setting enables warnings. It's recommended, but in some cases may + # be too noisy due to issues in dependencies. + config.warnings = true + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = 'doc' + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end +end diff --git a/test.rb b/test.rb deleted file mode 100755 index 731202069..000000000 --- a/test.rb +++ /dev/null @@ -1,145 +0,0 @@ -#! /usr/bin/env rvm ruby-1.9.3-head do ruby -# encoding: utf-8 - -require_relative 'togglV8' -require 'awesome_print' -require 'time' - -tog = Toggl.new - -# tog = Toggl.new(toggl_api_key) -# tog = Toggl.new(username, password) - -def print_projects(tog) - ws = tog.workspaces - ws.each do |w| - wid = w['id'] - @wname=w['name'] - - tp = Hash.new {|h,k| h[k]=[]} - - ts = tog.tasks(wid) - ts.each do |t| - tp[t['pid']] << t['name'] - # puts t - end - - ps = tog.projects(wid) - ps.each do |p| - @pname = p['name'] + " [" + p['id'].to_s + "] " - @tname = tp[p['id']] ? ' (' + tp[p['id']].to_s + ')' : '' - puts "|Project| #@wname - #@pname#@tname" - # puts p - end - - cs = tog.clients(wid) - cs.each do |c| - @cname = c['name'] + " [" + c['id'].to_s + "] " - puts "|Client| #@wname - #@cname" - # puts c - end - - # us = tog.users(wid) - # us.each do |u| - # @uname = u['fullname'] + "/" + u['email'] - # puts "|User| #@wname : #@uname" - # # puts u - # end - end -end - -#---- Workspaces ----# -# tk : 282224 # -# HomeAway : 344974 # -#------- User -------# -# uid : 360643 # -#--------------------# - -if __FILE__ == $0 - tog.debug_on - - # print_projects(tog) - - # ap tog.me - # ap tog.me(true) - # ap tog.me('false') - # ap tog.get_project(2882160) - - user = tog.me(true) - ap tog.my_clients(user) - ap tog.my_projects(user) - ap tog.my_tags(user) - ap tog.my_time_entries(user) - ap tog.my_workspaces(user) - - # ap tog.clients - # ap tog.clients(282224) - # ap tog.clients(344974) - # ap tog.create_client({name: "✈ Brazil", wid: 282224}) - # ap tog.update_client(1150763, {notes: "updated notes"}) - # ap tog.get_client(1150763) - # ap tog.delete_client(1101640) - - # ap tog.get_client_projects(1101632, {active: 'bot'}) - # ap tog.get_client_projects(1101640, {active: 'true'}) - # ap tog.get_client_projects(1150638, {active: 'false'}) - # ap tog.get_client_projects(1150488) - # ap tog.get_client_projects(1101650, {active: ''}) - - # ap tog.create_project({:name => "HUGE project", :wid => "282224"}) - # ap tog.projects(282224, {active: true}) - # ap tog.projects(344974, {active: 'both'}) - # ap tog.get_project(2931253) - # ap tog.update_project(2931253, {name: "Project %s" % Time.now.utc.iso8601 , active: false}) - # ap tog.get_project_users(2931296) - - # ap tog.create_project_user({pid: 2931296, uid: 509726}) - # ap tog.update_project_user(8310837, {manager: false, rate: 7}) - # ap tog.delete_project_user(8310837) - - # ap tog.create_time_entry({description: "Workspace time entry", duration:1200, start: "2013-03-05T07:55:55.000Z", wid:282224, created_with:"testing https://github.com/kanet77/togglv8"}) - # ap tog.create_time_entry({description: "Project time entry", duration:600, start: "2013-03-06T08:44:44.000Z", pid:2931296, created_with:"testing https://github.com/kanet77/togglv8"}) - # ap tog.create_time_entry({description: "Task time entry", duration:300, start: "2013-03-07T09:33:33.000Z", tid:1922686, created_with:"testing https://github.com/kanet77/togglv8"}) - - # ap tog.start_time_entry({wid:282224}) - # ap tog.stop_time_entry(86238653) - - # ap tog.get_time_entry(77633704) - # ap tog.get_time_entry(77633705) - # ap tog.get_time_entry(77633706) - # ap tog.update_time_entry(77633704, {description: "Workspace time entry - updated", duration:1300}) - # ap tog.update_time_entry(77633705, {description: "Project time entry - updated", duration:700}) - # ap tog.update_time_entry(77633706, {description: "Task time entry - updated", duration:400}) - # ap tog.delete_time_entry(77633704) - # ap tog.delete_time_entry(77633705) - # ap tog.delete_time_entry(77633706) - # ap tog.delete_time_entry(86238653) - - # ap tog.get_time_entries - # ap tog.get_time_entries("2013-06-04T18:32:12+00:00") - # ap tog.get_time_entries("2013-06-04T18:32:12+00:00", Time.new(2013, 6, 5, 2, 2, 2, "+02:00")) - - # tag = tog.create_tag({name: "tigger tag",wid: 282224}) - # ap tag - # ap tog.update_tag(tag["id"], {name: tag["name"].upcase}) - # ap tog.delete_tag(tag["id"]) - - # ap tog.create_task({}) # ERRORS - # ap tog.create_task({name: "TASK 4", pid: 2883126}) - # ap tog.create_task({name: "TASK 2", pid: 2883126}) - # ap tog.create_task({name: "TASK 3", pid: 2883126}) - # ap tog.update_task(1894758, {active: true, estimated_seconds: 45000, fields: "done_seconds,uname"}) - # ap tog.update_task(1894758, 1894732, {active: false}) - # ap tog.tasks(282224) - # ap tog.tasks(282224, {active: :true}) - # ap tog.tasks(282224, {active: false}) - # ap tog.tasks(282224, {active: 'both'}) - # ap tog.tasks(344974, {active: 'both'}) - # ap tog.get_task(2882160) - ap tog.delete_task(1893146) - # ap tog.delete_task(1922691) - # ap tog.delete_task(1922690, 1922688) - - # ap tog.users(282224) - -end \ No newline at end of file diff --git a/togglV8.rb b/togglV8.rb deleted file mode 100755 index 8ef6b1df7..000000000 --- a/togglV8.rb +++ /dev/null @@ -1,392 +0,0 @@ -#! /usr/bin/env rvm ruby-1.9.3-head do ruby -# encoding: utf-8 - -require 'rubygems' -require 'logger' -require 'faraday' -require 'json' - -require 'awesome_print' # for debug output - -class Toggl - attr_accessor :conn, :debug - - def initialize(username=nil, password='api_token', debug=nil) - self.debug_on(debug) if !debug.nil? - if (password.to_s == 'api_token' && username.to_s == '') - toggl_api_file = ENV['HOME']+'/.toggl' - if FileTest.exist?(toggl_api_file) then - username = IO.read(toggl_api_file) - else - raise SystemCallError, "Expecting api_token in file ~/.toggl or parameters (api_token) or (username, password)" - end - end - - @conn = connection(username, password) - end - - def connection(username, password) - Faraday.new(url: 'https://www.toggl.com/api/v8') do |faraday| - faraday.request :url_encoded - faraday.response :logger, Logger.new('faraday.log') - faraday.adapter Faraday.default_adapter - faraday.headers = {"Content-Type" => "application/json"} - faraday.basic_auth username, password - end - end - - def debug_on(debug=true) - puts "debugging is %s" % [debug ? "ON" : "OFF"] - @debug = debug - end - - def checkParams(params, fields=[]) - raise ArgumentError, 'params is not a Hash' unless params.is_a? Hash - return if fields.empty? - errors = [] - for f in fields - errors.push("params[#{f}] is required") unless params.has_key?(f) - end - raise ArgumentError, errors.join(', ') if !errors.empty? - end - -#----------# -#--- Me ---# -#----------# - - def me(all=nil) - # TODO: Reconcile this with get_client_projects - res = get "me%s" % [all.nil? ? "" : "?with_related_data=#{all}"] - end - - def my_clients(user) - user['projects'] - end - - def my_projects(user) - user['projects'] - end - - def my_tags(user) - user['tags'] - end - - def my_time_entries(user) - user['time_entries'] - end - - def my_workspaces(user) - user['workspaces'] - end - -#---------------# -#--- Clients ---# -#---------------# - -# name : The name of the client (string, required, unique in workspace) -# wid : workspace ID, where the client will be used (integer, required) -# notes : Notes for the client (string, not required) -# hrate : The hourly rate for this client (float, not required, available only for pro workspaces) -# cur : The name of the client's currency (string, not required, available only for pro workspaces) -# at : timestamp that is sent in the response, indicates the time client was last updated - - def create_client(params) - checkParams(params, [:name, :wid]) - post "clients", {client: params} - end - - def get_client(client_id) - get "clients/#{client_id}" - end - - def update_client(client_id, params) - put "clients/#{client_id}", {client: params} - end - - def delete_client(client_id) - delete "clients/#{client_id}" - end - - def get_client_projects(client_id, params={}) - active = params.has_key?(:active) ? "?active=#{params[:active]}" : "" - get "clients/#{client_id}/projects#{active}" - end - - -#----------------# -#--- Projects ---# -#----------------# - -# name : The name of the project (string, required, unique for client and workspace) -# wid : workspace ID, where the project will be saved (integer, required) -# cid : client ID(integer, not required) -# active : whether the project is archived or not (boolean, by default true) -# is_private : whether project is accessible for only project users or for all workspace users (boolean, default true) -# template : whether the project can be used as a template (boolean, not required) -# template_id : id of the template project used on current project's creation -# billable : whether the project is billable or not (boolean, default true, available only for pro workspaces) -# at : timestamp that is sent in the response for PUT, indicates the time task was last updated -# -- Undocumented -- -# color : number (in the range 0-23?) - - def create_project(params) - checkParams(params, [:name, :wid]) - post "projects", {project: params} - end - - def get_project(project_id) - get "projects/#{project_id}" - end - - def update_project(project_id, params) - put "projects/#{project_id}", {project: params} - end - - def get_project_users(project_id) - get "projects/#{project_id}/project_users" - end - -#---------------------# -#--- Project users ---# -#---------------------# - -# pid : project ID (integer, required) -# uid : user ID, who is added to the project (integer, required) -# wid : workspace ID, where the project belongs to (integer, not-required, project's workspace id is used) -# manager : admin rights for this project (boolean, default false) -# rate : hourly rate for the project user (float, not-required, only for pro workspaces) in the currency of the project's client or in workspace default currency. -# at : timestamp that is sent in the response, indicates when the project user was last updated -# -- Additional fields -- -# fullname : full name of the user, who is added to the project - - def create_project_user(params) - checkParams(params, [:pid, :uid]) - params[:fields] = "fullname" # for simplicity, always request fullname field - post "project_users", {project_user: params} - end - - def update_project_user(project_user_id, params) - params[:fields] = "fullname" # for simplicity, always request fullname field - put "project_users/#{project_user_id}", {project_user: params} - end - - def delete_project_user(project_user_id) - delete "project_users/#{project_user_id}" - end - -#------------# -#--- Tags ---# -#------------# - -# name : The name of the tag (string, required, unique in workspace) -# wid : workspace ID, where the tag will be used (integer, required) - - def create_tag(params) - checkParams(params, [:name, :wid]) - post "tags", {tag: params} - end - - # ex: update_tag(12345, {name: "same tame game"}) - def update_tag(tag_id, params) - put "tags/#{tag_id}", {tag: params} - end - - def delete_tag(tag_id) - delete "tags/#{tag_id}" - end - -#-------------# -#--- Tasks ---# -#-------------# - -# name : The name of the task (string, required, unique in project) -# pid : project ID for the task (integer, required) -# wid : workspace ID, where the task will be saved (integer, project's workspace id is used when not supplied) -# uid : user ID, to whom the task is assigned to (integer, not required) -# estimated_seconds : estimated duration of task in seconds (integer, not required) -# active : whether the task is done or not (boolean, by default true) -# at : timestamp that is sent in the response for PUT, indicates the time task was last updated -# -- Additional fields -- -# done_seconds : duration (in seconds) of all the time entries registered for this task -# uname : full name of the person to whom the task is assigned to - - def create_task(params) - checkParams(params, [:name, :pid]) - post "tasks", {task: params} - end - - def get_task(task_id) - get "tasks/#{task_id}" - end - - # ex: update_task(1894675, {active: true, estimated_seconds: 4500, fields: "done_seconds,uname"}) - def update_task(*task_id, params) - put "tasks/#{task_id.join(',')}", {task: params} - end - - def delete_task(*task_id) - delete "tasks/#{task_id.join(',')}" - end - -#--------------------# -#--- Time entries ---# -#--------------------# - -# description : (string, required) -# wid : workspace ID (integer, required if pid or tid not supplied) -# pid : project ID (integer, not required) -# tid : task ID (integer, not required) -# billable : (boolean, not required, default false, available for pro workspaces) -# start : time entry start time (string, required, ISO 8601 date and time) -# stop : time entry stop time (string, not required, ISO 8601 date and time) -# duration : time entry duration in seconds. If the time entry is currently running, the duration attribute contains a negative value, denoting the start of the time entry in seconds since epoch (Jan 1 1970). The correct duration can be calculated as current_time + duration, where current_time is the current time in seconds since epoch. (integer, required) -# created_with : the name of your client app (string, required) -# tags : a list of tag names (array of strings, not required) -# duronly : should Toggl show the start and stop time of this time entry? (boolean, not required) -# at : timestamp that is sent in the response, indicates the time item was last updated - - def create_time_entry(params) - checkParams(params, [:description, :start, :created_with]) - if !params.has_key?(:wid) and !params.has_key?(:pid) and !params.has_key?(:tid) then - raise ArgumentError, "one of params['wid'], params['pid'], params['tid'] is required" - end - post "time_entries", {time_entry: params} - end - - def start_time_entry(params) - if !params.has_key?(:wid) and !params.has_key?(:pid) and !params.has_key?(:tid) then - raise ArgumentError, "one of params['wid'], params['pid'], params['tid'] is required" - end - post "time_entries/start", {time_entry: params} - end - - def stop_time_entry(time_entry_id) - put "time_entries/#{time_entry_id}/stop", {} - end - - def get_time_entry(time_entry_id) - get "time_entries/#{time_entry_id}" - end - - def update_time_entry(time_entry_id, params) - put "time_entries/#{time_entry_id}", {time_entry: params} - end - - def delete_time_entry(time_entry_id) - delete "time_entries/#{time_entry_id}" - end - - def iso8601(date) - return nil if date.nil? - if date.is_a?(Time) or date.is_a?(Date) - iso = date.iso8601 - elsif date.is_a?(String) - iso = DateTime.parse(date).iso8601 - else - raise ArgumentError, "Can't convert #{date.class} to ISO-8601 Date/Time" - end - return Faraday::Utils.escape(iso) - end - - def get_time_entries(start_date=nil, end_date=nil) - params = [] - params.push("start_date=#{iso8601(start_date)}") if !start_date.nil? - params.push("end_date=#{iso8601(end_date)}") if !end_date.nil? - get "time_entries%s" % [params.empty? ? "" : "?#{params.join('&')}"] - end - -#-------------# -#--- Users ---# -#-------------# - -# api_token : (string) -# default_wid : default workspace id (integer) -# email : (string) -# jquery_timeofday_format : (string) -# jquery_date_format :(string) -# timeofday_format : (string) -# date_format : (string) -# store_start_and_stop_time : whether start and stop time are saved on time entry (boolean) -# beginning_of_week : (integer, Sunday=0) -# language : user's language (string) -# image_url : url with the user's profile picture(string) -# sidebar_piechart : should a piechart be shown on the sidebar (boolean) -# at : timestamp of last changes -# new_blog_post : an object with toggl blog post title and link - -#------------------# -#--- Workspaces ---# -#------------------# - -# name : (string, required) -# premium : If it's a pro workspace or not. Shows if someone is paying for the workspace or not (boolean, not required) -# at : timestamp that is sent in the response, indicates the time item was last updated - - def workspaces - get "workspaces" - end - - def clients(workspace=nil) - if workspace.nil? - get "clients" - else - get "workspaces/#{workspace}/clients" - end - end - - def projects(workspace, params={}) - active = params.has_key?(:active) ? "?active=#{params[:active]}" : "" - get "workspaces/#{workspace}/projects#{active}" - end - - def users(workspace) - get "workspaces/#{workspace}/users" - end - - def tasks(workspace, params={}) - active = params.has_key?(:active) ? "?active=#{params[:active]}" : "" - get "workspaces/#{workspace}/tasks#{active}" - end - -#---------------# -#--- Private ---# -#---------------# - - private - - def get(resource) - puts "GET #{resource}" if @debug - full_res = self.conn.get(resource) - # ap full_res.env if @debug - res = JSON.parse(full_res.env[:body]) - res.is_a?(Array) || res['data'].nil? ? res : res['data'] - end - - def post(resource, data) - puts "POST #{resource} / #{data}" if @debug - full_res = self.conn.post(resource, JSON.generate(data)) - ap full_res.env if @debug - if (200 == full_res.env[:status]) then - res = JSON.parse(full_res.env[:body]) - res['data'].nil? ? res : res['data'] - else - eval(full_res.env[:body]) - end - end - - def put(resource, data) - puts "PUT #{resource} / #{data}" if @debug - full_res = self.conn.put(resource, JSON.generate(data)) - # ap full_res.env if @debug - res = JSON.parse(full_res.env[:body]) - res['data'].nil? ? res : res['data'] - end - - def delete(resource) - puts "DELETE #{resource}" if @debug - full_res = self.conn.delete(resource) - # ap full_res.env if @debug - (200 == full_res.env[:status]) ? "" : eval(full_res.env[:body]) - end - -end \ No newline at end of file diff --git a/togglv8.gemspec b/togglv8.gemspec new file mode 100644 index 000000000..0e1d95603 --- /dev/null +++ b/togglv8.gemspec @@ -0,0 +1,27 @@ +# coding: utf-8 +lib = File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'togglv8/version' + +Gem::Specification.new do |spec| + spec.name = "togglv8" + spec.version = Toggl::V8::VERSION + spec.authors = ["Tom Kane"] + spec.email = ["kexf7pqsdu@snkmail.com"] + spec.summary = %q{Toggl v8 API wrapper (See https://github.com/toggl/toggl_api_docs)} + spec.homepage = "https://github.com/kanet77/togglv8" + spec.license = "MIT" + + spec.files = `git ls-files -z`.split("\x0") + spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } + spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) + spec.require_paths = ["lib"] + + spec.add_development_dependency "bundler", "~> 1.7" + spec.add_development_dependency "rake", "~> 10.4" + spec.add_development_dependency "rspec", "~> 3.3" + spec.add_development_dependency "awesome_print", "~> 1.6" + + spec.add_dependency "faraday", "~> 0.9" + spec.add_dependency "oj", "~>2.12" +end