#!/usr/bin/env python
"""Serve specially marked modules by XHTML.

  -H hostname   host name, default ""
  -p portnum    port number, default 8000

This script starts a web server, and publishes auto-detected functions
from the working directory.

from tagged.txt, 2006-10-06:

39. python opensourceprojectidea webapps rapidprototyping webapp auto
    automatic instant: Make a module like the AutoXmlRpcServer (or
    whatever) that takes a module and forms a collection of web pages
    out of it.  It looks at the function definitions and constructs the
    pages from that. It's for constructing rapid prototypes.  Method
    names start with get_ or post_ or both_ if they want to say a
    certain one, but default is just "post_".  Arguments are read out,
    and similarly interpreted: default to asking for a string.  But if
    you have str_ or int_ or float_ or whatever, you are more specific.
    You can also put at the end "_80" to mean "the input should be 80
    characters" or something. "_textarea" or "_big" or something for a
    mondo page-sized text area, or perhaps put two: _40_80 for a 40x80
    textarea.  (YEAH!)  Then read out the docstrings from the functions,
    and use that to be the text at the top of the page.  Use the very
    first string as the index listing entry, for approaching the app
    without a specific page.  Use: (function
    object).func_code.co_varnames to get at the filenames.  It MIGHT be
    possible to do something useful with returned information, too:
    Perhaps a list could be turned into an unordered list, or a list of
    lists into a table.  If you have a dictionary, make it turn into
    stuff that you can click, or something, and get to other pages with.
    It should be possible to adapt it into a GUI tool, as well, through
    wx, or something.  It could also automatically construct an XML-RPC
    interface, as well: you call the function name without the
    modifiers, and pass the args in order, and get back the results.
    xr_ as function name prefix, if you want to write an XML-RPC
    specific response.

Functions within Modules that define the name "publishing_namespace" are
published. Function names that begin with an underscore (ex: _eggs) are
not published. Functions are published on a page designated by the
publishing_namespace, or the base page if the value is None.

TODO:  Make functions show in order of original listing.
TODO:  Respect _xhtml, _text, _json, _xml, _yaml, _rss, _xhtmlfragment,
       at end of function name
TODO:  Just get modules with "SCRATCH-APP" in the docstring
TODO:  List functions by module, and allow seeing module docs by
       clicking on module name.
TODO:  Permit getting static files within the directory, or responding
       to other path & value bindings.
TODO:  Fill out documentation strings.
TODO:  Make it work as a CGI.

If people show an interest, that's the incentive to put in more work.
Money works too. :)

DOC
"""


__version__ = "0.1"


import time
import os
import sets
import imp
import types
import cgi
import re
import cPickle as pickle

import optparse
import DocXMLRPCServer  # XXX_DEL
import BaseHTTPServer


# str  -- a string (no more specific)
# int  -- an integer (no more specific)
# n  -- a number/count of something
# i  -- an index into something, up to a (n) number or count
# name  -- a name of something
# url  -- a URL of something
#
# webpath  -- a path along the web server ("/" for the base)
# webvals  -- a dictionary {str_formkey: str_formvalue} of GET/POSTed data
# rawwebvals -- a dictionary {str_formkey: lst_str_formvalues}
#
# http  -- HTTP response code (ex: 200, 404)
# ctype  -- content type
# body -- an HTTP response body, regardless text or XHTML
# xhtmlhb  -- complete XHTML page, including head and body
# xhtml  -- some XHTML content, or a full page
# response  -- something meeting the HttpResponse interface
#
# mod  -- a module
# fn  -- a function
# arg  -- a full argument name, for a function
# atype  -- argument type, a string, either "str" or "int"
# file  -- a file
# filename  -- a filename
# path  -- a string directory path
# exodir  -- an ExoDirectory
# exomod  -- an ExoModule
# exofn  -- an ExoFunction
# exoarg  -- an ExoArgument
#
# pns  -- a publishing namespace
#
# lst_X  -- list of X's
# set_X  -- set of X's
# dic_X_Y  -- dictionry from X's to Y's
#

lst_str_fnname_beginnings = ["post", "get", "xmlrpc"]
lst_str_fnname_endings = ["text", "txt", "html", "xhtml",
                          "rss", "json", "yaml", "xml"]
lst_atype_all = ["str", "int"]

url_scratch_py_website = "http://www.speakeasy.org/~lion/proj/scratch/"


re_number = re.compile(r'[0-9]+')


OBJ_SCRATCHCODESOURCE_SIGNIFIER = "scratch.py"


str_starttime = time.asctime()


def number(str_number):
    """Return number if it's a number, otherwise, None."""
    if re_number.match(str_number):
        return int(str_number)
    return None


class ExoDirectory:
    
    """DOC
    
    DOC
    
    DOC
    """
    
    def __init__(self, path):
        """DOC"""
        self.path = path
    
    def find_module_names(self):
        """Return names of modules in a directory.
        
        Returns module names in a list. Filenames that end in ".py" or
        ".pyc" are considered to be modules. The extension is not
        included in the returned list.
        """
        set_name_module = sets.Set()
        for filename in os.listdir(self.path):
            name_module = None
            if filename.endswith(".py"):
                name_module = filename[:-3]
            elif filename.endswith(".pyc"):
                name_module = filename[:-4]
            if name_module is not None:
                set_name_module.add(name_module)
        return list(set_name_module)
    
    def load_module(self, filename):
        """Return a named module found in the path."""
        print "loaded:", filename
        (file, path, str_description) = imp.find_module(filename,
                                                        [self.path])
        return imp.load_module(filename, file, path, str_description)
    
    def find_publishing_modules(self):
        """Find modules that want to be published.
        
        Loads all modules in the current working directory. Returns a
        list of modules, the modules that define publishing_namespace.

        TODO:  Find better technique for labeling modules.
        TODO:  Convert names in here to regular form.
        """
        modules = [self.load_module(m)
                   for m in self.find_module_names()]
        my_modules = []
        for m in modules:
            if m.__dict__.has_key("publishing_namespace"):
                my_modules.append(m)
        return [ExoModule(m) for m in my_modules]


class ExoModule:
    
    """DOC
    
    DOC
    
    DOC
    """
    
    def __init__(self, mod):
        """DOC"""
        assert "publishing_namespace" in vars(mod)
        self.mod = mod
    
    def name(self):
        return self.mod.__name__
    
    def one_line_description(self):
        """DOC"""
        try:
            if len(self.mod.__doc__) > 0:
                return self.mod.__doc__.splitlines()[0]
            else:
                return "(no description)"
        except:
            return "(no description)"
    
    def long_description(self):
        """DOC"""
        return self.__doc__
    
    def filename(self):
        """Return the filename for the module."""
        return self.mod.__name__ + ".py"
    
    def source_code(self):
        """Return the source code for the module."""
        return open(self.filename(), "r").read()
    
    def all_functions(self):
        """Find all functions in the module."""
        lst_exofn = []
        for obj in self.mod.__dict__.values():
            if isinstance(obj, types.FunctionType):
                lst_exofn.append(ExoFunction(obj, self))
        return lst_exofn
    
    def public_functions(self):
        """Find all functions that are public."""
        return filter(lambda exofn: exofn.show(),
                      self.all_functions())

    def load_from_pickle(self):
        """Restore from Pickle."""
        try:
            file_pickle = open(self.pickle_filename(), "r")
            for (name, obj) in pickle.load(file_pickle).items():
                self.mod.__dict__[name] = obj
        except IOError:
            pass
        except EOFError:
            pass
    
    def store_to_pickle(self):
        """Save to Pickle."""
        file_pickle = open(self.pickle_filename(), "w")
        pickle.dump(self.pickle_data(), file_pickle)
    
    def pickle_filename(self):
        """Filename for pickle."""
        return self.name() + "_pickle.txt"
    
    def pickle_data(self):
        """Collect dictionary of data to pickle or read."""
        dic_name_obj = {}
        for (name, obj) in self.mod.__dict__.items():
            if name.startswith("pickle_"):
                dic_name_obj[name] = obj
        return dic_name_obj


class ExoFunction:
    
    """DOC
    
    DOC
    
    DOC
    """
    
    def __init__(self, fn, exomod):
        """DOC"""
        assert not fn.__name__.startswith("_")
        assert "publishing_namespace" in vars(exomod.mod)
        self.fn = fn
        self.exomod = exomod
    
    def full_name(self):
        """DOC"""
        return self.fn.__name__
    
    def short_name(self):
        """DOC"""
        global lst_str_fnname_beginnings
        global lst_str_fnname_endings
        str_full = self.full_name()
        lst_str_parts = str_full.split("_")
        if lst_str_parts[0] in lst_str_fnname_beginnings:
            lst_str_parts = lst_str_parts[1:]
        if lst_str_parts[-1] in lst_str_fnname_endings:
            lst_str_parts = lst_str_parts[:-1]
        return "_".join(lst_str_parts)
    
    def publishing_namespace(self):
        """DOC"""
        return self.exomod.mod.publishing_namespace
    
    def one_line_description(self):
        """DOC"""
        if self.fn.__doc__:
            return self.fn.__doc__.splitlines()[0]
        else:
            return "(no documentation)"
    
    def long_description(self):
        """DOC"""
        if self.fn.__doc__:
            return self.fn.__doc__
        else:
            return "(no documentation)"
    
    def result_interpretation(self):
        """DOC"""
        str_full = self.full_name()
        if str_full.endswith("_html") or str_full.endswith("_xhtml"):
            return "xhtml"
        elif str_full.endswith("_xml"):
            return "xml"
        return "txt"
    
    def method(self):
        """DOC"""
        str_full = self.full_name()
        assert not str_full.startswith("xmlrpc_")
        if str_full.startswith("post_"):
            return "post"
        return "get"
    
    def show(self):
        """Return True if this function should be shown."""
        return not self.full_name().startswith("_")
    
    def args(self):
        """DOC"""
        n_args = self.fn.func_code.co_argcount
        lst_arg = self.fn.func_code.co_varnames[:n_args]
        return [ExoArgument(arg) for arg in lst_arg]


class ExoArgument:
    
    """DOC
    
    DOC
    
    .arg  -- full argument name
    .atype  -- argument type ("str", or "int")
    .str_shortname  -- short name of argument
    .width()  -- the width
    .height()  -- height, if any
    .page()  -- returns true if it has both width & height
    DOC
    """
    
    def __init__(self, arg):
        """DOC"""
        self.arg = arg
        self.lst_str_parts = arg.split("_")
        self.lst_int_dimensions = []
        if self.lst_str_parts[0] in lst_atype_all:
            self.atype = self.lst_str_parts[0]
            self.str_shortname = self.lst_str_parts[1]
            if len(self.lst_str_parts) > 2:
                lst_obj_dimensions = [number(str_part) for str_part
                                      in self.lst_str_parts[2:]]
                self.lst_int_dimensions = filter(lambda x: x is not None,
                                                 lst_obj_dimensions)
        else:
            self.atype = "str"
            self.str_shortname = self.lst_str_parts[0]
            if len(self.lst_str_parts) > 1:
                lst_obj_dimensions = [number(str_part) for str_part
                                      in self.lst_str_parts[1:]]
                self.lst_int_dimensions = filter(lambda x: x is not None,
                                                 lst_obj_dimensions)
    
    def width(self):
        """DOC"""
        if len(self.lst_int_dimensions) > 0:
            return self.lst_int_dimensions[0]
        if self.atype == "int":
            return 8  # TODO:  Magic #
        else:
            return 60  # TODO:  Magic #
    
    def height(self):
        """DOC"""
        if len(self.lst_int_dimensions) == 2:
            return self.lst_int_dimensions[-1]
        return None
    
    def page(self):
        """DOC"""
        return len(self.lst_int_dimensions) == 2

    def convert_value(self, value):
        if self.atype == "str":
            return value
        elif self.atype == "int":
            return int(value)
        else:
            return "unrecognized argument type -- no conversion"


class LinkingSystem:
    
    """Bind names of things to URLs, and vice versa.
    
    DOC
    
    DOC
    """
    
    def __init__(self):
        """DOC"""
        self.dic_url_object = {}
    
    def url_to_object(self, url):
        """DOC"""
        if url== "/scratch":
            return OBJ_SCRATCHCODESOURCE_SIGNIFIER
        return self.dic_url_object.get(url)
    
    def source_code_url(self):
        """Return the address to the scratch.py source."""
        return "/scratch"
    
    def module_url(self, exomod):
        """DOC"""
        return "/module/%s" % exomod.filename()
    
    def function_url(self, exofn):
        """DOC"""
        return "/%s" % exofn.short_name()
    
    def register_module(self, exomod):
        """DOC"""
        self.dic_url_object[self.module_url(exomod)] = exomod
    
    def register_function(self, exofn):
        """DOC"""
        self.dic_url_object[self.function_url(exofn)] = exofn


class HttpResponse:
    
    """A container for communicating HTTP Responses.
    
    The response doesn't really have any intelligence in it.  Rather,
    the intelligence is in the object that produces the response.
    
    DOC
    """
    
    def __init__(self):
        """DOC"""
        self.http = 200  # Default: "OK."
        self.ctype = "text/plain"
        self.body = "uninitialized response"
    
    def make_webpage(self, xhtmlhb):
        self.ctype = "text/html"
        self.body = xhtmlhb
    
    def make_text(self, str_body):
        self.ctype = "text/plain"
        self.body = str_body

    def make_xml(self, txt_xml):
        self.ctype = "text/xml"
        self.body = txt_xml


class XHTML:
    
    """A class enclosing XHTML shortcuts."""
    
    def __init__(self, linking_system):
        """DOC"""
        self.linking_system = linking_system
    
    def quote(self, str_toquote):
        """Quote text for web presentation."""
        return cgi.escape(str_toquote)
    
    def link(self, url, str):
        """Return a <a href=...>...</a>."""
        return "<a href=\"%s\">%s</a>" % (url, str)
    
    def module_link(self, exomod):
        """Return an <a href=...>...</a> targeting a module."""
        return self.link(self.linking_system.module_url(exomod),
                         exomod.filename())
    
    def function_link(self, exofn):
        """Return an <a href=...>...</a> targeting a function."""
        return self.link(self.linking_system.function_url(exofn),
                         exofn.short_name())
    
    def xhtmlhb(self, str_title, xhtml_content):
        """DOC"""
        return """
<html>
<head><title>%s</title></head>
<body>
%s
</body>
</html>""" % (str_title, xhtml_content)


class WebSite:
    
    """Mediator for website.
    
    The WebSite takes direction from the BaseHTTPServer system, and from
    the initialization loop.
    
    DOC
    
    DOC
    """
    
    def __init__(self, startpath="."):
        """DOC"""
        self.linking_system = LinkingSystem()
        self.xhtml = XHTML(self.linking_system)
        self.exodir = ExoDirectory(startpath)
        self.lst_exomod = self.exodir.find_publishing_modules()  # TODO
        for exomod in self.lst_exomod:
            self.linking_system.register_module(exomod)
            for exofn in exomod.public_functions():
                self.linking_system.register_function(exofn)
            exomod.load_from_pickle()
    
    def response(self, webpath, webvals):
        """Generate a response to a web path and values.

        This is where we check what type of thing it is, and then
        dispatch to something else that makes pages.
        """
        if webpath == "/":
            response = HttpResponse()
            response.make_webpage(self.global_index_page_xhtmlhb())
        else:
            obj = self.linking_system.url_to_object(webpath)
            if obj is None:
                response = HttpResponse()
                response.make_text("I don't know how to handle: %s"
                                   % webpath)
            elif obj == OBJ_SCRATCHCODESOURCE_SIGNIFIER:
                response = HttpResponse()
                response.make_text(open("scratch.py", "r").read())
            elif obj.__class__ == ExoModule:
                response = self.module_page_response(obj, webvals)
            elif obj.__class__ == ExoFunction:
                response = self.function_page_response(obj, webvals)
            else:
                response = HttpResponse()
                response.make_text("No code yet for handling: %s"
                                   % webpath)
        return response
    
    def module_page_response(self, exomod, webvals):
        """Construct response to a request for a module page.
        
        Presently, returns the full source code for the given module.
        """
        response = HttpResponse()
        response.make_text(exomod.source_code())
        return response
    
    def function_page_response(self, exofn, webvals):
        """Construct response to a request for a function page.

        This is where forms are checked.  If the form is fulfilled, the
        response is given.  If the form has empty fields, the form is
        drawn for the user.
        """
        # Are the arguments filled?
        filled = True
        for exoarg in exofn.args():
            if not webvals.has_key(exoarg.str_shortname):
                filled = False
        if filled:
            str_vals = [webvals[exoarg.str_shortname]
                        for exoarg in exofn.args()]
            obj_vals = [exoarg.convert_value(str_val) for 
                        (exoarg, str_val)
                        in zip(exofn.args(), str_vals)]
            response = HttpResponse()
            obj_result = exofn.fn(*obj_vals)
            if exofn.result_interpretation() == "xhtml":
                xhtmlhb = self.xhtml.xhtmlhb(exofn.short_name(),
                                             str(obj_result))
                response.make_webpage(xhtmlhb)
            elif exofn.result_interpretation() == "xml":
                response.make_xml(str(obj_result))
            else:
                response.make_text(str(obj_result))
            exofn.exomod.store_to_pickle()  # SAVE results
            return response
        else:
            response = HttpResponse()
            xhtml_body = self.function_formpage_xhtml(exofn, webvals)
            xhtmlhb = self.xhtml.xhtmlhb(exofn.short_name(), xhtml_body)
            response.make_webpage(xhtmlhb)
            return response
    
    def function_formpage_xhtml(self, exofn, webvals):
        """DOC"""
        lst_xhtml = ["<p><code><pre>"
                     + self.xhtml.quote(exofn.long_description())
                     + "</pre></code></p>"]
        lst_xhtml.append("<form method=\"%s\"><table>" % exofn.method())
        for exoarg in exofn.args():
            str_name = exoarg.str_shortname
            str_value = webvals.get(exoarg.str_shortname, "")
            int_width = exoarg.width()
            int_height = exoarg.height()
            if exoarg.page():
                str_input = "<textarea name=\"%s\"" \
                            " value=\"%s\"" \
                            " cols=\"%d\"" \
                            " rows=\"%d\">" \
                            "</textarea>" % (str_name, str_value,
                                             int_width, int_height)
            else:
                str_input = "<input type=\"text\"" \
                            " name=\"%s\" value=\"%s\"" \
                            " size=\"%d\">" % (str_name, str_value,
                                               int_width)
            lst_xhtml.append("<tr><td>%s:</td><td>%s</td></tr>"
                             % (exoarg.str_shortname, str_input))
        lst_xhtml.append("<td><input type=\"submit\"></td>")
        lst_xhtml.append("</table></form>")
        return "\n".join(lst_xhtml)
    
    def global_index_page_xhtmlhb(self):
        """DOC"""
        L = ["<ul>"]
        for exomod in self.lst_exomod:
            xhtml_link = self.xhtml.module_link(exomod)
            str_oneline = exomod.one_line_description()
            xhtml_desc = self.xhtml.quote(str_oneline)
            L.append("<li>%s - %s" % (xhtml_link, xhtml_desc))
            L.append("<ul>")
            for exofn in exomod.public_functions():
                xhtml_link = self.xhtml.function_link(exofn)
                str_oneline = exofn.one_line_description()
                xhtml_desc = self.xhtml.quote(str_oneline)
                L.append("<li>%s - %s</li>" % (xhtml_link, str_oneline))
            L.append("</li></ul>")
        L.append("</ul>")
        L.append("<p>This scratch site served by ")
        L.append("   <a href=\"%s\">scratch.py</a>"
                 % self.linking_system.source_code_url())
        L.append("   <a href=\"%s\">(Visit the project website to"
                 % url_scratch_py_website)
        L.append("   learn more!)</a></p>")
        import sys
        L.append("<p>Application started with arguments: %s</p>"
                 % sys.argv)
        L.append("<p>At time: %s</p>" % str_starttime)
        xhtml_content = "\n".join(L) + "\n"
        return self.xhtml.xhtmlhb("Index", xhtml_content)


class Py2WebHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
    
    """DOC
    
    DOC

    Everything with xxx_ in front, is written that way, to avoid
    potential collision with BaseHTTPRequestHandler members.
    
    DOC
    """
    
    server_version = "Py2WebHTTP/" + __version__
    
    def do_HEAD(self):
        """Serve a HEAD request.
        
        On a normal web page fetch, Firefox doesn't get the HEAD.  I
        really don't care much about this; I'm just doing this because
        that's what the code before did.
        """
        self.xxx_generate()
        self.xxx_send_head()
        self.xxx_flush()
    
    def do_GET(self):
        """Serve a GET request."""
        if "quit" in self.path:  # PLEASE DIE!
            import sys
            sys.exit(0)  # and STILL, you have to hit Ctrl-C too!
        self.xxx_generate()
        self.xxx_send_head()
        self.wfile.write(self.xxx_response.body)
        self.xxx_flush()
    
    def do_POST(self):
        """Service a POST request."""
        try:
            int_length = int(self.headers["Content-length"])
            error = False
        except AttributeError:
            int_length = 0
            error = True
        self.xxx_response = HttpResponse()
        if error:
            self.xxx_response.make_text("Require Content-length header")
        else:
            str_body = self.rfile.read(int_length)
            webvals = self.xxx_parseqs(str_body)
            self.xxx_response = self.server.website.response(self.path,
                                                             webvals)
        self.xxx_send_head()
        self.wfile.write(self.xxx_response.body)
        self.xxx_flush()
    
    def xxx_send_head(self):
        """Send the HTTP header, based on the response."""
        self.send_response(self.xxx_response.http)
        self.send_header("Content-type", self.xxx_response.ctype)
        self.send_header("Content-Length", len(self.xxx_response.body))
        self.end_headers()
    
    def xxx_flush(self):
        """DOC"""
        self.xxx_response = None
    
    def xxx_generate(self):
        """Generate a string that is the response to the request."""
        webvals = {}
        webpath = self.path.split("?", 1)[0]
        if len(self.path.split("?", 1)) > 1:
            str_remainder = self.path.split("?", 1)[1]
            webvals = self.xxx_parseqs(str_remainder)
        self.xxx_response = self.server.website.response(webpath,
                                                         webvals)
    
    def xxx_parseqs(self, str_formstring):
        """Create a simple k-v dictionary from the form string.
        
        DOC
        
        Toss out all values but the first.
        """
        webvals = {}
        dic_str_lst_str = cgi.parse_qs(str_formstring)
        for (str_key, lst_str_val) in dic_str_lst_str.items():
            webvals[str_key] = lst_str_val[0]
        return webvals


class Py2WebHTTPServer(BaseHTTPServer.HTTPServer):
    
    """DOC
    
    DOC
    
    DOC
    """

    def py2web_init(self, website):
        """DOC"""
        self.website = website


if __name__ == "__main__":
    parser = optparse.OptionParser(__doc__)
    parser.add_option("-H", "--host", dest="hostname",
                      default="127.0.0.1", type="string",
                      help="specify hostname to run on")
    parser.add_option("-p", "--port", dest="portnum", default=8000,
                      type="int", help="port number to run on")
    
    (options, args) = parser.parse_args()
    if len(args) != 0:
        parser.error("incorrect number of arguments")
    
    website = WebSite()
    
    server = Py2WebHTTPServer((options.hostname,
                               options.portnum),
                              Py2WebHTTPRequestHandler)
    server.py2web_init(website)
    
    print str_starttime, 'Application Starting.'
    server.serve_forever()
    print time.asctime(), 'Application Finishing.'


