beeminder 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.
- 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: []
|