├── .gitignore ├── LICENSE ├── README.md ├── examples ├── clock.py ├── joins.py └── news.py └── mTasks ├── __init__.py ├── maya_scheduler.py ├── scheduler.py ├── task.py ├── threads.py └── timers.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | 64 | .idea/ 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Steve Theodore 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mTasks 2 | simple, unthreaded multitasking with a Maya twist 3 | 4 | This module provides a limited form of coroutine based multi-tasking. 5 | 6 | It's not intended as a substitute for 'real' threads, but it does allow you to create the illusion of multiple, simultaneous processes without crossing thread boundaries. This is particularly important in Maya, where only the main thread is allowed to touch the contents of the Maya scene or the GUI. It can also be useful in other applications where you'd like concurrent behavior without worrying about the memory-security issues associated with multiple threads. For Maya programming it allows a degree of interacivity which is otherwise complicated hard to create with scriptJobs or threads and `executeDeferred()` -- and it also makes it easy to avoid the kinds of baffling behaviors and random crashes that are so common when trying to work with threads. 7 | 8 | Basics 9 | ------ 10 | 11 | The way it works is quite simple. Everything you want to run 'simultaneously' is a a function which uses Python's built-in `yield` keyword to release control. `mTasks` keeps a list of your functions and loops through the list, running each until it his a yield and then switching to the next. Here's a basic example: 12 | 13 | def hello(): 14 | print "hello" 15 | yield 16 | 17 | def world(): 18 | print "world" 19 | yield 20 | 21 | def exclaim(): 22 | print "!" 23 | yield 24 | 25 | spawn(hello) 26 | spawn(world) 27 | spawn(exclaim) 28 | 29 | run() 30 | 31 | # hello 32 | # world 33 | # ! 34 | 35 | If you just used `yield` in place of `return`, this would be the same as collecting the functions in a and running them in turn. However `yield` 36 | *does not have to end a function*. So you could rewrite the previous example like this: 37 | 38 | def hello(): 39 | print "hello" 40 | yield 41 | print "word" 42 | yield 43 | print "!" 44 | yield 45 | 46 | spawn(hello) 47 | run() 48 | 49 | # hello 50 | # world 51 | # ! 52 | 53 | Any number of functions containing yields can run at the saem time, and their results will be interleaved: 54 | 55 | def numbers(): 56 | for n in range (10): 57 | print "number: ", n 58 | yield 59 | 60 | def letters(): 61 | for a in 'abcdefghijklmnopqrstuvwxyz': 62 | print "letter: ", a 63 | yield 64 | 65 | spawn(numbers) 66 | spawn(letters) 67 | run() 68 | 69 | # number: 1 70 | # letter: a 71 | # number: 2 72 | # letter: b 73 | 74 | # and so... until numbers runs out, but letters keeps going: 75 | 76 | # letter: l 77 | # letter: m 78 | # letter: n 79 | 80 | Running tasks 81 | ---------- 82 | All of the tasks are owned by the `scheduler` module. The scheduler will step through each task in turn, executing until it finds a `yield` or the task terminates. The scheduler's `tick()` methods fires the next function/yield step -- in Maya the tick is usually hooked up to a Maya scriptJob that advances it on every Maya idle event. In a non-Maya context you can set the schedulder to run until all tasks are exhausted with the `run()` method. You generally **don't** want to call `run()` in Maya because it will act like a blocking function call until all of the queued tasks are done. 83 | 84 | Monitoring and killing tasks 85 | ---------- 86 | 87 | You can find all of the running tasks with `list_jobs()`. Each job is assigned an integer id when spawned. It can be killed using the `kill()` command. 88 | 89 | print list_jobs() 90 | # {1 : , 8: <__wrapped__@8> } 91 | kill (8) 92 | 93 | 94 | 95 | Scheduling 96 | ---------- 97 | 98 | The `spawn` function adds a callable to the mTasks system, starting it immediately (see `tick()`, below). You can also queue up functions to to run when other functions complete by using `defer_spawn()` -- which creates a task without running it -- and `join()`, which like a traditional thread join, waits for one task to finish before starting another. For example: 99 | 100 | def hello(): 101 | print "hello" 102 | yield 103 | print "word" 104 | yield 105 | print "!" 106 | yield 107 | 108 | def goodbye(): 109 | print "goodby cruel world" 110 | yield 111 | 112 | h = spawn(hello) 113 | g = defer_spawn(goodbye) 114 | join(h, g) 115 | run() 116 | 117 | # hello 118 | # world 119 | # ! 120 | # goodbye cruel world 121 | 122 | A task can join any number of other tasks and will only execute after they have all completed. 123 | 124 | It's often useful to defer an action for a set amount of time. In traditional threaded programming you would use `time.sleep()` to pause a thread. However you _don't_ want to do that in an `mTask`, since the entire system is running in the main thread! Instead you can use a `DelayTimer` or an `AwaitTimer` to defer execution by just yielding until its time to run: 125 | 126 | 127 | from mTasks.timers import DelayTimer 128 | 129 | def wait_five_seconds(): 130 | delay = DelayTimer(5) 131 | while delay: 132 | yield 133 | print "5 seconds have elapsed!" 134 | 135 | 136 | The `AwaitTimer` works the same way except you pass it an absolute time to begin, instead of a relative time. 137 | 138 | There are convenience functions to add a delay or wait to a regular function: 139 | 140 | delay(your_function, 5) #add a 5 second delay to your_function 141 | await(your_function, 1483185599) # wait until midnight on new years eve 2016 142 | 143 | 144 | 145 | 146 | This idea was pioneered by [David Beazley](http://www.dabeaz.com/generators/). 147 | -------------------------------------------------------------------------------- /examples/clock.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example uses a repeat timer to update a gui clock window once every second. The clock is non-blocking, so you can 3 | work or run scripts. The updates will suspend during playback or long running tasks but the clock will resume 4 | the correct time when Maya is idle again 5 | """ 6 | 7 | import maya.cmds as cmds 8 | import datetime 9 | import mTasks 10 | 11 | # create a clock window 12 | 13 | w = cmds.window(title = 'clock') 14 | c = cmds.columnLayout(backgroundColor = (.1, .05, .05)) 15 | r = cmds.rowLayout(nc=2) 16 | hour = cmds.text(width=128) 17 | cmds.columnLayout() 18 | secs = cmds.text() 19 | am = cmds.text() 20 | cmds.showWindow(w) 21 | 22 | 23 | 24 | 25 | def update_time(): 26 | ''' 27 | update the clock display. This will be called once very second by the repeat task. It 28 | does need to yield at the end to allow time-slicing, however 29 | ''' 30 | now = datetime.datetime.now().time() 31 | time_string = now.strftime("%-I:%M %S %p") 32 | 33 | hours, seconds, ampm = time_string.split() 34 | 35 | hour_style = "font-size:64px; font-family: Impact; color: #8A0F21" 36 | sec_style = "font-size:18px; font-family:Arial Black; color: #8A0F21" 37 | am_style = "font-size:24px; font-family:Arial Black; font-weight:900; color: #700D21" 38 | 39 | def set_control (ctl, value, style): 40 | def make_text(text, style): 41 | return '{1}'.format(style, text) 42 | cmds.text(ctl, e=True, label = make_text(value, style)) 43 | 44 | set_control(hour, hours, hour_style) 45 | set_control(secs, seconds, sec_style) 46 | set_control(am, ampm, am_style) 47 | yield 48 | 49 | # set up the update job to repeat every second 50 | update_task = mTasks.repeat(update_time, 0, 1, 0) 51 | mTasks.task_system.start() 52 | mTasks.spawn(update_task) -------------------------------------------------------------------------------- /examples/joins.py: -------------------------------------------------------------------------------- 1 | __author__ = 'stevet' 2 | import logging 3 | logging.basicConfig() 4 | 5 | import sys 6 | from mTasks.scheduler import * 7 | 8 | def days(): 9 | for item in 'monday tuesday wednesday thursday friday saturday sunday'.split(): 10 | sys.stdout.write( item ) 11 | yield 12 | 13 | 14 | def dates(): 15 | for item in range (30): 16 | sys.stdout.write( '\t') 17 | 18 | sys.stdout.write( str(item + 1)) 19 | sys.stdout.write( '\n') 20 | yield 21 | 22 | def done(): 23 | 24 | spawn(days) 25 | yield 26 | 27 | d = spawn(days) 28 | dd = spawn(dates) 29 | j = defer_spawn(done) 30 | join(d, j) 31 | 32 | run() -------------------------------------------------------------------------------- /examples/news.py: -------------------------------------------------------------------------------- 1 | __author__ = 'stevet' 2 | """ 3 | An example showing how to use an external thread without access problems. It creates a simple window 4 | which is updated in 'real time' for news stories, using an external thread to get the stories via 5 | http and an mTask ExternalResultTask object to update the gui in 'quasi-real-time'. 6 | 7 | The maya scene remains functional throughout: you can work or even play back animations and the window will continue 8 | to receive updates (although these will not show during playback -- they'll be queued up while Maya plays 9 | and will appear in a rush when playback stops). 10 | 11 | """ 12 | 13 | import urllib2 14 | import xml.etree.ElementTree as et 15 | import time 16 | import maya.cmds as cmds 17 | import mTasks 18 | import mTasks.threads 19 | 20 | 21 | def news_reader(): 22 | ''' 23 | 24 | Creates a news reader window with a scroll layout to display news stories coming from mTasks. This illustrates how 25 | mTasks can work directly on maya elements (in this case, gui -- but it could also be scene items) without the need 26 | for executeDeferredInMainThread. 27 | ''' 28 | 29 | # create and display the news reader 30 | window = cmds.window(title= 'Sporting News') 31 | layout = cmds.formLayout() 32 | display_list = cmds.textScrollList() 33 | delete_button = cmds.button(label = 'delete selected') 34 | button_attachments = [ (delete_button, item, 0) for item in ( 'top', 'left', 'right')] 35 | cmds.formLayout(layout, e= True, attachForm = button_attachments) 36 | 37 | text_attachments = [ (display_list, item, 0) for item in ( 'bottom', 'left', 'right')] 38 | cmds.formLayout(layout, e= True, attachForm = text_attachments) 39 | cmds.formLayout(layout, e= True, attachControl = (display_list, 'top', 0, delete_button)) 40 | 41 | # this deletes a story from the list -- it's useful for showing how there is no 42 | # problem with thread collisions 43 | 44 | def delete_item(_): 45 | selected = cmds.textScrollList(display_list, q=True, sii=True) 46 | if selected: 47 | cmds.textScrollList(display_list, e=True, rii=selected) 48 | 49 | cmds.button(delete_button, e=True, command = delete_item) 50 | cmds.showWindow(window) 51 | 52 | 53 | 54 | def poll_news(result_queue): 55 | ''' 56 | This function runs in its own thread, collecting rss feeds and putting them into the result queue 57 | ''' 58 | 59 | feeds = { 60 | 'http://feeds.thescore.com/uefa.rss', 61 | 'http://feeds.thescore.com/nfl.rss', 62 | 'http://feeds.thescore.com/nba.rss', 63 | 'http://feeds.thescore.com/chlg.rss', 64 | 'http://feeds.thescore.com/atp.rss', 65 | 'http://feeds.thescore.com/wta.rss', 66 | 'http://feeds.thescore.com/lpga.rss', 67 | 'http://feeds.thescore.com/nhl.rss' 68 | 69 | } 70 | 71 | existing_stories = set() 72 | 73 | def get_news(feed): 74 | response = urllib2.urlopen(feed) 75 | tree = et.ElementTree().parse(response) 76 | headlines = [] 77 | for item in tree: 78 | for child in item.findall('item'): 79 | headlines.append(child.find('title').text) 80 | return headlines 81 | 82 | while len(existing_stories) < 1000: 83 | for feed in feeds: 84 | 85 | new_news = get_news(feed) 86 | new_stories = set(new_news) - existing_stories 87 | for new_story in new_stories: 88 | existing_stories.add(new_story) 89 | result_queue.put(new_story) 90 | time.sleep(2.5) 91 | 92 | # this callback watches the result queue and updates the window as new items are added to the result queue 93 | 94 | def update_job(result_queue): 95 | if not result_queue.empty(): 96 | new_item = result_queue.get() 97 | cmds.textScrollList(display_list, e = True, append = new_item) 98 | 99 | # the AsyncPollTask object will start the thread and share a python Queue object 100 | # between the news reader thread and the callback which publishes the results to the gui 101 | 102 | news_thread = mTasks.threads.AsyncPollTask(poll_news, update_job) 103 | mTasks.spawn(news_thread) 104 | 105 | # start the task system and open the reader 106 | mTasks.task_system.start() 107 | news_reader() 108 | -------------------------------------------------------------------------------- /mTasks/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module provides a limited form of coroutine based multi-tasking. It's not a substitute for 'real' threads, 3 | but it does allow you to create the illusion of multiple, simultaneous processes without crossing thread 4 | boundaries in Maya or requiring executeDefered or executeDeferredInMainThreadWithResult -- since the scheduling lives 5 | entirely within the main Maya thread, it will not create cross-thread access bugs. 6 | 7 | """ 8 | 9 | from scheduler import * 10 | from timers import delay, repeat, after 11 | 12 | # if you're not planning on using this with Maya, remove this line 13 | try: 14 | import maya_scheduler as task_system 15 | except ImportError: 16 | print "maya task system unavailable" 17 | -------------------------------------------------------------------------------- /mTasks/maya_scheduler.py: -------------------------------------------------------------------------------- 1 | __author__ = 'stevet' 2 | 3 | import scheduler 4 | from maya.cmds import scriptJob 5 | 6 | _state = { 7 | 'job': -1 8 | } 9 | 10 | def sj_indices(): 11 | return set(int(i.partition(":")[0]) for i in scriptJob(lj=True)) 12 | 13 | def start(): 14 | if _state.get('job') in sj_indices(): 15 | # scheduler is already running 16 | return 17 | scheduler_job = scriptJob(e=('idle', scheduler.tick)) 18 | _state['job'] = scheduler_job 19 | 20 | 21 | 22 | def suspend(): 23 | existing = _state.get('job') 24 | if existing in sj_indices(): 25 | scriptJob(k=existing) 26 | _state['job'] = -1 27 | 28 | 29 | def stop(): 30 | ''' 31 | stop the scheduler and release any still-active tasks 32 | ''' 33 | suspend() 34 | scheduler.reset() -------------------------------------------------------------------------------- /mTasks/scheduler.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module acts as the task scheduler. Coroutine functions can be spawned, joined or killed 3 | """ 4 | import Queue 5 | import collections 6 | 7 | from .task import Task 8 | 9 | __author__ = 'stevet' 10 | 11 | _ready_queue = Queue.Queue() 12 | _job_registry = {} 13 | _signal_list = {} 14 | _join_list = collections.defaultdict(list) 15 | _await_list = collections.defaultdict(list) 16 | 17 | 18 | def spawn(coroutine, callback=None): 19 | """ 20 | add a new task for function , with optional function 21 | if defer is True, the task will not be started immediately 22 | 23 | returns the id of the new task 24 | """ 25 | new_task = Task(coroutine, callback) 26 | _job_registry[new_task.id] = new_task 27 | _ready_queue.put(new_task) 28 | return new_task.id 29 | 30 | def defer_spawn(coroutine, callback=None): 31 | """ 32 | Create a task without starting it -- used to create tasks for joins 33 | """ 34 | new_task = Task(coroutine, callback) 35 | _job_registry[new_task.id] = new_task 36 | return new_task.id 37 | 38 | 39 | def kill(task_id): 40 | """ 41 | remove task from the systems 42 | """ 43 | result = _job_registry.pop(task_id, None) 44 | _signal_list.pop(task_id, None) 45 | 46 | if result: 47 | deferred_tasks = _join_list.pop(result.id, tuple()) 48 | for deferred_task in deferred_tasks: 49 | _await_list[deferred_task].remove(task_id) 50 | if not _await_list[deferred_task]: 51 | _await_list.pop(deferred_task) 52 | waiting_task = _job_registry.get(deferred_task) 53 | _ready_queue.put(waiting_task) 54 | return result 55 | 56 | 57 | 58 | def join(existing_task, joining_task): 59 | """ 60 | make task with id dependent on task with id . Returns the ids 61 | of all the tasks on which depends 62 | """ 63 | if not existing_task in _job_registry: 64 | raise RuntimeError("No active task: %s" % existing_task) 65 | if not joining_task in _job_registry: 66 | raise RuntimeError("No active task: %s" % joining_task) 67 | 68 | _join_list[existing_task].append(joining_task) 69 | _await_list[joining_task].append(existing_task) 70 | 71 | return _await_list[joining_task] 72 | 73 | 74 | def signal(task_id, message): 75 | """ 76 | queues a message to send to the task at id . The signal will be passed to the 77 | task on its next time slice 78 | """ 79 | if task_id in _job_registry: 80 | _signal_list[task_id] = message 81 | 82 | 83 | def tick(): 84 | """ 85 | execute one time slice 86 | """ 87 | if _job_registry: 88 | task = _ready_queue.get() 89 | if task and task.id in _job_registry: 90 | message = _signal_list.pop(task.id, None) 91 | if task.tick(message): 92 | _ready_queue.put(task) 93 | else: 94 | kill(task.id) 95 | 96 | 97 | def list_jobs(): 98 | """ 99 | lists all of the jobs in the scheduler 100 | """ 101 | return _job_registry.items() 102 | 103 | 104 | def list_waiting(): 105 | """ 106 | list all the jobs waiting on other jobs 107 | """ 108 | return _join_list.items() 109 | 110 | 111 | def run(): 112 | """ 113 | run through all of the jobs in the scheduler 114 | """ 115 | while _job_registry: 116 | tick() 117 | 118 | 119 | def reset(): 120 | """ 121 | wipe all of the existing tasks and jobs. This will be 122 | """ 123 | _ready_queue = Queue.deque() 124 | _job_registry = {} 125 | _signal_list = {} 126 | _join_list = collections.defaultdict(list) 127 | 128 | 129 | __all__ = 'spawn defer_spawn kill join signal tick run reset list_jobs list_waiting'.split() 130 | -------------------------------------------------------------------------------- /mTasks/task.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | __author__ = 'stevet' 4 | import logging 5 | task_logger = logging.getLogger('mTask') 6 | 7 | 8 | class Task(object): 9 | ''' 10 | Wraps a maya coroutine function. 11 | 12 | Typically you won't construct these yourself, they are created by scheduler when calling 'spawn' or 'join' 13 | ''' 14 | __slots__ = ('id', 'fn', 'state', 'callback') 15 | _next_id = 0 16 | 17 | def __init__(self, fn, callback=None): 18 | 19 | # this will except if is not a coroutine or generator function 20 | # that can yield 21 | assert inspect.isgeneratorfunction(fn) or hasattr(fn, "__call__") 22 | 23 | Task._next_id += 1 24 | self.id = Task._next_id 25 | self.fn = fn() 26 | self.state = None 27 | if callable(callback) and not inspect.getargspec(callback).args: 28 | def cb(_): 29 | callback() 30 | 31 | self.callback = cb 32 | else: 33 | self.callback = callback 34 | 35 | def tick(self, signal): 36 | """ 37 | advance this task's coroutine by one tick. 38 | 39 | If it excepts or complete on this step, return false 40 | Otherwise, return true 41 | """ 42 | alive = False 43 | try: 44 | self.state = self.fn.send(signal) 45 | 46 | except StopIteration: 47 | if self.callback: 48 | try: 49 | self.callback(self.state) 50 | except Exception as exc: 51 | task_logger.critical("callback raised exception") 52 | task_logger.exception(exc) 53 | task_logger.debug("task %s completed" % self) 54 | except Exception as exc: 55 | # we don't propagate so a failed task 56 | # does not crash the system 57 | task_logger.exception(exc) 58 | self.state = exc 59 | 60 | else: 61 | alive = True 62 | 63 | finally: 64 | return alive 65 | 66 | def __repr__(self): 67 | return "{0}@{1}".format(self.fn.__name__, self.id) 68 | -------------------------------------------------------------------------------- /mTasks/threads.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from Queue import Queue 3 | from threading import * 4 | 5 | from task import task_logger 6 | from timers import * 7 | 8 | 9 | class AsyncTask(object): 10 | """ 11 | run in a new thread it until it ends or has run for longer than seconds. 12 | 13 | If is provided, it will be executed when the task completes or times out. The return value, 14 | if any, will be the final state of the task. 15 | """ 16 | 17 | def __init__(self, thread_function, callback=None, timeout=0): 18 | 19 | self.event = Event() 20 | 21 | wrapped_function = self.wrap_thread(thread_function) 22 | self.thread = Thread(target=wrapped_function) 23 | self.thread.daemon = True 24 | self.timeout = timeout 25 | self.callback = callback 26 | 27 | def timeout_test(self): 28 | if self.timeout: 29 | return DelayTimer(self.timeout) 30 | else: 31 | return True 32 | 33 | def wrap_thread(self, fn): 34 | def signal_done(): 35 | try: 36 | fn() 37 | finally: 38 | self.event.set() 39 | 40 | return signal_done 41 | 42 | def __call__(self): 43 | self.thread.start() 44 | not_expired = self.timeout_test() 45 | 46 | while not self.event.isSet(): 47 | if not_expired: 48 | yield 49 | else: 50 | task_logger.info("thread job timed out") 51 | return 52 | if self.callback: 53 | self.callback() 54 | 55 | task_logger.info("thread job completed") 56 | 57 | 58 | class AsyncResultTask(AsyncTask): 59 | """ 60 | run in a new thread it until it ends or has run for longer than seconds. 61 | 62 | The thread function will be passed a Queue.queue object which it can update with results, either 63 | incrementally or all at once. If takes no arguments, its results will be added to the 64 | queue object when it completes. 65 | 66 | if is provided, it will be called with the result queue as an argument when 67 | completes. 68 | 69 | 70 | It is possible to use both callbacks or either one alone. 71 | """ 72 | 73 | def __init__(self, thread_function, callback=None, timeout=0): 74 | 75 | self.result_queue = Queue() 76 | super(AsyncResultTask, self).__init__(thread_function, callback, timeout) 77 | 78 | def wrap_thread(self, fn): 79 | 80 | output_fn = fn 81 | if not inspect.getargspec(fn).args: 82 | def add_queue(q): 83 | q.put(fn()) 84 | 85 | output_fn = add_queue 86 | 87 | def signal_done(): 88 | try: 89 | output_fn(self.result_queue) 90 | finally: 91 | self.event.set() 92 | 93 | return signal_done 94 | 95 | def tick(self): 96 | return None 97 | 98 | def __call__(self): 99 | self.thread.start() 100 | not_expired = self.timeout_test() 101 | 102 | while not self.event.isSet(): 103 | if not_expired: 104 | yield self.tick() 105 | else: 106 | task_logger.info("thread job timed out") 107 | return 108 | if self.callback: 109 | self.callback(self.result_queue) 110 | task_logger.info("thread job completed") 111 | 112 | 113 | class AsyncPollTask(AsyncResultTask): 114 | def __init__(self, thread_function, monitor_callback, callback=None, timeout=0): 115 | 116 | self.monitor_callback = monitor_callback 117 | super(AsyncPollTask, self).__init__(thread_function, callback, timeout) 118 | 119 | def tick(self): 120 | return self.monitor_callback(self.result_queue) -------------------------------------------------------------------------------- /mTasks/timers.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | class DelayTimer(object): 4 | """ 5 | return True until (duration) seconds from instantiation time. The usual idiom is 6 | 7 | ... do something... 8 | 9 | waiting = DelayTimer(2.5) 10 | while waiting: 11 | yield 12 | 13 | ... do more stuff ... 14 | 15 | """ 16 | __slots__ = 'expiry' 17 | 18 | def __init__(self, delay): 19 | self.expiry = time.time() + delay 20 | 21 | def __nonzero__(self): 22 | return self.expiry > time.time() 23 | 24 | @classmethod 25 | def create(cls, duration, fn): 26 | def wrapper(): 27 | timer = cls(duration) 28 | while timer: 29 | yield 30 | inner = fn() 31 | inner.next() 32 | while True: 33 | yield inner.next() 34 | 35 | wrapper.func_name = "{0} ({1})".format(fn.func_name, cls.__name__) 36 | return wrapper 37 | 38 | 39 | class AwaitTimer(DelayTimer): 40 | """ 41 | return True until the specified time 42 | """ 43 | 44 | __slots__ = 'expiry' 45 | 46 | def __init__(self, target_time): 47 | self.expiry = target_time 48 | 49 | 50 | def delay(fn, delay_time): 51 | """ 52 | returns wrapped with a built-in delay of seconds. When scheduled, 53 | will wait for seconds before beginning. 54 | """ 55 | return DelayTimer.create(fn, delay_time) 56 | 57 | 58 | def after(fn, start_time): 59 | """ 60 | returns wrapped with a built-in delay timer that won't fire until 61 | 62 | is expressed in python seconds (the same format as time.time()) 63 | """ 64 | return AwaitTimer.create(fn, start_time) 65 | 66 | 67 | def repeat(fn, initial_delay, repeat_delay, repeats): 68 | """ 69 | returns wrapped with a delay timer and a repeat timer. When scheduled, will wait for 70 | seconds and fire. When completed, it will wait for seconds before 71 | starting again. It will continue for repeats. If repeats is set to 0, the function 72 | will repeat forever. 73 | """ 74 | def wrapper(): 75 | # have to make new values here, 'repeats' 76 | # can't be reassigned in a closure! 77 | repetitions = int(repeats) 78 | forever = repeats == 0 79 | 80 | start_delay = DelayTimer(initial_delay) 81 | while start_delay: 82 | yield 83 | 84 | while repetitions or forever: 85 | inner = fn() 86 | inner.next() 87 | try: 88 | yield inner.next() 89 | except StopIteration: 90 | wait_again = DelayTimer(repeat_delay) 91 | while wait_again: 92 | yield 93 | 94 | if repetitions: 95 | repetitions -= 1 96 | 97 | wrapper.func_name = "{0} ({1})".format(fn.func_name, 'Repeater') 98 | return wrapper 99 | 100 | 101 | __all__ = 'delay repeat after DelayTimer AwaitTimer'.split() --------------------------------------------------------------------------------