beeminder 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +1 -0
- data/README +47 -0
- data/Rakefile +1 -0
- data/TODO +6 -0
- data/beeminder.gemspec +20 -0
- data/bin/beemind +43 -0
- data/lib/beeminder.rb +11 -0
- data/lib/beeminder/goals.rb +198 -0
- data/lib/beeminder/user.rb +142 -0
- data/lib/beeminder/version.rb +3 -0
- metadata +80 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
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
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,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
|
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: []
|