{ "info": { "author": "Lovely Systems", "author_email": "office@lovelysystems.com", "bugtrack_url": null, "classifiers": [], "description": "=====================\nRemote Task Execution\n=====================\n\n.. contents::\n\nThis package provides an implementation of a remote task execution Web service\nthat allows to execute pre-defined tasks on another server. It is also\npossible to run cron jobs at specific times. Those services are useful in two\nways:\n\n1. They enable us to complete tasks that are not natively available on a\n particular machine. For example, it is not possible to convert an AVI file\n to a Flash(R) movie using Linux, the operating system our Web server might\n run on.\n\n2. They also allow to move expensive operations to other servers. This is\n valuable, for example, when converting videos on high-traffic sites.\n\nInstallation\n------------\n\nDefine the remotetasks that should be started on startup in zope.conf like\nthis::\n\n \n autostart site1@TestTaskService1, site2@TestTaskService2, @RootTaskService\n \n\nNote that services registered directly in the root folder can be referred to\nby just prefixing them with the `@` symbol. The site name can be omitted. An\nexample of this is `RootTaskService` referenced above.\n\nThis causes the Remotetasks being started upon zope startup.\n\nUsage\n_____\n\n >>> STOP_SLEEP_TIME = 0.02\n\nLet's now start by creating a single service:\n\n >>> from lovely import remotetask\n >>> service = remotetask.TaskService()\n\nThe object should be located, so it gets a name:\n\n >>> from zope.app.folder import Folder\n >>> site1 = Folder()\n >>> root['site1'] = site1\n >>> from zope.app.component.site import LocalSiteManager\n >>> from zope.security.proxy import removeSecurityProxy\n >>> sm = LocalSiteManager(removeSecurityProxy(site1))\n >>> site1.setSiteManager(sm)\n\n >>> sm['default']['testTaskService1'] = service\n >>> service = sm['default']['testTaskService1'] # caution! proxy\n >>> service.__name__\n u'testTaskService1'\n >>> service.__parent__ is sm['default']\n True\n\nLet's register it under the name `TestTaskService1`:\n\n >>> from zope import component\n >>> from lovely.remotetask import interfaces\n >>> sm = site1.getSiteManager()\n >>> sm.registerUtility(service, interfaces.ITaskService,\n ... name='TestTaskService1')\n\n\nWe can discover the available tasks:\n\n >>> service.getAvailableTasks()\n {}\n\nThis list is initially empty, because we have not registered any tasks. Let's\nnow define a task that simply echos an input string:\n\n >>> def echo(input):\n ... return input\n\n >>> import lovely.remotetask.task\n >>> echoTask = remotetask.task.SimpleTask(echo)\n\nThe only API requirement on the converter is to be callable. Now we make sure\nthat the task works:\n\n >>> echoTask(service, 1, input={'foo': 'blah'})\n {'foo': 'blah'}\n\nLet's now register the task as a utility:\n\n >>> import zope.component\n >>> zope.component.provideUtility(echoTask, name='echo')\n\nThe echo task is now available in the service:\n\n >>> service.getAvailableTasks()\n {u'echo': >}\n\nSince the service cannot instantaneously complete a task, incoming jobs are\nmanaged by a queue. First we request the echo task to be executed:\n\n >>> jobid = service.add(u'echo', {'foo': 'bar'})\n >>> jobid\n 1392637175\n\nThe ``add()`` function schedules the task called \"echo\" to be executed with\nthe specified arguments. The method returns a job id with which we can inquire\nabout the job.\nBy default the ``add()`` function adds and starts the job ASAP. Sometimes we need\nto have a jobid but not to start the job yet. See startlater.txt how.\n\n >>> service.getStatus(jobid)\n 'queued'\n\nSince the job has not been processed, the status is set to \"queued\". Further,\nthere is no result available yet:\n\n >>> service.getResult(jobid) is None\n True\n\nAs long as the job is not being processed, it can be cancelled:\n\n >>> service.cancel(jobid)\n >>> service.getStatus(jobid)\n 'cancelled'\n\nThe service isn't being started by default:\n\n >>> service.isProcessing()\n False\n\nThe ``TaskService`` is being started automatically - if specified in\n``zope.conf`` - as soon as the ``IDatabaseOpenedEvent`` is fired. Let's\nemulate the ``zope.conf`` settings:\n\n >>> class Config(object):\n ... mapping = {}\n ... def getSectionName(self):\n ... return 'lovely.remotetask'\n >>> config = Config()\n >>> config.mapping['autostart'] = (\n ... 'site1@TestTaskService1, site2@TestTaskService2,@RootTaskService')\n >>> from zope.app.appsetup.product import setProductConfigurations\n >>> setProductConfigurations([config])\n >>> from lovely.remotetask.service import getAutostartServiceNames\n >>> getAutostartServiceNames()\n ['site1@TestTaskService1', 'site2@TestTaskService2', '@RootTaskService']\n\nNote that `RootTaskService` is for a use-case where the service is directly\nregistered at the root. We test this use-case in a separate footnote so that\nthe flow of this document is not broken. [#1]_\n\nTo get a clean logging environment let's clear the logging stack:\n\n >>> log_info.clear()\n\nOn Zope startup the ``IDatabaseOpenedEvent`` is fired, and will call\nthe ``bootStrap()`` method:\n\n >>> from ZODB.tests import util\n >>> import transaction\n >>> db = util.DB()\n >>> from zope.app.publication.zopepublication import ZopePublication\n >>> conn = db.open()\n >>> conn.root()[ZopePublication.root_name] = root\n >>> transaction.commit()\n\nFire the event:\n\n >>> from zope.app.appsetup.interfaces import DatabaseOpenedWithRoot\n >>> from lovely.remotetask.service import bootStrapSubscriber\n >>> event = DatabaseOpenedWithRoot(db)\n >>> bootStrapSubscriber(event)\n\nand voila - the service is processing:\n\n >>> service.isProcessing()\n True\n\nChecking out the logging will prove the started service:\n\n >>> print log_info\n lovely.remotetask INFO\n handling event IStartRemoteTasksEvent\n lovely.remotetask INFO\n service TestTaskService1 on site site1 started\n lovely.remotetask ERROR\n site site2 not found\n lovely.remotetask INFO\n service RootTaskService on site root started\n\nThe verification for the jobs in the root-level service is done in another\nfootnote [#2]_\n\nTo deal with a lot of services in one sites it will be possible to use\nasterisks (*) to start services. In case of using site@* means start all\nservices in that site:\n\nBut first stop all processing services:\n\n >>> service.stopProcessing()\n >>> service.isProcessing()\n False\n\n >>> root_service.stopProcessing()\n >>> root_service.isProcessing()\n False\n\n >>> import time\n >>> time.sleep(STOP_SLEEP_TIME)\n\nAnd reset the logger:\n\n >>> log_info.clear()\n\nReset the product configuration with the asterisked service names:\n\n >>> config.mapping['autostart'] = 'site1@*'\n >>> setProductConfigurations([config])\n >>> getAutostartServiceNames()\n ['site1@*']\n\nFiring the event again will start all services in the configured site:\n\n >>> bootStrapSubscriber(event)\n\n >>> service.isProcessing()\n True\n\n >>> root_service.isProcessing()\n False\n\nLet's checkout the logging:\n\n >>> print log_info\n lovely.remotetask INFO\n handling event IStartRemoteTasksEvent\n lovely.remotetask INFO\n service TestTaskService1 on site site1 started\n\nTo deal with a lot of services in a lot of sites it possible to use\nasterisks (*) to start services. In case of using *@* means start all\nservices on all sites:\n\n >>> service.stopProcessing()\n >>> service.isProcessing()\n False\n\n >>> import time\n >>> time.sleep(STOP_SLEEP_TIME)\n\nReset the product configuration with the asterisked service names:\n\n >>> config.mapping['autostart'] = '*@*'\n >>> setProductConfigurations([config])\n >>> getAutostartServiceNames()\n ['*@*']\n\n...and reset the logger:\n\n >>> log_info.clear()\n\nAnd fire the event again. All services should be started now:\n\n >>> bootStrapSubscriber(event)\n\n >>> service.isProcessing()\n True\n\n >>> root_service.isProcessing()\n True\n\nLet's check the logging:\n\n >>> print log_info\n lovely.remotetask INFO\n handling event IStartRemoteTasksEvent\n lovely.remotetask INFO\n service RootTaskService on site root started\n lovely.remotetask INFO\n service TestTaskService1 on site site1 started\n\n\nTo deal with a specific service in a lot of sites it possible to use\nasterisks (*) to start services. In case of using \\*@service means start the\nservice called `service` on all sites:\n\n >>> service.stopProcessing()\n >>> service.isProcessing()\n False\n\n >>> root_service.stopProcessing()\n >>> root_service.isProcessing()\n False\n\n >>> import time\n >>> time.sleep(STOP_SLEEP_TIME)\n\nReset the product configuration with the asterisked service names:\n\n >>> config.mapping['autostart'] = '*@TestTaskService1'\n >>> setProductConfigurations([config])\n >>> getAutostartServiceNames()\n ['*@TestTaskService1']\n\n...and reset the logger:\n\n >>> log_info.clear()\n\nAnd fire the event again. All services should be started now:\n\n >>> bootStrapSubscriber(event)\n\n >>> service.isProcessing()\n True\n\n >>> root_service.isProcessing()\n False\n\nLet's checkout the logging:\n\n >>> print log_info\n lovely.remotetask INFO\n handling event IStartRemoteTasksEvent\n lovely.remotetask INFO\n service TestTaskService1 on site site1 started\n\nIn case of configuring a directive which does not match any service on\nany site logging will show a warning message:\n\n >>> service.stopProcessing()\n >>> service.isProcessing()\n False\n\n >>> import time\n >>> time.sleep(STOP_SLEEP_TIME)\n\n >>> config.mapping['autostart'] = '*@Foo'\n >>> setProductConfigurations([config])\n >>> getAutostartServiceNames()\n ['*@Foo']\n\n >>> log_info.clear()\n\n >>> bootStrapSubscriber(event)\n\n >>> service.isProcessing()\n False\n\n >>> root_service.isProcessing()\n False\n\n >>> print log_info\n lovely.remotetask INFO\n handling event IStartRemoteTasksEvent\n lovely.remotetask WARNING\n no services started by directive *@Foo\n\nFinally stop processing and kill the thread. We'll call service.process()\nmanually as we don't have the right environment in the tests.\n\n >>> service.stopProcessing()\n >>> service.isProcessing()\n False\n\n >>> root_service.stopProcessing()\n >>> root_service.isProcessing()\n False\n\n >>> import time\n >>> time.sleep(STOP_SLEEP_TIME)\n\nLet's now read a job:\n\n >>> jobid = service.add(u'echo', {'foo': 'bar'})\n >>> service.process()\n\n >>> service.getStatus(jobid)\n 'completed'\n >>> service.getResult(jobid)\n {'foo': 'bar'}\n\nNow, let's define a new task that causes an error:\n\n >>> def error(input):\n ... raise remotetask.task.TaskError('An error occurred.')\n\n >>> zope.component.provideUtility(\n ... remotetask.task.SimpleTask(error), name='error')\n\nNow add and execute it:\n\n >>> jobid = service.add(u'error')\n >>> service.process()\n\nLet's now see what happened:\n\n >>> service.getStatus(jobid)\n 'error'\n >>> service.getError(jobid)\n 'An error occurred.'\n\nFor management purposes, the service also allows you to inspect all jobs:\n\n >>> dict(service.jobs)\n {1392637176: , 1392637177: , 1392637175: }\n\n\nTo get rid of jobs not needed anymore one can use the clean method.\n\n >>> jobid = service.add(u'echo', {'blah': 'blah'})\n >>> sorted([job.status for job in service.jobs.values()])\n ['cancelled', 'completed', 'error', 'queued']\n\n >>> service.clean()\n\n >>> sorted([job.status for job in service.jobs.values()])\n ['queued']\n\n\nCron jobs\n---------\n\nCron jobs execute on specific times.\n\n >>> import time\n >>> from lovely.remotetask.job import CronJob\n >>> now = 0\n >>> time.gmtime(now)\n (1970, 1, 1, 0, 0, 0, 3, 1, 0)\n\nWe set up a job to be executed once an hour at the current minute. The next\ncall time is the one our from now.\n\nMinutes\n\n >>> cronJob = CronJob(-1, u'echo', (), minute=(0, 10))\n >>> time.gmtime(cronJob.timeOfNextCall(0))\n (1970, 1, 1, 0, 10, 0, 3, 1, 0)\n >>> time.gmtime(cronJob.timeOfNextCall(10*60))\n (1970, 1, 1, 1, 0, 0, 3, 1, 0)\n\nHour\n\n >>> cronJob = CronJob(-1, u'echo', (), hour=(2, 13))\n >>> time.gmtime(cronJob.timeOfNextCall(0))\n (1970, 1, 1, 2, 0, 0, 3, 1, 0)\n >>> time.gmtime(cronJob.timeOfNextCall(2*60*60))\n (1970, 1, 1, 13, 0, 0, 3, 1, 0)\n\nMonth\n\n >>> cronJob = CronJob(-1, u'echo', (), month=(1, 5, 12))\n >>> time.gmtime(cronJob.timeOfNextCall(0))\n (1970, 5, 1, 0, 0, 0, 4, 121, 0)\n >>> time.gmtime(cronJob.timeOfNextCall(cronJob.timeOfNextCall(0)))\n (1970, 12, 1, 0, 0, 0, 1, 335, 0)\n\nDay of week [0..6], jan 1 1970 is a wednesday.\n\n >>> cronJob = CronJob(-1, u'echo', (), dayOfWeek=(0, 2, 4, 5))\n >>> time.gmtime(cronJob.timeOfNextCall(0))\n (1970, 1, 2, 0, 0, 0, 4, 2, 0)\n >>> time.gmtime(cronJob.timeOfNextCall(60*60*24))\n (1970, 1, 3, 0, 0, 0, 5, 3, 0)\n >>> time.gmtime(cronJob.timeOfNextCall(2*60*60*24))\n (1970, 1, 5, 0, 0, 0, 0, 5, 0)\n >>> time.gmtime(cronJob.timeOfNextCall(4*60*60*24))\n (1970, 1, 7, 0, 0, 0, 2, 7, 0)\n\nDayOfMonth [1..31]\n\n >>> cronJob = CronJob(-1, u'echo', (), dayOfMonth=(1, 12, 21, 30))\n >>> time.gmtime(cronJob.timeOfNextCall(0))\n (1970, 1, 12, 0, 0, 0, 0, 12, 0)\n >>> time.gmtime(cronJob.timeOfNextCall(12*24*60*60))\n (1970, 1, 21, 0, 0, 0, 2, 21, 0)\n\nCombined\n\n >>> cronJob = CronJob(-1, u'echo', (), minute=(10,),\n ... dayOfMonth=(1, 12, 21, 30))\n >>> time.gmtime(cronJob.timeOfNextCall(0))\n (1970, 1, 1, 0, 10, 0, 3, 1, 0)\n >>> time.gmtime(cronJob.timeOfNextCall(10*60))\n (1970, 1, 1, 1, 10, 0, 3, 1, 0)\n\n >>> cronJob = CronJob(-1, u'echo', (), minute=(10,),\n ... hour=(4,),\n ... dayOfMonth=(1, 12, 21, 30))\n >>> time.gmtime(cronJob.timeOfNextCall(0))\n (1970, 1, 1, 4, 10, 0, 3, 1, 0)\n >>> time.gmtime(cronJob.timeOfNextCall(10*60))\n (1970, 1, 1, 4, 10, 0, 3, 1, 0)\n\n\nA cron job can also be used to delay the execution of a job.\n\n >>> cronJob = CronJob(-1, u'echo', (), delay=10,)\n >>> time.gmtime(cronJob.timeOfNextCall(0))\n (1970, 1, 1, 0, 0, 10, 3, 1, 0)\n >>> time.gmtime(cronJob.timeOfNextCall(1))\n (1970, 1, 1, 0, 0, 11, 3, 1, 0)\n\n\nCreating Delayed Jobs\n---------------------\n\nA delayed job is executed once after the given delay time in seconds.\n\n >>> count = 0\n >>> def counting(input):\n ... global count\n ... count += 1\n ... return count\n >>> countingTask = remotetask.task.SimpleTask(counting)\n >>> zope.component.provideUtility(countingTask, name='counter')\n\n >>> jobid = service.addCronJob(u'counter',\n ... {'foo': 'bar'},\n ... delay = 10,\n ... )\n >>> service.getStatus(jobid)\n 'delayed'\n >>> service.process(0)\n >>> service.getStatus(jobid)\n 'delayed'\n >>> service.process(9)\n >>> service.getStatus(jobid)\n 'delayed'\n\nAt 10 seconds the job is executed and completed.\n\n >>> service.process(10)\n >>> service.getStatus(jobid)\n 'completed'\n\n\nCreating Cron Jobs\n------------------\n\nHere we create a cron job which runs 10 minutes and 13 minutes past the hour.\n\n >>> count = 0\n\n >>> jobid = service.addCronJob(u'counter',\n ... {'foo': 'bar'},\n ... minute = (10, 13),\n ... )\n >>> service.getStatus(jobid)\n 'cronjob'\n\nWe process the remote task but our cron job is not executed because we are too\nearly in time.\n\n >>> service.process(0)\n >>> service.getStatus(jobid)\n 'cronjob'\n >>> service.getResult(jobid) is None\n True\n\nNow we run the remote task 10 minutes later and get a result.\n\n >>> service.process(10*60)\n >>> service.getStatus(jobid)\n 'cronjob'\n >>> service.getResult(jobid)\n 1\n\nAnd 1 minutes later it is not called.\n\n >>> service.process(11*60)\n >>> service.getResult(jobid)\n 1\n\nBut 3 minutes later it is called again.\n\n >>> service.process(13*60)\n >>> service.getResult(jobid)\n 2\n\nA job can be rescheduled.\n\n >>> job = service.jobs[jobid]\n >>> job.update(minute = (11, 13))\n\nAfter the update the job must be rescheduled in the service.\n\n >>> service.reschedule(jobid)\n\nNow the job is not executed at the old registration minute which was 10.\n\n >>> service.process(10*60+60*60)\n >>> service.getResult(jobid)\n 2\n\nBut it executes at the new minute which is set to 11.\n\n >>> service.process(11*60+60*60)\n >>> service.getResult(jobid)\n 3\n\n\nThreading behavior\n------------------\n\nEach task service runs in a separate thread, allowing them to operate\nindependently. Tasks should be designed to avoid conflict errors in\nthe database.\n\nLet's start the task services we have defined at this point, and see\nwhat threads are running as a result:\n\n >>> service.startProcessing()\n >>> root_service.startProcessing()\n\n >>> import pprint\n >>> import threading\n\n >>> def show_threads():\n ... threads = [t for t in threading.enumerate()\n ... if t.getName().startswith('remotetasks.')]\n ... threads.sort(key=lambda t: t.getName())\n ... pprint.pprint(threads)\n\n >>> show_threads()\n [,\n ]\n\nLet's add a second site containing a task service with the same name as the\nservice in the first site:\n\n >>> site2 = Folder()\n >>> service2 = remotetask.TaskService()\n\n >>> root['site2'] = site2\n >>> sm = LocalSiteManager(removeSecurityProxy(site2))\n >>> site2.setSiteManager(sm)\n\n >>> sm['default']['testTaskService1'] = service2\n >>> service2 = sm['default']['testTaskService1'] # caution! proxy\n\nLet's register it under the name `TestTaskService1`:\n\n >>> sm = site2.getSiteManager()\n >>> sm.registerUtility(\n ... service2, interfaces.ITaskService, name='TestTaskService1')\n\nThe service requires that it's been committed to the database before it can\nbe used:\n\n >>> transaction.commit()\n\nThe new service isn't currently processing:\n\n >>> service2.isProcessing()\n False\n\nIf we start the new service, we can see that there are now three background\nthreads:\n\n >>> service2.startProcessing()\n >>> show_threads()\n [,\n ,\n ]\n\nLet's stop the services, and give the background threads a chance to get the\nmessage:\n\n >>> service.stopProcessing()\n >>> service2.stopProcessing()\n >>> root_service.stopProcessing()\n\n >>> import time\n >>> time.sleep(STOP_SLEEP_TIME)\n\nThe threads have exited now:\n\n >>> print [t for t in threading.enumerate()\n ... if t.getName().startswith('remotetasks.')]\n []\n\n\nFootnotes\n---------\n\n.. [#1] Tests for use-cases where a service is registered at `root` level.\n\n Register service for RootLevelTask\n\n >>> root_service = remotetask.TaskService()\n >>> component.provideUtility(root_service, interfaces.ITaskService,\n ... name='RootTaskService')\n\n The object should be located, so it get's a name:\n\n >>> root['rootTaskService'] = root_service\n >>> root_service = root['rootTaskService'] # caution! proxy\n >>> root_service.__name__\n u'rootTaskService'\n >>> root_service.__parent__ is root\n True\n\n >>> r_jobid = root_service.add(\n ... u'echo', {'foo': 'this is for root_service'})\n >>> r_jobid\n 1506179619\n\n\n.. [#2] We verify the root_service does get processed:\n\n >>> root_service.isProcessing()\n True\n\n Cleaning up root-level service:\n\n >>> print root_service.getStatus(r_jobid)\n queued\n\n Thus the root-service is indeed enabled, which is what we wanted to verify.\n The rest of the API is tested in the main content above; so we don't need\n to test it again. We just clean up the the root service.\n\n >>> root_service.stopProcessing()\n >>> root_service.isProcessing()\n False\n\n >>> root_service.clean()\n\n\nCheck Interfaces and stuff\n--------------------------\n\n >>> from zope.interface.verify import verifyClass, verifyObject\n >>> verifyClass(interfaces.ITaskService, remotetask.TaskService)\n True\n >>> verifyObject(interfaces.ITaskService, service)\n True\n >>> interfaces.ITaskService.providedBy(service)\n True\n\n >>> from lovely.remotetask.job import Job\n >>> fakejob = Job(1, u'echo', {})\n >>> verifyClass(interfaces.IJob, Job)\n True\n >>> verifyObject(interfaces.IJob, fakejob)\n True\n >>> interfaces.IJob.providedBy(fakejob)\n True\n\n >>> fakecronjob = CronJob(1, u'echo', {})\n >>> verifyClass(interfaces.ICronJob, CronJob)\n True\n >>> verifyObject(interfaces.ICronJob, fakecronjob)\n True\n >>> interfaces.IJob.providedBy(fakecronjob)\n True\n\n\n=============================\nChanges for lovely.remotetask\n=============================\n\n0.5.2 (2010-04-30)\n------------------\n\n- Removed unnecessary version requirement for dependency zope.publisher.\n\n\n0.5.1 (2010-04-14)\n------------------\n\n- Convert logged exceptions to str because log messages should be strings.\n\n\n0.5 (2009-09-10)\n----------------\n\n- Fixed a bug with SimpleProcessor: if the job aborted the transaction, it would\n never be removed from the queue, but re-tried over and over again.\n\n2009/05/20 (0.4):\n-----------------\n\n- Randomized the generation of new job ids like intid does it: Try to allocate\n sequential ids so they fall into the same BTree bucket, and randomize if\n stumble upon a used one.\n\n2009/04/05 (0.3):\n-----------------\n\n- Use dropdown widget with available tasks in the cron job\n adding form, instead of text input.\n\n- Remove dependency on zope.app.zapi by using its wrapped api directly.\n\n- Use ISite from zope.location instead of zope.app.component\n\n- Use zc.queue.Queue instead of zc.queue.PersistentQueue because\n PersistentQueue is only to be used by the CompositeQueue.\n\n- Changed URL to pypi.\n\n- Using the correct plural form of status (which is status) in\n ITaskService.clean\n\n\n2008/11/07 0.2.15a1:\n--------------------\n\n- running could cause an AttributeError. added handling for it\n\n2008/02/08 0.2.14:\n------------------\n\n- commiting after each 100 jobs during 'clearAll' to avoid browser timeouts\n while canceling a huge amount of jobs\n\n\n2008/01/28 (new):\n-----------------\n\n- Some bugs smashed, improved tests.\n\n- Added ``startLater`` to ``TaskService.add``. See startlater.txt for more info.\n This facilitates to separate jobb add and start timepoints. (Not cron-like)\n\n\n2007/12/?? (new):\n-----------------\n\n- Switched index to Zope 3.4 KGS, so that we agree on used package versions.\n\n- Made the sleep time of the processor variable; this is needed for testing,\n so that the testing framework is not faster than the processor shutting\n down.\n\n- Added a small optimization to ``isProcessing()`` to stop looking through the\n threads once one with the correct name has been found.\n\n\n2007/11/12 0.2.13:\n------------------\n\n- added \"cancel all\" button\n- fixed bug in associating threads with task service instances\n\n\n2007/10/28 0.2.12:\n------------------\n\n- make the startup more robust\n If an already registered task service is remove via ZMI it's registration is\n not removed. If this happens zope can no longer be restarted if autostart is\n used.\n\n\n2007/10/28 0.2.11:\n------------------\n\n- allow '*' to select all possible times in the cron job add/edit forms\n\n- allow to cancel a delayed job\n\n\n2007/10/24 0.2.10:\n------------------\n\n- avoided deprecation warnings\n\n\n2007/10/08 0.2.9:\n-----------------\n\n- don't push a cron job back into the queue if it's status is ERROR\n\n\n2007/10/08 0.2.8:\n-----------------\n\n- enhanced logging during startup\n\n\n2007/10/02 0.2.7:\n-----------------\n\n- added index to buildout.cfg\n- enhanced autostart behaviour: Services can be started like: site@*,\n *@service and *@*\n\n\n2007/08/07 0.2.6:\n-----------------\n\n- fix bug in sorting that causes column headers to never be clickable\n\n\n2007/08/07 0.2.5:\n-----------------\n\n- no longer require session support for \"Jobs\" ZMI view\n\n\n2007/08/06 0.2.4:\n-----------------\n\n- fix bug that caused processing thread to keep the process alive unnecessarily\n\n\n2007/07/26 0.2.3:\n-----------------\n\n- Now handles the use-case where a task service is registered directly at the\n root. References to such services in the product configuration must begin\n with `@` instead of the `@`.\n\n\n2007/07/02 0.2.2:\n-----------------\n\n- ZMI menu to add cron jobs to a task service\n- named detail views can be registered for jobs specific to the task\n- edit view for cron jobs\n- improved ZMI views\n- catch exception if a job was added for which there is no task registered\n- fixed tests to work in all timezones\n\n\n2007/06/12 0.2.1:\n-----------------\n\n- Do not raise IndexError because of performance problems with tracebacks when\n using eggs.\n\n\n2007/06/12 0.2.0:\n-----------------\n\n - added namespace declaration in lovely/__init__.py\n - allow to delay a job", "description_content_type": null, "docs_url": null, "download_url": "UNKNOWN", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "http://pypi.python.org/pypi/lovely.remotetask", "keywords": "zope3 zope remotetask cache ram", "license": "ZPL 2.1", "maintainer": null, "maintainer_email": null, "name": "lovely.remotetask", "package_url": "https://pypi.org/project/lovely.remotetask/", "platform": "UNKNOWN", "project_url": "https://pypi.org/project/lovely.remotetask/", "project_urls": { "Download": "UNKNOWN", "Homepage": "http://pypi.python.org/pypi/lovely.remotetask" }, "release_url": "https://pypi.org/project/lovely.remotetask/0.5.2/", "requires_dist": null, "requires_python": null, "summary": "A remotetask client utiltiy for zope 3", "version": "0.5.2" }, "last_serial": 794371, "releases": { "0.2.13": [ { "comment_text": "", "digests": { "md5": "c750a5f36680893c171011389048b5b7", "sha256": "ba7d5a085487799c44a479fbacddfa5f5039bb027a85b1989951346a23df4508" }, "downloads": -1, "filename": "lovely.remotetask-0.2.13-py2.4.egg", "has_sig": false, "md5_digest": "c750a5f36680893c171011389048b5b7", "packagetype": "bdist_egg", "python_version": "2.4", "requires_python": null, "size": 68182, "upload_time": "2008-01-04T13:36:19", "url": "https://files.pythonhosted.org/packages/c2/15/3df0fecd7f5acaaf01826a331481dea38d3718028a37a7f9267caa29bde7/lovely.remotetask-0.2.13-py2.4.egg" }, { "comment_text": "", "digests": { "md5": "9b1ddc3f195af5e2198f8116d6d81567", "sha256": "0f34ca76d2737a875dd803b40ca06de9373aec23c488f597186fce2927adb3d2" }, "downloads": -1, "filename": "lovely.remotetask-0.2.13.tar.gz", "has_sig": false, "md5_digest": "9b1ddc3f195af5e2198f8116d6d81567", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 28583, "upload_time": "2008-01-04T13:36:18", "url": "https://files.pythonhosted.org/packages/80/c4/b46b137aec9b38f45a65f0d1fefe185763e4813073c3f4798ea12fb53025/lovely.remotetask-0.2.13.tar.gz" } ], "0.2.15a1": [ { "comment_text": "", "digests": { "md5": "e32cfcae3cdb7d9efa6b149fb7872efc", "sha256": "47264b0d5193daf44df4907dffa2cad34a575e0c90168eaa1b49126200c07b54" }, "downloads": -1, "filename": "lovely.remotetask-0.2.15a1-py2.5.egg", "has_sig": false, "md5_digest": "e32cfcae3cdb7d9efa6b149fb7872efc", "packagetype": "bdist_egg", "python_version": "2.5", "requires_python": null, "size": 78577, "upload_time": "2008-11-07T22:24:45", "url": "https://files.pythonhosted.org/packages/58/e6/52a96a858f73c857e8ab40f5d1b96f31302a12b4769187b224b0723cc227/lovely.remotetask-0.2.15a1-py2.5.egg" }, { "comment_text": "", "digests": { "md5": "af3e7c7e89f8b0ad94f4ff109fe4e64a", "sha256": "cce8031680b17b0d1c036fe4da86a32873f0b068a37d4e72fa1e008e6825ddd1" }, "downloads": -1, "filename": "lovely.remotetask-0.2.15a1.tar.gz", "has_sig": false, "md5_digest": "af3e7c7e89f8b0ad94f4ff109fe4e64a", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 34465, "upload_time": "2008-11-07T22:24:44", "url": "https://files.pythonhosted.org/packages/25/66/dca34c67e40e3f3d98f920f835b72478424b3ab825b4e4767fda1a17313f/lovely.remotetask-0.2.15a1.tar.gz" } ], "0.3": [ { "comment_text": "", "digests": { "md5": "9668b1db1cd260275836708e37759f90", "sha256": "1dd66baa478c58b0e03a2894a913639cda3f90b777476fe02f890e6c25082e13" }, "downloads": -1, "filename": "lovely.remotetask-0.3.tar.gz", "has_sig": false, "md5_digest": "9668b1db1cd260275836708e37759f90", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 46532, "upload_time": "2009-04-05T16:29:14", "url": "https://files.pythonhosted.org/packages/cd/eb/9ccad6d26c6f94961cbfb13b3d73d9a24fdf8b69477669a272ec905fd04e/lovely.remotetask-0.3.tar.gz" } ], "0.4": [ { "comment_text": "", "digests": { "md5": "d3c6bbaa29c524e2f44507dcce0a7d79", "sha256": "45333e027221fb0691f81cdcc6c4a6dc60f27768712fa6cdf8c1e5fa30b874f7" }, "downloads": -1, "filename": "lovely.remotetask-0.4.tar.gz", "has_sig": false, "md5_digest": "d3c6bbaa29c524e2f44507dcce0a7d79", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 47272, "upload_time": "2009-05-20T10:39:36", "url": "https://files.pythonhosted.org/packages/af/84/f9cdac18b7de3df35e63b13a6d928d14c0889f09571080181a29554513ce/lovely.remotetask-0.4.tar.gz" } ], "0.5": [ { "comment_text": "", "digests": { "md5": "fd494b8e67e8f2678dfe56539a3940b7", "sha256": "94279d8ce8d793513e2885ce0b2db441d42b87cefa9c6f379eecfa1baeb80cef" }, "downloads": -1, "filename": "lovely.remotetask-0.5.tar.gz", "has_sig": false, "md5_digest": "fd494b8e67e8f2678dfe56539a3940b7", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 47372, "upload_time": "2009-09-10T14:37:09", "url": "https://files.pythonhosted.org/packages/30/87/5a0b3d8bd490a608be260e8b7d4958159c67e6c9da016dfbd828c9600975/lovely.remotetask-0.5.tar.gz" } ], "0.5.1": [ { "comment_text": "", "digests": { "md5": "25a1c4a1a305d0cfd9d57ea1aabbeb08", "sha256": "54b7c6503f67bac09cf6dc05a25d50082b4b01290abb1b9bb78b1f6b8d8a2982" }, "downloads": -1, "filename": "lovely.remotetask-0.5.1.tar.gz", "has_sig": false, "md5_digest": "25a1c4a1a305d0cfd9d57ea1aabbeb08", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 48285, "upload_time": "2010-04-14T14:47:41", "url": "https://files.pythonhosted.org/packages/32/a6/3584a9d65e35dd237f1c2adf71974b5dac600b55af8bd60d115ab9e506e5/lovely.remotetask-0.5.1.tar.gz" } ], "0.5.2": [ { "comment_text": "", "digests": { "md5": "2e7f788bbb7afd841d985a51c27a42de", "sha256": "6d7fde948d1205c6438d19db22d14833fcbfdbdae1ec4aef39cea743dbc8ce81" }, "downloads": -1, "filename": "lovely.remotetask-0.5.2.tar.gz", "has_sig": false, "md5_digest": "2e7f788bbb7afd841d985a51c27a42de", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 48759, "upload_time": "2010-04-30T08:40:33", "url": "https://files.pythonhosted.org/packages/3b/d9/d729b5443e5273ff7631bf01b01a6df374a3d793114a4b2ee61294149800/lovely.remotetask-0.5.2.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "2e7f788bbb7afd841d985a51c27a42de", "sha256": "6d7fde948d1205c6438d19db22d14833fcbfdbdae1ec4aef39cea743dbc8ce81" }, "downloads": -1, "filename": "lovely.remotetask-0.5.2.tar.gz", "has_sig": false, "md5_digest": "2e7f788bbb7afd841d985a51c27a42de", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 48759, "upload_time": "2010-04-30T08:40:33", "url": "https://files.pythonhosted.org/packages/3b/d9/d729b5443e5273ff7631bf01b01a6df374a3d793114a4b2ee61294149800/lovely.remotetask-0.5.2.tar.gz" } ] }