ledger-rest 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
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
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ source :rubygems
2
+
3
+ gem "escape"
4
+ gem "sinatra"
5
+ gem "git"
6
+
7
+ group :development do
8
+ gem "rake"
9
+ gem "pry"
10
+ gem "rspec"
11
+ gem "rb-inotify"
12
+ gem "guard-rspec"
13
+ end
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
@@ -0,0 +1,5 @@
1
+ $: << "./lib"
2
+
3
+ require 'ledger-rest'
4
+
5
+ run LedgerRest::App
@@ -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.
@@ -0,0 +1,11 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'rubygems'
3
+ require 'fileutils'
4
+ require 'json'
5
+ require 'yaml'
6
+ require 'bundler'
7
+
8
+ Bundler.require
9
+
10
+ require 'ledger-rest/version'
11
+ require 'ledger-rest/app'
@@ -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