Re: Bi-directional sub-process communication
On Mon, Nov 23, 2015 at 10:25 PM, Cameron Simpsonwrote: > Then #3. I would have a common function/method for submitting a request to > go to the subprocess, and have that method return an Event on which to wait. > Then caller then just waits for the Event and collects the data. Obviously, > the method does not just return the Event, but an Event and something to > receive the return data. I've got a class called a Result for this kind of > thing; make a small class containing an Event and which will have a .result > attribute for the return information; the submitting method allocates one of > these and returns it. The response handler gets the instance (by looking it > up from the tag), sets the .result attribute and fires the Event. Your > caller wakes up from waiting on the Event and consults the .result > attribute. Your Result sounds suspiciously like a Future. ;-) -- https://mail.python.org/mailman/listinfo/python-list
Re: Bi-directional sub-process communication
On 24Nov2015 06:33, israelwrote: On 11/23/2015 20:29, Cameron Simpson wrote: On 24Nov2015 16:25, Cameron Simpson wrote: Completely untested example code: class ReturnEvent: def __init__(self): self.event = Event() With, of course: def wait(self): return self.event.wait() Of course :-) Ah, the Event() object comes from the threading module. That makes sense. This should work perfectly. Thanks so much for taking the time to help me out! Glad to be of service, Cameron Simpson All the doors in this ship have nice sunny dispositions. It is their pleasure to open for you, and their satisfaction to close with the knowledge of a job well done. - Marvin _The Hitchhiker's Guide to the Galaxy_ -- https://mail.python.org/mailman/listinfo/python-list
Re: Bi-directional sub-process communication
On 24Nov2015 14:53, Ian Kellywrote: On Mon, Nov 23, 2015 at 10:25 PM, Cameron Simpson wrote: Then #3. I would have a common function/method for submitting a request to go to the subprocess, and have that method return an Event on which to wait. Then caller then just waits for the Event and collects the data. Obviously, the method does not just return the Event, but an Event and something to receive the return data. I've got a class called a Result for this kind of thing; make a small class containing an Event and which will have a .result attribute for the return information; the submitting method allocates one of these and returns it. The response handler gets the instance (by looking it up from the tag), sets the .result attribute and fires the Event. Your caller wakes up from waiting on the Event and consults the .result attribute. Your Result sounds suspiciously like a Future. ;-) Yeah. I already had this stuff when the futures module was released, with some additional stuff futures didn't have (eg my dispatch queue is a priority queue, which is handy for some kinds of workflows; default dispatch is FIFO). My commonest use case/instance is a LateFunction, returned from my Later class. Use: L = Later() LF = L.defer(cllable, *a, *kw) ... result = LF() When you call a LateFunction you get the function result, blocking if it has not yet been run. It also raises if the deferred function raised. It has additional methods, but that is the core use: make it look like a function. You can do other easy things like: L.after(LFs, callable, *a, **kw) to have a function dispatched after the completion of other LateFunctions or: with L.ready(): ... suite ... to block until the Later has a slot, then run the suite. My Result and LateFunctions are subclasses of an Asynchron base class, which lets you wait for the result to arrive and has the common methods (wait, etc). The Result class supports this: R = Result() # caller, blocks until value received value = R.result # worker: deliver result R.result = func(blah) This makes thread cooperation far far more friendly. Cheers, Cameron Simpson Ride with a llama and you never ride alone. - Jeff Earls, DoD #0530, -- https://mail.python.org/mailman/listinfo/python-list
Re: Bi-directional sub-process communication
On 11/23/2015 20:29, Cameron Simpson wrote: On 24Nov2015 16:25, Cameron Simpsonwrote: Completely untested example code: class ReturnEvent: def __init__(self): self.event = Event() With, of course: def wait(self): return self.event.wait() Of course :-) Ah, the Event() object comes from the threading module. That makes sense. This should work perfectly. Thanks so much for taking the time to help me out! - Israel Brewster Cheers, Cameron Simpson Maintainer's Motto: If we can't fix it, it ain't broke. -- https://mail.python.org/mailman/listinfo/python-list
Re: Bi-directional sub-process communication
On Mon, Nov 23, 2015 at 10:54 AM, Israel Brewsterwrote: > Concern: Since the master process is multi-threaded, it seems likely enough > that multiple threads on the master side would make requests at the same > time. I understand that the Queue class has locks that make this fine (one > thread will complete posting the message before the next is allowed to > start), and since the child process only has a single thread processing > messages from the queue, it should process them in order and post the > responses (if any) to the master_queue in order. But now I have multiple > master processes all trying to read master_queue at the same time. Again, the > locks will take care of this and prevent any overlapping reads, but am I > guaranteed that the threads will obtain the lock and therefore read the > responses in the right order? Or is there a possibility that, say, thread > three will get the response that should have been for thread one? Is this > something I need to take into consideration, and if so, how? Yes, if multiple master threads are waiting on the queue, it's possible that a master thread could get a response that was not intended for it. As far as I know there's no guarantee that the waiting threads will be woken up in the order that they called get(), but even if there are, consider this case: Thread A enqueues a request. Thread B preempts A and enqueues a request. Thread B calls get on the response queue. Thread A calls get on the response queue. The response from A's request arrives and is given to B. Instead of having the master threads pull objects off the response queue directly, you might create another thread whose sole purpose is to handle the response queue. That could look like this: request_condition = threading.Condition() response_global = None def master_thread(): global response_global with request_condition: request_queue.put(request) request_condition.wait() # Note: the Condition should remain acquired until response_global is reset. response = response_global response_global = None if wrong_response(response): raise RuntimeError("got a response for the wrong request") handle_response(response) def response_thread(): global response_global while True: response = response_queue.get() with request_condition: response_global = response request_condition.notify() As another option you could use a multiprocessing.Manager to coordinate passing the response back more directly, but starting a third process seems like overkill for this. -- https://mail.python.org/mailman/listinfo/python-list
Re: Bi-directional sub-process communication
On Nov 23, 2015, at 12:45 PM, Cameron Simpsonwrote: > > On 23Nov2015 12:22, Israel Brewster wrote: >> On Nov 23, 2015, at 11:51 AM, Ian Kelly wrote: >>> Concurrency, ugh. > > I'm a big concurrency fan myself. > >>> It's probably better just to have a Condition/Event per thread and >>> have the response thread identify the correct one to notify, rather >>> than just notify a single shared Condition and hope the threads wake >>> up in the right order. >> >> Tell me about it :-) I've actually never worked with conditions or >> notifications (actually even this bi-drectional type of communication is new >> to me), so I'll have to look into that and figure it out. Thanks for the >> information! > > I include a tag with every request, and have the responses include the tag; > the request submission function records the response hander in a mapping by > tag and the response handing thread looks up the mapping and passes the > response to the right handler. > > Works just fine and avoids all the worrying about ordering etc. > > Israel, do you have control over the protocol between you and your > subprocess? If so, adding tags is easy and effective. I do, and the basic concept makes sense. The one difficulty I am seeing is getting back to the thread that requested the data. Let me know if this makes sense or I am thinking about it wrong: - When a thread requests some data, it sends the request as a dictionary containing a tag (unique to the thread) as well as the request - When the child processes the request, it encodes the response as a dictionary containing the tag and the response data - A single, separate thread on the "master" side parses out responses as they come in and puts them into a dictionary keyed by tag - The requesting threads, after putting the request into the Queue, would then block waiting for data to appear under their key in the dictionary Of course, that last step could be interesting - implementing the block in such a way as to not tie up the processor, while still getting the data "as soon" as it is available. Unless there is some sort of built-in notification system I could use for that? I.e. the thread would "subscribe" to a notification based on its tag, and then wait for notification. When the master processing thread receives data with said tag, it adds it to the dictionary and "publishes" a notification to that tag. Or perhaps the notification itself could contain the payload? Thanks for the information! > > Cheers, > Cameron Simpson -- https://mail.python.org/mailman/listinfo/python-list
Re: Bi-directional sub-process communication
On Mon, Nov 23, 2015 at 2:18 PM, Israel Brewsterwrote: > Of course, that last step could be interesting - implementing the block in > such a way as to not tie up the processor, while still getting the data "as > soon" as it is available. Unless there is some sort of built-in > notification system I could use for that? I.e. the thread would "subscribe" > to a notification based on its tag, and then wait for notification. When > the master processing thread receives data with said tag, it adds it to the > dictionary and "publishes" a notification to that tag. Or perhaps the > notification itself could contain the payload? There are a few ways I could see handling this, without having the threads spinning and consuming CPU: 1. Don't worry about having the follow-up code run in the same thread, and use a simple callback. This callback could be dispatched to a thread via a work queue, however you may not get the same thread as the one that made the request. This is probably the most efficient method to use, as the threads can continue doing other work while waiting for a reply, rather than blocking. It does make it harder to maintain state between the pre- and post-request functions, however. 2. Have a single, global, event variable that wakes all threads waiting on a reply, each of which then checks to see if the reply is for it, or goes back to sleep. This is good if most of the time, only a few threads will be waiting for a reply, and checking if the correct reply came in is cheap. This is probably good enough, unless you have a LOT of threads (hundreds). 3. Have an event per thread. This will use less CPU than the second option, however does require more memory and OS resources, and so will not be viable for huge numbers of threads, though if you hit the limit, you are probably using threads wrong. 4. Have an event per request. This is only better than #3 if a single thread may make multiple requests at once, and can do useful work when any of them get a reply back (if they need all, it will make no difference). Generally, I would use option #1 or #2. Option 2 has the advantage of making it easy to write the functions that use the functionality, while option 1 will generally use fewer resources, and allows threads to continue to be used while waiting for replies. How much of a benefit that is depends on exactly what you are doing. Option #4 would probably be better implemented using option #1 in all cases to avoid problems with running out of OS memory - threading features generally require more limited OS resources than memory. Option #3 will also often run into the same issues as option #4 in the cases it will provide any benefit over option #2. Chris -- https://mail.python.org/mailman/listinfo/python-list
Re: Bi-directional sub-process communication
On Nov 23, 2015, at 1:43 PM, Chris Kaynorwrote: > > On Mon, Nov 23, 2015 at 2:18 PM, Israel Brewster > wrote: > >> Of course, that last step could be interesting - implementing the block in >> such a way as to not tie up the processor, while still getting the data "as >> soon" as it is available. Unless there is some sort of built-in >> notification system I could use for that? I.e. the thread would "subscribe" >> to a notification based on its tag, and then wait for notification. When >> the master processing thread receives data with said tag, it adds it to the >> dictionary and "publishes" a notification to that tag. Or perhaps the >> notification itself could contain the payload? > > > There are a few ways I could see handling this, without having the threads > spinning and consuming CPU: > > 1. Don't worry about having the follow-up code run in the same thread, > and use a simple callback. This callback could be dispatched to a thread > via a work queue, however you may not get the same thread as the one that > made the request. This is probably the most efficient method to use, as the > threads can continue doing other work while waiting for a reply, rather > than blocking. It does make it harder to maintain state between the pre- > and post-request functions, however. > 2. Have a single, global, event variable that wakes all threads waiting > on a reply, each of which then checks to see if the reply is for it, or > goes back to sleep. This is good if most of the time, only a few threads > will be waiting for a reply, and checking if the correct reply came in is > cheap. This is probably good enough, unless you have a LOT of threads > (hundreds). > 3. Have an event per thread. This will use less CPU than the second > option, however does require more memory and OS resources, and so will not > be viable for huge numbers of threads, though if you hit the limit, you are > probably using threads wrong. > 4. Have an event per request. This is only better than #3 if a single > thread may make multiple requests at once, and can do useful work when any > of them get a reply back (if they need all, it will make no difference). > > Generally, I would use option #1 or #2. Option 2 has the advantage of > making it easy to write the functions that use the functionality, while > option 1 will generally use fewer resources, and allows threads to continue > to be used while waiting for replies. How much of a benefit that is depends > on exactly what you are doing. While I would agree with #1 in general, the threads, in this case, are CherryPy threads, so I need to get the data and return it to the client in the same function call, which of course means the thread needs to block until the data is ready - it can't return and let the result be processed "later". Essentially there are times that the web client needs some information that only the Child process has. So the web client requests the data from the master process, and the master process then turns around and requests the data from the child, but it needs to get the data back before it can return it to the web client. So it has to block waiting for the data. Thus we come to option #2 (or 3), which sounds good but I have no clue how to implement :-) Maybe something like http://pubsub.sourceforge.net ? I'll dig into that. > > Option #4 would probably be better implemented using option #1 in all cases > to avoid problems with running out of OS memory - threading features > generally require more limited OS resources than memory. Option #3 will > also often run into the same issues as option #4 in the cases it will > provide any benefit over option #2. > > Chris > -- > https://mail.python.org/mailman/listinfo/python-list -- https://mail.python.org/mailman/listinfo/python-list
Re: Bi-directional sub-process communication
On 23Nov2015 12:22, Israel Brewsterwrote: On Nov 23, 2015, at 11:51 AM, Ian Kelly wrote: Concurrency, ugh. I'm a big concurrency fan myself. It's probably better just to have a Condition/Event per thread and have the response thread identify the correct one to notify, rather than just notify a single shared Condition and hope the threads wake up in the right order. Tell me about it :-) I've actually never worked with conditions or notifications (actually even this bi-drectional type of communication is new to me), so I'll have to look into that and figure it out. Thanks for the information! I include a tag with every request, and have the responses include the tag; the request submission function records the response hander in a mapping by tag and the response handing thread looks up the mapping and passes the response to the right handler. Works just fine and avoids all the worrying about ordering etc. Israel, do you have control over the protocol between you and your subprocess? If so, adding tags is easy and effective. Cheers, Cameron Simpson -- https://mail.python.org/mailman/listinfo/python-list
Re: Bi-directional sub-process communication
On 24Nov2015 16:25, Cameron Simpsonwrote: Completely untested example code: class ReturnEvent: def __init__(self): self.event = Event() With, of course: def wait(self): return self.event.wait() Cheers, Cameron Simpson Maintainer's Motto: If we can't fix it, it ain't broke. -- https://mail.python.org/mailman/listinfo/python-list
Re: Bi-directional sub-process communication
On 23Nov2015 14:14, Israel Brewsterwrote: On Nov 23, 2015, at 1:43 PM, Chris Kaynor wrote: On Mon, Nov 23, 2015 at 2:18 PM, Israel Brewster wrote: 3. Have an event per thread. This will use less CPU than the second option, however does require more memory and OS resources, and so will not be viable for huge numbers of threads, though if you hit the limit, you are probably using threads wrong. [...] While I would agree with #1 in general, the threads, in this case, are CherryPy threads, so I need to get the data and return it to the client in the same function call, which of course means the thread needs to block until the data is ready - it can't return and let the result be processed "later". Then #3. I would have a common function/method for submitting a request to go to the subprocess, and have that method return an Event on which to wait. Then caller then just waits for the Event and collects the data. Obviously, the method does not just return the Event, but an Event and something to receive the return data. I've got a class called a Result for this kind of thing; make a small class containing an Event and which will have a .result attribute for the return information; the submitting method allocates one of these and returns it. The response handler gets the instance (by looking it up from the tag), sets the .result attribute and fires the Event. Your caller wakes up from waiting on the Event and consults the .result attribute. Completely untested example code: class ReturnEvent: def __init__(self): self.event = Event() seq = 0 re_by_tag = {} def submit_request(query): global seq, re_by_tag tag = seq seq += 1 RE = ReturnEvent() re_by_tag[tag] = RE send_request(tag, query) return RE def process_response(tag, response_data): RE = re_by_tag.pop(tag) RE.result = response_data RE.event.set() ... CherryPy request handler ... RE = submit_request(your_query_info) RE.wait() response_data = RE.result Cheers, Cameron Simpson -- https://mail.python.org/mailman/listinfo/python-list
Re: Bi-directional sub-process communication
On Nov 23, 2015, at 3:05 PM, Dennis Lee Bieberwrote: > > On Mon, 23 Nov 2015 08:54:38 -0900, Israel Brewster > declaimed the following: > >> Concern: Since the master process is multi-threaded, it seems likely enough >> that multiple threads on the master side would make requests at the same >> time. I understand that the Queue class has locks that make > > Multiple "master" threads, to me, means you do NOT have a "master > process". But I do: the CherryPy "application", which has multiple threads - one per request (and perhaps a few more) to be exact. It's these request threads that generate the calls to the child process. > > Let there be a Queue for EVERY LISTENER. > > Send the Queue as part of the request packet. No luck: "RuntimeError: Queue objects should only be shared between processes through inheritance" This IS a master process, with multiple threads, trying to communicate with a child process. That said, with some modifications this sort of approach could still work. --- Israel Brewster Systems Analyst II Ravn Alaska 5245 Airport Industrial Rd Fairbanks, AK 99709 (907) 450-7293 --- > > Let the subthread reply to the queue that was provided via the packet > > Voila! No intermixing of "master/slave" interaction; each slave only > replies to the master that sent it a command; each master only receives > replies from slaves it has commanded. Slaves can still be shared, as they > are given the information of which master they need to speak with. > > > > -- > Wulfraed Dennis Lee Bieber AF6VN >wlfr...@ix.netcom.comHTTP://wlfraed.home.netcom.com/ > > -- > https://mail.python.org/mailman/listinfo/python-list -- https://mail.python.org/mailman/listinfo/python-list
Bi-directional sub-process communication
I have a multi-threaded python app (CherryPy WebApp to be exact) that launches a child process that it then needs to communicate with bi-driectionally. To implement this, I have used a pair of Queues: a child_queue which I use for master->child communication, and a master_queue which is used for child->master communication. The way I have the system set up, the child queue runs a loop in a tread that waits for messages on child_queue, and when received responds appropriately depending on the message received, which sometimes involves posting a message to master_queue. On the master side, when it needs to communicate with the child process, it posts a message to child_queue, and if the request requires a response it will then immediately start waiting for a message on master_queue, typically with a timeout. While this process works well in testing, I do have one concern (maybe unfounded) and a real-world issue Concern: Since the master process is multi-threaded, it seems likely enough that multiple threads on the master side would make requests at the same time. I understand that the Queue class has locks that make this fine (one thread will complete posting the message before the next is allowed to start), and since the child process only has a single thread processing messages from the queue, it should process them in order and post the responses (if any) to the master_queue in order. But now I have multiple master processes all trying to read master_queue at the same time. Again, the locks will take care of this and prevent any overlapping reads, but am I guaranteed that the threads will obtain the lock and therefore read the responses in the right order? Or is there a possibility that, say, thread three will get the response that should have been for thread one? Is this something I need to take into consideration, and if so, how? Real-world problem: While as I said this system worked well in testing, Now that I have gotten it out into production I've occasionally run into a problem where the master thread waiting for a response on master_queue times out while waiting. This causes a (potentially) two-fold problem, in that first off the master process doesn't get the information it had requested, and secondly that I *could* end up with an "orphaned" message on the queue that could cause problems the next time I try to read something from it. I currently have the timeout set to 3 seconds. I can, of course, increase that, but that could lead to a bad user experience - and might not even help the situation if something else is going on. The actual exchange is quite simple: On the master side, I have this code: config.socket_queue.put('GET_PORT') try: port = config.master_queue.get(timeout=3) #wait up to three seconds for a response except Empty: port = 5000 # default. Can't hurt to try. Which, as you might have been able to guess, tries to ask the child process (an instance of a tornado server, btw) what port it is listening on. The child process then, on getting this message from the queue, runs the following code: elif item == 'GET_PORT': port = utils.config.getint('global', 'tornado.port') master_queue.put(port) So nothing that should take any significant time. Of course, since this is a single thread handling any number of requests, it is possible that the thread is tied up responding to a different request (or that the GIL is preventing the thread from running at all, since another thread might be commandeering the processor), but I find it hard to believe that it could be tied up for more than three seconds. So is there a better way to do sub-process bi-directional communication that would avoid these issues? Or do I just need to increase the timeout (or remove it altogether, at the risk of potentially causing the thread to hang if no message is posted)? And is my concern justified, or just paranoid? Thanks for any information that can be provided! --- Israel Brewster Systems Analyst II Ravn Alaska 5245 Airport Industrial Rd Fairbanks, AK 99709 (907) 450-7293 --- -- https://mail.python.org/mailman/listinfo/python-list
Re: Bi-directional sub-process communication
On Mon, Nov 23, 2015 at 12:55 PM, Ian Kellywrote: > On Mon, Nov 23, 2015 at 10:54 AM, Israel Brewster > wrote: >> Concern: Since the master process is multi-threaded, it seems likely enough >> that multiple threads on the master side would make requests at the same >> time. I understand that the Queue class has locks that make this fine (one >> thread will complete posting the message before the next is allowed to >> start), and since the child process only has a single thread processing >> messages from the queue, it should process them in order and post the >> responses (if any) to the master_queue in order. But now I have multiple >> master processes all trying to read master_queue at the same time. Again, >> the locks will take care of this and prevent any overlapping reads, but am I >> guaranteed that the threads will obtain the lock and therefore read the >> responses in the right order? Or is there a possibility that, say, thread >> three will get the response that should have been for thread one? Is this >> something I need to take into consideration, and if so, how? > > Yes, if multiple master threads are waiting on the queue, it's > possible that a master thread could get a response that was not > intended for it. As far as I know there's no guarantee that the > waiting threads will be woken up in the order that they called get(), > but even if there are, consider this case: > > Thread A enqueues a request. > Thread B preempts A and enqueues a request. > Thread B calls get on the response queue. > Thread A calls get on the response queue. > The response from A's request arrives and is given to B. > > Instead of having the master threads pull objects off the response > queue directly, you might create another thread whose sole purpose is > to handle the response queue. That could look like this: > > > request_condition = threading.Condition() > response_global = None > > def master_thread(): > global response_global > with request_condition: > request_queue.put(request) > request_condition.wait() > # Note: the Condition should remain acquired until > response_global is reset. > response = response_global > response_global = None > if wrong_response(response): > raise RuntimeError("got a response for the wrong request") > handle_response(response) > > def response_thread(): > global response_global > while True: > response = response_queue.get() > with request_condition: > response_global = response > request_condition.notify() Actually I realized that this fails because if two threads get notified at about the same time, they could reacquire the Condition in the wrong order and so get the wrong responses. Concurrency, ugh. It's probably better just to have a Condition/Event per thread and have the response thread identify the correct one to notify, rather than just notify a single shared Condition and hope the threads wake up in the right order. -- https://mail.python.org/mailman/listinfo/python-list
Re: Bi-directional sub-process communication
On Nov 23, 2015, at 11:51 AM, Ian Kellywrote: > > On Mon, Nov 23, 2015 at 12:55 PM, Ian Kelly wrote: >> On Mon, Nov 23, 2015 at 10:54 AM, Israel Brewster >> wrote: >>> Concern: Since the master process is multi-threaded, it seems likely enough >>> that multiple threads on the master side would make requests at the same >>> time. I understand that the Queue class has locks that make this fine (one >>> thread will complete posting the message before the next is allowed to >>> start), and since the child process only has a single thread processing >>> messages from the queue, it should process them in order and post the >>> responses (if any) to the master_queue in order. But now I have multiple >>> master processes all trying to read master_queue at the same time. Again, >>> the locks will take care of this and prevent any overlapping reads, but am >>> I guaranteed that the threads will obtain the lock and therefore read the >>> responses in the right order? Or is there a possibility that, say, thread >>> three will get the response that should have been for thread one? Is this >>> something I need to take into consideration, and if so, how? >> >> Yes, if multiple master threads are waiting on the queue, it's >> possible that a master thread could get a response that was not >> intended for it. As far as I know there's no guarantee that the >> waiting threads will be woken up in the order that they called get(), >> but even if there are, consider this case: >> >> Thread A enqueues a request. >> Thread B preempts A and enqueues a request. >> Thread B calls get on the response queue. >> Thread A calls get on the response queue. >> The response from A's request arrives and is given to B. >> >> Instead of having the master threads pull objects off the response >> queue directly, you might create another thread whose sole purpose is >> to handle the response queue. That could look like this: >> >> >> request_condition = threading.Condition() >> response_global = None >> >> def master_thread(): >>global response_global >>with request_condition: >>request_queue.put(request) >>request_condition.wait() >># Note: the Condition should remain acquired until >> response_global is reset. >>response = response_global >>response_global = None >>if wrong_response(response): >>raise RuntimeError("got a response for the wrong request") >>handle_response(response) >> >> def response_thread(): >>global response_global >>while True: >>response = response_queue.get() >>with request_condition: >>response_global = response >>request_condition.notify() > > Actually I realized that this fails because if two threads get > notified at about the same time, they could reacquire the Condition in > the wrong order and so get the wrong responses. > > Concurrency, ugh. > > It's probably better just to have a Condition/Event per thread and > have the response thread identify the correct one to notify, rather > than just notify a single shared Condition and hope the threads wake > up in the right order. Tell me about it :-) I've actually never worked with conditions or notifications (actually even this bi-drectional type of communication is new to me), so I'll have to look into that and figure it out. Thanks for the information! > -- > https://mail.python.org/mailman/listinfo/python-list -- https://mail.python.org/mailman/listinfo/python-list