Compare commits
1 commit
Author | SHA1 | Date | |
---|---|---|---|
8bcad17505 |
751 changed files with 12532 additions and 2777 deletions
15
.gitignore
vendored
15
.gitignore
vendored
|
@ -1,13 +1,6 @@
|
||||||
*.pyc
|
*.pyc
|
||||||
*.pyo
|
ajax/dojo/*
|
||||||
*/ajax/dojo/dojo*
|
.project
|
||||||
build/
|
.pydevproject
|
||||||
dist/
|
|
||||||
*.swp
|
|
||||||
*.egg-info
|
|
||||||
*.project
|
|
||||||
*.pydevproject
|
|
||||||
*.sublime-project
|
|
||||||
*.sublime-workspace
|
|
||||||
.settings
|
.settings
|
||||||
*.ropeproject
|
|
||||||
|
|
21
LICENSE
21
LICENSE
|
@ -1,21 +0,0 @@
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (C) 2023 cyberconcepts.org team
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
226
agent/README.txt
Normal file
226
agent/README.txt
Normal file
|
@ -0,0 +1,226 @@
|
||||||
|
================================================
|
||||||
|
Agents for Job Execution and Communication Tasks
|
||||||
|
================================================
|
||||||
|
|
||||||
|
Agents do some work specified by jobs, the main task being to collect
|
||||||
|
information objects from the local machine or some external source and
|
||||||
|
transfer them e.g. to a loops server on the same machine or another.
|
||||||
|
|
||||||
|
|
||||||
|
($Id$)
|
||||||
|
|
||||||
|
This package does not depend on Zope but represents a standalone application.
|
||||||
|
|
||||||
|
|
||||||
|
Sub-Packages
|
||||||
|
============
|
||||||
|
|
||||||
|
Top-level
|
||||||
|
Generic interfaces, ``commponents``: adapter registry,
|
||||||
|
``tests`` module, this ``README.txt`` file.
|
||||||
|
|
||||||
|
base
|
||||||
|
Base and sample classes.
|
||||||
|
|
||||||
|
core
|
||||||
|
Agent and scheduling implementations.
|
||||||
|
|
||||||
|
control
|
||||||
|
Communication with an external agent control and job database application.
|
||||||
|
|
||||||
|
crawl
|
||||||
|
Scanning/crawling some system, e.g. the database of an email application,
|
||||||
|
the local file system, or an external document source.
|
||||||
|
|
||||||
|
transport
|
||||||
|
Transfer of information objects to agents on another machine or
|
||||||
|
to an information management application (e.g. loops).
|
||||||
|
|
||||||
|
util
|
||||||
|
Various utility modules, e.g. a backport of the
|
||||||
|
``twisted.internet.task.coiterate()`` function from Twisted 2.5 so that
|
||||||
|
we can use the Twisted version coming with Zope 3.3.1 for
|
||||||
|
cybertools.agent.
|
||||||
|
|
||||||
|
All sub-packages except ``base`` depend on Twisted.
|
||||||
|
|
||||||
|
|
||||||
|
Object Structure and Control Flow
|
||||||
|
=================================
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
---------- ------------
|
||||||
|
-------- | |<---1| |
|
||||||
|
| config |--> | master |<---2| controller |
|
||||||
|
-------- /| |4--->| |
|
||||||
|
/ ---------- ------------
|
||||||
|
/ 1 ^ 2
|
||||||
|
/ | | \ -----------
|
||||||
|
/ | | `---->| scheduler |
|
||||||
|
v v 4 -----------
|
||||||
|
----- --------- 3
|
||||||
|
| log |<--| agent |<----------´
|
||||||
|
----- ---------
|
||||||
|
|
||||||
|
(1) Agent specifications control creation and setup of agents
|
||||||
|
|
||||||
|
(2) Job specifications control scheduling of jobs
|
||||||
|
|
||||||
|
(3) Scheduler triggers job execution by agent
|
||||||
|
|
||||||
|
(4) Results are recorded, possibly triggering further actions
|
||||||
|
|
||||||
|
|
||||||
|
Basic Stuff
|
||||||
|
===========
|
||||||
|
|
||||||
|
While the real application is based on the asynchronous communication
|
||||||
|
framework Twisted there is some basic stuff (mainly interfaces and
|
||||||
|
base classes with basic, sample, or dummy implementations) that is
|
||||||
|
independent of Twisted.
|
||||||
|
|
||||||
|
The code for this resides in in the ``base`` sub-package.
|
||||||
|
|
||||||
|
Master Agent and Configuration
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
All activity is controlled by the master agent.
|
||||||
|
|
||||||
|
The master agent is set up according to the specifications in a
|
||||||
|
configuration file.
|
||||||
|
|
||||||
|
The configuration typically provides only basic informations, e.g. about
|
||||||
|
the controller(s) and logger(s) to be used; details on jobs and agent
|
||||||
|
configuration are provided by the controller.
|
||||||
|
|
||||||
|
>>> from cybertools.agent.tests import baseDir
|
||||||
|
>>> import os
|
||||||
|
>>> configFile = open(os.path.join(baseDir, 'base', 'sample.cfg'))
|
||||||
|
|
||||||
|
So we are now ready to create a master agent and configure it by supplying
|
||||||
|
the path to the configuration file.
|
||||||
|
|
||||||
|
>>> from cybertools.agent.main import setup
|
||||||
|
>>> master = setup(configFile)
|
||||||
|
Starting agent application...
|
||||||
|
Using controllers base.sample.
|
||||||
|
>>> configFile.close()
|
||||||
|
|
||||||
|
>>> master.config
|
||||||
|
controller.names = ['base.sample']
|
||||||
|
logger.name = 'default'
|
||||||
|
logger.standard = 30
|
||||||
|
scheduler.name = 'sample'
|
||||||
|
|
||||||
|
Controllers
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Creation of agents and scheduling of jobs is controlled by controller
|
||||||
|
objects. These are typically associated with a sort of control storage that
|
||||||
|
provides agent and job specifications and receives the results of job
|
||||||
|
execution.
|
||||||
|
|
||||||
|
>>> master.controllers
|
||||||
|
[<cybertools.agent.base.control.SampleController object ...>]
|
||||||
|
|
||||||
|
We make the contollers provide the specifications via the master agent's
|
||||||
|
``setup()`` method.
|
||||||
|
|
||||||
|
>>> master.setup()
|
||||||
|
Starting agent application...
|
||||||
|
Using controllers base.sample.
|
||||||
|
|
||||||
|
Other Agents
|
||||||
|
------------
|
||||||
|
|
||||||
|
The above ``setup()`` call has triggered the creation of one child agent -
|
||||||
|
that is all the sample controller provides.
|
||||||
|
|
||||||
|
>>> master.children
|
||||||
|
{'sample01': <cybertools.agent.base.agent.SampleAgent object ...>}
|
||||||
|
|
||||||
|
Let's check a few attributes of the newly created agent.
|
||||||
|
|
||||||
|
>>> agent01 = master.children['sample01']
|
||||||
|
>>> agent01.master is master
|
||||||
|
True
|
||||||
|
>>> agent01.config is master.config
|
||||||
|
True
|
||||||
|
|
||||||
|
Job Scheduling and Execution
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
A scheduler is responsible for triggering the execution of a job at the
|
||||||
|
appropriate time. The master agent schedules the jobs based upon the
|
||||||
|
information (job specifications) it gets from the controller. There
|
||||||
|
is just one scheduler associated with the master agent.
|
||||||
|
|
||||||
|
>>> master.scheduler
|
||||||
|
<cybertools.agent.base.schedule.Scheduler object ...>
|
||||||
|
|
||||||
|
We schedule a sample job by calling an internal method of the agent's
|
||||||
|
controller. In addition to the output of the job execution itself we
|
||||||
|
also get a note from the controller about the feedback it received
|
||||||
|
about the outcome of the job execution.
|
||||||
|
|
||||||
|
>>> master.controllers[0].enterJob('sample', 'sample01')
|
||||||
|
Job 00001 on agent sample01 has been executed.
|
||||||
|
Job 00001 completed; result: None;
|
||||||
|
|
||||||
|
Logging
|
||||||
|
-------
|
||||||
|
|
||||||
|
>>> master.logger
|
||||||
|
<cybertools.agent.base.log.Logger object ...>
|
||||||
|
>>> agent01.logger is master.logger
|
||||||
|
True
|
||||||
|
|
||||||
|
>>> master.config.logger.standard = 20
|
||||||
|
>>> master.logger.setup()
|
||||||
|
>>> master.controllers[0].enterJob('sample', 'sample01')
|
||||||
|
Job 00002 on agent sample01 has been executed.
|
||||||
|
2... agent:sample01 job:00002 message:job execution result:OK
|
||||||
|
Job 00002 completed; result: None;
|
||||||
|
|
||||||
|
>>> for r in master.logger.records:
|
||||||
|
... print r
|
||||||
|
2... agent:sample01 job:00001 message:job execution result:OK
|
||||||
|
2... agent:sample01 job:00002 message:job execution result:OK
|
||||||
|
|
||||||
|
|
||||||
|
Using the Twisted-based Scheduler
|
||||||
|
=================================
|
||||||
|
|
||||||
|
By specifying the core scheduler in the agent's configuration this will be
|
||||||
|
used automatically for scheduling.
|
||||||
|
|
||||||
|
In addition, we use another sample controller, now also the twisted-based
|
||||||
|
from the core package. This will in turn set up a queueable agent from
|
||||||
|
the core package so that now everything is running under the control of
|
||||||
|
the twisted reactor.
|
||||||
|
|
||||||
|
>>> config = '''
|
||||||
|
... controller(names=['core.sample'])
|
||||||
|
... scheduler(name='core')
|
||||||
|
... logger(name='default', standard=30)
|
||||||
|
... '''
|
||||||
|
>>> master = setup(config)
|
||||||
|
Starting agent application...
|
||||||
|
Using controllers core.sample.
|
||||||
|
|
||||||
|
>>> master.scheduler
|
||||||
|
<cybertools.agent.core.schedule.Scheduler object ...>
|
||||||
|
|
||||||
|
We enter the same job specification as above.
|
||||||
|
|
||||||
|
>>> master.controllers[0].enterJob('sample', 'sample01')
|
||||||
|
|
||||||
|
Now the job is not executed immediately - we have to hand over control to
|
||||||
|
the twisted reactor first. The running of the reactor is simulated by
|
||||||
|
the ``iterate()`` method provided for testing.
|
||||||
|
|
||||||
|
>>> from cybertools.agent.tests import tester
|
||||||
|
>>> tester.iterate()
|
||||||
|
Job 00001 on agent sample01 has been executed.
|
||||||
|
Job 00001 completed; result: Done;
|
10
agent/agent.cfg
Normal file
10
agent/agent.cfg
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
#
|
||||||
|
# Standard configuration for agent application
|
||||||
|
#
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
|
||||||
|
controller(names=['cmdline', 'telnet'])
|
||||||
|
controller.telnet.port = 5001
|
||||||
|
scheduler(name='core')
|
||||||
|
logger(name='default', standard=30)
|
31
agent/app.py
Executable file
31
agent/app.py
Executable file
|
@ -0,0 +1,31 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Agent application.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
from twisted.application import service
|
||||||
|
|
||||||
|
from cybertools.agent import main
|
||||||
|
|
||||||
|
|
||||||
|
application = main.application = service.Application('Agent Application')
|
||||||
|
main.setup()
|
121
agent/base/agent.py
Normal file
121
agent/base/agent.py
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Agent base and sample classes.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
from zope.interface import implements
|
||||||
|
|
||||||
|
from cybertools.agent.common import states
|
||||||
|
from cybertools.agent.components import agents, controllers, jobs
|
||||||
|
from cybertools.agent.components import loggers, schedulers
|
||||||
|
from cybertools.agent.components import servers, clients
|
||||||
|
from cybertools.agent.interfaces import IAgent
|
||||||
|
from cybertools.util.config import Configurator
|
||||||
|
|
||||||
|
|
||||||
|
class Agent(object):
|
||||||
|
|
||||||
|
implements(IAgent)
|
||||||
|
|
||||||
|
name = '???'
|
||||||
|
master = None
|
||||||
|
config = None
|
||||||
|
logger = None
|
||||||
|
|
||||||
|
def __init__(self, master):
|
||||||
|
self.master = master
|
||||||
|
self.config = master.config
|
||||||
|
self.logger = master.logger
|
||||||
|
|
||||||
|
def send(self, job):
|
||||||
|
self.execute(job)
|
||||||
|
|
||||||
|
def execute(self, job):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def log(self, job, result='OK'):
|
||||||
|
self.logger.log(dict(message='job execution', job=job.identifier,
|
||||||
|
agent=self.name, result=result))
|
||||||
|
|
||||||
|
|
||||||
|
class Master(Agent):
|
||||||
|
|
||||||
|
name = 'master'
|
||||||
|
scheduler = None
|
||||||
|
logger = None
|
||||||
|
|
||||||
|
def __init__(self, configuration):
|
||||||
|
if isinstance(configuration, Configurator):
|
||||||
|
self.config = configuration
|
||||||
|
else: # configuration is path to config file
|
||||||
|
self.config = Configurator()
|
||||||
|
self.config.load(configuration)
|
||||||
|
self.master = self
|
||||||
|
self.controllers = []
|
||||||
|
self.children = {}
|
||||||
|
self.servers = []
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
config = self.config
|
||||||
|
self.logger = loggers(self, name=config.logger.name)
|
||||||
|
print 'Starting agent application...'
|
||||||
|
for n in config.controller.names:
|
||||||
|
self.controllers.append(controllers(self, n))
|
||||||
|
self.scheduler = schedulers(self, name=config.scheduler.name)
|
||||||
|
for cont in self.controllers:
|
||||||
|
cont.setup()
|
||||||
|
print 'Using controllers %s.' % ', '.join(config.controller.names)
|
||||||
|
for n in config.talk.server.names:
|
||||||
|
server = servers(self, n)
|
||||||
|
self.servers.append(server)
|
||||||
|
server.setup()
|
||||||
|
|
||||||
|
def setupAgents(self, controller, agentSpecs):
|
||||||
|
for spec in agentSpecs:
|
||||||
|
agent = agents(self, spec.type)
|
||||||
|
agent.name = spec.name
|
||||||
|
self.children[spec.name] = agent
|
||||||
|
|
||||||
|
def setupJobs(self, controller, jobSpecs):
|
||||||
|
for spec in jobSpecs:
|
||||||
|
job = jobs(self.scheduler, spec.type)
|
||||||
|
job.agent = self.children[spec.agent]
|
||||||
|
job.identifier = spec.identifier
|
||||||
|
job.controller = controller
|
||||||
|
job.params = spec.params
|
||||||
|
self.scheduler.schedule(job, spec.startTime)
|
||||||
|
|
||||||
|
def notify(self, job, result=None, message=''):
|
||||||
|
if job.state.hasFinished():
|
||||||
|
job.controller.notify(job.identifier, job.state, result, message)
|
||||||
|
|
||||||
|
|
||||||
|
class SampleAgent(Agent):
|
||||||
|
|
||||||
|
def execute(self, job):
|
||||||
|
job.state = states.running
|
||||||
|
print 'Job %s on agent %s has been executed.' % (job.identifier, self.name)
|
||||||
|
self.log(job)
|
||||||
|
job.state = states.completed
|
||||||
|
self.master.notify(job)
|
||||||
|
|
||||||
|
agents.register(SampleAgent, Master, name='base.sample')
|
100
agent/base/control.py
Normal file
100
agent/base/control.py
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Base/sample controller implementation.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
from zope.interface import implements
|
||||||
|
|
||||||
|
from cybertools.agent.base.agent import Master
|
||||||
|
from cybertools.agent.components import controllers
|
||||||
|
from cybertools.agent.interfaces import IController
|
||||||
|
|
||||||
|
|
||||||
|
class Controller(object):
|
||||||
|
|
||||||
|
implements(IController)
|
||||||
|
|
||||||
|
def __init__(self, agent):
|
||||||
|
self.agent = agent
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
self.agent.setupAgents(self, self._getAgents())
|
||||||
|
self.agent.setupJobs(self, self._getCurrentJobs())
|
||||||
|
|
||||||
|
def _getAgents(self):
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _getCurrentJobs(self):
|
||||||
|
return []
|
||||||
|
|
||||||
|
def notify(self, identifier, state, result=None, message=''):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SampleController(Controller):
|
||||||
|
|
||||||
|
jobNumber = 0
|
||||||
|
result = None
|
||||||
|
|
||||||
|
agents = (('sample01', 'base.sample'),)
|
||||||
|
|
||||||
|
def notify(self, identifier, state, result=None, message=''):
|
||||||
|
self.result = result
|
||||||
|
msg = ('Job %s %s; result: %s; %s' %
|
||||||
|
(identifier, state, result, message))
|
||||||
|
print msg
|
||||||
|
|
||||||
|
def _getAgents(self):
|
||||||
|
return [AgentSpecification(name, type) for name, type in self.agents]
|
||||||
|
|
||||||
|
def createAgent(self, agentType, name):
|
||||||
|
spec = AgentSpecification(name, agentType)
|
||||||
|
self.agent.setupAgents(self, [spec])
|
||||||
|
|
||||||
|
def enterJob(self, jobType, agent, **kw):
|
||||||
|
self.jobNumber += 1
|
||||||
|
spec = JobSpecification(jobType, '%05i' % self.jobNumber, agent=agent, **kw)
|
||||||
|
self.agent.setupJobs(self, [spec])
|
||||||
|
|
||||||
|
controllers.register(SampleController, Master, name='base.sample')
|
||||||
|
|
||||||
|
|
||||||
|
class AgentSpecification(object):
|
||||||
|
|
||||||
|
def __init__(self, name, type, **kw):
|
||||||
|
self.name = name
|
||||||
|
self.type = type
|
||||||
|
for k, v in kw.items():
|
||||||
|
setattr(self, k, v)
|
||||||
|
|
||||||
|
|
||||||
|
class JobSpecification(object):
|
||||||
|
|
||||||
|
startTime = None
|
||||||
|
|
||||||
|
def __init__(self, type, identifier, **kw):
|
||||||
|
self.type = type
|
||||||
|
self.identifier = identifier
|
||||||
|
self.params = kw.pop('params', {})
|
||||||
|
for k, v in kw.items():
|
||||||
|
setattr(self, k, v)
|
||||||
|
|
63
agent/base/job.py
Normal file
63
agent/base/job.py
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
The real agent stuff.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
from zope.interface import implements
|
||||||
|
|
||||||
|
from cybertools.agent.base.schedule import Scheduler
|
||||||
|
from cybertools.agent.common import states
|
||||||
|
from cybertools.agent.components import jobs
|
||||||
|
from cybertools.agent.interfaces import IScheduledJob
|
||||||
|
|
||||||
|
|
||||||
|
class Job(object):
|
||||||
|
|
||||||
|
implements(IScheduledJob)
|
||||||
|
|
||||||
|
identifier = '???'
|
||||||
|
agent = None
|
||||||
|
startTime = None
|
||||||
|
repeat = 0
|
||||||
|
state = states.initialized
|
||||||
|
|
||||||
|
def __init__(self, scheduler):
|
||||||
|
self.scheduler = scheduler
|
||||||
|
self.params = {}
|
||||||
|
self.successors = []
|
||||||
|
|
||||||
|
def execute(self):
|
||||||
|
if self.agent is not None:
|
||||||
|
self.agent.send(self)
|
||||||
|
self.state = states.submitted
|
||||||
|
|
||||||
|
def reschedule(self, startTime=None):
|
||||||
|
self.scheduler.schedule(self.copy(), startTime)
|
||||||
|
|
||||||
|
def copy(self):
|
||||||
|
newJob = self.__class__(self.scheduler)
|
||||||
|
newJob.agent = self.agent
|
||||||
|
newJob.params = self.params
|
||||||
|
newJob.repeat = self.repeat
|
||||||
|
newJob.successors = [s.copy() for s in self.successors]
|
||||||
|
|
||||||
|
jobs.register(Job, Scheduler, name='sample')
|
81
agent/base/log.py
Normal file
81
agent/base/log.py
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Log information management.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from zope.interface import implements
|
||||||
|
|
||||||
|
from cybertools.agent.base.agent import Agent
|
||||||
|
from cybertools.agent.components import loggers
|
||||||
|
from cybertools.agent.interfaces import ILogger, ILogRecord
|
||||||
|
|
||||||
|
|
||||||
|
class LogRecord(object):
|
||||||
|
|
||||||
|
implements(ILogRecord)
|
||||||
|
|
||||||
|
datefmt = '%Y-%m-%dT%H:%S'
|
||||||
|
|
||||||
|
def __init__(self, logger, data):
|
||||||
|
self.logger = logger
|
||||||
|
self.data = data
|
||||||
|
self.timeStamp = time.time()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
msg = [str(time.strftime(self.datefmt, time.localtime(self.timeStamp)))]
|
||||||
|
for k in sorted(self.data):
|
||||||
|
msg.append('%s:%s' % (str(k), str(self.data[k])))
|
||||||
|
return ' '.join(msg)
|
||||||
|
|
||||||
|
|
||||||
|
class Logger(object):
|
||||||
|
|
||||||
|
implements(ILogger)
|
||||||
|
|
||||||
|
recordFactory = LogRecord
|
||||||
|
|
||||||
|
def __init__(self, agent):
|
||||||
|
self.agent = agent
|
||||||
|
self.records = []
|
||||||
|
self.setup()
|
||||||
|
self.externalLoggers = []
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
conf = self.agent.config.logger
|
||||||
|
self.externalLoggers = []
|
||||||
|
if conf.standard:
|
||||||
|
logger = logging.getLogger()
|
||||||
|
logger.level = conf.standard
|
||||||
|
logger.addHandler(logging.StreamHandler(sys.stdout))
|
||||||
|
self.externalLoggers.append(logger)
|
||||||
|
|
||||||
|
def log(self, data):
|
||||||
|
record = self.recordFactory(self, data)
|
||||||
|
self.records.append(record)
|
||||||
|
for logger in self.externalLoggers:
|
||||||
|
logger.info(str(record))
|
||||||
|
|
||||||
|
|
||||||
|
loggers.register(Logger, Agent, name='default')
|
9
agent/base/sample.cfg
Normal file
9
agent/base/sample.cfg
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
#
|
||||||
|
# sample.cfg - agent configuration for demonstration and testing purposes
|
||||||
|
#
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
|
||||||
|
controller(names=['base.sample'])
|
||||||
|
scheduler(name='sample')
|
||||||
|
logger(name='default', standard=30)
|
46
agent/base/schedule.py
Normal file
46
agent/base/schedule.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Basic (sample) job scheduler.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
from time import time
|
||||||
|
from zope.interface import implements
|
||||||
|
|
||||||
|
from cybertools.agent.base.agent import Master
|
||||||
|
from cybertools.agent.common import states
|
||||||
|
from cybertools.agent.components import schedulers
|
||||||
|
from cybertools.agent.interfaces import IScheduler
|
||||||
|
|
||||||
|
|
||||||
|
class Scheduler(object):
|
||||||
|
|
||||||
|
implements(IScheduler)
|
||||||
|
|
||||||
|
def __init__(self, agent):
|
||||||
|
self.agent = agent
|
||||||
|
|
||||||
|
def schedule(self, job, startTime=None):
|
||||||
|
job.startTime = startTime or int(time())
|
||||||
|
job.state = states.scheduled
|
||||||
|
job.execute() # the sample scheduler does not care about startTime
|
||||||
|
|
||||||
|
schedulers.register(Scheduler, Master, name='sample')
|
50
agent/common.py
Normal file
50
agent/common.py
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Miscellaneous common stuff.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
from cybertools.util.jeep import Jeep
|
||||||
|
|
||||||
|
|
||||||
|
class JobState(object):
|
||||||
|
|
||||||
|
def __init__(self, name, value):
|
||||||
|
self.name = name
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
def hasError(self):
|
||||||
|
return self.value < 0
|
||||||
|
|
||||||
|
def hasFinished(self):
|
||||||
|
return (self.value <= states.aborted.value or
|
||||||
|
self.value >= states.completed.value)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<JobState %s>' % self.name
|
||||||
|
|
||||||
|
|
||||||
|
states = Jeep([JobState(n, v) for n, v in
|
||||||
|
(('initialized', 0), ('scheduled', 1), ('submitted', 2),
|
||||||
|
('running', 3), ('completed', 4), ('aborted', -1))])
|
34
agent/components.py
Normal file
34
agent/components.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Component registries.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
from cybertools.util.adapter import AdapterFactory
|
||||||
|
|
||||||
|
|
||||||
|
agents = AdapterFactory()
|
||||||
|
controllers = AdapterFactory()
|
||||||
|
jobs = AdapterFactory()
|
||||||
|
loggers = AdapterFactory()
|
||||||
|
schedulers = AdapterFactory()
|
||||||
|
servers = AdapterFactory()
|
||||||
|
clients = AdapterFactory()
|
145
agent/control/cmdline.py
Normal file
145
agent/control/cmdline.py
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Base/sample controller implementation.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
from twisted.application import service, internet
|
||||||
|
from twisted.internet import protocol, reactor, stdio
|
||||||
|
from twisted.protocols import basic
|
||||||
|
from zope.interface import implements
|
||||||
|
|
||||||
|
from cybertools.agent.base.agent import Master
|
||||||
|
from cybertools.agent.core.control import SampleController
|
||||||
|
from cybertools.agent.components import controllers
|
||||||
|
|
||||||
|
|
||||||
|
class CmdlineController(SampleController):
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
super(CmdlineController, self).setup()
|
||||||
|
prot = CmdlineProtocol()
|
||||||
|
prot.controller = self
|
||||||
|
stdio.StandardIO(prot)
|
||||||
|
self.results = {}
|
||||||
|
|
||||||
|
|
||||||
|
controllers.register(CmdlineController, Master, name='cmdline')
|
||||||
|
|
||||||
|
|
||||||
|
class TelnetController(CmdlineController):
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
super(CmdlineController, self).setup()
|
||||||
|
port = self.agent.config.controller.telnet.port
|
||||||
|
from cybertools.agent.main import application
|
||||||
|
if application is None:
|
||||||
|
reactor.listenTCP(port, TelnetServerFactory(self))
|
||||||
|
else:
|
||||||
|
service = internet.TCPServer(port, TelnetServerFactory(self))
|
||||||
|
service.setServiceParent(application)
|
||||||
|
|
||||||
|
controllers.register(TelnetController, Master, name='telnet')
|
||||||
|
|
||||||
|
|
||||||
|
class CmdlineProtocol(basic.LineReceiver):
|
||||||
|
|
||||||
|
delimiter = '\n'
|
||||||
|
controller = None
|
||||||
|
|
||||||
|
def connectionMade(self):
|
||||||
|
self.sendLine("Agent console. Type 'help' for help.")
|
||||||
|
self.transport.write('> ')
|
||||||
|
|
||||||
|
def lineReceived(self, line):
|
||||||
|
if not line:
|
||||||
|
return
|
||||||
|
commandParts = line.split()
|
||||||
|
command = commandParts[0].lower()
|
||||||
|
args = commandParts[1:]
|
||||||
|
posArgs = []
|
||||||
|
kwArgs = {}
|
||||||
|
for arg in args:
|
||||||
|
if '=' in arg: # directory='...'
|
||||||
|
key, value = arg.split('=', 1)
|
||||||
|
if value in ('True', 'False'):
|
||||||
|
value = eval(value)
|
||||||
|
#elif value.isdigit():
|
||||||
|
# value = int(value)
|
||||||
|
kwArgs[key] = value
|
||||||
|
else:
|
||||||
|
posArgs.append(arg)
|
||||||
|
try:
|
||||||
|
method = getattr(self, 'do_' + command)
|
||||||
|
except AttributeError, e:
|
||||||
|
self.sendLine('Error: no such command.')
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
method(*posArgs, **kwArgs)
|
||||||
|
except Exception, e:
|
||||||
|
self.sendLine('Error: ' + str(e))
|
||||||
|
self.transport.write('> ')
|
||||||
|
|
||||||
|
def do_help(self, command=None):
|
||||||
|
"Help"
|
||||||
|
if command:
|
||||||
|
self.sendLine(getattr(self, 'do_' + command).__doc__)
|
||||||
|
else:
|
||||||
|
commands = [cmd[3:] for cmd in dir(self) if cmd.startswith('do_')]
|
||||||
|
self.sendLine("Valid commands: " +" ".join(commands))
|
||||||
|
|
||||||
|
def do_shutdown(self):
|
||||||
|
"Shut down"
|
||||||
|
self.sendLine('Shutting down.')
|
||||||
|
reactor.stop()
|
||||||
|
|
||||||
|
def do_agent(self, agentType='crawl.filesystem', name='a1'):
|
||||||
|
"Create agent"
|
||||||
|
self.controller.createAgent(agentType, name)
|
||||||
|
|
||||||
|
def do_job(self, jobType='sample', agent='a1', **kw):
|
||||||
|
"Enter job"
|
||||||
|
self.controller.enterJob(jobType, agent, params=kw)
|
||||||
|
|
||||||
|
def do_showresult(self):
|
||||||
|
"Show last result"
|
||||||
|
print self.controller.result
|
||||||
|
|
||||||
|
|
||||||
|
class TelnetProtocol(CmdlineProtocol):
|
||||||
|
|
||||||
|
delimiter = '\r\n'
|
||||||
|
|
||||||
|
def do_quit(self):
|
||||||
|
self.sendLine('Goodbye.')
|
||||||
|
self.transport.loseConnection()
|
||||||
|
|
||||||
|
|
||||||
|
class TelnetServerFactory(protocol.ServerFactory):
|
||||||
|
|
||||||
|
def __init__(self, controller):
|
||||||
|
self.controller = controller
|
||||||
|
|
||||||
|
def protocol(self, *args, **kw):
|
||||||
|
prot = TelnetProtocol(*args, **kw)
|
||||||
|
prot.controller = self.controller
|
||||||
|
return prot
|
||||||
|
|
38
agent/control/remote.py
Normal file
38
agent/control/remote.py
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Controller that accepts and processes requests via XML-RPC.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
from zope.interface import implements
|
||||||
|
|
||||||
|
from cybertools.agent.base.agent import Master
|
||||||
|
from cybertools.agent.core.control import SampleController
|
||||||
|
from cybertools.agent.components import controllers
|
||||||
|
|
||||||
|
|
||||||
|
class RemoteController(SampleController):
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
super(RemoteController, self).setup()
|
||||||
|
|
||||||
|
|
||||||
|
controllers.register(RemoteController, Master, name='remote')
|
87
agent/core/agent.py
Normal file
87
agent/core/agent.py
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Queueable agent base/sample classes.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
from twisted.internet.defer import succeed
|
||||||
|
from zope.interface import implements
|
||||||
|
|
||||||
|
from cybertools.agent.base.agent import Agent, Master
|
||||||
|
from cybertools.agent.common import states
|
||||||
|
from cybertools.agent.components import agents
|
||||||
|
from cybertools.agent.interfaces import IQueueableAgent
|
||||||
|
|
||||||
|
|
||||||
|
class QueueableAgent(Agent):
|
||||||
|
|
||||||
|
implements(IQueueableAgent)
|
||||||
|
|
||||||
|
currentJob = None
|
||||||
|
|
||||||
|
def __init__(self, master):
|
||||||
|
super(QueueableAgent, self).__init__(master)
|
||||||
|
self.queue = []
|
||||||
|
|
||||||
|
def send(self, job):
|
||||||
|
if self.currentJob is None:
|
||||||
|
if self.queue: # this should not happen...
|
||||||
|
self.queue.insert(0, job)
|
||||||
|
job = self.queue.pop()
|
||||||
|
self.execute(job)
|
||||||
|
else:
|
||||||
|
self.queue.insert(0, job)
|
||||||
|
|
||||||
|
def execute(self, job):
|
||||||
|
job.state = states.running
|
||||||
|
self.currentJob = job
|
||||||
|
self.params = job.params
|
||||||
|
d = self.process()
|
||||||
|
d.addCallbacks(self.completed, self.error)
|
||||||
|
|
||||||
|
def process(self):
|
||||||
|
# do something with the current job, return a deferred
|
||||||
|
print ('Job %s on agent %s has been executed.'
|
||||||
|
% (self.currentJob.identifier, self.name))
|
||||||
|
return succeed('Done')
|
||||||
|
|
||||||
|
def completed(self, result):
|
||||||
|
job = self.currentJob
|
||||||
|
job.state = states.completed
|
||||||
|
self.log(job)
|
||||||
|
self.master.notify(job, result)
|
||||||
|
self.finishJob()
|
||||||
|
|
||||||
|
def error(self, result):
|
||||||
|
print '*** error', result
|
||||||
|
job = self.currentJob
|
||||||
|
job.state = states.aborted
|
||||||
|
self.log(self.currentJob, result='Error')
|
||||||
|
self.master.notify(job, result)
|
||||||
|
self.finishJob()
|
||||||
|
|
||||||
|
def finishJob(self):
|
||||||
|
self.currentJob = None
|
||||||
|
if self.queue:
|
||||||
|
job = self.queue.pop()
|
||||||
|
self.execute(job, job.params)
|
||||||
|
|
||||||
|
agents.register(QueueableAgent, Master, name='core.sample')
|
36
agent/core/control.py
Normal file
36
agent/core/control.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Base/sample controller implementation.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
from zope.interface import implements
|
||||||
|
|
||||||
|
from cybertools.agent.base.agent import Master
|
||||||
|
from cybertools.agent.base.control import SampleController, AgentSpecification
|
||||||
|
from cybertools.agent.components import controllers
|
||||||
|
|
||||||
|
|
||||||
|
class SampleController(SampleController):
|
||||||
|
|
||||||
|
agents = (('sample01', 'core.sample'),)
|
||||||
|
|
||||||
|
controllers.register(SampleController, Master, name='core.sample')
|
57
agent/core/schedule.py
Normal file
57
agent/core/schedule.py
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Basic job scheduler using twisted.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
from time import time
|
||||||
|
from zope.interface import implements
|
||||||
|
|
||||||
|
from cybertools.agent.base.agent import Master
|
||||||
|
from cybertools.agent.base.schedule import Scheduler as BaseScheduler
|
||||||
|
from cybertools.agent.common import states
|
||||||
|
from cybertools.agent.components import schedulers
|
||||||
|
from cybertools.agent.interfaces import IScheduler
|
||||||
|
|
||||||
|
from twisted.internet import reactor
|
||||||
|
from twisted.internet.defer import Deferred
|
||||||
|
|
||||||
|
|
||||||
|
class Scheduler(BaseScheduler):
|
||||||
|
|
||||||
|
implements(IScheduler)
|
||||||
|
|
||||||
|
def __init__(self, agent):
|
||||||
|
self.agent = agent
|
||||||
|
|
||||||
|
def schedule(self, job, startTime=None):
|
||||||
|
job.startTime = startTime or int(time())
|
||||||
|
if startTime is None:
|
||||||
|
startTime = int(time())
|
||||||
|
job.startTime = startTime
|
||||||
|
job.state = states.scheduled
|
||||||
|
reactor.callLater(startTime-int(time()), job.execute)
|
||||||
|
return startTime
|
||||||
|
|
||||||
|
def getJobsToExecute(startTime=0):
|
||||||
|
return [j for j in self.queue.values() if startTime <= j.startTime]
|
||||||
|
|
||||||
|
schedulers.register(Scheduler, Master, name='core')
|
46
agent/crawl/README.txt
Normal file
46
agent/crawl/README.txt
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
================================================
|
||||||
|
Agents for Job Execution and Communication Tasks
|
||||||
|
================================================
|
||||||
|
|
||||||
|
($Id$)
|
||||||
|
|
||||||
|
>>> config = '''
|
||||||
|
... controller(names=['core.sample'])
|
||||||
|
... scheduler(name='core')
|
||||||
|
... logger(name='default', standard=30)
|
||||||
|
... '''
|
||||||
|
>>> from cybertools.agent.main import setup
|
||||||
|
>>> master = setup(config)
|
||||||
|
Starting agent application...
|
||||||
|
Using controllers core.sample.
|
||||||
|
|
||||||
|
|
||||||
|
Crawler
|
||||||
|
=======
|
||||||
|
|
||||||
|
The agent uses Twisted's cooperative multitasking model.
|
||||||
|
|
||||||
|
Crawler is the base class for all derived crawlers like the filesystem crawler
|
||||||
|
and the mailcrawler. The SampleCrawler returns a deferred that already had a
|
||||||
|
callback being called, so it will return at once.
|
||||||
|
|
||||||
|
Returns a deferred that must be supplied with a callback method (and in
|
||||||
|
most cases also an errback method).
|
||||||
|
|
||||||
|
We create the sample crawler via the master's controller. The sample
|
||||||
|
controller provides a simple method for this purpose.
|
||||||
|
|
||||||
|
>>> controller = master.controllers[0]
|
||||||
|
>>> controller.createAgent('crawl.sample', 'crawler01')
|
||||||
|
|
||||||
|
In the next step we request the start of a job, again via the controller.
|
||||||
|
|
||||||
|
>>> controller.enterJob('sample', 'crawler01')
|
||||||
|
|
||||||
|
The job is not executed immediately - we have to hand over control to
|
||||||
|
the twisted reactor first.
|
||||||
|
|
||||||
|
>>> from cybertools.agent.tests import tester
|
||||||
|
>>> tester.iterate()
|
||||||
|
SampleCrawler is collecting.
|
||||||
|
Job 00001 completed; result: [];
|
92
agent/crawl/base.py
Normal file
92
agent/crawl/base.py
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Crawl base and sample classes.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
from zope.interface import implements
|
||||||
|
|
||||||
|
from cybertools.agent.base.agent import Master
|
||||||
|
from cybertools.agent.core.agent import QueueableAgent
|
||||||
|
from cybertools.agent.interfaces import ICrawler
|
||||||
|
from cybertools.agent.interfaces import IResource, IMetadataSet
|
||||||
|
from cybertools.agent.components import agents
|
||||||
|
from twisted.internet.defer import succeed
|
||||||
|
|
||||||
|
|
||||||
|
class Crawler(QueueableAgent):
|
||||||
|
|
||||||
|
implements(ICrawler)
|
||||||
|
|
||||||
|
def __init__(self, master, params={}):
|
||||||
|
super(Crawler, self).__init__(master)
|
||||||
|
|
||||||
|
def process(self):
|
||||||
|
return self.collect()
|
||||||
|
|
||||||
|
def collect(self, filter=None):
|
||||||
|
d = defer.succeed([])
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
class SampleCrawler(Crawler):
|
||||||
|
|
||||||
|
def collect(self, filter=None):
|
||||||
|
print 'SampleCrawler is collecting.'
|
||||||
|
d = succeed([])
|
||||||
|
return d
|
||||||
|
|
||||||
|
agents.register(SampleCrawler, Master, name='crawl.sample')
|
||||||
|
|
||||||
|
|
||||||
|
class Resource(object):
|
||||||
|
|
||||||
|
implements(IResource)
|
||||||
|
|
||||||
|
data = file = path = None
|
||||||
|
type = 'sample'
|
||||||
|
contentType = 'text/plain'
|
||||||
|
encoding = ''
|
||||||
|
application = 'sample'
|
||||||
|
metadata = None
|
||||||
|
|
||||||
|
def __init__(self, data=None, **kw):
|
||||||
|
if data is not None:
|
||||||
|
self.data = data
|
||||||
|
for k, v in kw.items():
|
||||||
|
setattr(self, k, v)
|
||||||
|
self.subResources = []
|
||||||
|
|
||||||
|
|
||||||
|
class Metadata(dict):
|
||||||
|
|
||||||
|
implements(IMetadataSet)
|
||||||
|
|
||||||
|
def __init__(self, data=dict()):
|
||||||
|
self.update(data)
|
||||||
|
|
||||||
|
def asXML(self):
|
||||||
|
# TODO...
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def set(self, key, value):
|
||||||
|
self['key'] = value
|
||||||
|
|
85
agent/crawl/filesystem.py
Normal file
85
agent/crawl/filesystem.py
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Filesystem crawler.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from fnmatch import filter
|
||||||
|
from datetime import datetime
|
||||||
|
from twisted.internet.defer import Deferred
|
||||||
|
from zope.interface import implements
|
||||||
|
|
||||||
|
from cybertools.agent.base.agent import Master
|
||||||
|
from cybertools.agent.components import agents
|
||||||
|
from cybertools.agent.crawl.base import Resource, Metadata
|
||||||
|
from cybertools.agent.crawl.base import Crawler
|
||||||
|
from cybertools.agent.util.task import coiterate
|
||||||
|
|
||||||
|
|
||||||
|
class FilesystemCrawler(Crawler):
|
||||||
|
|
||||||
|
def collect(self):
|
||||||
|
self.collected = []
|
||||||
|
coiterate(self.crawlFilesystem()).addCallback(self.finished)
|
||||||
|
# TODO: addErrback()
|
||||||
|
self.deferred = Deferred()
|
||||||
|
return self.deferred
|
||||||
|
|
||||||
|
def finished(self, result):
|
||||||
|
self.deferred.callback(self.collected)
|
||||||
|
|
||||||
|
def crawlFilesystem(self):
|
||||||
|
directory = self.params.get('directory')
|
||||||
|
pattern = self.params.get('pattern') or '*'
|
||||||
|
lastRun = self.params.get('lastrun') or datetime(1980, 1, 1)
|
||||||
|
for path, dirs, files in os.walk(directory):
|
||||||
|
if '.svn' in dirs:
|
||||||
|
del dirs[dirs.index('.svn')]
|
||||||
|
for x in self.loadFiles(path, files, pattern, lastRun):
|
||||||
|
yield None
|
||||||
|
|
||||||
|
def loadFiles(self, path, files, pattern, lastRun):
|
||||||
|
for f in filter(files, pattern):
|
||||||
|
filename = os.path.join(path, f)
|
||||||
|
mtime = datetime.fromtimestamp(os.path.getmtime(filename))
|
||||||
|
if mtime <= lastRun: # file not changed
|
||||||
|
continue
|
||||||
|
meta = dict(
|
||||||
|
path=filename,
|
||||||
|
)
|
||||||
|
self.collected.append(FileResource(path=filename, metadata=Metadata(meta)))
|
||||||
|
yield None
|
||||||
|
|
||||||
|
agents.register(FilesystemCrawler, Master, name='crawl.filesystem')
|
||||||
|
|
||||||
|
|
||||||
|
class FileResource(Resource):
|
||||||
|
|
||||||
|
type = 'file'
|
||||||
|
application = 'filesystem'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data(self):
|
||||||
|
f = open(self.path, 'r')
|
||||||
|
text = f.read()
|
||||||
|
f.close()
|
||||||
|
return text
|
42
agent/crawl/filesystem.txt
Normal file
42
agent/crawl/filesystem.txt
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
================================================
|
||||||
|
Agents for Job Execution and Communication Tasks
|
||||||
|
================================================
|
||||||
|
|
||||||
|
($Id$)
|
||||||
|
|
||||||
|
>>> import os
|
||||||
|
>>> from time import time
|
||||||
|
|
||||||
|
>>> from cybertools.agent.tests import tester, baseDir
|
||||||
|
>>> config = '''
|
||||||
|
... controller(names=['core.sample'])
|
||||||
|
... scheduler(name='core')
|
||||||
|
... logger(name='default', standard=30)
|
||||||
|
... '''
|
||||||
|
>>> from cybertools.agent.main import setup
|
||||||
|
>>> master = setup(config)
|
||||||
|
Starting agent application...
|
||||||
|
Using controllers core.sample.
|
||||||
|
|
||||||
|
|
||||||
|
Filesystem Crawler
|
||||||
|
==================
|
||||||
|
|
||||||
|
>>> controller = master.controllers[0]
|
||||||
|
>>> controller.createAgent('crawl.filesystem', 'sample03')
|
||||||
|
|
||||||
|
In the next step we request the start of a job, again via the controller.
|
||||||
|
|
||||||
|
>>> path = os.path.join(baseDir, 'testing', 'data')
|
||||||
|
>>> controller.enterJob('sample', 'sample03', params=dict(directory=path))
|
||||||
|
|
||||||
|
The job is not executed immediately - we have to hand over control to
|
||||||
|
the twisted reactor first.
|
||||||
|
|
||||||
|
>>> from cybertools.agent.tests import tester
|
||||||
|
>>> tester.iterate()
|
||||||
|
Job 00001 completed; result: [..., ...];
|
||||||
|
|
||||||
|
>>> r0 = controller.result[0]
|
||||||
|
>>> r0.metadata, r0.data
|
||||||
|
({'path': '...file1.txt'}, 'Data from file1.txt')
|
76
agent/crawl/mail.py
Normal file
76
agent/crawl/mail.py
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Crawl base and sample classes.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
from zope.interface import implements
|
||||||
|
|
||||||
|
from cybertools.agent.base.agent import Agent, Master
|
||||||
|
from cybertools.agent.crawl.base import Resource
|
||||||
|
from cybertools.agent.crawl.base import Metadata
|
||||||
|
from cybertools.agent.crawl.base import Crawler
|
||||||
|
from cybertools.agent.components import agents
|
||||||
|
from twisted.internet.defer import succeed
|
||||||
|
|
||||||
|
|
||||||
|
class MailCrawler(Crawler):
|
||||||
|
|
||||||
|
def __init__(self, master):
|
||||||
|
super(MailCrawler, self).__init__(master)
|
||||||
|
self.result = []
|
||||||
|
|
||||||
|
def collect(self, filter=None):
|
||||||
|
print 'MailCrawler is collecting.'
|
||||||
|
d = self.crawlFolders()
|
||||||
|
return d
|
||||||
|
|
||||||
|
def fetchCriteria(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def crawlFolders(self):
|
||||||
|
return succeed([])
|
||||||
|
|
||||||
|
def loadMailsFromFolder(self, folder):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def createResource(self, data, path=None, application=None, metadata=None):
|
||||||
|
resource = MailResource(data=data, path=path, application=application,
|
||||||
|
metadata=metadata)
|
||||||
|
self.result.append(resource)
|
||||||
|
|
||||||
|
def createMetadata(self, metadata):
|
||||||
|
metadata = Metadata(metadata)
|
||||||
|
## for k, v in kw.items():
|
||||||
|
## metadata[k] = v
|
||||||
|
return metadata
|
||||||
|
|
||||||
|
def login(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
agents.register(MailCrawler, Master, name='crawl.mail')
|
||||||
|
|
||||||
|
|
||||||
|
class MailResource(Resource):
|
||||||
|
|
||||||
|
type = 'email'
|
||||||
|
application = 'mailclient'
|
||||||
|
|
231
agent/crawl/outlook.py
Normal file
231
agent/crawl/outlook.py
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Outlook Crawler Class.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from email import MIMEMultipart
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
|
||||||
|
from zope.interface import implements
|
||||||
|
from twisted.internet import defer
|
||||||
|
#from pywintypes import com_error
|
||||||
|
#The watsup import is needed as soon as we start handling the Outlook Pop-Up
|
||||||
|
#again
|
||||||
|
#This should also be integrated within the wrapper-api for doctests
|
||||||
|
#from watsup.winGuiAuto import findTopWindow, findControl, findControls, clickButton, \
|
||||||
|
# getComboboxItems, selectComboboxItem, setCheckBox
|
||||||
|
|
||||||
|
from cybertools.agent.base.agent import Agent, Master
|
||||||
|
from cybertools.agent.crawl.mail import MailCrawler
|
||||||
|
from cybertools.agent.crawl.mail import MailResource
|
||||||
|
from cybertools.agent.crawl.filesystem import FileResource
|
||||||
|
from cybertools.agent.components import agents
|
||||||
|
from cybertools.agent.system.windows import api
|
||||||
|
from cybertools.agent.util.task import coiterate
|
||||||
|
from cybertools.agent.system.windows.codepages import codepages
|
||||||
|
|
||||||
|
# some constants
|
||||||
|
COMMASPACE = ', '
|
||||||
|
|
||||||
|
class OutlookCrawler(MailCrawler):
|
||||||
|
|
||||||
|
keys = ""
|
||||||
|
inbox = ""
|
||||||
|
subfolders = ""
|
||||||
|
pattern = ""
|
||||||
|
|
||||||
|
def collect(self, filter=None):
|
||||||
|
self.result = []
|
||||||
|
self.d = defer.Deferred()
|
||||||
|
self.oOutlookApp = None
|
||||||
|
if self.findOutlook():
|
||||||
|
self.fetchCriteria()
|
||||||
|
coiterate(self.crawlFolders()).addCallback(self.finished).addErrback(self.error)
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
#self.d.addErrback([])
|
||||||
|
return self.d
|
||||||
|
|
||||||
|
def error(self, reason):
|
||||||
|
print '***** error',
|
||||||
|
print reason
|
||||||
|
|
||||||
|
def finished(self, result):
|
||||||
|
self.d.callback(self.result)
|
||||||
|
|
||||||
|
def fetchCriteria(self):
|
||||||
|
criteria = self.params
|
||||||
|
self.keys = criteria.get('keys')
|
||||||
|
self.inbox = criteria.get('inbox') #boolean
|
||||||
|
self.subfolders = criteria.get('subfolders') #boolean
|
||||||
|
self.pattern = criteria.get('pattern')
|
||||||
|
if self.pattern != '' and self.pattern != None:
|
||||||
|
self.pattern = re.compile(criteria.get('pattern') or '.*')
|
||||||
|
|
||||||
|
def crawlFolders(self):
|
||||||
|
onMAPI = self.oOutlookApp.GetNamespace("MAPI")
|
||||||
|
ofInbox = \
|
||||||
|
onMAPI.GetDefaultFolder(api.client.constants.olFolderInbox)
|
||||||
|
# fetch mails from inbox
|
||||||
|
if self.inbox:
|
||||||
|
for m in self.loadMailsFromFolder(ofInbox):
|
||||||
|
yield None
|
||||||
|
# fetch mails of inbox subfolders
|
||||||
|
if self.subfolders and self.pattern is None:
|
||||||
|
lInboxSubfolders = getattr(ofInbox, 'Folders')
|
||||||
|
for of in range(lInboxSubfolders.__len__()):
|
||||||
|
# get a MAPI-subfolder object and load its emails
|
||||||
|
for m in self.loadMailsFromFolder(lInboxSubfolders.Item(of + 1)):
|
||||||
|
yield None
|
||||||
|
elif self.subfolders and self.pattern:
|
||||||
|
lInboxSubfolders = getattr(ofInbox, 'Folders')
|
||||||
|
for of in range(lInboxSubfolders.__len__()):
|
||||||
|
# get specified MAPI-subfolder object and load its emails
|
||||||
|
if self.pattern.match(getattr(lInboxSubfolders.Item(of + 1), 'Name')):
|
||||||
|
for m in self.loadMailsFromFolder(lInboxSubfolders.Item(of + 1)):
|
||||||
|
yield None
|
||||||
|
|
||||||
|
def loadMailsFromFolder(self, folder):
|
||||||
|
# get items of the folder
|
||||||
|
folderItems = getattr(folder, 'Items')
|
||||||
|
for item in range(len(folderItems)):
|
||||||
|
mail = folderItems.Item(item+1)
|
||||||
|
if mail.Class == api.client.constants.olMail:
|
||||||
|
if self.keys is None:
|
||||||
|
self.keys = []
|
||||||
|
for key in mail._prop_map_get_.items():
|
||||||
|
try:
|
||||||
|
if isinstance(key[0], (int, str, unicode, bool)):
|
||||||
|
self.keys.append(key[0])
|
||||||
|
except api.com_error:
|
||||||
|
pass
|
||||||
|
record = {}
|
||||||
|
for key in self.keys:
|
||||||
|
try:
|
||||||
|
if (hasattr(mail, key)):
|
||||||
|
value = getattr(mail, key)
|
||||||
|
if isinstance(value, (int, str, unicode, bool)):
|
||||||
|
record[key] = value
|
||||||
|
else:
|
||||||
|
record[key] = None
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
metadata = self.assembleMetadata(folder, record)
|
||||||
|
# Create a resource and append it to the result list
|
||||||
|
self.createResource(mail, folder, metadata)
|
||||||
|
yield None
|
||||||
|
|
||||||
|
def findOutlook(self):
|
||||||
|
outlookFound = False
|
||||||
|
try:
|
||||||
|
self.oOutlookApp = \
|
||||||
|
api.client.gencache.EnsureDispatch("Outlook.Application")
|
||||||
|
outlookFound = True
|
||||||
|
except com_error:
|
||||||
|
pass
|
||||||
|
return outlookFound
|
||||||
|
|
||||||
|
def assembleMetadata(self, folder, mailAttr):
|
||||||
|
meta = {}
|
||||||
|
for key in mailAttr.keys():
|
||||||
|
if isinstance(mailAttr[key], (str, unicode))\
|
||||||
|
and mailAttr[key] != 'Body' and mailAttr[key] != 'HTMLBody':
|
||||||
|
meta[key] = mailAttr[key].encode('utf-8')
|
||||||
|
elif isinstance(mailAttr[key], (list, tuple, dict)):
|
||||||
|
lst = []
|
||||||
|
for rec in mailAttr[key]:
|
||||||
|
lst.append(rec)
|
||||||
|
meta[key] = COMMASPACE.join(lst)
|
||||||
|
else:
|
||||||
|
meta[key] = mailAttr[key]
|
||||||
|
meta["path"] = folder
|
||||||
|
metadata = self.createMetadata(meta)
|
||||||
|
return metadata
|
||||||
|
|
||||||
|
def createResource(self, mail, folder, metadata):
|
||||||
|
enc = None
|
||||||
|
textType = "application/octet-stream"
|
||||||
|
attachments = []
|
||||||
|
mailContent = ""
|
||||||
|
ident = None
|
||||||
|
if (hasattr(mail, 'BodyFormat')):
|
||||||
|
value = getattr(mail, 'BodyFormat')
|
||||||
|
if value == 1:
|
||||||
|
#1: it is a plain text mail, that is maybe decorated with
|
||||||
|
#some html Tags by Outlook for formatting
|
||||||
|
#so save it as plain text mail
|
||||||
|
if hasattr(mail, 'Body'):
|
||||||
|
mailContent = getattr(mail, 'Body')
|
||||||
|
textType = "text/plain"
|
||||||
|
else:
|
||||||
|
mailContent = ""
|
||||||
|
textType = "text/plain"
|
||||||
|
elif value == 2:
|
||||||
|
#2: it is a HTML mail
|
||||||
|
if hasattr(mail, 'HTMLBody'):
|
||||||
|
mailContent = getattr(mail, 'HTMLBody')
|
||||||
|
textType = "text/html"
|
||||||
|
else:
|
||||||
|
mailContent = ""
|
||||||
|
textType = "text/html"
|
||||||
|
else:
|
||||||
|
#Could not determine BodyFormat. Try to retrieve plain text
|
||||||
|
if hasattr(mail, 'Body'):
|
||||||
|
mailContent = getattr(mail, 'Body')
|
||||||
|
else:
|
||||||
|
mailContent = ""
|
||||||
|
if hasattr(mail, 'InternetCodepage'):
|
||||||
|
Codepage = getattr(mail, 'InternetCodepage')
|
||||||
|
if codepages.has_key(Codepage):
|
||||||
|
enc = codepages[Codepage]
|
||||||
|
if hasattr(mail, 'EntryID'):
|
||||||
|
ident = getattr(mail, 'EntryID')
|
||||||
|
if hasattr(mail, 'Attachments'):
|
||||||
|
attachedElems = getattr(mail, 'Attachments')
|
||||||
|
for item in range(1, len(attachedElems)+1):
|
||||||
|
fileHandle, filePath = tempfile.mkstemp(prefix="outlook")
|
||||||
|
attachedItem = attachedElems.Item(item)
|
||||||
|
attachedItem.SaveAsFile(filePath)
|
||||||
|
os.close(fileHandle)
|
||||||
|
metadat = self.createMetadata(dict(filename=filePath))
|
||||||
|
fileRes = FileResource(data=None,
|
||||||
|
path=filePath,
|
||||||
|
metadata=metadat)
|
||||||
|
attachments.append(fileRes)
|
||||||
|
fileHandle, filePath = tempfile.mkstemp(prefix="olmail")
|
||||||
|
filePointer = os.fdopen(fileHandle, "w")
|
||||||
|
mailContent = mailContent.encode('utf-8')
|
||||||
|
filePointer.write(mailContent)
|
||||||
|
filePointer.close()
|
||||||
|
resource = MailResource(data=mailContent,
|
||||||
|
contentType=textType,
|
||||||
|
encoding=enc,
|
||||||
|
path=filePath,
|
||||||
|
application='outlook',
|
||||||
|
identifier=ident,
|
||||||
|
metadata=metadata,
|
||||||
|
subResources=attachments)
|
||||||
|
self.result.append(resource)
|
||||||
|
|
||||||
|
agents.register(OutlookCrawler, Master, name='crawl.outlook')
|
53
agent/crawl/outlook.txt
Normal file
53
agent/crawl/outlook.txt
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
================================================
|
||||||
|
Agents for Job Execution and Communication Tasks
|
||||||
|
================================================
|
||||||
|
|
||||||
|
($Id$)
|
||||||
|
|
||||||
|
>>> config = '''
|
||||||
|
... controller(names=['core.sample'])
|
||||||
|
... scheduler(name='core')
|
||||||
|
... logger(name='default', standard=30)
|
||||||
|
... system.winapi = 'testing'
|
||||||
|
... '''
|
||||||
|
>>> from cybertools.agent.main import setup
|
||||||
|
>>> master = setup(config)
|
||||||
|
Starting agent application...
|
||||||
|
Using controllers core.sample.
|
||||||
|
|
||||||
|
|
||||||
|
OutlookCrawler
|
||||||
|
==============
|
||||||
|
|
||||||
|
The agent uses Twisted's cooperative multitasking model.
|
||||||
|
|
||||||
|
OutlookCrawler is derived from MailCrawler. The OutlookCrawler returns a deferred
|
||||||
|
which itself holds a list of MailResource Objects.
|
||||||
|
|
||||||
|
Returns a deferred that must be supplied with a callback method (and in
|
||||||
|
most cases also an errback method).
|
||||||
|
|
||||||
|
The TestCase here is using subsidiary methods which replace calls to the "real Outlook
|
||||||
|
dlls".
|
||||||
|
|
||||||
|
>>> controller = master.controllers[0]
|
||||||
|
>>> controller.createAgent('crawl.outlook', 'sample02')
|
||||||
|
|
||||||
|
In the next step we request the start of a job, again via the controller.
|
||||||
|
|
||||||
|
>>> controller.enterJob('sample', 'sample02', params=dict(inbox=True))
|
||||||
|
|
||||||
|
The job is not executed immediately - we have to hand over control to
|
||||||
|
the twisted reactor first.
|
||||||
|
|
||||||
|
>>> from cybertools.agent.tests import tester
|
||||||
|
>>> tester.iterate()
|
||||||
|
Outlook.Application retrieved
|
||||||
|
Namespace MAPI retrieved
|
||||||
|
retrieving Outlook default folder
|
||||||
|
collecting Mails from folder
|
||||||
|
Attachment: Invitation.pdf
|
||||||
|
Attachment: 21.pdf
|
||||||
|
Attachment saved
|
||||||
|
Attachment saved
|
||||||
|
Job 00001 completed; result: [<...MailResource...>, <...MailResource...>, <...MailResource...>];
|
278
agent/interfaces.py
Normal file
278
agent/interfaces.py
Normal file
|
@ -0,0 +1,278 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
cybertools agent interfaces.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
from zope.interface import Interface, Attribute
|
||||||
|
|
||||||
|
from cybertools.util.jeep import Jeep
|
||||||
|
|
||||||
|
|
||||||
|
# agents
|
||||||
|
|
||||||
|
class IAgent(Interface):
|
||||||
|
""" An agent waits for jobs to execute.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = Attribute('A name identifying the agent.')
|
||||||
|
master = Attribute('IMaster instance.')
|
||||||
|
config = Attribute('Configuration settings.')
|
||||||
|
logger = Attribute('Logger instance to be used for recording '
|
||||||
|
'job execution and execution results.')
|
||||||
|
|
||||||
|
def send(job):
|
||||||
|
""" If the agent supports queueing and the agent is busy put
|
||||||
|
job in queue, otherwise just execute the job.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def execute(job):
|
||||||
|
""" Execute a job using the parameters given.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class IQueueableAgent(Interface):
|
||||||
|
""" An agent that keeps a queue of jobs. A queueable agent
|
||||||
|
executes not more than one job at a time; when a running
|
||||||
|
job is finished the next one will be taken from the queue.
|
||||||
|
"""
|
||||||
|
|
||||||
|
queue = Attribute('A sequence of jobs to execute.')
|
||||||
|
|
||||||
|
def process():
|
||||||
|
""" Do the real work asynchronously, returning a deferred.
|
||||||
|
This method will be called by execute().
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class IMaster(IAgent):
|
||||||
|
""" The top-level controller agent.
|
||||||
|
"""
|
||||||
|
|
||||||
|
config = Attribute('Central configuration settings.')
|
||||||
|
controllers = Attribute('Collection of IController instances.')
|
||||||
|
scheduler = Attribute('IScheduler instance.')
|
||||||
|
children = Attribute('A collection of agents that are managed by this '
|
||||||
|
'master.')
|
||||||
|
|
||||||
|
def setup():
|
||||||
|
""" Set up the master agent by triggering all assigned controllers.
|
||||||
|
Each controller will then call the master agent's callback
|
||||||
|
methods ``setupAgents()`` and ``setupJobs()``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setupAgents(controller, agentSpecs):
|
||||||
|
""" Callback for loading agent specifications from the controller
|
||||||
|
and setting up the corresponding agents.
|
||||||
|
|
||||||
|
Will be called upon agent setup and later when the controller
|
||||||
|
wants to provide new agent information.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setupJobs(controller, jobSpecs):
|
||||||
|
""" Callback for loading the specifications of active jobs from
|
||||||
|
the controller and scheduling the corresponding jobs.
|
||||||
|
|
||||||
|
Will be called upon agent setup and later when the controller
|
||||||
|
wants to provide new job information.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def notify(job, result=None, message=''):
|
||||||
|
""" Callback for informing the master about the state of a job.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class ICrawler(IAgent):
|
||||||
|
""" Collects resources.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def collect(filter=None):
|
||||||
|
""" Return a deferred that upon callback will provide a
|
||||||
|
collection of resource objects that should be transferred
|
||||||
|
to the server.
|
||||||
|
|
||||||
|
Use the selection criteria given to filter the resources that
|
||||||
|
should be collected.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class ITransporter(IAgent):
|
||||||
|
""" Transfers one or more collected resources and corresponding
|
||||||
|
metadata to another entity - a remote agent or another application.
|
||||||
|
"""
|
||||||
|
|
||||||
|
serverURL = Attribute('URL of the server the resources will be '
|
||||||
|
'transferred to. The URL also determines the '
|
||||||
|
'transfer protocol, e.g. HTTP or SFTP.')
|
||||||
|
method = Attribute('Transport method, e.g. PUT.')
|
||||||
|
machineName = Attribute('Name under which the local machine is '
|
||||||
|
'known to the server.')
|
||||||
|
userName = Attribute('User name for logging in to the server.')
|
||||||
|
password = Attribute('Password for logging in to the server.')
|
||||||
|
|
||||||
|
def transfer(resource):
|
||||||
|
""" Transfer the resource (an object providing IResource)
|
||||||
|
to the server and return a Deferred.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# job control
|
||||||
|
|
||||||
|
class IController(Interface):
|
||||||
|
""" Fetches agent and job specifications from a control
|
||||||
|
storage and updates the storage with the status and result
|
||||||
|
information.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setup():
|
||||||
|
""" Set up the controller; e.g. create agents and jobs by calling
|
||||||
|
the agent's callback methods.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class IScheduler(Interface):
|
||||||
|
""" Manages jobs and cares that they are started at the appropriate
|
||||||
|
time by the agents responsible for it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def schedule(job, startTime=None):
|
||||||
|
""" Register the job given for execution at the intended start
|
||||||
|
date/time (an integer timestamp) and return the job.
|
||||||
|
|
||||||
|
If the start time is not given schedule the job for immediate
|
||||||
|
start. Return the start time with which the job has been
|
||||||
|
scheduled - this may be different from the start time
|
||||||
|
supplied.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# jobs
|
||||||
|
|
||||||
|
class IScheduledJob(Interface):
|
||||||
|
""" A job that will be executed on some external triggering at
|
||||||
|
a predefined date and time - this is the basic job interface.
|
||||||
|
"""
|
||||||
|
|
||||||
|
identifier = Attribute('A name/ID unique within the realm of the '
|
||||||
|
'controller.')
|
||||||
|
scheduler = Attribute('Scheduler that controls this job.')
|
||||||
|
agent = Attribute('Agent responsible for executing the job.')
|
||||||
|
controller = Attribute('Controller that issued the job.')
|
||||||
|
startTime = Attribute('Date/time at which the job should be executed.')
|
||||||
|
params = Attribute('Mapping with key/value pairs to be used by '
|
||||||
|
'the ``execute()`` method.')
|
||||||
|
state = Attribute('An object representing the current state of the job.')
|
||||||
|
repeat = Attribute('Number of seconds after which the job should be '
|
||||||
|
'rescheduled. Do not repeat if 0.')
|
||||||
|
successors = Attribute('Jobs to execute immediately after this '
|
||||||
|
'one has been finished.')
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
""" Execute the job, typically by calling the ``execute()`` method
|
||||||
|
of the agent responsible for it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def reschedule(startTime):
|
||||||
|
""" Re-schedule the job, setting the date/time the job should be
|
||||||
|
executed again.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# information objects
|
||||||
|
|
||||||
|
class IResource(Interface):
|
||||||
|
""" Represents a data object that is collected by a crawler and
|
||||||
|
will be transferred to the server.
|
||||||
|
"""
|
||||||
|
|
||||||
|
data = Attribute('A string representation of the '
|
||||||
|
'resource\'s content; may be None if the receiver of '
|
||||||
|
'the information can retrieve the data from the file or path '
|
||||||
|
'attribute.')
|
||||||
|
file = Attribute('A file-like object providing the data via its read() '
|
||||||
|
'method; may be None if the data or path attribute '
|
||||||
|
'is given.')
|
||||||
|
path = Attribute('A filesystem path for accessing the resource; may be '
|
||||||
|
'None if the data or file attribute is given.')
|
||||||
|
identifier = Attribute('A string (usually derived from the path) that '
|
||||||
|
'uniquely identifies the resource.')
|
||||||
|
type = Attribute('A string denoting the type of the resource, e.g. '
|
||||||
|
'"file" or "email".')
|
||||||
|
contentType = Attribute('A string denoting the MIME type of the data, '
|
||||||
|
'e.g. "text/plain" or "application/octet-stream"')
|
||||||
|
encoding = Attribute('Optional: a string denoting the encoding of the '
|
||||||
|
'file data, e.g. "UTF-8".')
|
||||||
|
application = Attribute('The name of the application that provided '
|
||||||
|
'the resource, e.g. "filesystem" or "mail".')
|
||||||
|
metadata = Attribute('Information describing this resource; '
|
||||||
|
'should be an IMetadataSet object.')
|
||||||
|
subResources = Attribute('A collection of resources that are inherently '
|
||||||
|
'connected to or parts of this resource, e.g. attachments '
|
||||||
|
'of an email. Will be None or empty in most cases.')
|
||||||
|
|
||||||
|
|
||||||
|
class IMetadataSet(Interface):
|
||||||
|
""" Metadata associated with a resource; a mapping.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def asXML():
|
||||||
|
""" Return an XML string representing the metadata set.
|
||||||
|
|
||||||
|
If this metadata set contains other metadata sets
|
||||||
|
(nested metadata) these will be converted to XML as well.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def set(key, value):
|
||||||
|
""" Set a metadata element.
|
||||||
|
|
||||||
|
The value may be a string or another metadata set
|
||||||
|
(nested metadata).
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# logging
|
||||||
|
|
||||||
|
class ILogger(Interface):
|
||||||
|
""" Ordered collection (list) of log records, probably stored on some
|
||||||
|
external device.
|
||||||
|
"""
|
||||||
|
|
||||||
|
externalLoggers = Attribute('A collection of logger objects '
|
||||||
|
'to which the logging records should be written.')
|
||||||
|
|
||||||
|
def setup():
|
||||||
|
""" Initialize the logger with the current configuration settings.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def log(data):
|
||||||
|
""" Record the information given by the ``data`` argument
|
||||||
|
(a mapping).
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class ILogRecord(Interface):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __str__():
|
||||||
|
""" Return a string representation suitable for writing to a
|
||||||
|
log file.
|
||||||
|
"""
|
77
agent/main.py
Executable file
77
agent/main.py
Executable file
|
@ -0,0 +1,77 @@
|
||||||
|
#! /usr/bin/env python2.4
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Agent application.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from twisted.internet import reactor
|
||||||
|
|
||||||
|
from cybertools.agent.base.agent import Master
|
||||||
|
|
||||||
|
|
||||||
|
application = None # contains application object if started via twistd
|
||||||
|
|
||||||
|
|
||||||
|
def getConfig():
|
||||||
|
agentHome = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
configName = 'agent.cfg'
|
||||||
|
configFile = open(os.path.join(agentHome, configName))
|
||||||
|
config = configFile.read()
|
||||||
|
configFile.close()
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def setup(configInfo=None):
|
||||||
|
if configInfo is None:
|
||||||
|
configInfo = getConfig()
|
||||||
|
master = Master(configInfo)
|
||||||
|
setupEnvironment(master.config)
|
||||||
|
master.setup()
|
||||||
|
return master
|
||||||
|
|
||||||
|
|
||||||
|
def setupEnvironment(config):
|
||||||
|
# API registration:
|
||||||
|
from cybertools.agent.system.windows import api
|
||||||
|
api.setup(config)
|
||||||
|
from cybertools.agent.system import http, xmlrpc, sftp
|
||||||
|
http.setup(config)
|
||||||
|
xmlrpc.setup(config)
|
||||||
|
sftp.setup(config)
|
||||||
|
# self registration of components:
|
||||||
|
from cybertools.agent.base import agent, control, job, log, schedule
|
||||||
|
from cybertools.agent.core import agent, control, schedule
|
||||||
|
from cybertools.agent.control import cmdline, remote
|
||||||
|
from cybertools.agent.crawl import base, filesystem, outlook
|
||||||
|
from cybertools.agent.talk import http
|
||||||
|
from cybertools.agent.transport import remote, loops
|
||||||
|
|
||||||
|
|
||||||
|
def startReactor():
|
||||||
|
reactor.run()
|
||||||
|
print 'Agent application has been stopped.'
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
setup()
|
||||||
|
startReactor()
|
31
agent/system/http.py
Normal file
31
agent/system/http.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Configuration-controlled import of HTTP communication functionality.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setup(config):
|
||||||
|
global listener, getPage
|
||||||
|
if config.talk.http.handler == 'testing':
|
||||||
|
from cybertools.agent.testing.http import listener, getPage
|
||||||
|
else:
|
||||||
|
from twisted.internet import reactor as listener
|
||||||
|
from twisted.web.client import getPage
|
30
agent/system/sftp.py
Normal file
30
agent/system/sftp.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Configuration-controlled import of sftp functionality.
|
||||||
|
|
||||||
|
$Id: rpcapi.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setup(config):
|
||||||
|
global FileTransfer
|
||||||
|
if config.transport.remote.sftp == 'testing':
|
||||||
|
from cybertools.agent.testing.sftp import FileTransfer
|
||||||
|
else:
|
||||||
|
from cybertools.agent.transport.file.sftp import FileTransfer
|
39
agent/system/windows/api.py
Normal file
39
agent/system/windows/api.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Conficuration-controlled import of Windows API functions.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setup(config):
|
||||||
|
global client, ctypes, win32api, win32process, win32con, com_error
|
||||||
|
if config.system.winapi == 'testing':
|
||||||
|
from cybertools.agent.testing.winapi import \
|
||||||
|
client, ctypes, win32api, win32process, win32con, com_error
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
from win32com import client
|
||||||
|
import ctypes
|
||||||
|
import win32api, win32process, win32con
|
||||||
|
from pywintypes import com_error
|
||||||
|
except ImportError:
|
||||||
|
from cybertools.agent.testing.winapi import \
|
||||||
|
client, ctypes, win32api, win32process, win32con, com_error
|
||||||
|
|
59
agent/system/windows/codepages.py
Normal file
59
agent/system/windows/codepages.py
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Codepages Module
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
codepages = {28596: 'iso-8859-6',\
|
||||||
|
1256: 'windows-1256',\
|
||||||
|
28594: 'iso-8859-4',\
|
||||||
|
1257: 'windows-1257',\
|
||||||
|
28592: 'iso-8859-2',\
|
||||||
|
1250: 'windows-1250',\
|
||||||
|
936: 'gb2312',\
|
||||||
|
52936: 'hz-gb-2312',\
|
||||||
|
950: 'big5',\
|
||||||
|
28595: 'iso-8859-5',\
|
||||||
|
20866: 'koi8-r',\
|
||||||
|
21866: 'koi8-u',\
|
||||||
|
1251: 'windows-1251',\
|
||||||
|
28597: 'iso-8859-7',\
|
||||||
|
1253: 'windows-1253',\
|
||||||
|
38598: 'iso-8859-8-i',\
|
||||||
|
1255: 'windows-1255',\
|
||||||
|
51932: 'euc-jp',\
|
||||||
|
50220: 'iso-2022-jp',\
|
||||||
|
50221: 'csISO2022JP',\
|
||||||
|
932: 'iso-2022-jp',\
|
||||||
|
949: 'ks_c_5601-1987',\
|
||||||
|
51949: 'euc-kr',\
|
||||||
|
28593: 'iso-8859-3',\
|
||||||
|
28605: 'iso-8859-15',\
|
||||||
|
874: 'windows-874',\
|
||||||
|
28599: 'iso-8859-9',\
|
||||||
|
1254: 'windows-1254',\
|
||||||
|
65000: 'utf-7',\
|
||||||
|
65001: 'utf-8',\
|
||||||
|
20127: 'us-ascii',\
|
||||||
|
1258: 'windows-1258',\
|
||||||
|
28591: 'iso-8859-1',\
|
||||||
|
1252: 'Windows-1252'
|
||||||
|
}
|
||||||
|
|
56
agent/system/windows/outlookdialog.py
Normal file
56
agent/system/windows/outlookdialog.py
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Module for handling Outlook dialoges
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
def login(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def handleOutlookDialog(self):
|
||||||
|
"""
|
||||||
|
This function handles the outlook dialog, which appears if someone
|
||||||
|
tries to access to MS Outlook.
|
||||||
|
"""
|
||||||
|
hwnd = None
|
||||||
|
while True:
|
||||||
|
hwnd = api.ctypes.windll.user32.FindWindowExA(None, hwnd, None, None)
|
||||||
|
if hwnd == None:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
val = u"\0" * 1024
|
||||||
|
api.ctypes.windll.user32.GetWindowTextW(hwnd, val, len(val))
|
||||||
|
val = val.replace(u"\000", u"")
|
||||||
|
if val and repr(val) == "u'Microsoft Office Outlook'":
|
||||||
|
print repr(val)
|
||||||
|
# get the Main Control
|
||||||
|
form = api.findTopWindow(wantedText='Microsoft Office Outlook')
|
||||||
|
controls = findControls(form)
|
||||||
|
# get the check box
|
||||||
|
checkBox = findControl(form, wantedText='Zugriff')
|
||||||
|
setCheckBox(checkBox, 1)
|
||||||
|
# get the combo box
|
||||||
|
comboBox = findControl(form, wantedClass='ComboBox')
|
||||||
|
items = getComboboxItems(comboBox)
|
||||||
|
selectComboboxItem(comboBox, items[3])#'10 Minuten'
|
||||||
|
# finally get the button and click it
|
||||||
|
button = findControl(form, wantedText = 'Erteilen')
|
||||||
|
clickButton(button)
|
||||||
|
break
|
30
agent/system/xmlrpc.py
Normal file
30
agent/system/xmlrpc.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Configuration controlled import of twisted xmlrpc functionality
|
||||||
|
|
||||||
|
$Id: rpcapi.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setup(config):
|
||||||
|
global xmlrpc
|
||||||
|
if config.transport.remote.server == 'testing':
|
||||||
|
from cybertools.agent.testing.rpcserver import xmlrpc
|
||||||
|
else:
|
||||||
|
from twisted.web import xmlrpc
|
73
agent/talk/README.txt
Normal file
73
agent/talk/README.txt
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
================================================
|
||||||
|
Agents for Job Execution and Communication Tasks
|
||||||
|
================================================
|
||||||
|
|
||||||
|
($Id$)
|
||||||
|
|
||||||
|
>>> from cybertools.agent.tests import tester
|
||||||
|
|
||||||
|
|
||||||
|
Communication Handling
|
||||||
|
======================
|
||||||
|
|
||||||
|
Communication services are provided by handlers specified in the ``talk``
|
||||||
|
package.
|
||||||
|
|
||||||
|
Set up and start an agent with a server
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
>>> config = '''
|
||||||
|
... controller(names=['core.sample'])
|
||||||
|
... scheduler(name='core')
|
||||||
|
... logger(name='default', standard=30)
|
||||||
|
... talk.server(names=['http'])
|
||||||
|
... talk.server.http(port=8081)
|
||||||
|
... talk.http(handler='testing')
|
||||||
|
... '''
|
||||||
|
>>> from cybertools.agent.main import setup
|
||||||
|
>>> master = setup(config)
|
||||||
|
Starting agent application...
|
||||||
|
Using controllers core.sample.
|
||||||
|
Setting up HTTP handler for port 8081.
|
||||||
|
|
||||||
|
>>> master.servers
|
||||||
|
[<cybertools.agent.talk.http.HttpServer object...>]
|
||||||
|
|
||||||
|
We also provide a class to be used for creating subscribers, i.e. objects
|
||||||
|
that receive messages.
|
||||||
|
|
||||||
|
>>> class Subscriber(object):
|
||||||
|
... def __init__(self, name):
|
||||||
|
... self.name = name
|
||||||
|
... def onMessage(self, interaction, data):
|
||||||
|
... print ('%s receiving: interaction=%s, data=%s' %
|
||||||
|
... (self.name, interaction, data))
|
||||||
|
... tester.stop()
|
||||||
|
|
||||||
|
>>> serverSub = Subscriber('server')
|
||||||
|
>>> master.servers[0].subscribe(serverSub, 'testing')
|
||||||
|
|
||||||
|
Set up a client
|
||||||
|
---------------
|
||||||
|
|
||||||
|
In order to simplify the testing we do not set up a separate agent to
|
||||||
|
work with the client but handle the client directly.
|
||||||
|
|
||||||
|
>>> from cybertools.agent.talk.http import HttpClient
|
||||||
|
>>> client = HttpClient(master)
|
||||||
|
|
||||||
|
>>> clientSub = Subscriber('client')
|
||||||
|
|
||||||
|
>>> session = client.connect(clientSub, 'http://localhost:8081/')
|
||||||
|
|
||||||
|
Run the communication dialog
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
>>> tester.run()
|
||||||
|
client receiving: interaction=None, data={u'status': u'OK'}
|
||||||
|
|
||||||
|
|
||||||
|
Fin de Partie
|
||||||
|
=============
|
||||||
|
|
||||||
|
>>> tester.stopThreads()
|
93
agent/talk/base.py
Normal file
93
agent/talk/base.py
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Handling asynchronous communication tasks - common and base classes.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
from twisted.web.client import getPage
|
||||||
|
from zope.interface import implements
|
||||||
|
|
||||||
|
from cybertools.agent.talk.interfaces import ISession, IInteraction
|
||||||
|
from cybertools.util import json
|
||||||
|
|
||||||
|
|
||||||
|
class Session(object):
|
||||||
|
|
||||||
|
implements(ISession)
|
||||||
|
|
||||||
|
def __init__(self, id, manager, subscriber, url):
|
||||||
|
self.id = id
|
||||||
|
self.manager = manager
|
||||||
|
self.subscriber = subscriber
|
||||||
|
self.url = url
|
||||||
|
self.state = 'logon'
|
||||||
|
self.sending = False
|
||||||
|
self.queue = []
|
||||||
|
self.interactions = {}
|
||||||
|
self.interactionCount = 0
|
||||||
|
|
||||||
|
def received(self, data):
|
||||||
|
data = json.loads(data)
|
||||||
|
# TODO: check data; notify sender?
|
||||||
|
self.sending = False
|
||||||
|
self._processQueue()
|
||||||
|
|
||||||
|
def send(self, data, interaction):
|
||||||
|
data['interaction'] = interaction.id
|
||||||
|
if self.sending or self.queue:
|
||||||
|
self.queue.append(data)
|
||||||
|
else:
|
||||||
|
self._sendData(data)
|
||||||
|
|
||||||
|
def processQueue(self):
|
||||||
|
if not self.queue:
|
||||||
|
return
|
||||||
|
self._sendData(self.queue.pop(0))
|
||||||
|
|
||||||
|
def sendData(self, data, command='send'):
|
||||||
|
self.sending = True
|
||||||
|
content = dict(id=self.id, command=command, data=data)
|
||||||
|
d = getPage(self.url, postdata=json.dumps(content))
|
||||||
|
d.addCallback(s.received)
|
||||||
|
|
||||||
|
def connected(self, data):
|
||||||
|
data = json.loads(data)
|
||||||
|
self.state = 'open'
|
||||||
|
self.subscriber.onMessage(None, data)
|
||||||
|
self.sending = False
|
||||||
|
self.processQueue()
|
||||||
|
|
||||||
|
def generateInteractionId(self):
|
||||||
|
self.interactionCount += 1
|
||||||
|
return '%07i' % self.interactionCount
|
||||||
|
|
||||||
|
|
||||||
|
class Interaction(object):
|
||||||
|
|
||||||
|
implements(IInteraction)
|
||||||
|
|
||||||
|
finished = False
|
||||||
|
|
||||||
|
def __init__(self, session):
|
||||||
|
self.session = session
|
||||||
|
self.id = self.session.generateInteractionId()
|
||||||
|
self.session.interactions[self.id] = self
|
||||||
|
|
181
agent/talk/http.py
Normal file
181
agent/talk/http.py
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2009 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Handling asynchronous and possibly asymmetric communication tasks via HTTP.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
from time import time
|
||||||
|
from twisted.web.client import getPage
|
||||||
|
from twisted.web.resource import Resource
|
||||||
|
from twisted.web.server import Site, NOT_DONE_YET
|
||||||
|
from zope.interface import implements
|
||||||
|
|
||||||
|
from cybertools.agent.base.agent import Master
|
||||||
|
from cybertools.agent.components import servers, clients
|
||||||
|
from cybertools.agent.system.http import listener
|
||||||
|
from cybertools.agent.talk.base import Session, Interaction
|
||||||
|
from cybertools.agent.talk.interfaces import IServer, IClient
|
||||||
|
from cybertools.util import json
|
||||||
|
|
||||||
|
|
||||||
|
# server implementation
|
||||||
|
|
||||||
|
#@server
|
||||||
|
class HttpServer(object):
|
||||||
|
|
||||||
|
implements(IServer)
|
||||||
|
|
||||||
|
def __init__(self, agent):
|
||||||
|
self.agent = agent
|
||||||
|
self.port = agent.config.talk.server.http.port
|
||||||
|
self.subscribers = {}
|
||||||
|
self.sessions = {}
|
||||||
|
self.site = Site(RootResource(self))
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
print 'Setting up HTTP handler for port %i.' % self.port
|
||||||
|
listener.listenTCP(self.port, self.site)
|
||||||
|
|
||||||
|
def subscribe(self, subscriber, aspect):
|
||||||
|
subs = self.subscribers.setdefault(aspect, [])
|
||||||
|
if subscriber not in subs:
|
||||||
|
subs.append(subscriber)
|
||||||
|
|
||||||
|
def unsubscribe(self, subscriber, aspect):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def send(self, session, data, interaction=None):
|
||||||
|
if interaction is None:
|
||||||
|
interaction = Interaction(session)
|
||||||
|
# check session's queue
|
||||||
|
# check open poll - write response
|
||||||
|
return interaction
|
||||||
|
|
||||||
|
def process(self, client, data):
|
||||||
|
action = data.get('action')
|
||||||
|
if not action:
|
||||||
|
return self._error('missing action')
|
||||||
|
amethod = self.actions.get(action)
|
||||||
|
if amethod is None:
|
||||||
|
return self._error('illegal action %r' % action)
|
||||||
|
sid = data.get('session')
|
||||||
|
if not sid:
|
||||||
|
return self._error('missing session id')
|
||||||
|
sessionId = ':'.join((client, sid))
|
||||||
|
message = amethod(self, sessionId, client, data)
|
||||||
|
if message:
|
||||||
|
return self._error(message)
|
||||||
|
return '{"status": "OK"}'
|
||||||
|
|
||||||
|
def _connect(self, sessionId, client, data):
|
||||||
|
if sessionId in self.sessions:
|
||||||
|
return 'duplicate session id %r' % sessionId
|
||||||
|
self.sessions[sessionId] = HttpServerSession(sessionId, self, None, client)
|
||||||
|
# TODO: notify subscribers
|
||||||
|
|
||||||
|
def _poll(self, sessionId, client, data):
|
||||||
|
# record deferred with session
|
||||||
|
return NOT_DONE_YET
|
||||||
|
|
||||||
|
def _send(self, sessionId, client, data):
|
||||||
|
for sub in self.subscribers.values():
|
||||||
|
sub.onMessage(data)
|
||||||
|
|
||||||
|
def _error(self, message):
|
||||||
|
return json.dumps(dict(status='error', message=message))
|
||||||
|
|
||||||
|
actions = dict(connect=_connect, poll=_poll, send=_send)
|
||||||
|
|
||||||
|
servers.register(HttpServer, Master, name='http')
|
||||||
|
|
||||||
|
|
||||||
|
class RootResource(Resource):
|
||||||
|
|
||||||
|
isLeaf = True
|
||||||
|
|
||||||
|
def __init__(self, server):
|
||||||
|
self.server = server
|
||||||
|
|
||||||
|
def render(self, request):
|
||||||
|
client = request.getClient()
|
||||||
|
data = json.loads(request.content.read())
|
||||||
|
return self.server.process(client, data)
|
||||||
|
|
||||||
|
|
||||||
|
class HttpServerSession(Session):
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# client implementation
|
||||||
|
|
||||||
|
#@client
|
||||||
|
class HttpClient(object):
|
||||||
|
|
||||||
|
implements(IClient)
|
||||||
|
|
||||||
|
def __init__(self, agent):
|
||||||
|
self.agent = agent
|
||||||
|
self.sessions = {}
|
||||||
|
|
||||||
|
def connect(self, subscriber, url, credentials=None):
|
||||||
|
id = self.generateSessionId()
|
||||||
|
s = HttpClientSession(self, id, subscriber, url)
|
||||||
|
self.sessions[id] = s
|
||||||
|
data = dict(action='connect', session=id)
|
||||||
|
if credentials is not None:
|
||||||
|
data.update(credentials)
|
||||||
|
# s.send(data, None)
|
||||||
|
d = getPage(url, postdata=json.dumps(data))
|
||||||
|
d.addCallback(s.connected)
|
||||||
|
return s
|
||||||
|
|
||||||
|
def disconnect(self, session):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def send(self, session, data, interaction=None):
|
||||||
|
if interaction is None:
|
||||||
|
interaction = Interaction(session)
|
||||||
|
session.send(data, interaction)
|
||||||
|
return interaction
|
||||||
|
|
||||||
|
def generateSessionId(self):
|
||||||
|
return '%.7f' % time()
|
||||||
|
|
||||||
|
clients.register(HttpClient, Master, name='http')
|
||||||
|
|
||||||
|
|
||||||
|
class HttpClientSession(Session):
|
||||||
|
|
||||||
|
def connected(self, data):
|
||||||
|
super(HttpClientSession, self).connected(data)
|
||||||
|
# self.poll()
|
||||||
|
|
||||||
|
def pollReceived(self, data):
|
||||||
|
data = json.loads(data)
|
||||||
|
if data.get('action') != 'idle':
|
||||||
|
self.subscriber.onMessage(interaction, data)
|
||||||
|
# self.poll()
|
||||||
|
|
||||||
|
def poll(self):
|
||||||
|
content = dict(id=self.id, command='poll')
|
||||||
|
d = getPage(self.url, postdata=json.dumps(content))
|
||||||
|
d.addCallback(s.pollReceived)
|
121
agent/talk/interfaces.py
Normal file
121
agent/talk/interfaces.py
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Interfaces for handling asynchronous communication tasks.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
from zope.interface import Interface, Attribute
|
||||||
|
|
||||||
|
|
||||||
|
class IServer(Interface):
|
||||||
|
""" A server waits for connection requests from a client. A connected
|
||||||
|
client may then send data to or receive messages from the server.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def subscribe(subscriber, aspect):
|
||||||
|
""" The subscriber will receive messages via its ``onMesssage`` method.
|
||||||
|
|
||||||
|
The aspect is a dotted string used to select the kind of
|
||||||
|
sessions/remote clients the subscriber wants to receive messages
|
||||||
|
from.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def unsubscribe(subscriber, aspect):
|
||||||
|
""" Stop receiving messages.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def send(session, data, interaction=None):
|
||||||
|
""" Send data to the remote client specified via the session given.
|
||||||
|
The session has to be created previously by a connect attempt
|
||||||
|
from the client.
|
||||||
|
|
||||||
|
If interaction is None, create a new one.
|
||||||
|
Return the interaction.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class IClient(Interface):
|
||||||
|
""" A client initiates a connection (session) to a server and may then
|
||||||
|
sent data to or receive data from the server.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def connect(subscriber, url, credentials=None):
|
||||||
|
""" Connect to a server using the URL given, optionally logging in
|
||||||
|
with the credentials given.
|
||||||
|
|
||||||
|
The subscriber will receive messages via its ``onMesssage`` callback.
|
||||||
|
|
||||||
|
Return a an ISession implementation that may be used for sending
|
||||||
|
data to the server.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def disconnect(session):
|
||||||
|
""" Close the connection for the session given.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def send(session, data, interaction=None):
|
||||||
|
""" Send data to the server specified via the session given.
|
||||||
|
|
||||||
|
If interaction is None, create a new one.
|
||||||
|
Return the interaction.
|
||||||
|
|
||||||
|
Sending an interaction with ``finished`` set to True signifies
|
||||||
|
the last message of an interaction.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# auxiliary interfaces
|
||||||
|
|
||||||
|
class ISubscriber(Interface):
|
||||||
|
""" May receive message notifications.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def onMessage(interaction, data):
|
||||||
|
""" Callback method for message notifications.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def onError(interaction, data):
|
||||||
|
""" Callback method for error notifications.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class ISession(Interface):
|
||||||
|
""" Represents the connection to a server within a client or
|
||||||
|
a remote client connection within a server.
|
||||||
|
"""
|
||||||
|
|
||||||
|
manager = Attribute("""The server or client object, respectively, that
|
||||||
|
created the session.""")
|
||||||
|
subscriber = Attribute("The subscriber that initiated the session.")
|
||||||
|
url = Attribute("The URL of the server (or client) the session connects to.")
|
||||||
|
state = Attribute("""A string specifying the current state of the session:
|
||||||
|
'logon': The remote client is trying to connect/log in,
|
||||||
|
data may contain credential information;
|
||||||
|
'logoff': The remote client is closing the connection;
|
||||||
|
'open': The connection is open.""")
|
||||||
|
|
||||||
|
|
||||||
|
class IInteraction(Interface):
|
||||||
|
""" Represents a set of message exchanges belonging together.
|
||||||
|
"""
|
||||||
|
|
||||||
|
session = Attribute("The session the interaction belongs to.")
|
||||||
|
finished = Attribute("The interaction is finished, interaction data may be cleared.")
|
9
agent/testing/agent.cfg
Normal file
9
agent/testing/agent.cfg
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
#
|
||||||
|
# Standard configuration for agent application
|
||||||
|
#
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
|
||||||
|
controller(names=['test'])
|
||||||
|
scheduler(name='core')
|
||||||
|
logger(name='default', standard=30)
|
43
agent/testing/control.py
Normal file
43
agent/testing/control.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Controller for testing purposes.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
from zope.interface import implements
|
||||||
|
|
||||||
|
from cybertools.agent.base.agent import Master
|
||||||
|
from cybertools.agent.base.control import SampleController, JobSpecification
|
||||||
|
from cybertools.agent.components import controllers
|
||||||
|
from cybertools.agent.crawl.mail import MailResource
|
||||||
|
|
||||||
|
|
||||||
|
class Controller(SampleController):
|
||||||
|
|
||||||
|
agents = (('tr1', 'transport.remote'),)
|
||||||
|
|
||||||
|
def _getCurrentJobs(self):
|
||||||
|
print '_getCurrentJobs'
|
||||||
|
return [JobSpecification('sample', '00001', agent='tr1',
|
||||||
|
params=dict(resource=MailResource()))]
|
||||||
|
|
||||||
|
|
||||||
|
controllers.register(Controller, Master, name='test')
|
1
agent/testing/data/file1.txt
Normal file
1
agent/testing/data/file1.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Data from file1.txt
|
1
agent/testing/data/subdir/file2.txt
Normal file
1
agent/testing/data/subdir/file2.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Data from file2.txt
|
44
agent/testing/http.py
Normal file
44
agent/testing/http.py
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Fake testing objects/functions for HTTP communication.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
from twisted.internet.defer import Deferred, succeed
|
||||||
|
|
||||||
|
|
||||||
|
class Listener(object):
|
||||||
|
|
||||||
|
site = port = None
|
||||||
|
|
||||||
|
def listenTCP(self, port, site):
|
||||||
|
self.port = port
|
||||||
|
self.site = site
|
||||||
|
self.resource = site.resource
|
||||||
|
deferred = self.deferred = Deferred()
|
||||||
|
return deferred
|
||||||
|
|
||||||
|
|
||||||
|
listener = Listener()
|
||||||
|
|
||||||
|
|
||||||
|
def getPage(url, contextFactory=None, method='GET', postdata=None, **kwargs):
|
||||||
|
return succeed('{"message": "OK"}')
|
73
agent/testing/main.py
Executable file
73
agent/testing/main.py
Executable file
|
@ -0,0 +1,73 @@
|
||||||
|
#! /usr/bin/env python2.4
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Agent application.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from twisted.internet import reactor
|
||||||
|
|
||||||
|
from cybertools.agent.base.agent import Master
|
||||||
|
|
||||||
|
|
||||||
|
application = None # contains application object if started via twistd
|
||||||
|
|
||||||
|
|
||||||
|
def getConfig():
|
||||||
|
home = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
configName = 'agent.cfg'
|
||||||
|
configFile = open(os.path.join(home, configName))
|
||||||
|
config = configFile.read()
|
||||||
|
configFile.close()
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def setup(configInfo=None):
|
||||||
|
if configInfo is None:
|
||||||
|
configInfo = getConfig()
|
||||||
|
master = Master(configInfo)
|
||||||
|
setupEnvironment(master.config)
|
||||||
|
master.setup()
|
||||||
|
print 'Starting agent application...'
|
||||||
|
print 'Using controllers %s.' % ', '.join(master.config.controller.names)
|
||||||
|
return master
|
||||||
|
|
||||||
|
|
||||||
|
def setupEnvironment(config):
|
||||||
|
from cybertools.agent.base import agent, control, job, log, schedule
|
||||||
|
from cybertools.agent.core import agent, control, schedule
|
||||||
|
from cybertools.agent.control import cmdline, remote
|
||||||
|
from cybertools.agent.transport import remote, loops
|
||||||
|
from cybertools.agent.testing import control
|
||||||
|
from cybertools.agent.system.windows import api
|
||||||
|
api.setup(config)
|
||||||
|
from cybertools.agent.crawl import base, filesystem, outlook
|
||||||
|
|
||||||
|
|
||||||
|
def startReactor():
|
||||||
|
reactor.run()
|
||||||
|
print 'Agent application has been stopped.'
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
setup()
|
||||||
|
startReactor()
|
74
agent/testing/main_outlook.py
Normal file
74
agent/testing/main_outlook.py
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
#! /usr/bin/env python2.4
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Agent application.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from twisted.internet import reactor
|
||||||
|
|
||||||
|
from cybertools.agent.base.agent import Master
|
||||||
|
|
||||||
|
|
||||||
|
application = None # contains application object if started via twistd
|
||||||
|
|
||||||
|
|
||||||
|
def getConfig():
|
||||||
|
agentHome = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
configName = 'outlook.cfg'
|
||||||
|
configFile = open(os.path.join(agentHome, configName))
|
||||||
|
config = configFile.read()
|
||||||
|
configFile.close()
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def setup(configInfo=None):
|
||||||
|
if configInfo is None:
|
||||||
|
configInfo = getConfig()
|
||||||
|
master = Master(configInfo)
|
||||||
|
setupEnvironment(master.config)
|
||||||
|
master.setup()
|
||||||
|
print 'Starting agent application...'
|
||||||
|
print 'Using controllers %s.' % ', '.join(master.config.controller.names)
|
||||||
|
return master
|
||||||
|
|
||||||
|
|
||||||
|
def setupEnvironment(config):
|
||||||
|
from cybertools.agent.base import agent, control, job, log, schedule
|
||||||
|
from cybertools.agent.core import agent, control, schedule
|
||||||
|
from cybertools.agent.control import cmdline
|
||||||
|
from cybertools.agent.system.windows import api
|
||||||
|
api.setup(config)
|
||||||
|
from cybertools.agent.crawl import base, outlook
|
||||||
|
|
||||||
|
|
||||||
|
def startReactor():
|
||||||
|
reactor.run()
|
||||||
|
print 'Agent application has been stopped.'
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
master = setup()
|
||||||
|
controller = master.controllers[0]
|
||||||
|
controller.createAgent('crawl.outlook', 'sample02')
|
||||||
|
controller.enterJob('sample', 'sample02', params=dict(inbox=True))
|
||||||
|
startReactor()
|
82
agent/testing/main_transport.py
Normal file
82
agent/testing/main_transport.py
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
#! /usr/bin/env python2.4
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Agent application.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from twisted.internet import reactor
|
||||||
|
|
||||||
|
from cybertools.agent.base.agent import Master
|
||||||
|
from cybertools.agent.crawl.base import Metadata, Resource
|
||||||
|
|
||||||
|
|
||||||
|
application = None # contains application object if started via twistd
|
||||||
|
|
||||||
|
|
||||||
|
def getConfig():
|
||||||
|
agentHome = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
configName = 'transporter.cfg'
|
||||||
|
configFile = open(os.path.join(agentHome, configName))
|
||||||
|
config = configFile.read()
|
||||||
|
configFile.close()
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def setup(configInfo=None):
|
||||||
|
if configInfo is None:
|
||||||
|
configInfo = getConfig()
|
||||||
|
master = Master(configInfo)
|
||||||
|
setupEnvironment(master.config)
|
||||||
|
master.setup()
|
||||||
|
print 'Starting agent application...'
|
||||||
|
print 'Using controllers %s.' % ', '.join(master.config.controller.names)
|
||||||
|
return master
|
||||||
|
|
||||||
|
|
||||||
|
def setupEnvironment(config):
|
||||||
|
from cybertools.agent.base import agent, control, job, log, schedule
|
||||||
|
from cybertools.agent.core import agent, control, schedule
|
||||||
|
from cybertools.agent.control import cmdline
|
||||||
|
from cybertools.agent.system import rpcapi
|
||||||
|
rpcapi.setup(config)
|
||||||
|
from cybertools.agent.system import sftpapi
|
||||||
|
sftpapi.setup(config)
|
||||||
|
from cybertools.agent.transport import remote
|
||||||
|
|
||||||
|
|
||||||
|
def startReactor():
|
||||||
|
reactor.run()
|
||||||
|
print 'Agent application has been stopped.'
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
master = setup()
|
||||||
|
controller = master.controllers[0]
|
||||||
|
controller.createAgent('transport.remote', 'sample03')
|
||||||
|
metadata01 = Metadata(dict(filename='dummy.txt'))
|
||||||
|
res01 = Resource()
|
||||||
|
res01.metadata = metadata01
|
||||||
|
res01.path = 'data/file1.txt'
|
||||||
|
controller.enterJob('sample', 'sample03', params=dict(resource=res01))
|
||||||
|
startReactor()
|
10
agent/testing/outlook.cfg
Normal file
10
agent/testing/outlook.cfg
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
#
|
||||||
|
# Standard configuration for agent application
|
||||||
|
#
|
||||||
|
# $Id: outlook.cfg 2496 2008-04-04 08:07:22Z helmutm $
|
||||||
|
#
|
||||||
|
|
||||||
|
controller(names=['core.sample'])
|
||||||
|
scheduler(name='core')
|
||||||
|
logger(name='default', standard=30)
|
||||||
|
system.winapi = 'use_outlook'
|
135
agent/testing/rpcserver.py
Normal file
135
agent/testing/rpcserver.py
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Fake rpcserver for testing purposes
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
from twisted.internet.defer import succeed
|
||||||
|
|
||||||
|
|
||||||
|
class RPCServer(object):
|
||||||
|
|
||||||
|
serverURL = ''
|
||||||
|
method = ''
|
||||||
|
machineName = ''
|
||||||
|
userName = ''
|
||||||
|
password = ''
|
||||||
|
controller = ''
|
||||||
|
|
||||||
|
def __init__(self, serverURL = '', method = '', machineName = '',
|
||||||
|
userName = '', password = '', controlObj= None):
|
||||||
|
self.serverURL = serverURL
|
||||||
|
self.method = method
|
||||||
|
self.machineName = machineName
|
||||||
|
self.userName = userName
|
||||||
|
self.password = password
|
||||||
|
self.controller = controlObj
|
||||||
|
|
||||||
|
def getMetadata(self, metadata):
|
||||||
|
if self.controller is not None:
|
||||||
|
# pass metadata to controller
|
||||||
|
# this is done AFTER the resource (like e.g. file or mail)
|
||||||
|
# is handed over
|
||||||
|
pass
|
||||||
|
deferred = succeed('Metadata accepted by server')
|
||||||
|
return deferred
|
||||||
|
|
||||||
|
def xmlrpc_shutdownRPCServer():
|
||||||
|
return "xmlrRPC server shutdown completed!"
|
||||||
|
|
||||||
|
|
||||||
|
class XmlRpc(object):
|
||||||
|
|
||||||
|
Proxy = None
|
||||||
|
XMLRPC = None
|
||||||
|
Handler = None
|
||||||
|
XMLRPCIntrospection = None
|
||||||
|
QueryProtocol = None
|
||||||
|
_QueryFactory = None
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.Proxy = Proxy
|
||||||
|
#self.XMLRPC = XMLRPC()
|
||||||
|
#self.Handler = Handler()
|
||||||
|
#self.XMLRPCIntrospection = XMLRPCIntrospection()
|
||||||
|
#self.QueryProtocol = QueryProtocol()
|
||||||
|
#self._QueryFactory = _QueryFactory()
|
||||||
|
|
||||||
|
def addIntrospection(self, xmlrpc):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Proxy(object):
|
||||||
|
|
||||||
|
url = ''
|
||||||
|
user = None
|
||||||
|
password = None
|
||||||
|
allowNone = False
|
||||||
|
queryFactory = None
|
||||||
|
|
||||||
|
def __init__(self, url, user=None, password=None, allowNone=False):
|
||||||
|
self.url = url
|
||||||
|
self.user = user
|
||||||
|
self.password = password
|
||||||
|
self.allowNone = allowNone
|
||||||
|
self.RPCServer = RPCServer()
|
||||||
|
|
||||||
|
def callRemote(self, methodName, *params):
|
||||||
|
"""
|
||||||
|
intended to simulate the callRemote command of a real xmlrpcserver
|
||||||
|
that takes a method name and calls the method, returning the results
|
||||||
|
as xml formatted strings
|
||||||
|
"""
|
||||||
|
method = getattr(self.RPCServer, methodName)
|
||||||
|
return method(*params)
|
||||||
|
|
||||||
|
|
||||||
|
xmlrpc = XmlRpc()
|
||||||
|
|
||||||
|
|
||||||
|
class XMLRPC(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Handler(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class XMLRPCIntrospection(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class QueryProtocol(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class _QueryFactory(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
37
agent/testing/sftp.py
Normal file
37
agent/testing/sftp.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Fake sftp class for testing purposes
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
from twisted.internet.defer import succeed
|
||||||
|
|
||||||
|
|
||||||
|
class FileTransfer(object):
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, host, port, username, password):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def upload(self, localPath, remotePath):
|
||||||
|
deferred = succeed('Upload completed')
|
||||||
|
return deferred
|
||||||
|
|
80
agent/testing/test_rpcserver.py
Normal file
80
agent/testing/test_rpcserver.py
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Providing access for remote agent instances by listening for requests
|
||||||
|
from remote transport agents.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
from twisted.web import xmlrpc, server, resource
|
||||||
|
from twisted.internet import defer, reactor
|
||||||
|
from cybertools.agent.base.agent import Agent
|
||||||
|
|
||||||
|
application = None
|
||||||
|
|
||||||
|
class RPCServer(xmlrpc.XMLRPC):
|
||||||
|
|
||||||
|
serverURL = ''
|
||||||
|
method = ''
|
||||||
|
machineName = ''
|
||||||
|
userName = ''
|
||||||
|
password = ''
|
||||||
|
controller = ''
|
||||||
|
close = reactor.stop
|
||||||
|
|
||||||
|
def __init__(self, serverURL = '', method = '', machineName = '',
|
||||||
|
userName = '', password = '', controlObj= None):
|
||||||
|
self.serverURL = serverURL
|
||||||
|
self.method = method
|
||||||
|
self.machineName = machineName
|
||||||
|
self.userName = userName
|
||||||
|
self.password = password
|
||||||
|
self.controller = controlObj
|
||||||
|
xmlrpc.XMLRPC.__init__(self)
|
||||||
|
|
||||||
|
def xmlrpc_transfer(self, resource):
|
||||||
|
if self.controller is not None:
|
||||||
|
# pass resource object to controller
|
||||||
|
# this is done BEFORE the metadata is handed over
|
||||||
|
# call notify method of controller
|
||||||
|
pass
|
||||||
|
print resource
|
||||||
|
return "Resource received: ", resource
|
||||||
|
|
||||||
|
def xmlrpc_getMetadata(self, metadata):
|
||||||
|
if self.controller is not None:
|
||||||
|
# pass metadata to controller
|
||||||
|
# this is done AFTER the resource (like e.g. file or mail)
|
||||||
|
# is handed over
|
||||||
|
pass
|
||||||
|
print '*** metadata', metadata
|
||||||
|
metadata = "Echo: ", metadata
|
||||||
|
return metadata
|
||||||
|
|
||||||
|
def xmlrpc_shutdownRPCServer():
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from twisted.internet import reactor
|
||||||
|
site = RPCServer()
|
||||||
|
reactor.listenTCP(8082, server.Site(site))
|
||||||
|
print '*** listening...'
|
||||||
|
reactor.run()
|
18
agent/testing/test_sftp.py
Normal file
18
agent/testing/test_sftp.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
|
||||||
|
|
||||||
|
from twisted.internet import reactor
|
||||||
|
|
||||||
|
from cybertools.agent.transport.file.sftp import FileTransfer
|
||||||
|
|
||||||
|
def output(x):
|
||||||
|
print x
|
||||||
|
|
||||||
|
ft = FileTransfer('cy05.de', 22, 'scrat', '...')
|
||||||
|
|
||||||
|
d = ft.upload('d:\\text2.rtf', 'text.txt')
|
||||||
|
d.addCallback(output)
|
||||||
|
|
||||||
|
reactor.callLater(21, ft.close)
|
||||||
|
reactor.callLater(32, reactor.stop)
|
||||||
|
|
||||||
|
reactor.run()
|
17
agent/testing/transporter.cfg
Normal file
17
agent/testing/transporter.cfg
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
#
|
||||||
|
# sample.cfg - agent configuration for demonstration and testing purposes
|
||||||
|
#
|
||||||
|
# $Id$
|
||||||
|
#
|
||||||
|
# transportserver.xmlrpc='testing'
|
||||||
|
|
||||||
|
controller(names=['core.sample'])
|
||||||
|
scheduler(name='core')
|
||||||
|
logger(name='default', standard=30)
|
||||||
|
#transport.remote.server = 'testing'
|
||||||
|
transport.remote.url = 'http://localhost:8082'
|
||||||
|
transport.remote.ftp.url = 'cy05.de'
|
||||||
|
transport.remote.ftp.user = 'scrat'
|
||||||
|
transport.remote.ftp.password = '...'
|
||||||
|
transport.remote.sftp = 'http://cy05.de'
|
||||||
|
transport.remote.chunksize = 4096
|
240
agent/testing/winapi.py
Normal file
240
agent/testing/winapi.py
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Fake Windows API functions for testing purposes.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
win32api = win32process = win32con = None
|
||||||
|
|
||||||
|
|
||||||
|
class com_error(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Attachments(list):
|
||||||
|
|
||||||
|
elemCount = 0
|
||||||
|
data = []
|
||||||
|
|
||||||
|
def __init__(self, params=[]):
|
||||||
|
for elem in params:
|
||||||
|
fileitem = Attachment(filename=elem[0], ParentMail=elem[1])
|
||||||
|
self.data.append(fileitem)
|
||||||
|
print "Attachment: ", fileitem.FileName
|
||||||
|
|
||||||
|
@property
|
||||||
|
def Application(self):
|
||||||
|
print "Outlook application instance"
|
||||||
|
return "Outlook application instance"
|
||||||
|
|
||||||
|
def Item(self, index):
|
||||||
|
return self.data[index-1]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def count(self):
|
||||||
|
return len(data)
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.data)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
yield self.data
|
||||||
|
|
||||||
|
def __getitem__(self, idx):
|
||||||
|
return self.data[idx]
|
||||||
|
|
||||||
|
|
||||||
|
class Attachment(object):
|
||||||
|
|
||||||
|
File = ""
|
||||||
|
parentMailObject = None
|
||||||
|
|
||||||
|
def __init__(self, ParentMail, filename=""):
|
||||||
|
self.File = filename
|
||||||
|
self.parentMailObject = ParentMail
|
||||||
|
|
||||||
|
def SaveAsFile(self, path=""):
|
||||||
|
print "Attachment saved"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def Parent(self):
|
||||||
|
" return value of Attribute Parent is of type _MailItem"
|
||||||
|
return self.parentMailObject
|
||||||
|
|
||||||
|
@property
|
||||||
|
def Type(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def Size(self):
|
||||||
|
# the size property is not available in Outlook 2000
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def Application(self):
|
||||||
|
" Actual instance of Outlook application"
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def FileName(self):
|
||||||
|
return self.File
|
||||||
|
|
||||||
|
|
||||||
|
class Mail(object):
|
||||||
|
|
||||||
|
#this is just a guess what a Outlook Mail Object Probably returns
|
||||||
|
#Class = client.constants.olMail
|
||||||
|
|
||||||
|
def __init__(self, subj="", sendName="", to="", body="", **kw):
|
||||||
|
self.Class = client.constants.olMail
|
||||||
|
self.Subject = subj
|
||||||
|
self.SenderName = sendName
|
||||||
|
self.To = to
|
||||||
|
self.Body = body
|
||||||
|
for k, v in kw.items():
|
||||||
|
setattr(self, k, v)
|
||||||
|
|
||||||
|
def addAttachment(self, **kw):
|
||||||
|
"""
|
||||||
|
this is a method which probably does not exist in a real mail
|
||||||
|
Currently this is a work around to add attachments to a mail
|
||||||
|
"""
|
||||||
|
for k, v in kw.items():
|
||||||
|
setattr(self, k, v)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _prop_map_get_(self):
|
||||||
|
#here it is necessary of what attributes (called keys in outlok.py)
|
||||||
|
#an Outlook Mail typically has
|
||||||
|
return self.__dict__
|
||||||
|
|
||||||
|
|
||||||
|
class Items(object):
|
||||||
|
|
||||||
|
temp = {}
|
||||||
|
data = []
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.data.append(Mail(subj="Python Training",
|
||||||
|
sendName="Mark Pilgrim",
|
||||||
|
to="allPythonics@python.org",
|
||||||
|
body="The training will take place on Wed, 21st Dec.\
|
||||||
|
Kindly check the enclosed invitation.",
|
||||||
|
BodyFormat=1
|
||||||
|
))
|
||||||
|
self.data[0].addAttachment(Attachments=Attachments([("Invitation.pdf", self.data[0]), ("21.pdf", self.data[0])]))
|
||||||
|
self.data.append(Mail(subj="Information Technolgies Inc. Test it!",
|
||||||
|
sendName="IT.org",
|
||||||
|
to="allUser@internet.com",
|
||||||
|
BodyFormat=2,
|
||||||
|
HTMLBody="<html>\
|
||||||
|
<head>\
|
||||||
|
<title>Test-HTML-Mail</title>\
|
||||||
|
</head>\
|
||||||
|
<body>\
|
||||||
|
<h1>Das ist eine HTML-Mail</h1>\
|
||||||
|
<div align='center'>Hier steht \
|
||||||
|
<b>Beispiel</b>-Text</div>\
|
||||||
|
</body>\
|
||||||
|
</html>",
|
||||||
|
SentOn="21.04.07"
|
||||||
|
))
|
||||||
|
self.data.append(Mail(subj="@ Product Details @",
|
||||||
|
sendName="",
|
||||||
|
senderEmailAddress="custominfo@enterprise.com",
|
||||||
|
to="recipient1@mail.com, customer@web.de",
|
||||||
|
BodyFormat=1,
|
||||||
|
body="Dear customer,\
|
||||||
|
Hereby we submit you the information you ordered.\
|
||||||
|
Please feel free to ask anytime you want.\
|
||||||
|
Sincerely, Customer Support",
|
||||||
|
SentOn="30.07.07"
|
||||||
|
))
|
||||||
|
|
||||||
|
def Item(self, idx):
|
||||||
|
return self.data[idx-1]
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.data)
|
||||||
|
|
||||||
|
|
||||||
|
class OutlookFolder(object):
|
||||||
|
|
||||||
|
# Folders defines in Outlook the sub folders under the "Main" Folder
|
||||||
|
Folders = None
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
print "collecting Mails from folder"
|
||||||
|
self.Items = Items()
|
||||||
|
|
||||||
|
|
||||||
|
class OutlookNamespace(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def GetDefaultFolder(self, message=""):
|
||||||
|
print "retrieving Outlook default folder"
|
||||||
|
folder = OutlookFolder()
|
||||||
|
return folder
|
||||||
|
|
||||||
|
|
||||||
|
class OutlookApp(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def GetNamespace(self, message=""):
|
||||||
|
print "Namespace " + message + " retrieved"
|
||||||
|
oNamespace = OutlookNamespace()
|
||||||
|
return oNamespace
|
||||||
|
|
||||||
|
|
||||||
|
class Message(object):
|
||||||
|
|
||||||
|
olFolderInbox = None
|
||||||
|
# esp. for olMail, for further dummy implementations it is necessary
|
||||||
|
# to find out, what class is expected. Meaning what type of object has
|
||||||
|
# to be faked and what attributes it has. see outlook.py
|
||||||
|
# loadMailsfromFolder
|
||||||
|
olMail = Mail
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def EnsureDispatch(self, message=""):
|
||||||
|
print message + " retrieved"
|
||||||
|
oApp = OutlookApp()
|
||||||
|
return oApp
|
||||||
|
|
||||||
|
|
||||||
|
class client(object):
|
||||||
|
|
||||||
|
gencache = Message()
|
||||||
|
constants = Message()
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ctypes(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
74
agent/tests.py
Executable file
74
agent/tests.py
Executable file
|
@ -0,0 +1,74 @@
|
||||||
|
#! /usr/bin/env python
|
||||||
|
|
||||||
|
# $Id$
|
||||||
|
|
||||||
|
import os, time
|
||||||
|
import unittest, doctest
|
||||||
|
from zope.testing.doctestunit import DocFileSuite
|
||||||
|
from twisted.internet import reactor
|
||||||
|
#from twisted.internet.defer import Deferred
|
||||||
|
#from twisted.trial import unittest as trial_unittest
|
||||||
|
|
||||||
|
baseDir = os.path.dirname(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
class Tester(object):
|
||||||
|
""" Used for controlled execution of reactor iteration cycles.
|
||||||
|
"""
|
||||||
|
|
||||||
|
stopped = False
|
||||||
|
|
||||||
|
def iterate(self, n=10, delay=0):
|
||||||
|
self.stopped = False
|
||||||
|
for i in range(n):
|
||||||
|
if self.stopped:
|
||||||
|
return
|
||||||
|
reactor.iterate(delay)
|
||||||
|
|
||||||
|
def run(self, maxduration=1.0, delay=0):
|
||||||
|
self.stopped = False
|
||||||
|
end = time.time() + maxduration
|
||||||
|
while not self.stopped:
|
||||||
|
reactor.iterate(delay)
|
||||||
|
if time.time() >= end:
|
||||||
|
return
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.stopped = True
|
||||||
|
|
||||||
|
def stopThreads(self):
|
||||||
|
reactor.threadpool.stop()
|
||||||
|
reactor.threadpool = None
|
||||||
|
|
||||||
|
tester = Tester()
|
||||||
|
|
||||||
|
|
||||||
|
class Test(unittest.TestCase):
|
||||||
|
"Basic tests for the cybertools.agent package."
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def testBasicStuff(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_suite():
|
||||||
|
flags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
|
||||||
|
testSuite = unittest.TestSuite((
|
||||||
|
unittest.makeSuite(Test),
|
||||||
|
DocFileSuite('README.txt', optionflags=flags),
|
||||||
|
DocFileSuite('crawl/README.txt', optionflags=flags),
|
||||||
|
DocFileSuite('crawl/filesystem.txt', optionflags=flags),
|
||||||
|
DocFileSuite('crawl/outlook.txt', optionflags=flags),
|
||||||
|
DocFileSuite('transport/transporter.txt', optionflags=flags),
|
||||||
|
DocFileSuite('talk/README.txt', optionflags=flags),
|
||||||
|
))
|
||||||
|
return testSuite
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
standard_unittest.main(defaultTest='test_suite')
|
160
agent/transport/file/sftp.py
Normal file
160
agent/transport/file/sftp.py
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Transferring files to a remote site via SFTP.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
from twisted.conch.ssh import channel, common, connection
|
||||||
|
from twisted.conch.ssh import filetransfer, transport, userauth
|
||||||
|
from twisted.internet import defer, protocol, reactor
|
||||||
|
|
||||||
|
CHUNKSIZE = 4096
|
||||||
|
|
||||||
|
|
||||||
|
class FileTransfer(protocol.ClientFactory):
|
||||||
|
""" Transfers files to a remote SCP/SFTP server.
|
||||||
|
"""
|
||||||
|
channel = None
|
||||||
|
|
||||||
|
def __init__(self, host, port, username, password):
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
self.queue = []
|
||||||
|
reactor.connectTCP(host, port, self)
|
||||||
|
|
||||||
|
def buildProtocol(self, addr):
|
||||||
|
protocol = self.protocol = ClientTransport(self)
|
||||||
|
return protocol
|
||||||
|
|
||||||
|
def upload(self, localPath, remotePath):
|
||||||
|
""" Copies a file, returning a deferred.
|
||||||
|
"""
|
||||||
|
d = self.deferred = defer.Deferred()
|
||||||
|
# we put everything in a queue so that more than one file may
|
||||||
|
# be transferred in one connection.
|
||||||
|
self.queue.append(dict(deferred=d,
|
||||||
|
command='upload',
|
||||||
|
localPath=localPath,
|
||||||
|
remotePath=remotePath))
|
||||||
|
if len(self.queue) == 1 and self.channel is not None:
|
||||||
|
# the channel has emptied the queue
|
||||||
|
self.channel.execute()
|
||||||
|
return d
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
# TODO: put in queue...
|
||||||
|
self.protocol.transport.loseConnection()
|
||||||
|
print 'connection closed'
|
||||||
|
|
||||||
|
|
||||||
|
class SFTPChannel(channel.SSHChannel):
|
||||||
|
""" An SSH channel using the SFTP subsystem for transferring files
|
||||||
|
and issuing other filesystem requests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = 'session'
|
||||||
|
remFile = ''
|
||||||
|
remOffset = 0
|
||||||
|
|
||||||
|
def channelOpen(self, data):
|
||||||
|
d = self.conn.sendRequest(self, 'subsystem', common.NS('sftp'), wantReply=1)
|
||||||
|
d.addCallback(self.channelOpened)
|
||||||
|
|
||||||
|
def channelOpened(self, data):
|
||||||
|
self.client = filetransfer.FileTransferClient()
|
||||||
|
self.client.makeConnection(self)
|
||||||
|
self.dataReceived = self.client.dataReceived
|
||||||
|
self.execute()
|
||||||
|
self.conn.factory.channel = self
|
||||||
|
|
||||||
|
def execute(self):
|
||||||
|
queue = self.conn.factory.queue
|
||||||
|
if queue:
|
||||||
|
command = queue.pop()
|
||||||
|
commandName = command.pop('command')
|
||||||
|
method = getattr(self, 'command_' + commandName, None)
|
||||||
|
if method is not None:
|
||||||
|
self.params = command
|
||||||
|
method()
|
||||||
|
|
||||||
|
def command_upload(self):
|
||||||
|
params = self.params
|
||||||
|
remotePath = params['remotePath']
|
||||||
|
localPath = params['localPath']
|
||||||
|
self.localFile = open(localPath, 'rb')
|
||||||
|
d = self.client.openFile(remotePath,
|
||||||
|
filetransfer.FXF_WRITE | filetransfer.FXF_CREAT, {})
|
||||||
|
d.addCallbacks(self.writeChunk, self.logError)
|
||||||
|
|
||||||
|
def writeChunk(self, remoteFile):
|
||||||
|
if isinstance(remoteFile, tuple) == False:
|
||||||
|
self.remFile = remoteFile
|
||||||
|
data = self.localFile.read(CHUNKSIZE)
|
||||||
|
if len(data) < CHUNKSIZE:
|
||||||
|
self.d = self.remFile.writeChunk(self.remOffset, data)
|
||||||
|
self.d.addCallbacks(self.finished, self.logError)
|
||||||
|
else:
|
||||||
|
self.d = self.remFile.writeChunk(self.remOffset, data)
|
||||||
|
self.remOffset = self.remOffset + CHUNKSIZE
|
||||||
|
self.d.addCallbacks(self.writeChunk, self.logError)
|
||||||
|
|
||||||
|
def logError(self, reason):
|
||||||
|
print 'error', reason
|
||||||
|
|
||||||
|
def finished(self, result):
|
||||||
|
self.localFile.close()
|
||||||
|
self.remFile.close()
|
||||||
|
#self.d.callback('finished')
|
||||||
|
self.conn.factory.deferred.callback('finished')
|
||||||
|
|
||||||
|
# classes for managing the SSH protocol and connection
|
||||||
|
|
||||||
|
class ClientTransport(transport.SSHClientTransport):
|
||||||
|
|
||||||
|
def __init__(self, factory):
|
||||||
|
self.factory = factory
|
||||||
|
|
||||||
|
def verifyHostKey(self, pubKey, fingerprint):
|
||||||
|
# this is insecure!!!
|
||||||
|
return defer.succeed(True)
|
||||||
|
|
||||||
|
def connectionSecure(self):
|
||||||
|
self.requestService(UserAuth(self.factory, ClientConnection(self.factory)))
|
||||||
|
|
||||||
|
|
||||||
|
class ClientConnection(connection.SSHConnection):
|
||||||
|
|
||||||
|
def __init__(self, factory):
|
||||||
|
connection.SSHConnection.__init__(self)
|
||||||
|
self.factory = factory
|
||||||
|
|
||||||
|
def serviceStarted(self):
|
||||||
|
self.openChannel(SFTPChannel(conn=self))
|
||||||
|
|
||||||
|
|
||||||
|
class UserAuth(userauth.SSHUserAuthClient):
|
||||||
|
|
||||||
|
def __init__(self, factory, connection):
|
||||||
|
userauth.SSHUserAuthClient.__init__(self, factory.username, connection)
|
||||||
|
self.password = factory.password
|
||||||
|
|
||||||
|
def getPassword(self, prompt=None):
|
||||||
|
return defer.succeed(self.password)
|
27
agent/transport/loops.py
Normal file
27
agent/transport/loops.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Transferring information to a loops site on a local Zope instance.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
from zope.interface import implements
|
||||||
|
|
||||||
|
|
92
agent/transport/remote.py
Normal file
92
agent/transport/remote.py
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Transferring information to or requesting information from a remote
|
||||||
|
cybertools.agent instance by transferring files to the remote system
|
||||||
|
and sending requests to a corresponding remote controller.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
from twisted.internet import defer
|
||||||
|
from zope.interface import implements
|
||||||
|
import os
|
||||||
|
|
||||||
|
from cybertools.agent.system import xmlrpc
|
||||||
|
from cybertools.agent.system import sftp
|
||||||
|
from cybertools.agent.base.agent import Master
|
||||||
|
from cybertools.agent.core.agent import QueueableAgent
|
||||||
|
from cybertools.agent.interfaces import ITransporter
|
||||||
|
from cybertools.agent.crawl.base import Metadata
|
||||||
|
from cybertools.agent.crawl.mail import MailResource
|
||||||
|
from cybertools.agent.crawl.filesystem import FileResource
|
||||||
|
from cybertools.agent.components import agents
|
||||||
|
from cybertools.util.config import Configurator
|
||||||
|
|
||||||
|
|
||||||
|
class Transporter(QueueableAgent):
|
||||||
|
|
||||||
|
implements(ITransporter)
|
||||||
|
|
||||||
|
port = 22
|
||||||
|
machineName = ''
|
||||||
|
|
||||||
|
def __init__(self, master):
|
||||||
|
super(Transporter, self).__init__(master)
|
||||||
|
config = master.config
|
||||||
|
serverURL = config.transport.remote.url
|
||||||
|
self.server = xmlrpc.xmlrpc.Proxy(serverURL)
|
||||||
|
userName = config.transport.remote.ftp.user
|
||||||
|
password = config.transport.remote.ftp.password
|
||||||
|
host = config.transport.remote.ftp.url
|
||||||
|
self.ftpServer = sftp.FileTransfer(host, self.port, userName, password)
|
||||||
|
|
||||||
|
def process(self):
|
||||||
|
return self.transfer(self.params['resource'])
|
||||||
|
|
||||||
|
def transfer(self, resource):
|
||||||
|
""" Transfer the resource (an object providing IResource)
|
||||||
|
to the server and return a Deferred.
|
||||||
|
"""
|
||||||
|
self.deferred = defer.Deferred()
|
||||||
|
remoteFile = os.path.basename(resource.path)
|
||||||
|
d = self.ftpServer.upload(resource.path, remoteFile)
|
||||||
|
d.addErrback(self.errorHandler)
|
||||||
|
d.addCallback(lambda result:
|
||||||
|
self.server.callRemote('getMetadata', dict(resource.metadata)))
|
||||||
|
d.addCallback(self.transferDone)
|
||||||
|
return self.deferred
|
||||||
|
|
||||||
|
def errorHandler(self, errorInfo):
|
||||||
|
"""
|
||||||
|
Invoked as a callback from self.transfer
|
||||||
|
Error handler.
|
||||||
|
"""
|
||||||
|
print errorInfo
|
||||||
|
#self.server.close()
|
||||||
|
|
||||||
|
def transferDone(self, result):
|
||||||
|
"""
|
||||||
|
Invoked as a callback from self.transfer
|
||||||
|
This callback method is called when resource and metadata
|
||||||
|
have been transferred successfully.
|
||||||
|
"""
|
||||||
|
self.deferred.callback(result)
|
||||||
|
|
||||||
|
agents.register(Transporter, Master, name='transport.remote')
|
52
agent/transport/transporter.txt
Normal file
52
agent/transport/transporter.txt
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
================================================
|
||||||
|
Agents for Job Execution and Communication Tasks
|
||||||
|
================================================
|
||||||
|
|
||||||
|
($Id$)
|
||||||
|
|
||||||
|
>>> config = '''
|
||||||
|
... controller(names=['core.sample'])
|
||||||
|
... scheduler(name='core')
|
||||||
|
... logger(name='default', standard=30)
|
||||||
|
... transport.remote.server = 'testing'
|
||||||
|
... transport.remote.sftp = 'testing'
|
||||||
|
... transport.remote.url = 'http://localhost:8123'
|
||||||
|
... '''
|
||||||
|
>>> from cybertools.agent.main import setup
|
||||||
|
>>> master = setup(config)
|
||||||
|
Starting agent application...
|
||||||
|
Using controllers core.sample.
|
||||||
|
|
||||||
|
|
||||||
|
Transporter
|
||||||
|
===========
|
||||||
|
|
||||||
|
The agent uses Twisted's cooperative multitasking model.
|
||||||
|
|
||||||
|
The Transporter is used to contact an xmlrpc Server and transmit the metadata
|
||||||
|
to the other loops system. The Transporter is derived from Queueable agent
|
||||||
|
to ensure that only one item at a time is transmitted.
|
||||||
|
|
||||||
|
Returns a deferred that must be supplied with a callback method (and in
|
||||||
|
most cases also an errback method).
|
||||||
|
|
||||||
|
This Testcase is using subsidiary methods to simulate a real xmlrpc server.
|
||||||
|
|
||||||
|
>>> controller = master.controllers[0]
|
||||||
|
>>> controller.createAgent('transport.remote', 'sample03')
|
||||||
|
|
||||||
|
In the next step we request the start of a job, again via the controller.
|
||||||
|
|
||||||
|
>>> from cybertools.agent.crawl.base import Metadata, Resource
|
||||||
|
>>> md01 = Metadata(dict(filename='dummy.txt'))
|
||||||
|
>>> r01 = Resource()
|
||||||
|
>>> r01.metadata = md01
|
||||||
|
>>> r01.path = 'resource.txt'
|
||||||
|
>>> controller.enterJob('sample', 'sample03', params=dict(resource=r01))
|
||||||
|
|
||||||
|
The job is not executed immediately - we have to hand over control to
|
||||||
|
the twisted reactor first.
|
||||||
|
|
||||||
|
>>> from cybertools.agent.tests import tester
|
||||||
|
>>> tester.iterate()
|
||||||
|
Job 00001 completed; result: Metadata accepted by server;
|
368
agent/util/task.py
Normal file
368
agent/util/task.py
Normal file
|
@ -0,0 +1,368 @@
|
||||||
|
# -*- test-case-name: twisted.test.test_task -*-
|
||||||
|
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
|
||||||
|
# See LICENSE for details.
|
||||||
|
|
||||||
|
|
||||||
|
"""Scheduling utility methods and classes.
|
||||||
|
|
||||||
|
API Stability: Unstable
|
||||||
|
|
||||||
|
@author: U{Jp Calderone<mailto:exarkun@twistedmatrix.com>}
|
||||||
|
"""
|
||||||
|
|
||||||
|
__metaclass__ = type
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
from twisted.python.runtime import seconds
|
||||||
|
from twisted.python import reflect
|
||||||
|
|
||||||
|
from twisted.internet import base, defer
|
||||||
|
|
||||||
|
|
||||||
|
class LoopingCall:
|
||||||
|
"""Call a function repeatedly.
|
||||||
|
|
||||||
|
@ivar f: The function to call.
|
||||||
|
@ivar a: A tuple of arguments to pass the function.
|
||||||
|
@ivar kw: A dictionary of keyword arguments to pass to the function.
|
||||||
|
|
||||||
|
If C{f} returns a deferred, rescheduling will not take place until the
|
||||||
|
deferred has fired. The result value is ignored.
|
||||||
|
"""
|
||||||
|
|
||||||
|
call = None
|
||||||
|
running = False
|
||||||
|
deferred = None
|
||||||
|
interval = None
|
||||||
|
count = None
|
||||||
|
starttime = None
|
||||||
|
|
||||||
|
def _callLater(self, delay):
|
||||||
|
from twisted.internet import reactor
|
||||||
|
return reactor.callLater(delay, self)
|
||||||
|
|
||||||
|
_seconds = staticmethod(seconds)
|
||||||
|
|
||||||
|
def __init__(self, f, *a, **kw):
|
||||||
|
self.f = f
|
||||||
|
self.a = a
|
||||||
|
self.kw = kw
|
||||||
|
|
||||||
|
def start(self, interval, now=True):
|
||||||
|
"""Start running function every interval seconds.
|
||||||
|
|
||||||
|
@param interval: The number of seconds between calls. May be
|
||||||
|
less than one. Precision will depend on the underlying
|
||||||
|
platform, the available hardware, and the load on the system.
|
||||||
|
|
||||||
|
@param now: If True, run this call right now. Otherwise, wait
|
||||||
|
until the interval has elapsed before beginning.
|
||||||
|
|
||||||
|
@return: A Deferred whose callback will be invoked with
|
||||||
|
C{self} when C{self.stop} is called, or whose errback will be
|
||||||
|
invoked when the function raises an exception or returned a
|
||||||
|
deferred that has its errback invoked.
|
||||||
|
"""
|
||||||
|
assert not self.running, ("Tried to start an already running "
|
||||||
|
"LoopingCall.")
|
||||||
|
if interval < 0:
|
||||||
|
raise ValueError, "interval must be >= 0"
|
||||||
|
self.running = True
|
||||||
|
d = self.deferred = defer.Deferred()
|
||||||
|
self.starttime = self._seconds()
|
||||||
|
self.count = 0
|
||||||
|
self.interval = interval
|
||||||
|
if now:
|
||||||
|
self()
|
||||||
|
else:
|
||||||
|
self._reschedule()
|
||||||
|
return d
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Stop running function.
|
||||||
|
"""
|
||||||
|
assert self.running, ("Tried to stop a LoopingCall that was "
|
||||||
|
"not running.")
|
||||||
|
self.running = False
|
||||||
|
if self.call is not None:
|
||||||
|
self.call.cancel()
|
||||||
|
self.call = None
|
||||||
|
d, self.deferred = self.deferred, None
|
||||||
|
d.callback(self)
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
def cb(result):
|
||||||
|
if self.running:
|
||||||
|
self._reschedule()
|
||||||
|
else:
|
||||||
|
d, self.deferred = self.deferred, None
|
||||||
|
d.callback(self)
|
||||||
|
|
||||||
|
def eb(failure):
|
||||||
|
self.running = False
|
||||||
|
d, self.deferred = self.deferred, None
|
||||||
|
d.errback(failure)
|
||||||
|
|
||||||
|
self.call = None
|
||||||
|
d = defer.maybeDeferred(self.f, *self.a, **self.kw)
|
||||||
|
d.addCallback(cb)
|
||||||
|
d.addErrback(eb)
|
||||||
|
|
||||||
|
def _reschedule(self):
|
||||||
|
if self.interval == 0:
|
||||||
|
self.call = self._callLater(0)
|
||||||
|
return
|
||||||
|
|
||||||
|
fromNow = self.starttime - self._seconds()
|
||||||
|
|
||||||
|
while self.running:
|
||||||
|
self.count += 1
|
||||||
|
fromStart = self.count * self.interval
|
||||||
|
delay = fromNow + fromStart
|
||||||
|
if delay > 0:
|
||||||
|
self.call = self._callLater(delay)
|
||||||
|
return
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
if hasattr(self.f, 'func_name'):
|
||||||
|
func = self.f.func_name
|
||||||
|
if hasattr(self.f, 'im_class'):
|
||||||
|
func = self.f.im_class.__name__ + '.' + func
|
||||||
|
else:
|
||||||
|
func = reflect.safe_repr(self.f)
|
||||||
|
|
||||||
|
return 'LoopingCall<%r>(%s, *%s, **%s)' % (
|
||||||
|
self.interval, func, reflect.safe_repr(self.a),
|
||||||
|
reflect.safe_repr(self.kw))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class SchedulerStopped(Exception):
|
||||||
|
"""
|
||||||
|
The operation could not complete because the scheduler was stopped in
|
||||||
|
progress or was already stopped.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class _Timer(object):
|
||||||
|
MAX_SLICE = 0.01
|
||||||
|
def __init__(self):
|
||||||
|
self.end = time.time() + self.MAX_SLICE
|
||||||
|
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
return time.time() >= self.end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_EPSILON = 0.00000001
|
||||||
|
def _defaultScheduler(x):
|
||||||
|
from twisted.internet import reactor
|
||||||
|
return reactor.callLater(_EPSILON, x)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Cooperator(object):
|
||||||
|
"""
|
||||||
|
Cooperative task scheduler.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
terminationPredicateFactory=_Timer,
|
||||||
|
scheduler=_defaultScheduler,
|
||||||
|
started=True):
|
||||||
|
"""
|
||||||
|
Create a scheduler-like object to which iterators may be added.
|
||||||
|
|
||||||
|
@param terminationPredicateFactory: A no-argument callable which will
|
||||||
|
be invoked at the beginning of each step and should return a
|
||||||
|
no-argument callable which will return False when the step should be
|
||||||
|
terminated. The default factory is time-based and allows iterators to
|
||||||
|
run for 1/100th of a second at a time.
|
||||||
|
|
||||||
|
@param scheduler: A one-argument callable which takes a no-argument
|
||||||
|
callable and should invoke it at some future point. This will be used
|
||||||
|
to schedule each step of this Cooperator.
|
||||||
|
|
||||||
|
@param started: A boolean which indicates whether iterators should be
|
||||||
|
stepped as soon as they are added, or if they will be queued up until
|
||||||
|
L{Cooperator.start} is called.
|
||||||
|
"""
|
||||||
|
self.iterators = []
|
||||||
|
self._metarator = iter(())
|
||||||
|
self._terminationPredicateFactory = terminationPredicateFactory
|
||||||
|
self._scheduler = scheduler
|
||||||
|
self._delayedCall = None
|
||||||
|
self._stopped = False
|
||||||
|
self._started = started
|
||||||
|
|
||||||
|
|
||||||
|
def coiterate(self, iterator, doneDeferred=None):
|
||||||
|
"""
|
||||||
|
Add an iterator to the list of iterators I am currently running.
|
||||||
|
|
||||||
|
@return: a Deferred that will fire when the iterator finishes.
|
||||||
|
"""
|
||||||
|
if doneDeferred is None:
|
||||||
|
doneDeferred = defer.Deferred()
|
||||||
|
if self._stopped:
|
||||||
|
doneDeferred.errback(SchedulerStopped())
|
||||||
|
return doneDeferred
|
||||||
|
self.iterators.append((iterator, doneDeferred))
|
||||||
|
self._reschedule()
|
||||||
|
return doneDeferred
|
||||||
|
|
||||||
|
|
||||||
|
def _tasks(self):
|
||||||
|
terminator = self._terminationPredicateFactory()
|
||||||
|
while self.iterators:
|
||||||
|
for i in self._metarator:
|
||||||
|
yield i
|
||||||
|
if terminator():
|
||||||
|
return
|
||||||
|
self._metarator = iter(self.iterators)
|
||||||
|
|
||||||
|
|
||||||
|
def _tick(self):
|
||||||
|
"""
|
||||||
|
Run one scheduler tick.
|
||||||
|
"""
|
||||||
|
self._delayedCall = None
|
||||||
|
for taskObj in self._tasks():
|
||||||
|
iterator, doneDeferred = taskObj
|
||||||
|
try:
|
||||||
|
result = iterator.next()
|
||||||
|
except StopIteration:
|
||||||
|
self.iterators.remove(taskObj)
|
||||||
|
doneDeferred.callback(iterator)
|
||||||
|
except:
|
||||||
|
self.iterators.remove(taskObj)
|
||||||
|
doneDeferred.errback()
|
||||||
|
else:
|
||||||
|
if isinstance(result, defer.Deferred):
|
||||||
|
self.iterators.remove(taskObj)
|
||||||
|
def cbContinue(result, taskObj=taskObj):
|
||||||
|
self.coiterate(*taskObj)
|
||||||
|
result.addCallbacks(cbContinue, doneDeferred.errback)
|
||||||
|
self._reschedule()
|
||||||
|
|
||||||
|
|
||||||
|
_mustScheduleOnStart = False
|
||||||
|
def _reschedule(self):
|
||||||
|
if not self._started:
|
||||||
|
self._mustScheduleOnStart = True
|
||||||
|
return
|
||||||
|
if self._delayedCall is None and self.iterators:
|
||||||
|
self._delayedCall = self._scheduler(self._tick)
|
||||||
|
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""
|
||||||
|
Begin scheduling steps.
|
||||||
|
"""
|
||||||
|
self._stopped = False
|
||||||
|
self._started = True
|
||||||
|
if self._mustScheduleOnStart:
|
||||||
|
del self._mustScheduleOnStart
|
||||||
|
self._reschedule()
|
||||||
|
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""
|
||||||
|
Stop scheduling steps. Errback the completion Deferreds of all
|
||||||
|
iterators which have been added and forget about them.
|
||||||
|
"""
|
||||||
|
self._stopped = True
|
||||||
|
for iterator, doneDeferred in self.iterators:
|
||||||
|
doneDeferred.errback(SchedulerStopped())
|
||||||
|
self.iterators = []
|
||||||
|
if self._delayedCall is not None:
|
||||||
|
self._delayedCall.cancel()
|
||||||
|
self._delayedCall = None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
_theCooperator = Cooperator()
|
||||||
|
def coiterate(iterator):
|
||||||
|
"""
|
||||||
|
Cooperatively iterate over the given iterator, dividing runtime between it
|
||||||
|
and all other iterators which have been passed to this function and not yet
|
||||||
|
exhausted.
|
||||||
|
"""
|
||||||
|
return _theCooperator.coiterate(iterator)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Clock:
|
||||||
|
"""
|
||||||
|
Provide a deterministic, easily-controlled implementation of
|
||||||
|
L{IReactorTime.callLater}. This is commonly useful for writing
|
||||||
|
deterministic unit tests for code which schedules events using this API.
|
||||||
|
"""
|
||||||
|
rightNow = 0.0
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.calls = []
|
||||||
|
|
||||||
|
def seconds(self):
|
||||||
|
"""
|
||||||
|
Pretend to be time.time(). This is used internally when an operation
|
||||||
|
such as L{IDelayedCall.reset} needs to determine a a time value
|
||||||
|
relative to the current time.
|
||||||
|
|
||||||
|
@rtype: C{float}
|
||||||
|
@return: The time which should be considered the current time.
|
||||||
|
"""
|
||||||
|
return self.rightNow
|
||||||
|
|
||||||
|
|
||||||
|
def callLater(self, when, what, *a, **kw):
|
||||||
|
"""
|
||||||
|
See L{twisted.internet.interfaces.IReactorTime.callLater}.
|
||||||
|
"""
|
||||||
|
self.calls.append(
|
||||||
|
base.DelayedCall(self.seconds() + when,
|
||||||
|
what, a, kw,
|
||||||
|
self.calls.remove,
|
||||||
|
lambda c: None,
|
||||||
|
self.seconds))
|
||||||
|
self.calls.sort(lambda a, b: cmp(a.getTime(), b.getTime()))
|
||||||
|
return self.calls[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def advance(self, amount):
|
||||||
|
"""
|
||||||
|
Move time on this clock forward by the given amount and run whatever
|
||||||
|
pending calls should be run.
|
||||||
|
|
||||||
|
@type amount: C{float}
|
||||||
|
@param amount: The number of seconds which to advance this clock's
|
||||||
|
time.
|
||||||
|
"""
|
||||||
|
self.rightNow += amount
|
||||||
|
while self.calls and self.calls[0].getTime() <= self.seconds():
|
||||||
|
call = self.calls.pop(0)
|
||||||
|
call.called = 1
|
||||||
|
call.func(*call.args, **call.kw)
|
||||||
|
|
||||||
|
|
||||||
|
def pump(self, timings):
|
||||||
|
"""
|
||||||
|
Advance incrementally by the given set of times.
|
||||||
|
|
||||||
|
@type timings: iterable of C{float}
|
||||||
|
"""
|
||||||
|
for amount in timings:
|
||||||
|
self.advance(amount)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'LoopingCall',
|
||||||
|
|
||||||
|
'Clock',
|
||||||
|
|
||||||
|
'SchedulerStopped', 'Cooperator', 'coiterate',
|
||||||
|
]
|
|
@ -1,10 +1,28 @@
|
||||||
# cybertools.ajax.dojo.layout
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz - helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
"""
|
||||||
|
Embed Dojo using the cybertools.composer.layout procedure.
|
||||||
|
|
||||||
""" Embed Dojo using the cybertools.composer.layout procedure.
|
$Id$
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from io import StringIO
|
from cStringIO import StringIO
|
||||||
from zope.browserpage import ViewPageTemplateFile
|
from zope.app.pagetemplate import ViewPageTemplateFile
|
||||||
from zope.cachedescriptors.property import Lazy
|
from zope.cachedescriptors.property import Lazy
|
||||||
|
|
||||||
from cybertools.browser.renderer import RendererFactory
|
from cybertools.browser.renderer import RendererFactory
|
|
@ -1,18 +1,38 @@
|
||||||
# cybertools.brain.neuron
|
#
|
||||||
|
# Copyright (c) 2006 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
""" A simple basic implementation of Neuron and Synapsis.
|
"""
|
||||||
|
A simple basic implementation of Neuron and Synapsis.
|
||||||
|
|
||||||
|
$Id$
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from zope.interface import implementer
|
from zope.interface import implements
|
||||||
from cybertools.brain.interfaces import INeuron, ISynapsis
|
from cybertools.brain.interfaces import INeuron, ISynapsis
|
||||||
from cybertools.brain.state import State, Transition
|
from cybertools.brain.state import State, Transition
|
||||||
|
|
||||||
|
|
||||||
@implementer(ISynapsis)
|
|
||||||
class Synapsis(object):
|
class Synapsis(object):
|
||||||
""" A synapsis connects two neurons.
|
""" A synapsis connects two neurons.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
implements(ISynapsis)
|
||||||
|
|
||||||
def __init__(self, sender, receiver):
|
def __init__(self, sender, receiver):
|
||||||
self.sender = sender
|
self.sender = sender
|
||||||
sender.receivers.append(self)
|
sender.receivers.append(self)
|
||||||
|
@ -26,9 +46,10 @@ class Synapsis(object):
|
||||||
receiver.notify(session)
|
receiver.notify(session)
|
||||||
|
|
||||||
|
|
||||||
@implementer(INeuron)
|
|
||||||
class Neuron(object):
|
class Neuron(object):
|
||||||
|
|
||||||
|
implements(INeuron)
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.senders = []
|
self.senders = []
|
||||||
self.receivers = []
|
self.receivers = []
|
41
brain/session.py
Normal file
41
brain/session.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2006 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Transaction management.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
from zope.interface import implements
|
||||||
|
from cybertools.brain.interfaces import ISession
|
||||||
|
|
||||||
|
|
||||||
|
class Session(object):
|
||||||
|
|
||||||
|
implements(ISession)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.states = {}
|
||||||
|
|
||||||
|
def setState(self, neuron, state):
|
||||||
|
self.states[neuron] = state
|
||||||
|
|
||||||
|
def getState(self, neuron):
|
||||||
|
return self.states.get(neuron, neuron.state)
|
||||||
|
|
55
brain/state.py
Normal file
55
brain/state.py
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
#
|
||||||
|
# Copyright (c) 2006 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Base classes for state and state manipulations using a float-based state.
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
"""
|
||||||
|
|
||||||
|
from zope.interface import implements
|
||||||
|
from cybertools.brain.interfaces import IState, ITransition
|
||||||
|
|
||||||
|
|
||||||
|
class State(object):
|
||||||
|
""" The state of a neuron.
|
||||||
|
"""
|
||||||
|
|
||||||
|
implements(IState)
|
||||||
|
|
||||||
|
def __init__(self, value=0.0):
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<State %0.1f>' % self.value
|
||||||
|
|
||||||
|
|
||||||
|
class Transition(object):
|
||||||
|
|
||||||
|
implements(ITransition)
|
||||||
|
|
||||||
|
def __init__(self, synapsis, factor=1.0):
|
||||||
|
self.synapsis = synapsis
|
||||||
|
self.factor = factor
|
||||||
|
|
||||||
|
def execute(self, session=None):
|
||||||
|
oldState = self.synapsis.receiver.getState(session)
|
||||||
|
senderState = self.synapsis.sender.getState(session)
|
||||||
|
return State(oldState.value + senderState.value * self.factor)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# cybertools.brain.tests
|
# $Id$
|
||||||
|
|
||||||
import unittest, doctest
|
import unittest, doctest
|
||||||
|
from zope.testing.doctestunit import DocFileSuite
|
||||||
from zope.interface.verify import verifyClass
|
from zope.interface.verify import verifyClass
|
||||||
|
|
||||||
from cybertools.brain.interfaces import INeuron, ISynapsis
|
from cybertools.brain.interfaces import INeuron, ISynapsis
|
||||||
|
@ -19,8 +20,9 @@ class TestBrain(unittest.TestCase):
|
||||||
def test_suite():
|
def test_suite():
|
||||||
flags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
|
flags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
|
||||||
return unittest.TestSuite((
|
return unittest.TestSuite((
|
||||||
unittest.TestLoader().loadTestsFromTestCase(TestBrain),
|
unittest.makeSuite(TestBrain),
|
||||||
doctest.DocFileSuite('README.txt', optionflags=flags,),
|
DocFileSuite('README.txt',
|
||||||
|
optionflags=flags,),
|
||||||
))
|
))
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
|
@ -3,7 +3,7 @@ Browser View Tools
|
||||||
==================
|
==================
|
||||||
|
|
||||||
>>> from zope import component, interface
|
>>> from zope import component, interface
|
||||||
>>> from zope.interface import Interface, implementer
|
>>> from zope.interface import Interface, implements
|
||||||
>>> from zope.publisher.interfaces.browser import IBrowserRequest
|
>>> from zope.publisher.interfaces.browser import IBrowserRequest
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,10 +17,8 @@ the common and node modules there.)
|
||||||
|
|
||||||
Let's start with a dummy content object and create a view on it:
|
Let's start with a dummy content object and create a view on it:
|
||||||
|
|
||||||
>>> #@implementer(Interface)
|
|
||||||
>>> class SomeObject(object):
|
>>> class SomeObject(object):
|
||||||
... pass
|
... implements(Interface)
|
||||||
>>> SomeObject = implementer(Interface)(SomeObject)
|
|
||||||
>>> obj = SomeObject()
|
>>> obj = SomeObject()
|
||||||
|
|
||||||
>>> from cybertools.browser.view import GenericView
|
>>> from cybertools.browser.view import GenericView
|
||||||
|
@ -124,7 +122,7 @@ ZPT macros:
|
||||||
>>> len(cssMacros)
|
>>> len(cssMacros)
|
||||||
4
|
4
|
||||||
>>> m1 = cssMacros[0]
|
>>> m1 = cssMacros[0]
|
||||||
>>> print(m1.name, m1.media, m1.resourceName)
|
>>> print m1.name, m1.media, m1.resourceName
|
||||||
css all zope3_tablelayout.css
|
css all zope3_tablelayout.css
|
||||||
|
|
||||||
Calling a macro provided by Controller.macros[] returns the real ZPT macro:
|
Calling a macro provided by Controller.macros[] returns the real ZPT macro:
|
||||||
|
@ -140,7 +138,7 @@ The pre-set collection of macros for a certain slot may be extended
|
||||||
>>> len(controller.macros['css'])
|
>>> len(controller.macros['css'])
|
||||||
5
|
5
|
||||||
>>> m5 = controller.macros['css'][4]
|
>>> m5 = controller.macros['css'][4]
|
||||||
>>> print(m5.name, m5.media, m5.resourceName)
|
>>> print m5.name, m5.media, m5.resourceName
|
||||||
css all node.css
|
css all node.css
|
||||||
|
|
||||||
If an identifier is given (the second parameter) a certain macro is only
|
If an identifier is given (the second parameter) a certain macro is only
|
||||||
|
@ -223,7 +221,7 @@ controller issues a redirect.
|
||||||
>>> from cybertools.browser.form import IFormController, FormController
|
>>> from cybertools.browser.form import IFormController, FormController
|
||||||
>>> class MyController(FormController):
|
>>> class MyController(FormController):
|
||||||
... def update(self):
|
... def update(self):
|
||||||
... print('updating...')
|
... print 'updating...'
|
||||||
... return True
|
... return True
|
||||||
|
|
||||||
>>> component.provideAdapter(MyController, (View, IBrowserRequest),
|
>>> component.provideAdapter(MyController, (View, IBrowserRequest),
|
|
@ -1,12 +1,31 @@
|
||||||
# cybertools.browser.action
|
#
|
||||||
|
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; either version 2 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
#
|
||||||
|
|
||||||
""" Base classes (sort of views) for action portlet items.
|
"""
|
||||||
|
Base classes (sort of views) for action portlet items.
|
||||||
|
|
||||||
|
$Id$
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from copy import copy
|
from copy import copy
|
||||||
from urllib.parse import urlencode
|
from urllib import urlencode
|
||||||
from zope import component
|
from zope import component
|
||||||
from zope.browserpage import ViewPageTemplateFile
|
from zope.app.pagetemplate import ViewPageTemplateFile
|
||||||
from zope.cachedescriptors.property import Lazy
|
from zope.cachedescriptors.property import Lazy
|
||||||
|
|
||||||
action_macros = ViewPageTemplateFile('action_macros.pt')
|
action_macros = ViewPageTemplateFile('action_macros.pt')
|
Before Width: | Height: | Size: 655 B After Width: | Height: | Size: 655 B |
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue