[02:06:57] ** gpciceri has joined us [05:06:00] jack-e|away is now known as jack-e [05:50:48] ** gpciceri has joined us [11:08:53] ** hazmat has joined us [13:28:36] * _jpl_ waves [14:11:05] * Maniac waves backly [16:54:26] _jpl_, you rely on reactor.run() in junction iirc [18:04:44] <_jpl_> Not anymore [18:11:15] ** pje has joined us [18:11:24] The examples still use it, though. [18:11:52] I'd suggest subclassing commands.EventDriven instead of subclassing AbstractCommand. [18:12:42] Then, if you make services register with the junction uponAssembly, there isn't a need for e.g. Server to be anything but: [18:12:49] class Server(commands.EventDriven): [18:13:03] myService = binding.Make(EchoService, uponAssembly=True) [18:13:13] And that's the whole thing, right there. [18:15:20] For Client, you still need the reactor, though, at least until I write an events.IScheduler wrapper for Twisted reactors. [18:15:39] Once that exists, the Client example wouldn't need to mess with the reactor any more. [18:16:38] The _run method would go away, and doWork would become an events.Thread, created uponAssembly. [18:18:05] It actually looks like it might be interesting to make junctions supply 'IEventSource' objects for certain things. [18:20:56] But I see that's already on your TODO. :) [18:21:03] howdy pje! [18:21:08] Hey. [18:21:48] long time no IRC ;P [18:23:09] Real life often intrudes. [18:23:48] yes [18:23:54] As it apparently is doing with JPL at the moment. ;) [18:24:11] yeah your reponse was within minutes :) [18:24:44] so can i use peak.events from my peak/twisted app (does that even make sense) [18:25:34] Not until there's a reactor-based IScheduler. [18:25:59] The current IScheduler implementation is standalone. [18:26:15] The UntwistedReactor uses the standalone IScheduler, so it's compatible with peak.events [18:26:44] But Twisted reactors have their own internal scheduler, so the peak.events one wouldn't run. [18:27:01] You'd have to do something like put in a bunch of 'callLater(0,scheduler.tick)' operations. [18:27:36] Or else help me out by hacking up a Scheduler subclass that used a reactor to do its callLaters. [18:27:44] :) [18:27:48] hahahhahahahahah [18:28:06] my code-fu is lowly [18:28:16] i am a white belt [18:28:30] you are 4th degree black belt [18:29:02] Actually, now that I look at it, it probably requires only changing tick() to call 'reactor.runUntilCurrent()' instead of looping through the appointments... [18:29:24] changing _callAt to use reactor.callLater to schedule a callback... [18:29:52] ...and changing time_available() to always return 0 (since there's no way to ask a reactor how long it is till the next callLater. [18:35:22] Something like: [18:35:29] class TwistedScheduler(binding.Component,events.Scheduler): [18:35:30] """'events.IScheduler' that uses a Twisted reactor for timing""" [18:35:30] reactor = binding.Obtain(IBasicReactor) [18:35:30] _time = binding.Obtain('import:time.time') [18:35:34] def time_available(self): [18:35:36] return 0 # no way to find this out from a reactor [18:35:40] def tick(self): [18:35:44] now = self._time() [18:35:46] self.now.set(now) [18:35:48] self.reactor.runUntilCurrent() [18:35:52] def _callAt(self, what, when): [18:35:54] self.reactor.callLater(when-self._time(), what, self, when) [18:35:56] Hm, I guess that should actually depend on running.ITwistedReactor. [18:37:03] In theory, using that as your IScheduler component factory would let you use peak.events threads that wait for time events. [18:37:51] But there's no ISelector available yet in either reactor-based or standalone form, so peak.events doesn't support waiting for I/O yet. [18:59:04] by waiting for I/O you mean? [19:00:56] I mean that you can't wait for I/O in an events.Thread. [19:01:16] THe idiom would be 'yield selector.readable(stream)' [19:01:24] But there are no selector implementations yet. [19:03:22] if i understand the terminology i'm waiting for time events anyways [19:04:09] well, what i'm doing is this sort of thing: d = threads.deferToThread(rssfinder.getFeeds, args[0]) [19:08:08] IIUC, deferToThread runs the code in a thread and then calls you back when it's done. [19:08:15] Nothing to do with time or I/O, as such. [19:08:44] An events.Value is sort of like a deferred. [19:09:24] But it has no support for "real" threads ATM. [19:13:14] ah [19:13:40] ok [19:14:03] so when you say 'time' what do you mean? [19:14:08] for example? [19:14:21] yield scheduler.sleep(10) [19:14:49] and execution would block waiting for that [19:14:50] ? [19:14:59] Yep. [19:15:14] Technically, execution would be *suspended*. [19:15:21] fair enough [19:15:24] Until resumed by a callback taking place "under the hood" [19:15:27] Another example: [19:15:28] yield events.AnyOf(selector.readable(sock), scheduler.timeout(30)) [19:15:43] Suspends until either sock becomes readable, or 30 seconds elapse. [19:16:17] so in the case where i actually dont' want to "suspend" the thread solution is best [19:16:19] * pje still isn't entirely happy with the 'selector' term. [19:16:33] Huh? [19:16:41] mm [19:16:51] What do you mean "don't want to suspend"? [19:17:14] i dont want program execution to 'pause' or 'block' waiting for rssfinder.getFeeds() [19:17:15] Don't confuse suspending in peak.events with blocking in Twisted. [19:17:43] In peak.events, suspending = returning from a callback in Twisted [19:18:02] IOW, "yield" statements are where you *unblock* [19:18:47] Yield interrupts your routine, almost as if you'd done a 'return', and returns to the caller. [19:19:41] Now, if rssfinder.getFeeds() is not written as an events.Thread, you still have to run it in a "real" thread. [19:20:35] PEAK probably needs an IThreadPool and a setup to allow values to be returned from functions run in threads. [19:21:14] ("real" threads, that is.) But I'm not going to add that to my plate right now. [19:21:29] For what you're doing right now, Twisted is the way to go. [19:21:37] no since i'm using ITwistedReactor right now it's easy enough [19:22:07] * Maniac notes i'm agreeing not disagreeing :) [19:22:14] s/no/yes [19:22:23] * Maniac heads for supper [19:22:40] * pje waves [19:49:01] * Maniac waves back (with a full stomach) [19:51:49] * pje is looking at adding the 'csv' module to PEAK, for Python 2.2 [19:52:50] * Maniac wonders why [19:53:10] i specifically up'd to 2.3 for the csv module [19:55:41] * pje is supporting a 2.2 production environment and will be for months to come. [19:56:04] That's why 'datetime' is also bundled in PEAK, instead of using 2.3. [20:03:04] * _jpl_ returns [20:03:14] <_jpl_> Wow, all that talk about Junction and I'm not here to respond. :( [20:04:44] <_jpl_> If nothing else, having the reactor.run() in the examples would prove to Twisted-types that, indeed, Twisted is being used *somewhere* in there... :) [20:04:56] * pje laughs [20:05:12] I'd have thought that the use of PB would be enough to prove that. [20:05:17] <_jpl_> Otherwise Junction does all the PB stuff for you, you only need to use callLater once in a while. [20:05:45] <_jpl_> Right, but in just using Junction you never have to work with any of the PB classes directly. [20:06:22] * Maniac pokes jack-e with a sharp stick [20:06:57] Ah. [20:07:33] Well, at least now I've added a prototype TwistedScheduler, so you can now at least use timed events with Twisted. [20:07:47] So, that 'reactor.run()' might be the only Twisted-ism left. ;) [20:08:05] to replace reactor.CallLater? [20:08:08] <_jpl_> It might be amusing to keep that there for a while... [20:08:36] Maniac: yes, an events.IScheduler gives you sleep(), timeout() etc. objects that you can yield on in an events.Thread. [20:09:06] So instead of calling yourself back later, you just suspend yourself (allowing other things to run) until the desired time. [20:09:49] <_jpl_> So what would be the equivalent of self.reactor.callLater(x, y)? [20:09:57] * Maniac echo's the question [20:13:21] scheduler.sleep(x).addCallback(lamdba s,e: y() or True) [20:13:59] Or, more sensibly... [20:14:06] yield scheduler.sleep(x); events.resume() [20:14:08] y() [20:14:12] * pje grins [20:14:53] I suppose if you want to continue on with something else in the meantime, the previous approach is better. [20:15:24] But, in all use cases I know of where I currently use callLater(), I'd rather be suspending and then just doing whatever it was I wanted to do later. [20:15:30] so much for "looking at adding csv" [20:15:52] * pje chuckles. [20:15:56] <_jpl_> But what about cases like in an uponAssembly method, where you just want to schedule something else to happen and complete? [20:16:02] It was easy; so sue me. :) [20:16:22] * Maniac subscribed to checkins last week [20:16:26] jpl: just create a Thread uponAssembly instead. [20:16:41] See peak.tools.supervisor.busy.BusyStarter for an example... [20:16:59] <_jpl_> Ah, ok [20:17:00] the 'busyMonitor()' method is a binding.Make(events.threaded(busyMonitor),uponAssembly=True) [20:17:18] <_jpl_> I've been meaning to grok the supervisor anyway. [20:17:59] The binding.Make() will return a Thread object as soon as it hits the first 'yield' statement in the threaded method. [20:18:12] So, you just start a thread that yields to whatever delay is desired. [20:18:36] (And zero delay is fine if you just want it to wait until the reactor has started.) [20:19:51] <_jpl_> I don't see busyMonitor... [20:20:10] <_jpl_> Do you mean busyCount? [20:20:15] monitorUsage, sorry. [20:20:52] <_jpl_> just found it [20:21:00] It times "peak usage" events, where all child processes are serving web hits, and the maximum number of processes are running. [20:21:25] Very simple to code in threaded form, but in callback form it would need state variables or multiple routines. [20:22:05] The 'yield busyCount' statements wait until the value of 'busyCount' is changed. [20:23:12] ok here's my use case for reactor.callLater (or reactor.loopingCall) scheduling my rss downloader to download my rss feeds every X minutes [20:23:30] Maniac... [20:23:33] while True: [20:23:43] # run the downloader [20:23:58] yield scheduler.sleep(x*60); events.resume() [20:24:10] What could be easier? :) [20:24:21] <_jpl_> Nifty. [20:24:51] JPL: which? [20:24:55] <_jpl_> This is really going to make Twisted people grumpy. :) [20:25:13] <_jpl_> (well, most are already pretty grumpy) [20:25:28] * pje laughs [20:25:39] <_jpl_> The 'while True' example; it's much more intuitive than reactor.callLater. [20:25:58] It's not really any different than twisted.flow... except that it's easier to understand than twisted.flow. :) [20:26:01] At least, I hope it is. [20:26:36] i never understood twisted.flow :) (read it once) [20:26:47] <_jpl_> It is, because with flow you still have to use the reactor directly for everything else. [20:27:12] <_jpl_> Hmm, now I'm wondering how you might fit deferreds into peak.events. [20:27:37] jpl: You'd create an IEventSource adapter for them. [20:27:39] <_jpl_> I think I'm going to have to untwist my brain a bit to get back to normal async programming again. [20:27:50] *normal*???? [20:28:12] Or do you mean peak.events == normal? :) [20:28:44] <_jpl_> er, I mean normal *synchronous* programming [20:28:56] <_jpl_> See what it's done to me? [20:29:11] <_jpl_> I didn't have this problem until a few months ago. :) [20:29:34] IEventSource only has two methods you need to implement, btw... nextAction and addCallback. [20:30:01] If you want deferreds to be more like events.Value, you can also implement __call__ and set, making it an IWritableSource. [20:30:40] You should be able to implement nextAction as something like: [20:30:49] def nextAction(self, thread=None, state=None): [20:30:54] if thread is not None: [20:31:03] <_jpl_> Are there any examples of using an IEventSource? [20:31:29] self.subject.addCallback(lambda value: thread.step(self,value)) [20:31:44] There's probably a bit more to it than that, but that'd be the basic idea. [20:31:58] jpl: the example in peak.tools.supervisor [20:32:05] events.Value is an IEventSource. [20:32:13] <_jpl_> Oh right, I'm seeing that now... [20:33:20] twisted people are not grumpy if you remember : "asyncronicity is next to godliness" and "threads are the devil" [20:33:27] <_jpl_> So with an appropriate adapter, you could just yield the deferred? [20:33:35] Yep. [20:33:59] <_jpl_> Maniac: They're grumpy about a lot of other things, too. [20:34:00] I think that nextAction would actually need to do a little more, in order to handle errBack. [20:34:27] _jpl_, figure it all out and i'll just copy you :) [20:34:39] You'd probably need to push a generator into the thread, that would re-raise the error inside the thread. [20:35:08] Then, yielding on the deferred would raise an error if the deferred resulted in an error. [20:35:39] <_jpl_> Which you could just wrap in a local try/except block like in any other sane program... [20:35:46] Right, exactly. [20:35:52] <_jpl_> So nice. [20:36:30] Maniac, if he gets it working, I'll add it to peak with a 'whenImported("twisted.internet.defer")' block. :) [20:36:30] <_jpl_> Out of curiosity, is it resume() that raises the exception if one is pending for the thread? [20:36:37] Yes. [20:36:47] See 'peak help events.resume' :) [20:36:48] <_jpl_> That makes perfect sense. [20:37:29] And if the thing you yielded on produces a value, it's returned by events.resume. [20:37:41] <_jpl_> But I'm not sure I know how to "push a generator into the thread". [20:37:43] (As you can see in the supervisor example.) [20:37:49] * _jpl_ nods [20:38:18] jpl: see the 'TaskAsTaskSwitch' class in events.event_threads [20:38:38] essentially, 'state.CALL(iterator)' does the trick. [20:39:01] So, if you have a generator function f, 'state.CALL(f())' "pushes the generator" into the thread. [20:39:29] You can only do this in a nextAction() call that has received a non-None 'state'. [20:40:42] <_jpl_> So the generator could just be a locally defined function with a "yield SomeException"? [20:41:57] You mean raise. [20:42:14] <_jpl_> But it wouldn't be a generator without yield. [20:42:21] There should be a yield somewhere *after* the raise. :) [20:42:41] Question: do deferreds really do anything different for callback vs errback? [20:42:43] <_jpl_> Umm.. [20:43:29] <_jpl_> AFAIK the only difference is that an errback gets a twisted.something.Failure instance. [20:43:55] Yeah, but looking at the source it looks like a regular callback can get that, too. [20:44:23] <_jpl_> (twisted.pythong.failure.Failure) [20:44:30] Oh wait, never mind, I see it now. [20:44:32] <_jpl_> oh? [20:45:08] <_jpl_> s/pythong/python/ [20:45:18] * _jpl_ wonders where his mind is atm [20:45:32] Okay, so I guess you could pass the same 'lambda v: thread.step(self,v)' as a callback *and* an errback. [20:48:03] <_jpl_> Then in the thread test to see if you get a Failure instance from resume()? [20:48:13] Yeah... something like... [20:48:18] def nextAction(self,thread=None,state=None): [20:48:18] def handler(): [20:48:19] v = events.resume() [20:48:19] if isinstance(v,failure.Failure): [20:48:19] raise v [20:48:21] else: [20:48:25] yield v [20:48:29] if state is not None: [20:48:31] state.CALL(handler()) [20:48:33] cb = lambda v: thread.step(self,v) [20:48:35] self.subject.addCallbacks(cb,cb) [20:49:34] This is rather tricky code, even for me, I'm afraid... [20:50:01] The trick is that handler() doesn't start running until the thread is resumed by the callback calling step(self,v) [20:50:34] So, it can use events.resume to get at the value, even though it hasn't yielded yet. [20:50:51] At least, that's the theory, cause I haven't actually run this code. :) [20:51:26] <_jpl_> I'll see if I can get it to work. It'll be a good intro to peak.events for me. :) [20:53:37] If you want it to be a full eventsource (as opposed to just an ITaskSwitch), you'll also need to implement addCallback. [20:54:06] However, that will basically amount to just the cb=/addCallbacks part. [20:54:08] <_jpl_> What would be the advantage of being a full eventsource? [20:54:30] You can use it with AnyOf. [20:55:29] However, when using it with AnyOf, you won't be able to support errback with a raise-in-the-thread paradigm. [20:55:50] * _jpl_ nods [20:56:51] The other use of IEventSource is if we ever start making push-transform event sources that register callbacks with an upstream source to apply a transformation to the event. [20:57:49] <_jpl_> Which version of PEAK is that slated for? :) [20:58:25] * pje laughs [20:58:45] Notice I said "if we ever". I.e., if/when I need it for work. :) [20:59:22] * _jpl_ noticed [20:59:25] Anyway, being able to yield on a twisted deferred, just by registering an adapter, is no small potatoes. [20:59:45] So, don't feel too bad if you don't make it a full event source. :) [21:00:57] <_jpl_> I thought I'd just start with taskswitch and see where that goes. [21:02:01] Let me know how it works out. [21:02:22] It'd be really cool being able to do e.g. [21:02:48] yield reactor.resolve('somehost'); ipaddr=events.resume() [21:02:51] * pje grins [21:03:16] <_jpl_> reactor.resolve()? [21:03:35] Er, isn't that the standard method in IReactorCore for resolving a hostname? [21:03:44] Or is it deprecated now? [21:04:15] <_jpl_> Sorry, just looked at the code... hadn't seen that one before. [21:06:09] Hmmm... interesting... [21:06:28] <_jpl_> And like a fool I've been using socket.getfqdn all this time. [21:06:42] Looks like you can shortcut some of the logic in the event the deferred has already been called... [21:07:16] In 'nextAction', you can start w/something like: [21:07:21] if self.subject.called: [21:07:57] if isinstance(subject.result,failure.Failure): [21:08:10] if state: [21:08:16] raise subjec.result [21:08:20] Oops. [21:08:25] Bad indent. Let me try that again... [21:10:25] if self.subject.called: [21:10:25] if state is not None: [21:10:25] result = self.subject.result [21:10:25] if isinstance(result,failure.Failure): [21:10:25] raise result [21:10:27] else: [21:10:29] state.YIELD(result) [21:10:31] return True [21:10:57] There. That will allow the thread to proceed immediately with the result, if the deferred has already been called back. [21:11:51] Then, you do the stuff I said before, in case you need to wait. [21:12:20] <_jpl_> So that code would go at the beginning of nextAction? [21:12:23] Yeah. [21:13:27] <_jpl_> Should there be a 'return True' at the end of the method? [21:13:36] No... [21:13:43] just in the 'if subject.called' block. [21:13:54] Returning true means the thread can continue executing *now*... [21:14:05] returning false means it must suspend. [21:14:25] <_jpl_> (TaskAsTaskSwitch.nextAction had a 'return True' at the end, I didn't know if this would need it or not) [21:15:15] <_jpl_> What's the equivalent of reactor.run() with peak.events? [21:15:39] reactor.run(). :) [21:15:55] Here's a refactored version of nextAction: [21:15:57] def nextAction(self, thread=None, state=None): [21:15:57] if state is not None: [21:15:57] def handler(): [21:15:59] v = events.resume() [21:16:01] if isinstance(v,failure.Failure): [21:16:03] raise v [21:16:05] else: [21:16:07] yield v [21:16:11] state.CALL(handler()) [21:16:13] [21:16:15] if self.subject.called: [21:16:17] state.YIELD(self.subject.result) [21:16:19] else: [21:16:21] cb = lambda v: thread.step(self,v) [21:16:23] self.subject.addCallbacks(cb,cb) [21:16:29] return self.subject.called [21:16:33] This *always* pushes the handler, but then decides whether to yield the value immediately or to use a callback to run later. [21:16:52] Then, it returns a flag to indicate whether the thread should continue. [21:17:23] As for reactor.run(), technically the correct thing is mainloop.run() [21:17:28] Where mainloop is an IMainLoop. [21:17:56] I never use reactor.run() in app-level code. [21:18:19] mainloop can manage inactivity timeouts, and overall run-length. [21:18:23] <_jpl_> Ok. Trying to write a rudimentary test program. [21:20:05] <_jpl_> Hmm, in a really rudimentary program how would I "fire off" a generator? [21:20:13] Of course, this needs to be a protocols.Adapter with adapterForTypes=[twisted.internet.defer.Deferred] [21:20:19] Fire off a generator? [21:20:22] What do you mean? [21:20:27] Make it a thread? [21:20:47] simplest way is events.Thread(someGenerator()) [21:21:24] If it's a component method, 'aMethod = binding.Make(events.threaded(aMethod))' [21:21:26] <_jpl_> So I could just instantiate a Thread like that and then reactor.run()? [21:21:33] optionally w/uponAssembly... [21:21:39] jpl: yep. [21:21:50] Though to test it with e.g. resolve(), you don't even need the reactor.run() part. [21:22:27] You could just write a generator that yields "reactor.resolve()' [21:22:31] er, I mean... [21:22:46] yield reactor.resolve("something"); ipaddr=events.resume() [21:22:51] print ipaddr [21:23:14] That'd be about it, though it'll only test the "subject.called" branch of the adapter. [21:23:39] <_jpl_> But assuming I don't have a mainloop, just a couple of lines of setup code... [21:24:19] <_jpl_> ie. something like [21:24:21] <_jpl_> r = config.makeRoot() [21:24:21] <_jpl_> j = Junction(r) [21:24:21] <_jpl_> config.setPropertyFor(j, PropertyName('junction.server'), 'prodeng') [21:24:21] <_jpl_> config.setPropertyFor(j, PropertyName('junction.port'), 11111) [21:24:39] <_jpl_> def test(): [21:24:39] <_jpl_> d = j.callServiceMethod('ConfigurationService', 'getConfiguration') [21:24:39] <_jpl_> yield d; v = events.resume() [21:24:39] <_jpl_> print v [21:24:40] Feh. How's this: [21:24:42] def testDeferred(d): [21:24:42] yield d; print events.resume() [21:24:42] d = defer.Deferred() [21:24:42] t = events.Thread(testDeferred(d)) [21:24:42] d.callback(42) [21:25:09] That should print 42, if this works right. [21:25:14] No reactor needed. [21:25:30] <_jpl_> Ah yes, that's much easier to test. [21:25:57] And you can try creating the thread either before or after the callback(42) part, so as to test both branches. [21:26:13] And, you can try errback'ing it instead of callbacking, to see what kind of traceback you get. [21:26:22] Again, both before and after thread creation. [21:26:54] <_jpl_> Btw, did you get the patch I sent to add install support for ZConfig's schema.dtd? [21:27:15] Yes. I take it you mean I've forgotten to apply it? :) [21:27:59] <_jpl_> (maybe I should check my e-mail for the first time today...) [21:29:02] Er, I just realized... I don't have a patch, just your comment that it was broken. [21:29:21] Please send the patch. [21:29:33] Meanwhile, how'd the deferred wrapper work? [21:29:40] <_jpl_> Oh. I sent a patch file yesterday from the wrong address. It must still be in the mailing list pending queue. [21:30:49] <_jpl_> Looks like it works. :) [21:36:17] <_jpl_> Calling d.errback before the thread creation causes an exception to be raised, but when called after the thread creation the Failure is simply returned. [21:36:48] Hm. [21:37:14] Take a look at http://peak.telecommunity.com/DevCenter/TwistedDeferredForPeakEvents [21:38:48] Does that look like your code? [21:41:06] <_jpl_> Except for some newlines and less test code, it's exact. [21:41:12] Mine also prints '1' for the second test, which looks wrong too. [21:41:28] <_jpl_> er, "exactly the same" [21:44:19] Ah, crud... I see the problem. [21:45:42] The 'cb' callback needs to return v back to the deferred. [21:46:29] <_jpl_> Oh, right, since callbacks on deferreds are supposed to be filters. [21:46:48] That fixes the first problem. [21:47:10] The second is that doing an errback() after the thread has started doesn't throw the error in the thread, AFAICT. [21:47:24] Starting the thread after the errback() works. [21:47:45] <_jpl_> That's what I was seeing. [21:49:13] Oh wait... I see the problem... [21:49:21] (Maybe) [21:50:03] Okay, got it... [21:50:18] The problem is that the deferred catches the error and throws it away. :) [21:50:32] Changing the testDeferred method so it traps the error, makes it work. [21:50:34] * _jpl_ nods [21:51:58] Yeeha. I've updated the wiki page with the correct code. [21:52:29] It's still problematic that the deferred will trap the error. :( [21:52:40] But I don't see any way around it, and I should really go home now... [21:52:50] <_jpl_> Can't we just re-raise it? [21:53:11] No... because it's the invocation of 'errback()' that's trapping the error. [21:53:37] Because it's errback() that's calling thread.step(), you see. [21:53:46] <_jpl_> I've modified mine to append the values to a list, so it'll be a quick step to a unit test. [21:54:28] So, if the error isn't handled by the thread, it gets trapped by errback, which then sets a new failure result. [21:55:21] Anyway, it looks pretty cool. So you can now go out and amaze your friends with untwisted threads... :) [21:55:54] Although I predict that Twisted mavens will simply say that the proper way to do it is to use twisted.flow. [21:56:01] <_jpl_> This is really great, it's going to vastly simplify a lot of things I've been working on. [21:56:08] <_jpl_> Thanks for all the help! [21:56:20] No problem. Have fun! [21:56:32] ** pje has left us [21:56:38] <_jpl_> I think at least some of the mavens realize flow isn't all that hot, though. [22:01:58] _jpl_, so you have it working or only in theory? [22:06:20] <_jpl_> It could use a little tweaking, I think, but it's pretty functional. [22:07:32] <_jpl_> There's still a rogue somewhere printing out the value of a Failure. [22:11:25] <_jpl_> I'll see if I can make it work with Junction now. [22:18:36] rogue? [22:25:10] <_jpl_> Some rogue bit of code.