I think it's time for a primer on using Python to extend your Ledger
experience. But first, a word must be said about Ledger's data model, so that
other things make sense later.
------------------------------------------------------------------------------
# Basic data traversal
Every interaction with Ledger happens in the context of a Session. Even if
you don't create a session manually, one is created for you by the top-level
interface functions. The Session is where objects live like the Commodity's
that Amount's refer to.
The make a Session useful, you must read a Journal into it, using the function
`read_journal`. This reads Ledger data from the given file, populates a
Journal object within the current Session, and returns a reference to that
Journal object.
Within the Journal live all the Transaction's, Posting's, and other objects
related to your data. There are also AutomatedTransaction's and
PeriodicTransaction's, etc.
Here is how you would traverse all the postings in your data file:
import ledger
for xact in ledger.read_journal("sample.dat").xacts:
for post in xact.posts:
print "Transferring %s to/from %s" % (post.amount, post.account)
------------------------------------------------------------------------------
# Raw vs. Cooked
Ledger data exists in one of two forms: raw and cooked. Raw objects are what
you get from a traversal like the above, and represent exactly what was seen
in the data file. Consider this journal:
= true
(Assets:Cash) $100
2012-03-01 KFC
Expenses:Food $100
Assets:Credit
In this case, the *raw* regular transaction in this file is:
2012-03-01 KFC
Expenses:Food $100
Assets:Credit
While the cooked form is:
2012-03-01 KFC
Expenses:Food $100
Assets:Credit $-100
(Assets:Cash) $100
So the easy way to think about raw vs. cooked is that raw is the unprocessed
data, and cooked has had all considerations applied.
When you traverse a Journal by iterating its transactions, you are generally
looking at raw data. In order to look at cooked data, you must generate a
report of some kind by querying the journal:
for post in ledger.read_journal("sample.dat").query("food"):
print "Transferring %s to/from %s" % (post.amount, post.account)
The reason why queries iterate over postings instead of transactions is that
queries often return only a "slice" of the transactions they apply to. You
can always get at a matching posting's transaction by looking at its "xact"
member:
last_xact = None
for post in ledger.read_journal("sample.dat").query(""):
if post.xact != last_xact:
for post in post.xact.posts:
print "Transferring %s to/from %s" % (post.amount,
post.account)
last_xact = post.xact
This query ends up reporting every cooked posting in the Journal, but does it
transaction-wise. It relies on the fact that an unsorted report returns
postings in the exact order they were parsed from the journal file.
------------------------------------------------------------------------------
# Queries
The Journal.query() method accepts every argument you can specify on the
command-line, including --options.
Since a query "cooks" the journal it applies to, only one query may be active
for that journal at a given time. Once the query object is gone (after the
for loop), then the data reverts back to its raw state.
------------------------------------------------------------------------------
# Embedded Python
Can you embed Python into your data files using the 'python' directive:
python
import so
def check_path(path_value):
print "%s => %s" % (str(path_value), os.path.isfile(str(path_value)))
return os.path.isfile(str(path_value))
tag PATH
assert check_path(value)
2012-02-29 KFC
; PATH: somebogusfile.dat
Expenses:Food $20
Assets:Cash
Any Python functions you define this way become immediately available as
valexpr functions.
------------------------------------------------------------------------------
# Amounts
When numbers come from Ledger, like post.amount, the type of the value is
Amount. It can be used just like an ordinary number, except that addition
and subtraction are restricted to amounts with the same commodity. If you
need to create sums of multiple commodities, use a Balance. For example:
total = Balance()
for post in ledger.read_journal("sample.dat").query(""):
total += post.amount
print total
John
> Long story short, I haven't found a way to access this Python interface, my
> intent is to produce a series of scripts to output HMTL5 files with my
> reports on a regular basis (probably a cron-job). How can I do this? Is
> there any way to communicate with Ledger without having to put it in REPL
> mode and creating the interface myself?
Make sure to pass --python to acprep when you build (you'll have to make
distclean if you're just adding that flag now). Then you use 'ledger python
foo.py' to run a Python script with the ability to 'import ledger' in that
script. See earlier postings in this mailing list for some examples of what
you can do then...
John
>>>>> Raphael Lorenzeto writes:
> Thanks! I managed to compile with the --python passed to acprep but I get
> the following error while trying to execute the examples shown on this post:
> ./ledger/ledger python ledger-script.py Traceback (most recent call last):
> File "ledger-script.py", line 6, in <module>
> for post in post.xact.posts:
> TypeError: 'instancemethod' object is not iterable Abort trap: 6
It should be post.xacts.posts, I believe.
John
>> ./ledger/ledger python ledger-script.py Traceback (most recent call last):
>> File "ledger-script.py", line 6, in <module> for post in post.xact.posts:
>> TypeError: 'instancemethod' object is not iterable Abort trap: 6
> It should be post.xacts.posts, I believe.
No, you're right, post.xact.posts. I'll enter a bug for this into Bugzilla.
John
>>>>> Raphael Lorenzeto writes:
for xact in ledger.read_journal("ledger.dat").xacts(): # Note the () after xacts
for post in xact.posts(): # Note the () after posts
print "Transferring %s to/from %s" % (post.amount, post.account)
If you think we can get to where we want with Python + Javascript, then I can
be talked away from trying to develop a full RESTful API.
John
I am willing to invest time developing this.
So Tim and John, if you need help, let's team up and see if we can get
this going.
Regards, Stefan Tunsch
And none of this involves writing any Python! I don't enjoy this kind of work, but I see it as preparatory for the more-fun stuff.
Any feedback?
Tim Crews