beeminder 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ vendor
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in beeminder-gem.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1 @@
1
+ Do whatever you want. This is just a simple convenience gem, not some military secret.
data/README ADDED
@@ -0,0 +1,47 @@
1
+ # Beeminder
2
+
3
+ Convenient access to [Beeminder](http://www.beeminder.com)'s API.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'beeminder'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install beeminder-gem
18
+
19
+ ## Usage
20
+
21
+ First, get your token [here](https://www.beeminder.com/api/v1/auth_token.json) and log in:
22
+
23
+ bee = Beeminder::User.new "username", "token"
24
+
25
+ Now you can do a bunch of stuff. You'll probably want to send a new datapoint:
26
+
27
+ # short form
28
+ bee.send "weight", 86.3
29
+
30
+ # long form
31
+ goal = bee.goal "weight"
32
+ dp = Beeminder::Datapoint.new :value => 86.3, :comment => "I loves cheeseburgers :3"
33
+ goal.add dp
34
+
35
+ Or you can find all goals of a certain type:
36
+
37
+ odometer_goals = bee.goals.select {|g| g.goal_type == :biker}
38
+
39
+ Or maybe show the last updated graph in a widget somewhere:
40
+
41
+ puts bee.goals.max_by(:updated_at).graph_url
42
+
43
+ There's also a simple tool called `beemind` to update graphs:
44
+
45
+ $ beemind pushups 4
46
+
47
+ Check the [gem doc]() and [API](https://www.beeminder.com/api-docs) for what else you can do.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/TODO ADDED
@@ -0,0 +1,6 @@
1
+ * TODO [3/5]
2
+ - [X] document code
3
+ - [ ] handle oauth tokens?
4
+ - [X] beemind binary
5
+ - [X] implement actual API calls
6
+ - [ ] add a few more sanity checks
data/beeminder.gemspec ADDED
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'beeminder/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "beeminder"
8
+ gem.version = Beeminder::VERSION
9
+ gem.authors = ["muflax"]
10
+ gem.email = ["mail@muflax.com"]
11
+ gem.description = "Convenient access to Beeminder's API."
12
+ gem.summary = "access Beeminder API"
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.require_paths = ["lib"]
18
+
19
+ gem.add_dependency 'highline', '~> 1.6'
20
+ end
data/bin/beemind ADDED
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env ruby
2
+ # coding: utf-8
3
+
4
+ require 'yaml'
5
+
6
+ # load library
7
+ file = File.symlink?(__FILE__) ? File.readlink(__FILE__) : __FILE__
8
+ lib = File.join File.dirname(file), "/../lib/beeminder"
9
+
10
+ if File.exists? lib
11
+ # using local version
12
+ require lib
13
+ else
14
+ require 'beeminder'
15
+ end
16
+
17
+ config = "#{Dir.home}/.beeminderrc"
18
+ if not File.exists? config
19
+ # create config
20
+ require 'highline/import'
21
+ yaml = {
22
+ "account" => ask("Beeminder account:"),
23
+ "token" => ask("Auth token:")
24
+ }
25
+ File.open(config, "w+") {|f| YAML.dump yaml, f}
26
+ File.chmod 0600, config
27
+ puts "Written config to '#{config}.'"
28
+ end
29
+
30
+ # load config
31
+ yaml = YAML.load File.open(config)
32
+
33
+ # login
34
+ bee = Beeminder::User.new yaml["account"], yaml["token"]
35
+
36
+ if not (2..3).include? ARGV.size
37
+ puts "usage: beemind goal value [comment]"
38
+ exit 1
39
+ end
40
+
41
+ goal, value, comment = ARGV
42
+
43
+ bee.send goal, value, comment || ""
data/lib/beeminder.rb ADDED
@@ -0,0 +1,11 @@
1
+ # coding: utf-8
2
+
3
+ require 'date'
4
+ require 'json'
5
+ require 'net/https'
6
+ require 'uri'
7
+
8
+ # local libs
9
+ Dir["#{File.join(File.dirname(__FILE__), "beeminder")}/*.rb"].each do |lib|
10
+ require lib
11
+ end
@@ -0,0 +1,198 @@
1
+ # coding: utf-8
2
+
3
+ module Beeminder
4
+ class Goal
5
+ # @return [String] The final part of the URL of the goal, used as an identifier.
6
+ attr_accessor :slug
7
+
8
+ # @return [DateTime] Last time this goal was updated.
9
+ attr_reader :updated_at
10
+
11
+ # @return [String] The title that the user specified for the goal.
12
+ attr_accessor :title
13
+
14
+ # @return [DateTime] Goal date.
15
+ attr_reader :goaldate
16
+
17
+ # @return [Numeric] Goal value.
18
+ attr_reader :goalval
19
+
20
+ # @return [Numeric] The slope of the (final section of the) yellow brick road.
21
+ attr_reader :rate
22
+
23
+ # @return [Symbol] One of the following symbols:
24
+ # - `:fatloser`: Weight loss
25
+ # - `:hustler`: Do More
26
+ # - `:biker`: Odometer
27
+ # - `:inboxer`: Inbox Fewer
28
+ # - `:gainer`: Gain Weight
29
+ # - `:drinker`: Set a Limit
30
+ # - `:custom`: Full access to the underlying goal parameters
31
+ attr_reader :goal_type
32
+
33
+ # @return [DateTime] Date of derailment.
34
+ attr_reader :losedate
35
+
36
+ # @return [String] URL for the goal's graph image.
37
+ attr_reader :graph_url
38
+
39
+ # @return [Numeric] Panic threshold. How long before derailment to panic.
40
+ attr_accessor :panic
41
+
42
+ # @return [true|false] Whether the graph is currently being updated to reflect new data.
43
+ attr_reader :queued
44
+
45
+ # @return [true|false] Whether the graph was created in test mode.
46
+ attr_accessor :ephem
47
+
48
+ # @return [true|false] Whether you have to be signed in as owner of the goal to view it.
49
+ attr_accessor :secret
50
+
51
+ # @return [true|false] Whether you have to be signed in as the owner of the goal to view the datapoints.
52
+ attr_accessor :datapublic
53
+
54
+ # @return [Beeminder::User] User that owns this goal.
55
+ attr_reader :user
56
+
57
+ def initialize user, name
58
+ @user = user
59
+ @slug = name
60
+
61
+ reload
62
+ end
63
+
64
+ # Reload data from Beeminder.
65
+ def reload
66
+ info = @user.get "users/#{@user.name}/goals/#{@slug}.json"
67
+
68
+ # set variables
69
+ info.each do |k,v|
70
+ instance_variable_set "@#{k}", v
71
+ end
72
+
73
+ # some conversions
74
+ @goaldate = DateTime.strptime(@goaldate.to_s, '%s') unless @goaldate.nil?
75
+ @goal_type = @goal_type.to_sym unless @goal_type.nil?
76
+ @losedate = DateTime.strptime(@losedate.to_s, '%s') unless @losedate.nil?
77
+ @updated_at = DateTime.strptime(@updated_at.to_s, '%s')
78
+ end
79
+
80
+ # List of datapoints.
81
+ #
82
+ # @return [Array<Beeminder::Datapoint>] returns list of datapoints
83
+ def datapoints
84
+ info = @user.get "users/#{@user.name}/goals/#{slug}/datapoints.json"
85
+ datapoints = info.map{|d| Datapoint.new d}
86
+
87
+ datapoints
88
+ end
89
+
90
+ # Send updated meta-data to Beeminder.
91
+ def update
92
+ data = {
93
+ "slug" => @slug,
94
+ "title" => @title,
95
+ "ephem" => @ephem || false,
96
+ "panic" => @panic || 86400,
97
+ "secret" => @secret || false,
98
+ "datapublic" => @datapublic || false,
99
+ }
100
+
101
+ @user.put "users/#{@user.name}/goals/#{@slug}.json", data
102
+ end
103
+
104
+ # Send new road setting to Beeminder.
105
+ #
106
+ # @param dials [Hash] Set exactly two of `"rate"`, `"goaldate"` and `"goalval"`. The third is implied.
107
+ def dial_road dials={}
108
+ raise "Set exactly two dials." if dials.keys.size != 2
109
+
110
+ dials["goaldate"] = dials["goaldate"].strftime('%s') unless dials["goaldate"].nil?
111
+
112
+ @user.post "users/#{@user.name}/goals/#{@slug}/dial_road.json", dials
113
+ end
114
+
115
+ # Add one or more datapoints to the goal.
116
+ #
117
+ # @param datapoints [Beeminder::Datapoint, Array<Beeminder::Datapoint>] one or more datapoints to add to goal
118
+ def add datapoints, opts={}
119
+ datapoints = [*datapoints]
120
+
121
+ # FIXME create_all doesn't work because Ruby's POST encoding of arrays is broken.
122
+ datapoints.each do |dp|
123
+ data = {
124
+ "sendmail" => opts[:sendmail] || false
125
+ }.merge(dp.to_hash)
126
+
127
+ @user.post "users/#{@user.name}/goals/#{@slug}/datapoints.json", data
128
+ end
129
+ end
130
+
131
+ # Delete one or more datapoints from the goal.
132
+ #
133
+ # @param datapoints [Beeminder::Datapoint, Array<Beeminder::Datapoint>] one or more datapoints to delete
134
+ def delete datapoints
135
+ datapoints = [*datapoints]
136
+ datapoints.each do |dp|
137
+ @user.delete "/users/#{@user.name}/goals/#{@slug}/datapoints/#{dp.id}.json"
138
+ end
139
+ end
140
+
141
+ # Convert datapoint to hash for POSTing.
142
+ # @return [Hash]
143
+ def to_hash
144
+ {
145
+ "slug" => @slug,
146
+ "title" => @title,
147
+ "goal_type" => @goal_type.to_s,
148
+ "ephem" => @ephem || false,
149
+ "panic" => @panic || 86400,
150
+ "secret" => @secret || false,
151
+ "datapublic" => @datapublic || false,
152
+ }
153
+ end
154
+ end
155
+
156
+ class Datapoint
157
+ # @return [DateTime] Time of the datapoint.
158
+ attr_accessor :timestamp
159
+
160
+ # @return [Numeric] Value of the datapoint.
161
+ attr_accessor :value
162
+
163
+ # @return [String] An optional comment about the datapoint.
164
+ attr_accessor :comment
165
+
166
+ # @return [String] A unique ID, used to identify a datapoint when deleting or editing it.
167
+ attr_reader :id
168
+
169
+ # @return [DateTime] The time that this datapoint was entered or last updated.
170
+ attr_reader :updated_at
171
+
172
+ def initialize info
173
+ default = {
174
+ timestamp: DateTime.now.strftime('%s')
175
+ }
176
+ info = default.merge(info)
177
+
178
+ # set variables
179
+ info.each do |k,v|
180
+ instance_variable_set "@#{k}", v
181
+ end
182
+
183
+ # some conversions
184
+ @timestamp = DateTime.strptime(@timestamp.to_s, '%s')
185
+ @updated_at = DateTime.strptime(@updated_at.to_s, '%s') unless @updated_at.nil?
186
+ end
187
+
188
+ # Convert datapoint to hash for POSTing.
189
+ # @return [Hash]
190
+ def to_hash
191
+ {
192
+ "timestamp" => @timestamp.strftime('%s'),
193
+ "value" => @value || 0,
194
+ "comment" => @comment || "",
195
+ }
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,142 @@
1
+ # coding: utf-8
2
+
3
+ module Beeminder
4
+ class User
5
+ # @return [String] User name.
6
+ attr_reader :name
7
+
8
+ # @return [String] Auth token.
9
+ attr_reader :token
10
+
11
+ # @return [DateTime] Last time user made any changes.
12
+ attr_reader :updated_at
13
+
14
+ # @return [String] Timezone.
15
+ attr_reader :timezone
16
+
17
+ def initialize name, token
18
+ @name = name
19
+ @token = token
20
+
21
+ info = get "users/#{@name}.json"
22
+
23
+ @timezone = info["timezone"]
24
+ @updated_at = DateTime.strptime(info["updated_at"].to_s, '%s')
25
+ end
26
+
27
+ # List of goals.
28
+ #
29
+ # @param filter [Symbol] filter goals, can be `:all` (default), `:frontburner` or `:backburner`
30
+ # @ return [Array<Beeminder::Goal>] returns list of goals
31
+ def goals filter=:all
32
+ raise "invalid goal filter: #{filter}" unless [:all, :frontburner, :backburner].include? filter
33
+
34
+ info = get "users/#{@name}.json", :filter => filter.to_s
35
+ goals = info["goals"].map do |goal|
36
+ Beeminder::Goal.new self, goal
37
+ end unless info["goals"].nil?
38
+
39
+ goals || []
40
+ end
41
+
42
+ # Return specific goal.
43
+ #
44
+ # @param name [String] Name of the goal.
45
+ # @return [Beeminder::Goal] Returns goal.
46
+ def goal name
47
+ Beeminder::Goal.new self, name
48
+ end
49
+
50
+ # Convenience function to add datapoint to a goal.
51
+ #
52
+ # @param name [String] Goal name.
53
+ # @param value [Numeric] Datapoint value.
54
+ # @param comment [String] Optional comment.
55
+ def send name, value, comment=""
56
+ goal = self.goal name
57
+ dp = Beeminder::Datapoint.new value: value, comment: comment
58
+ goal.add dp
59
+ end
60
+
61
+ # Create new goal.
62
+ #
63
+ # @param opts [Hash] Goal options.
64
+ def create_goal opts={}
65
+ post "users/#{@name}/goals.json", opts
66
+ end
67
+
68
+ # Send GET request to API.
69
+ #
70
+ # @param cmd [String] the API command, like `users/#{user.name}.json`
71
+ # @param data [Hash] data to send; auth_token is included by default (optional)
72
+ def get cmd, data={}
73
+ _connection :get, cmd, data
74
+ end
75
+
76
+ # Send POST request to API.
77
+ #
78
+ # @param cmd [String] the API command, like `users/#{user.name}.json`
79
+ # @param data [Hash] data to send; auth_token is included by default (optional)
80
+ def post cmd, data={}
81
+ _connection :post, cmd, data
82
+ end
83
+
84
+ # Send DELETE request to API.
85
+ #
86
+ # @param cmd [String] the API command, like `users/#{user.name}.json`
87
+ # @param data [Hash] data to send; auth_token is included by default (optional)
88
+ def delete cmd, data={}
89
+ _connection :delete, cmd, data
90
+ end
91
+
92
+ # Send PUT request to API.
93
+ #
94
+ # @param cmd [String] the API command, like `users/#{user.name}.json`
95
+ # @param data [Hash] data to send; auth_token is included by default (optional)
96
+ def put cmd, data={}
97
+ _connection :put, cmd, data
98
+ end
99
+
100
+ private
101
+
102
+ # Establish HTTPS connection to API.
103
+ def _connection type, cmd, data
104
+ api = "https://www.beeminder.com/api/v1/#{cmd}"
105
+ data = {"auth_token" => @token}.merge(data)
106
+
107
+ url = URI.parse(api)
108
+ http = Net::HTTP.new(url.host, url.port)
109
+ http.read_timeout = 8640
110
+ http.use_ssl = true
111
+
112
+ json = ""
113
+ http.start do |http|
114
+ req = case type
115
+ when :post
116
+ Net::HTTP::Post.new(url.path)
117
+ when :get
118
+ Net::HTTP::Get.new(url.path)
119
+ when :delete
120
+ Net::HTTP::Delete.new(url.path)
121
+ when :put
122
+ Net::HTTP::Put.new(url.path)
123
+ else
124
+ raise "invalid connection type"
125
+ end
126
+ req.set_form_data(data)
127
+
128
+ res = http.request(req)
129
+ if not res.is_a? Net::HTTPSuccess
130
+ raise "request failed: #{res.body}"
131
+ end
132
+
133
+ json = res.body
134
+ end
135
+
136
+ # parse json
137
+ json = JSON.load(json)
138
+
139
+ json
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,3 @@
1
+ module Beeminder
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: beeminder
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - muflax
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-09-06 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: highline
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.6'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.6'
30
+ description: Convenient access to Beeminder's API.
31
+ email:
32
+ - mail@muflax.com
33
+ executables:
34
+ - beemind
35
+ extensions: []
36
+ extra_rdoc_files: []
37
+ files:
38
+ - .gitignore
39
+ - Gemfile
40
+ - LICENSE.txt
41
+ - README
42
+ - Rakefile
43
+ - TODO
44
+ - beeminder.gemspec
45
+ - bin/beemind
46
+ - lib/beeminder.rb
47
+ - lib/beeminder/goals.rb
48
+ - lib/beeminder/user.rb
49
+ - lib/beeminder/version.rb
50
+ homepage: ''
51
+ licenses: []
52
+ post_install_message:
53
+ rdoc_options: []
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ segments:
63
+ - 0
64
+ hash: 350792412057985642
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ! '>='
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ segments:
72
+ - 0
73
+ hash: 350792412057985642
74
+ requirements: []
75
+ rubyforge_project:
76
+ rubygems_version: 1.8.23
77
+ signing_key:
78
+ specification_version: 3
79
+ summary: access Beeminder API
80
+ test_files: []