ledger-rest 2.0.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 +17 -0
- data/.rspec +2 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +57 -0
- data/Guardfile +24 -0
- data/LICENSE.txt +22 -0
- data/README.md +17 -0
- data/Rakefile +1 -0
- data/config.ru +5 -0
- data/ledger-rest.gemspec +19 -0
- data/ledger-rest.org +14 -0
- data/lib/ledger-rest.rb +11 -0
- data/lib/ledger-rest/app.rb +124 -0
- data/lib/ledger-rest/core_ext.rb +23 -0
- data/lib/ledger-rest/git.rb +68 -0
- data/lib/ledger-rest/ledger.rb +87 -0
- data/lib/ledger-rest/ledger/balance.rb +44 -0
- data/lib/ledger-rest/ledger/budget.rb +33 -0
- data/lib/ledger-rest/ledger/entry.rb +27 -0
- data/lib/ledger-rest/ledger/parser.rb +178 -0
- data/lib/ledger-rest/ledger/register.rb +39 -0
- data/lib/ledger-rest/ledger/transaction.rb +99 -0
- data/lib/ledger-rest/version.rb +3 -0
- data/spec/ledger-rest/ledger/parser_spec.rb +364 -0
- data/spec/ledger-rest/ledger/transaction_spec.rb +41 -0
- data/spec/spec_helper.rb +9 -0
- metadata +78 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
coderay (1.0.8)
|
5
|
+
diff-lcs (1.1.3)
|
6
|
+
escape (0.0.4)
|
7
|
+
ffi (1.2.0)
|
8
|
+
git (1.2.5)
|
9
|
+
guard (1.5.4)
|
10
|
+
listen (>= 0.4.2)
|
11
|
+
lumberjack (>= 1.0.2)
|
12
|
+
pry (>= 0.9.10)
|
13
|
+
thor (>= 0.14.6)
|
14
|
+
guard-rspec (2.1.2)
|
15
|
+
guard (>= 1.1)
|
16
|
+
rspec (~> 2.11)
|
17
|
+
listen (0.6.0)
|
18
|
+
lumberjack (1.0.2)
|
19
|
+
method_source (0.8.1)
|
20
|
+
pry (0.9.10)
|
21
|
+
coderay (~> 1.0.5)
|
22
|
+
method_source (~> 0.8)
|
23
|
+
slop (~> 3.3.1)
|
24
|
+
rack (1.4.1)
|
25
|
+
rack-protection (1.2.0)
|
26
|
+
rack
|
27
|
+
rake (0.9.2.2)
|
28
|
+
rb-inotify (0.8.8)
|
29
|
+
ffi (>= 0.5.0)
|
30
|
+
rspec (2.12.0)
|
31
|
+
rspec-core (~> 2.12.0)
|
32
|
+
rspec-expectations (~> 2.12.0)
|
33
|
+
rspec-mocks (~> 2.12.0)
|
34
|
+
rspec-core (2.12.0)
|
35
|
+
rspec-expectations (2.12.0)
|
36
|
+
diff-lcs (~> 1.1.3)
|
37
|
+
rspec-mocks (2.12.0)
|
38
|
+
sinatra (1.3.3)
|
39
|
+
rack (~> 1.3, >= 1.3.6)
|
40
|
+
rack-protection (~> 1.2)
|
41
|
+
tilt (~> 1.3, >= 1.3.3)
|
42
|
+
slop (3.3.3)
|
43
|
+
thor (0.16.0)
|
44
|
+
tilt (1.3.3)
|
45
|
+
|
46
|
+
PLATFORMS
|
47
|
+
ruby
|
48
|
+
|
49
|
+
DEPENDENCIES
|
50
|
+
escape
|
51
|
+
git
|
52
|
+
guard-rspec
|
53
|
+
pry
|
54
|
+
rake
|
55
|
+
rb-inotify
|
56
|
+
rspec
|
57
|
+
sinatra
|
data/Guardfile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard 'rspec' do
|
5
|
+
watch(%r{^spec/.+_spec\.rb$})
|
6
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
7
|
+
watch('spec/spec_helper.rb') { "spec" }
|
8
|
+
|
9
|
+
# Rails example
|
10
|
+
watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
11
|
+
watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
|
12
|
+
watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] }
|
13
|
+
watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
|
14
|
+
watch('config/routes.rb') { "spec/routing" }
|
15
|
+
watch('app/controllers/application_controller.rb') { "spec/controllers" }
|
16
|
+
|
17
|
+
# Capybara features specs
|
18
|
+
watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/features/#{m[1]}_spec.rb" }
|
19
|
+
|
20
|
+
# Turnip features and steps
|
21
|
+
watch(%r{^spec/acceptance/(.+)\.feature$})
|
22
|
+
watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' }
|
23
|
+
end
|
24
|
+
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Max Wolter,Arthur Andersen
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# ledger-rest - REST assured your finances are in good hands ;-)
|
2
|
+
|
3
|
+
ledger-rest is a REST webservice to access your ledger account data
|
4
|
+
|
5
|
+
## What already works
|
6
|
+
|
7
|
+
* balance reports (``/balance``)
|
8
|
+
* register reports (``/register``)
|
9
|
+
* budget reports (``/budget``)
|
10
|
+
* version query (``/version``)
|
11
|
+
|
12
|
+
## What you need
|
13
|
+
|
14
|
+
* ruby
|
15
|
+
* sinatra
|
16
|
+
* escape
|
17
|
+
* ledger
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/config.ru
ADDED
data/ledger-rest.gemspec
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'ledger-rest/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "ledger-rest"
|
8
|
+
gem.version = LedgerRest::VERSION
|
9
|
+
gem.authors = ["Prof. MAAD", "Arthur Andersen"]
|
10
|
+
gem.email = ["leoc.git@gmail.com"]
|
11
|
+
gem.description = %q{Provide a REST web service for ledger.}
|
12
|
+
gem.summary = %q{Ledger via REST.}
|
13
|
+
gem.homepage = "https://github.com/leoc/ledger-rest"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
end
|
data/ledger-rest.org
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
* TODO ledger-rest
|
2
|
+
** DONE move responsibilities into classes
|
3
|
+
CLOSED: [2012-11-22 Thu 19:03]
|
4
|
+
- State "DONE" from "TODO" [2012-11-22 Thu 19:03]
|
5
|
+
** TODO implement entry GET
|
6
|
+
** TODO implement entry POST
|
7
|
+
** TODO implement transactions GET method
|
8
|
+
Get all transactions.
|
9
|
+
** TODO implement transactions POST method
|
10
|
+
Create a new transaction in the respective file.
|
11
|
+
** TODO implement transactions PUT method
|
12
|
+
Modify a transaction in the respective file.
|
13
|
+
** TODO implement transactions DELETE method
|
14
|
+
Remove a transaction from the respective file.
|
data/lib/ledger-rest.rb
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require 'ledger-rest/ledger'
|
3
|
+
require 'ledger-rest/ledger/balance'
|
4
|
+
require 'ledger-rest/ledger/transaction'
|
5
|
+
require 'ledger-rest/ledger/register'
|
6
|
+
require 'ledger-rest/ledger/budget'
|
7
|
+
require 'ledger-rest/ledger/entry'
|
8
|
+
require 'ledger-rest/git'
|
9
|
+
require 'ledger-rest/core_ext'
|
10
|
+
|
11
|
+
require 'pry'
|
12
|
+
|
13
|
+
module LedgerRest
|
14
|
+
class App < Sinatra::Base
|
15
|
+
CONFIG_FILE = "ledger-rest.yml"
|
16
|
+
|
17
|
+
configure do |c|
|
18
|
+
begin
|
19
|
+
config = YAML.load_file(CONFIG_FILE)
|
20
|
+
config.symbolize_keys!
|
21
|
+
rescue Exception => e
|
22
|
+
puts "Failed to load config."
|
23
|
+
end
|
24
|
+
|
25
|
+
Ledger.configure config
|
26
|
+
Git.configure config
|
27
|
+
end
|
28
|
+
|
29
|
+
get '/version' do
|
30
|
+
content_type :json
|
31
|
+
{
|
32
|
+
"version" => LedgerRest::VERSION,
|
33
|
+
"ledger-version" => Ledger.version
|
34
|
+
}.to_json
|
35
|
+
end
|
36
|
+
|
37
|
+
get '/balance/?:query?' do
|
38
|
+
content_type :json
|
39
|
+
Ledger::Balance.json(params[:query])
|
40
|
+
end
|
41
|
+
|
42
|
+
get '/budget/?:query?' do
|
43
|
+
content_type :json
|
44
|
+
Ledger::Budget.json(params[:query])
|
45
|
+
end
|
46
|
+
|
47
|
+
get '/register/?:query?' do
|
48
|
+
content_type :json
|
49
|
+
Ledger::Register.json(params[:query])
|
50
|
+
end
|
51
|
+
|
52
|
+
get '/accounts/?:query?' do
|
53
|
+
content_type :json
|
54
|
+
Ledger.accounts(params[:query]).to_json
|
55
|
+
end
|
56
|
+
|
57
|
+
get '/payees/?:query?' do
|
58
|
+
content_type :json
|
59
|
+
Ledger.payees(params[:query]).to_json
|
60
|
+
end
|
61
|
+
|
62
|
+
# gets a potential new entry via the entry command
|
63
|
+
get '/transactions/entry/?:desc?' do
|
64
|
+
content_type :json
|
65
|
+
{ :transaction => Ledger::Entry.get(params[:desc]) }.to_json
|
66
|
+
end
|
67
|
+
|
68
|
+
# creates a new entry based on the
|
69
|
+
post '/transactions/entry/?:desc?' do
|
70
|
+
content_type :json
|
71
|
+
{ :transaction => Ledger::Entry.append(params[:desc]) }.to_json
|
72
|
+
end
|
73
|
+
|
74
|
+
get '/transactions/:meta' do
|
75
|
+
"Not yet implemented!"
|
76
|
+
end
|
77
|
+
|
78
|
+
get '/transactions' do
|
79
|
+
"Not yet implemented!"
|
80
|
+
end
|
81
|
+
|
82
|
+
post '/transactions' do
|
83
|
+
content_type :json
|
84
|
+
begin
|
85
|
+
params = JSON.parse(params[:transaction], :symbolize_names => true)
|
86
|
+
|
87
|
+
transaction = Transaction.create params
|
88
|
+
|
89
|
+
raise "Verification error" unless transaction.valid?
|
90
|
+
|
91
|
+
Git.invoke :before_write
|
92
|
+
|
93
|
+
@@ledger.append transaction
|
94
|
+
|
95
|
+
Git.invoke :after_write
|
96
|
+
|
97
|
+
[201, { transaction: transaction }.to_json]
|
98
|
+
rescue JSON::ParserError => e
|
99
|
+
[422,
|
100
|
+
{
|
101
|
+
:error => true,
|
102
|
+
:message => "Unprocessible Entity: '#{e}'"
|
103
|
+
}.to_json
|
104
|
+
]
|
105
|
+
rescue RuntimeError => e
|
106
|
+
[400,
|
107
|
+
{
|
108
|
+
:error => true,
|
109
|
+
:message => "Adding the transaction failed: '#{e}'"
|
110
|
+
}.to_json
|
111
|
+
]
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
put '/transactions/:meta' do
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
delete '/transactions/:meta' do
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class Hash
|
2
|
+
def symbolize_keys
|
3
|
+
hash = {}
|
4
|
+
self.each_pair do |key, val|
|
5
|
+
hash[key.to_sym] = val
|
6
|
+
end
|
7
|
+
hash
|
8
|
+
end
|
9
|
+
|
10
|
+
def symbolize_keys!
|
11
|
+
self.keys.each do |key|
|
12
|
+
self[key.to_sym] = self.delete key
|
13
|
+
end
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
def inject acc, &block
|
18
|
+
self.each_pair do |key, val|
|
19
|
+
acc = block.call acc, key, val
|
20
|
+
end
|
21
|
+
acc
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module LedgerRest
|
2
|
+
class Git
|
3
|
+
class << self
|
4
|
+
attr_reader :repository, :remote, :branch, :read_pull_block_time
|
5
|
+
|
6
|
+
def configure options
|
7
|
+
@repository = options[:git_repository] or File.dirname(options[:ledger_file] || 'main.ledger')
|
8
|
+
@pull_before_read = options[:git_pull_before_read] || false
|
9
|
+
@pull_before_write = options[:git_pull_before_write] || false
|
10
|
+
@push_after_write = options[:git_push_after_write] || false
|
11
|
+
@remote = options[:git_remote] || 'origin'
|
12
|
+
@branch = options[:git_branch] || 'master'
|
13
|
+
@read_pull_block_time = options[:git_read_pull_block_time] || 10*60
|
14
|
+
|
15
|
+
@last_read_pull = Time.new
|
16
|
+
if pull_before_read? or pull_before_write? or push_after_write?
|
17
|
+
@git_repo = ::Git.open(repository)
|
18
|
+
FileUtils.touch(options[:ledger_append_file])
|
19
|
+
@git_repo.add(options[:ledger_append_file])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def invoke hook
|
24
|
+
case hook
|
25
|
+
when :before_read
|
26
|
+
pull if pull_before_read? and not blocked?
|
27
|
+
when :before_write
|
28
|
+
pull if pull_before_write?
|
29
|
+
when :after_write
|
30
|
+
push if push_after_write?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def blocked_read_pull?
|
35
|
+
(Time.new - @last_read_pull) > read_pull_block_time
|
36
|
+
end
|
37
|
+
|
38
|
+
def pull_before_read?
|
39
|
+
@pull_before_read
|
40
|
+
end
|
41
|
+
|
42
|
+
def pull_before_write?
|
43
|
+
@pull_before_write
|
44
|
+
end
|
45
|
+
|
46
|
+
def push_after_write?
|
47
|
+
@push_after_write
|
48
|
+
end
|
49
|
+
|
50
|
+
# Execute the pull command.
|
51
|
+
def pull
|
52
|
+
@git_repo.pull(remote, branch)
|
53
|
+
@last_read_pull = Time.new
|
54
|
+
rescue Exception => e
|
55
|
+
$stderr.puts "Git pull failed: #{e}"
|
56
|
+
end
|
57
|
+
|
58
|
+
# Execute the push command after commiting all.
|
59
|
+
def push
|
60
|
+
@git_repo.commit_all("transaction added via ledger-rest")
|
61
|
+
@git_repo.push(remote, branch)
|
62
|
+
rescue Exception => e
|
63
|
+
$stderr.put "Git push failed: #{e}"
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require 'ledger-rest/ledger/parser'
|
3
|
+
|
4
|
+
module LedgerRest
|
5
|
+
class Ledger
|
6
|
+
class << self
|
7
|
+
|
8
|
+
attr_accessor :rcfile, :bin, :file, :append_file, :home
|
9
|
+
|
10
|
+
def configure(options)
|
11
|
+
@bin = options[:ledger_bin] || "/usr/bin/ledger"
|
12
|
+
@file = options[:ledger_file] || ENV['LEDGER_FILE']
|
13
|
+
@append_file = options[:ledger_append_file] || ENV['LEDGER_FILE']
|
14
|
+
@home = options[:ledger_home] || '' # Return a budget representation
|
15
|
+
end
|
16
|
+
|
17
|
+
# Execute ledger command with given parameters
|
18
|
+
def exec(cmd, params = {})
|
19
|
+
Git.invoke :before_read
|
20
|
+
|
21
|
+
params = {
|
22
|
+
'-f' => @file
|
23
|
+
}.merge(params)
|
24
|
+
|
25
|
+
params = params.inject '' do |acc, key, val|
|
26
|
+
acc << " #{key} #{Escape.shell_single_word(val)}"
|
27
|
+
end
|
28
|
+
|
29
|
+
command = "#{bin} #{params} #{cmd}"
|
30
|
+
|
31
|
+
puts command
|
32
|
+
`#{command}`.rstrip
|
33
|
+
end
|
34
|
+
|
35
|
+
# Append a new transaction to the append_file.
|
36
|
+
def append transaction
|
37
|
+
File.open(append_file, "a+") do |f|
|
38
|
+
if f.pos == 0
|
39
|
+
last_char = "\n"
|
40
|
+
else
|
41
|
+
f.pos = f.pos-1
|
42
|
+
last_char = f.getc
|
43
|
+
end
|
44
|
+
|
45
|
+
f.write "\n" unless last_char == "\n"
|
46
|
+
f.write(transaction_string)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Return the ledger version.
|
51
|
+
def version
|
52
|
+
exec("--version").match(/^Ledger (.*),/)[1]
|
53
|
+
end
|
54
|
+
|
55
|
+
# Get a new transaction entry based on previous entries found
|
56
|
+
# in the append file.
|
57
|
+
def entry description
|
58
|
+
result = exec "entry #{description}", '-f' => append_file
|
59
|
+
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
# Return a list of payees.
|
64
|
+
def payees(query = nil)
|
65
|
+
result = exec "payees #{query if query}"
|
66
|
+
{ :payees => result.split("\n") }
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns an Array of payees with their respective usage count.
|
70
|
+
def payees_with_usage
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
# Return an array of accounts.
|
75
|
+
def accounts(query = nil)
|
76
|
+
result = exec "accounts #{query if query}"
|
77
|
+
{ :accounts => result.split("\n") }
|
78
|
+
end
|
79
|
+
|
80
|
+
# Return an Array of accounts with their respective usage count.
|
81
|
+
def accounts_with_usage
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|