Given that you have already built the bond and the discount curve, and you have linked them in some way similar to:
discount_handle = RelinkableYieldTermStructureHandle(discount_curve)
bond.setPricingEngine(DiscountingBondEngine(discount_handle))
you can first add a spread over the existing discount curve and then use the modified curve to price the bond. Something like:
nodes = [ 1, 2, 5, 7, 10 ] # the durations
dates = [ today + Period(n, Years) for n in nodes ]
spreads = [ SimpleQuote(0.0) for n in nodes ] # null spreads to begin
new_curve = SpreadedLinearZeroInterpolatedTermStructure(
YieldTermStructureHandle(discount_curve),
[ QuoteHandle(q) for q in spreads ],
dates)
will give you a new curve with initial spreads all at 0 (and a horrible class name) that you can use instead of the original discount curve:
discount_handle.linkTo(new_curve)
After the above, the bond should still return the same price (since the spreads are all null).
When you want to calculate a particular key-rate duration, you can move the corresponding quote: for instance, if you want to bump the 5-years quote (the third in the list above), execute
spreads[2].setValue(0.001) # 10 bps
the curve will update accordingly, and the bond price should change.
A note: the above will interpolate between spreads, so if you move the 5-years points by 10 bps and you leave the 2-years point unchanged, then a rate around 3 years would move by about 3 bps. To mitigate this (in case that's not what you want), you can add more points to the curve and restrict the range that varies. For instance, if you add a point at 5 years minus one month and another at 5 years plus 1 month, then moving the 5-years point will only affect the two months around it.