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)

Attachment: pgpdQ18hLT76Y.pgp
Description: PGP signature

_______________________________________________
Paste-users mailing list
[email protected]
http://webwareforpython.org/cgi-bin/mailman/listinfo/paste-users

Reply via email to