Hi all, I'm writing a webapp that mails a newsletter to registered users every month. It requires access to the same database that the webapp uses and so I'd like to have it use the same pastedeploy config file where the dsn is defined. I'm currently doing this with a standard configparser but it makes me think that I'm looking at a pattern that is not implemented in paste yet; that is, often there are batch applications that are used with web apps and they should share configuration, transaction support, and possibly other framework details.
I've attached patches to pastescript and pastedeploy to support what I had in mind: a pastedeploy abstract factory for commands that receive the standard config and then do anything they want and a simple entry_point supporting runner command for paster. See the test case in pastedeploy and after patching paste you can run with a command such as $ paster cmd -n batch basic_app.ini After discussing this with Ian on IRC yesterday, he stressed two points: # pastedeploy's existing configparser is brittle and should probably not be reused further. # paste's existing transaction and synchronization support would require a large refactoring to support non-WSGI apps easily. This implies it would be better to make this work as wrapping the command up into a simple WSGI app and running it with an empty request, then discarding the output. I don't really like that idea, and so would like to discuss it further here. Should the patch, as written or improved but keeping the same design, be included in paste? Are there serious problems that require a different design? -- David D. Smith
Index: tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt
===================================================================
--- tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt (リビジョン 6037)
+++ tests/fake_packages/FakeApp.egg/FakeApp.egg-info/entry_points.txt (作業コピー)
@@ -20,3 +20,9 @@
caps=fakeapp.apps:make_cap_filter
+[paste.command_factory]
+
+ batch_cmd=fakeapp.apps:batch_cmd
+ another_cmd=fakeapp.apps:BatchApp
+
+
Index: tests/fake_packages/FakeApp.egg/fakeapp/apps.py
===================================================================
--- tests/fake_packages/FakeApp.egg/fakeapp/apps.py (リビジョン 6037)
+++ tests/fake_packages/FakeApp.egg/fakeapp/apps.py (作業コピー)
@@ -14,10 +14,25 @@
def basic_app2(environ, start_response):
return simple_app('basic app2', environ, start_response)
-
+
def make_basic_app2(global_conf, **conf):
return basic_app2
+class BatchApp(object):
+ def __init__(self, message="Hello World"):
+ self.message = message
+
+ def __call__(self):
+ import os
+ print os.getuid()
+ print self.message
+
+def batch_cmd(global_conf, **conf):
+ if global_conf.has_key('message'):
+ return BatchApp(global_conf['message'])
+ else:
+ return BatchApp(conf['message'])
+
############################################################
## Composits
############################################################
Index: tests/fake_packages/FakeApp.egg/setup.py
===================================================================
--- tests/fake_packages/FakeApp.egg/setup.py (リビジョン 6037)
+++ tests/fake_packages/FakeApp.egg/setup.py (作業コピー)
@@ -19,5 +19,9 @@
'paste.filter_app_factory': """
caps2=fakeapp.apps:CapFilter
""",
+ 'paste.command_factory': """
+ batch_cmd=fakeapp.apps:batch_cmd
+ another_cmd=fakeapp.apps:BatchApp
+ """,
},
)
Index: tests/sample_configs/basic_app.ini
===================================================================
--- tests/sample_configs/basic_app.ini (リビジョン 6037)
+++ tests/sample_configs/basic_app.ini (作業コピー)
@@ -12,3 +12,6 @@
app.2 = other
addr.2 = 0.0.0.0
+[command:batch]
+use = egg:FakeApp#batch_cmd
+message = Basic App :: Batch 1
Index: tests/sample_configs/executable.ini
===================================================================
--- tests/sample_configs/executable.ini (リビジョン 6037)
+++ tests/sample_configs/executable.ini (作業コピー)
@@ -8,3 +8,6 @@
[app]
use = egg:FakeApp#basic_app
+
+[cmd]
+use = egg:FakeApp#batch_cmd
Index: tests/sample_configs/test_config.ini
===================================================================
--- tests/sample_configs/test_config.ini (リビジョン 6037)
+++ tests/sample_configs/test_config.ini (作業コピー)
@@ -16,7 +16,7 @@
[app:test3]
use = test2
set def1 = test3
-another = something more
+another = something more
across several
lines
@@ -34,3 +34,8 @@
[app:test_global_conf]
use = egg:FakeApp#configed
test_interp = this:%(inherit)s
+
+[cmd:batch1]
+use = egg:FakeApp#batch_cmd
+message = Test Config :: Batch 1
+
Index: paste/deploy/loadwsgi.py
===================================================================
--- paste/deploy/loadwsgi.py (リビジョン 6037)
+++ paste/deploy/loadwsgi.py (作業コピー)
@@ -8,7 +8,8 @@
import pkg_resources
from paste.deploy.util.fixtypeerror import fix_call
-__all__ = ['loadapp', 'loadserver', 'loadfilter', 'appconfig']
+__all__ = ['loadapp', 'loadserver', 'loadfilter', 'loadcmd',
+ 'appconfig', 'cmdconfig']
############################################################
## Utility functions
@@ -144,6 +145,22 @@
SERVER = _Server()
+
+class _Command(_ObjectType):
+
+ name = 'command'
+ egg_protocols = ['paste.command_factory']
+ config_prefixes = ['command', 'cmd']
+ def invoke(self, context):
+ if context.protocol == 'paste.command_factory':
+ return fix_call(context.object,
+ context.global_conf,
+ **context.local_conf)
+ else:
+ assert 0, "Protocol %r unknown" % context.protocol
+
+COMMAND = _Command()
+
# Virtual type: (@@: There's clearly something crufty here;
# this probably could be more elegant)
class _PipeLine(_ObjectType):
@@ -198,12 +215,21 @@
def loadserver(uri, name=None, **kw):
return loadobj(SERVER, uri, name=name, **kw)
-def appconfig(uri, name=None, relative_to=None, global_conf=None):
- context = loadcontext(APP, uri, name=name,
+def loadcmd(uri, name=None, **kw):
+ return loadobj(COMMAND, uri, name=name, **kw)
+
+def _generic_config(obj, uri, name=None, relative_to=None, global_conf=None):
+ context = loadcontext(obj, uri, name=name,
relative_to=relative_to,
global_conf=global_conf)
return context.config()
+def appconfig(*args, **kwargs):
+ return _generic_config(APP, *args, **kwargs)
+
+def cmdconfig(*args, **kwargs):
+ return _generic_config(COMMAND, *args, **kwargs)
+
_loaders = {}
def loadobj(object_type, uri, name=None, relative_to=None,
@@ -293,6 +319,10 @@
return self.server_context(
name=name, global_conf=global_conf).create()
+ def get_command(self, name=None, global_conf=None):
+ return self.command_context(
+ name=name, global_conf=global_conf).create()
+
def app_context(self, name=None, global_conf=None):
return self.get_context(
APP, name=name, global_conf=global_conf)
@@ -305,6 +335,10 @@
return self.get_context(
SERVER, name=name, global_conf=global_conf)
+ def command_context(self, name=None, global_conf=None):
+ return self.get_context(
+ COMMAND, name=name, global_conf=global_conf)
+
_absolute_re = re.compile(r'^[a-zA-Z]+:')
def absolute_name(self, name):
"""
@@ -312,7 +346,7 @@
"""
if name is None:
return False
- return self._absolute_re.search(name)
+ return self._absolute_re.search(name)
class ConfigLoader(_Loader):
Index: paste/deploy/epdesc.py
===================================================================
--- paste/deploy/epdesc.py (リビジョン 6037)
+++ paste/deploy/epdesc.py (作業コピー)
@@ -35,3 +35,9 @@
This gives a factory/function that, given a WSGI application and
configuration, will serve the application indefinitely.
"""
+
+class CmmandFactoryDescription(object):
+ description = """
+ This gives a factory/function that, that can do anything
+ given a configuration.
+ """
Index: paste/deploy/interfaces.py
===================================================================
--- paste/deploy/interfaces.py (リビジョン 6037)
+++ paste/deploy/interfaces.py (作業コピー)
@@ -7,7 +7,7 @@
def loadapp(uri, name=None, relative_to=None, global_conf=None):
"""
Provided by ``paste.deploy.loadapp``.
-
+
Load the specified URI as a WSGI application (returning IWSGIApp).
The ``name`` can be in the URI (typically as ``#name``). If it is
and ``name`` is given, the keyword argument overrides the URI.
@@ -33,6 +33,12 @@
Like ``loadapp()``, except returns in IServer object.
"""
+def loadcommand(uri, name=None, relative_to=None, global_conf=None):
+ """
+ Provided by ``paste.deploy.loadcommand``.
+
+ Like ``loadapp()``, except returns an ICommand object.
+ """
############################################################
## Factories
############################################################
@@ -91,7 +97,7 @@
This is the spec for the ``paste.filter_app_factory``
protocol/entry_point.
"""
-
+
def __call__(wsgi_app, global_conf, **local_conf):
"""
Returns a WSGI application that wraps ``wsgi_app``.
@@ -129,6 +135,18 @@
objects that implement the IServer interface.
"""
+class IPasteCommandFactory:
+
+ """
+ This is the spec for the ``paste.command_factory``
+ protocol/entry_point.
+ """
+
+ def __call__(global_conf, **local_conf):
+ """
+ Returns an ICommand object.
+ """
+
class ILoader:
"""
@@ -151,12 +169,17 @@
"""
Return an IFilter object, like ``get_app``.
"""
-
+
def get_server(name_or_uri, global_conf=None):
"""
Return an IServer object, like ``get_app``.
"""
+ def get_command(name_or_uri, global_conf=None):
+ """
+ Return an ICommand object, like ``get_app``.
+ """
+
############################################################
## Objects
############################################################
@@ -200,3 +223,14 @@
times, forever; nothing about how the server works is
specified here.
"""
+
+class ICommand:
+
+ """
+ A simple command interface.
+ """
+
+ def __call__():
+ """
+ Runs a command (anything; that's it).
+ """
Index: setup.py
===================================================================
--- setup.py (リビジョン 6024)
+++ setup.py (作業コピー)
@@ -89,6 +89,7 @@
help=paste.script.help:HelpCommand
create=paste.script.create_distro:CreateDistroCommand [Templating]
serve=paste.script.serve:ServeCommand [Config]
+ cmd=paste.script.run:RunCommand [Config]
exe=paste.script.exe:ExeCommand
points=paste.script.entrypoints:EntryPointCommand
make-config=paste.script.appinstall:MakeConfigCommand
@@ -131,7 +132,7 @@
egg_info.writers = paste.script.epdesc:EggInfoWriters
# @@: Not sure what this does:
#setuptools.file_finders = paste.script.epdesc:SetuptoolsFileFinders
-
+
[console_scripts]
paster=paste.script.command:run
""",
Index: paste/script/serve.py
===================================================================
--- paste/script/serve.py (リビジョン 6024)
+++ paste/script/serve.py (作業コピー)
@@ -27,15 +27,15 @@
summary = "Serve the described application"
description = """\
This command serves a web application that uses a paste.deploy
- configuration file for the server and application.
-
+ configuration file for the server and application.
+
If start/stop/restart is given, then --daemon is implied, and it will
start (normal operation), stop (--stop-daemon), or do both.
You can also include variable assignments like 'http_port=8080'
and then use %(http_port)s in your config files.
"""
-
+
# used by subclasses that configure apps and servers differently
requires_config_file = True
@@ -109,8 +109,8 @@
# Windows case:
self.options.set_user = self.options.set_group = None
# @@: Is this the right stage to set the user at?
- self.change_user_group(
- self.options.set_user, self.options.set_group)
+ change_user_group(
+ self, self.options.set_user, self.options.set_group)
if self.requires_config_file:
if not self.args:
@@ -205,12 +205,12 @@
else:
msg = ''
print 'Exiting%s (-v to see traceback)' % msg
-
+
def loadserver(self, server_spec, name, relative_to, **kw):
return loadserver(
server_spec, name=name,
relative_to=relative_to, **kw)
-
+
def loadapp(self, app_spec, name, relative_to, **kw):
return loadapp(
app_spec, name=name, relative_to=relative_to,
@@ -253,7 +253,7 @@
# Duplicate standard input to standard output and standard error.
os.dup2(0, 1) # standard output (1)
os.dup2(0, 2) # standard error (2)
-
+
if not self.options.pid_file:
self.options.pid_file = 'paster.pid'
if not self.options.log_file:
@@ -335,50 +335,14 @@
if self.verbose > 0:
print '-'*20, 'Restarting', '-'*20
- def change_user_group(self, user, group):
- if not user and not group:
- return
- import pwd, grp
- uid = gid = None
- if group:
- try:
- gid = int(group)
- group = grp.getgrgid(gid).gr_name
- except ValueError:
- import grp
- try:
- entry = grp.getgrnam(group)
- except KeyError:
- raise BadCommand(
- "Bad group: %r; no such group exists" % group)
- gid = entry.gr_gid
- try:
- uid = int(user)
- user = pwd.getpwuid(uid).pw_name
- except ValueError:
- try:
- entry = pwd.getpwnam(user)
- except KeyError:
- raise BadCommand(
- "Bad username: %r; no such user exists" % user)
- if not gid:
- gid = entry.pw_gid
- uid = entry.pw_uid
- if self.verbose > 0:
- print 'Changing user to %s:%s (%s:%s)' % (
- user, group or '(unknown)', uid, gid)
- if gid:
- os.setgid(gid)
- if uid:
- os.setuid(uid)
-
+
class LazyWriter(object):
def __init__(self, filename):
self.filename = filename
self.fileobj = None
self.lock = threading.Lock()
-
+
def open(self):
if self.fileobj is None:
self.lock.acquire()
@@ -429,7 +393,7 @@
return None
else:
return None
-
+
def _remove_pid_file(written_pid, filename, verbosity):
current_pid = os.getpid()
if written_pid != current_pid:
@@ -467,8 +431,8 @@
print 'Stale PID left in file: %s (%e)' % (filename, e)
else:
print 'Stale PID removed'
-
-
+
+
def ensure_port_cleanup(bound_addresses, maxtries=30, sleeptime=2):
"""
This makes sure any open ports are closed.
@@ -500,3 +464,40 @@
else:
raise SystemExit('Timeout waiting for port.')
sock.close()
+
+def change_user_group(command, user, group):
+ if not user and not group:
+ return
+ import pwd, grp
+ uid = gid = None
+ if group:
+ try:
+ gid = int(group)
+ group = grp.getgrgid(gid).gr_name
+ except ValueError:
+ import grp
+ try:
+ entry = grp.getgrnam(group)
+ except KeyError:
+ raise BadCommand(
+ "Bad group: %r; no such group exists" % group)
+ gid = entry.gr_gid
+ try:
+ uid = int(user)
+ user = pwd.getpwuid(uid).pw_name
+ except ValueError:
+ try:
+ entry = pwd.getpwnam(user)
+ except KeyError:
+ raise BadCommand(
+ "Bad username: %r; no such user exists" % user)
+ if not gid:
+ gid = entry.pw_gid
+ uid = entry.pw_uid
+ if command.verbose > 0:
+ print 'Changing user to %s:%s (%s:%s)' % (
+ user, group or '(unknown)', uid, gid)
+ if gid:
+ os.setgid(gid)
+ if uid:
+ os.setuid(uid)
pgpdQ18hLT76Y.pgp
Description: PGP signature
_______________________________________________ Paste-users mailing list [email protected] http://webwareforpython.org/cgi-bin/mailman/listinfo/paste-users
