Motivation
Giving implements what we may call functional reactive logging. The basic idea is that you can log objects anywhere in your program, as easily as you can print them. These objects are then aggregated by Giving into Observable streams of dictionaries with which you can do pretty much anything you want, including printing them.
The main advantage of Giving is decoupling: calls to give
in your code produce a stream of raw data, and then that stream can be manipulated in powerful ways to create advanced displays, to forward data to an external server, and so on.
Using give
uniformly for logging data also allows deferring the decision of what to log where. For instance, you might want to log some piece of data continuously, some other piece of data every 30 seconds, and you might want to send another into a web service. And you might want to change these decisions later on.
Getting started
Install
You can trivially install giving
via pip
:
pip install giving
Usage
Giving provides two main functions, give()
and given()
.
give is used to log data into a stream. For example,
give(a=1, b=2)
will push{"a": 1, "b": 2}
onto the stream.given is a context manager that gives access to that stream. Using methods such as
where()
,map()
orfilter()
, the stream can be transformed.
Here is a simple example. First, add calls to give
into the main code:
from giving import give
def bisect(arr, key):
lo = -1
hi = len(arr)
while lo < hi - 1:
mid = lo + (hi - lo) // 2
give(mid) # push {"mid": mid}
if arr[mid] > key:
hi = mid
give() # push {"hi": hi}
else:
lo = mid
give() # push {"lo": lo}
return lo + 1
Second, wrap the invocation in a given
block:
from giving import given
with given() as gv: # gv represents the stream
# We will print everything that is given as-is
gv.print()
# The function must be run *after* we set up our pipeline
bisect(list(range(10)), 3)
When the above is run, the following is printed:
{'mid': 4}
{'hi': 4}
{'mid': 1}
{'lo': 1}
{'mid': 2}
{'lo': 2}
{'mid': 3}
{'lo': 3}
This is simple enough (and useful enough), but the fun starts when you use gv
’s many methods:
with given() as gv:
# gv["?mid"] is equivalent to gv.where("mid")["mid"], it extracts mid and ignores the rest
# min() outputs the minimum at the end of the stream
gv["?mid"].min().print("min(mid): {}")
# kscan() incrementally merges dictionaries in the stream with the previous ones
# The first few outputs of kscan() will not have all 3 values, hence skip_missing
gv.kscan().print("{lo} <= {mid} <= {hi}", skip_missing=True)
# Trigger a breakpoint whenever lo > hi
gv.kscan().where("lo", "hi").kfilter(lambda lo, hi: lo > hi).breakpoint()
# Put the values of mid in an array
mids = gv["?mid"].accum()
bisect(list(range(10)), 3)
print(mids)
The above will output this:
1 <= 1 <= 4
1 <= 2 <= 4
2 <= 2 <= 4
2 <= 3 <= 4
3 <= 3 <= 4
min(mid): 1
[4, 1, 2, 3]