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 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