Mercurial > cgi-bin > hgweb.cgi > tincan
changeset 61:55828c01e38f draft
More documenting.
author | David Barts <n5jrn@me.com> |
---|---|
date | Sun, 09 Jun 2019 10:37:45 -0700 |
parents | 682cd33e564c |
children | fd8c558a89bb |
files | doc/api_reference.rst doc/configuration.rst doc/deployment.rst doc/index.rst doc/introduction.rst doc/philosophy.rst |
diffstat | 6 files changed, 333 insertions(+), 20 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/api_reference.rst Sun Jun 09 10:37:45 2019 -0700 @@ -0,0 +1,163 @@ +************* +API Reference +************* + +=================== +The Launch Function +=================== + +.. function:: launch(fsroot=None, urlroot='/', multithread=True, logger=None, encoding='utf-8', static=False) + +Launch and return a TinCan webapp object. Returns a tuple containing two items: an instance of ``tincan.TinCan`` and an error count. This function *does not run or serve the webapp;* it is the caller's responsibility to perform one of the latter operations. This function accepts the following keyword arguments: + +*fsroot* + Path to the root directory of this webapp on the filesystem. If no path is specified, the current working directory will be used. + +*urlroot* + This defines a directory prefix that is added to all routes. It is mostly for use by the ``launch`` shell command; when running under WSGI this should almost always be left to default to ``/``. + +*multithread* + By default, TinCan assumes that multiple threads per process are being used. Setting this parameter to ``False`` will result in code that fails to multithread properly, but which runs more efficiently while not multithreading. It is recommended to set this to ``False`` if you are not going to be using multithreading. + +*logger* + If set, the name of the Python ``logging.Logger`` object to use for logging. If not set, no messages will be logged. + +*encoding* + Character set used by all text files in the webapp. Note that this is different from the character set that TinCan will use to serve responses; the latter is inevitably UTF-8. + +*static* + Whether or not to create routes to serve static content in the webapp. If you're using the built-in server, this should probably be ``True``. If you're running via WSGI, it should probably be ``False``. + +================= +The TinCan Object +================= + +This is a subclass of ``bottle.Bottle``. It is not intended for end users to create instances of this object themselves; use the ``launch`` function above to do that. + +This class contains one extra method above and beyond the methods ``bottle.Bottle`` objects have: + +.. method:: forward(target) + +Perform a programmatic, request-time, server-side redirect to the route specified by *target*, which may be either relative to the route issuing the redirect or an absolute route. This differs from the ``#forward`` header directive in that this method causes a server-side redirect to be set up at route creation time. The ``#forward`` directive is both more efficient and less flexible than the ``forward`` method. + +One may only forward from a normal page to another normal page; neither the source nor the target of a forward may be an error page. Attempts to create forward loops will also cause an error. + +TinCan Configuration Variables +------------------------------ + +The ``config`` dictionary of a ``TinCan`` object contains ``tincan.fsroot``, ``tincan.urlroot``, ``tincan.logger``, and ``tincan.encoding`` keys (in addition to the standard ones defined by WSGI and Bottle); these contain the values of the corresponding parameters passed to the ``launch`` function that created this webapp. In general, any key starting with ``tincan.`` is reserved for use by TinCan. + +============================= +Page, BasePage, and ErrorPage +============================= + +These classes are typically subclassed in the code-behind for a page. Any page without code-behind will get a bare ``Page`` or ``ErrorPage`` object associated with it, as appropriate. + +.. class:: tincan.BasePage + +This is the parent class of all code-behind classes. All such classes contain two standard methods: + +.. method:: handle() + +The ``handle`` method is called by TinCan to handle a request. No arguments are passed to it (other than the implied ``self`` argument). By default, this is a no-op method. + +.. method:: export() + +This exports all non-hidden instance variables; it does not export attributes that define callable objects (e.g. methods). A "hidden" instance variable means any one whose name *does not* start with an underscore; the ``request`` and ``response`` instance variables of type ``tincan.Page`` are also considered hidden. Finally, this object itself is exported as the ``page`` variable. This method returns a dict containing the exported items. + +Note that the exporting happens *after* header processing; thus, if there is a conflict between a template variable defined by the ``#load`` header directive and this exporting logic, the value exported by this method will always overwrite the earlier one. + +If the above exporting behavior is for some reason unsuitable, it is permitted to override this method. + +.. class tincan.Page + +The parent class of all normal (non-error) pages. This is a subclass of ``tincan.BasePage`` above. + +.. class tincan.ErrorPage + +The parent class of all error pages. + +============================ +Request and Response Objects +============================ + +The ``request`` and ``response`` instance variables of ``tincan.Page`` are standard ``bottle.Request`` and ``bottle.Response`` objects. In the ``environ`` attribute of the ``request`` object, any key beginning with ``tincan.`` is reserved. + +================= +Header Directives +================= + +A ``.pspx`` file is a standard Chameleon template, plus a set of optional *header directives* that may be present to override the default behavior of the created page or its associated route. + +``#end`` + Marks the last line of the headers. This is not currently necessary, as headers are implicitly ended by the first line which does not start with a leading "#". This directive mainly here to facilitate future support of alternate templating engines. + +``#errors`` + This is an error page which handles the specified error codes; any code-behind associated with this page must be based on the ``tincan.ErrorPage`` class. + +``#forward`` + Ignore everything else in this template (and any code-behind associated with it), using the specified route to serve it instead. The route specified with ``#forward`` may itself contain a ``#forward``, but attempts to create a ``#forward`` loop are not allowed and will cause a ``TinCanError`` to be raised at initialization time. + +``#hidden`` + This is a hidden page; do not create a route for it. The page can only be displayed by a server-side forward. + +``#load`` + Load the specified Chameleon template file and make the loaded template available as a template variable. Useful for importing and invoking macros. See the :ref:`loading-templates` below for more information. + +``#methods`` + A list of HTTP request methods, separated by whitespace, follows. The route will allow all specified methods. Not specifying this line is equivalent to specifying ``#methods GET``. + +``#python`` + What follows is the name of the Python file containing the code-behind for this route; the file name must end in ``.py``. + +``#template`` + Ignore the body of this file and instead use the template in the body of the specified file, which must end in .pspx. Any headers in the referred template file are ignored. + +Error Pages +----------- + +Error pages supersede the standard Bottle error handling, and are created by using the ``#errors`` page header. The ``#hidden`` and ``#method`` header directives are ignored in error pages (error pages are effectively hidden anyhow, by virtue of never having normal routes created for them). + +The ``#errors`` directive takes a list of numeric error codes (values from 400 to 599 are allowed), separated by spaces; the page is created to handle the specified errors. If no error codes are specified, the page will handle all errors. The behavior of specifying multiple error pages for the same error code is undefined; doing so is best avoided. + +Templates with No Explicit Code-Behind +-------------------------------------- + +Code-behind is optional for both normal and error page templates. If code-behind is not provided, TinCan will use the Page or ErrorPage class as appropriate. + +.. _loading-templates: + +Loading Templates +----------------- + +The ``#load`` directive may be used to load additional templates, e.g. ones containing macro definitions. Note that the loaded files are standard Chameleon templates and *not* TinCan ``.pspx`` files (i.e. they cannot contain any header directives); as such, loaded files must have the standard ``.pt`` extension for Chameleon template files. + +In the normal case, ``#load foo.pt`` will load a file relative to the same directory as the page containing the ``#load`` directive itself. The loaded template object will be made available as a template variable matching the file name sans extension (e.g. ``foo``). One can change the name of the variable created by prefixing the file specification with a variable name followed by an equals sign, e.g. ``#load t=foo.pt``. If one places the specification inside angle brackets (e.g. ``#load <t=foo.pt>``), loaded files are searched for in ``WEB-INF/tlib`` instead. + +Finally, as is allowed for all arguments to header directives, one may enclose the argument to ``#load`` inside single or double quotes and use the normal Python string syntax. + +Using Loaded Macros +------------------- + +Once a template has been loaded, it will be available as a sub-attribute of the macros attribute of the associated template object. E.g.:: + + #load foo.pt + <!DOCTYPE html> + <html> + <head> + <title>Macro Example</title> + </head><body> + <p metal:use-macro="foo.macros.bar"></p> + </body> + </html> + +=================== +Chameleon Templates +=================== + +TinCan templates are Chameleon templates, as documented `here <http://chameleon.readthedocs.io/en/latest/>`_. Note that the templates in ``.pspx`` files are processed as strings, so ``load:`` expressions will not work in them (hence the ``#load`` header directive). Templates loaded via ``#load`` are processed as files, so the ``load:`` expression *will* work from within them. + +TinCan provides ``chameleon_template`` and ``chameleon_view`` callables, which follow the standard Bottle conventions for templating. Thus, if it is desired to define routes the "Bottle way", the following ``import`` line will make Chameleon the default templating engine:: + + from tincan import chameleon_view as view, chameleon_template as template +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/configuration.rst Sun Jun 09 10:37:45 2019 -0700 @@ -0,0 +1,20 @@ +************* +Configuration +************* + +Basically, there isn't very much configuration to contend with. It's all in the files you create in your webapp's directory tree. TinCan figures it out from them and takes it from there: + +* A route is created for each ``.pspx`` file. +* By default, a ``.pspx`` file's code-behind is find in a ``.py`` file with the same base name. +* Header directives in the ``.pspx`` files can be used to alter the default behavior. +* The ``tincan.launch`` call accepts optional arguments which can be used to tailor how a webapp is launched and run. + +=============================================== +Case-Sensitive and Case-Preserving File Systems +=============================================== + +A word here is necessary about case-sensitive versus case-preserving files systems. Unix and Linux systems have case-sensitive file systems; ``file.pspx``, ``File.Pspx``, and ``FILE.PSPX`` are three completely different files. It is possible for all three files to coexist in the same directory. Windows and MacOS are case-preserving file systems; only one of the three is allowed to exist at any time, and a request for any one of those names will match the existing one. + +TinCan knows about both kinds of file systems, and acts slightly differently depending on which kind the system it is being run under has. When run on a case-preserving system, ``FILE.PSPX``, ``File.Pspx``, ``file.pspx``, and even ``fIlE.pSpX`` will be recognized as having the correct sort of extension to define a route and template. On a case-sensitive system, only ``file.pspx`` will be so recognized. + +It is important to note, however, that while a file system might not be case sensitive, *routes are always case sensitive*. A TinCan webapp under Windows that contains ``FILE.PSPX`` will get a route created that matches *only* the capitalized version of the name that appears on the Windows file system. It is for this reason that it is probably best to stick to one capitalization strategy, even on non-case-sensitive systems. \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/deployment.rst Sun Jun 09 10:37:45 2019 -0700 @@ -0,0 +1,138 @@ +********** +Deployment +********** + +======================== +Bottle's Built-In Server +======================== + +The ``launch`` command will simply use the WSGI server built into Bottle (built into the Python standard library, actually) to serve the routes defined by the webapp. This is a multithreaded server (see the :ref:`multithreading-problem` section on the issues with multithreading), which is primarily intended for debugging and light-duty use. + +Static and Dynamic Routes +------------------------- + +If you're using the built-in server, one thing to be aware of is that *by default, only dynamic content is served*. That means, if you create pages that reference style sheet, image, or font files, all these resources will cause 404 errors when a user agent attempts to request them from the server, because no routes exist to serve such content. + +If you specify the ``--static`` option to ``launch``, then for every file that is not related to dynamic content being generated by TinCan, a route will be created to serve that file's content. The HTTP ``Content-Type`` header for such routes will be set based on the file's extension, using the Python ``mimetype`` library. + +.. _example-apache-config: + +==== +WSGI +==== + +Bottle, and by implication TinCan, support WSGI, so any web server that supports WSGI can be used to serve a TinCan webapp. This is generally to be preferred for production use. + +How exactly to do this is beyond the scope of this document, but here's an example of serving a webapp using ``mod_wsgi`` under Apache. Note how rewrites are used to ensure that only *dynamic* content is served by TinCan, leaving static content to be served by Apache itself. + +In the configuration file for the site in question:: + + WSGIScriptAlias /ti_webapp /home/davidb/webapp.wsgi + RewriteRule ^/ti/(.*\.pspx)$ /ti_webapp/$1 [PT] + <LocationMatch "^/ti/WEB-INF/"> + Order deny,allow + Deny from all + </LocationMatch> + <LocationMatch "^/ti/.*\.(py|pyc|pt)$"> + Order deny,allow + Deny from all + </LocationMatch> + <Directory "/var/www/html/ti"> + DirectoryIndex index.pspx index.html + </Directory> + <Directory "/home/davidb"> + <Files "webapp.wsgi"> + Order deny,allow + Allow from all + Require all granted + </Files> + </Directory> + +The ``webapp.wsgi`` script:: + + #!/usr/bin/env python3 + + # C o n s t a n t s + + # Set the following appropriately. + TINCAN_HOME = "/home/davidb/src/tincan" + WEBAPP_HOME = "/var/www/html/ti" + LOG_HOME = "/var/log/wsgi" + LOG_NAME = "webapp" + + # I m p o r t s + + # First, fix up the path + import os, sys + if sys.path[0] == "": + sys.path[0] = TINCAN_HOME + else: + sys.path.insert(0, TINCAN_HOME) + + # Then do the importing + import bottle + import logging + from logging.handlers import RotatingFileHandler + import tincan + + # M a i n P r o g r a m + + # Set up logging + logger = logging.getLogger(LOG_NAME) + logger.setLevel(logging.INFO) + handler = RotatingFileHandler(os.path.join(LOG_HOME, LOG_NAME+".log"), + maxBytes=1048576, backupCount=5) + handler.setFormatter( + logging.Formatter(fmt="%(asctime)s - %(levelname)s: %(message)s")) + logger.addHandler(handler) + + # And away we go + application, errors = tincan.launch(fsroot=WEBAPP_HOME, logger=logger, + multithread=False) + +Note the bit about setting up logging. The WSGI standard provides no standard means by which to log errors. It *does* mention that WSGI code is strictly forbidden to write to either the standard output or the standard error stream. What this all means is that *unless you explicitly tell TinCan what to do with its error messages, it has no place to log them when running via WSGI. Thus, by default, TinCan is silent under WSGI.* So you almost certainly will want to create a logger and pass it to ``tincan.launch`` as in the example above. + +TinCan is not particularly chatty with its logging, preferring to only log what routes it creates when starting up and then only logging something when a truly exceptional condition happens. Logging of requests is left to the main server. Therefore, running with a logging level of ``INFO`` will not produce an undesirably large amount of output to the logs. + +.. _multithreading-problem: + +========================== +The Multithreading Problem +========================== + +Due to fundamantal aspects of the language's design and implementation, Python isn't that great at running multithreaded code. The *global interpreter lock* (GIL) lets only a single thread execute Python bytecode at any given time. Thus, a busy multithreaded WSGI server is likely to spend much of its time unable to service new, incoming requests. + +The simplest workaround is to avoid using threads with Python. This is what the default ``prefork`` execution model of ``mod_wsgi`` does, and it is recommended that you run ``mod_wsgi`` this way. If you do, you should set ``multithread=False`` when calling ``tincan.launch``, as it will optimize memory use somewhat by maximizing the degree to which templates are shared. + +That said, using threads *could* conceivably be useful for webapps that spend a large amount of their time doing I/O (e.g. database queries), as when a Python app is waiting on I/O, it is in the operating system kernel, and thus not actively executing Python bytecode, allowing another thread to hold the GIL. + +The simple server created by the ``launch`` command runs in a single process and thus does always use multithreading. + +So far as being thread safe goes, you don't have to much worry about it. TinCan gives each request a completely separate copy of a page's code-behind, request, and response objects. Although templates *are* shared to some degree, locking is used to ensure only a single thread has access to shared template objects at any one time. In short, multithreading might cause performance issues, but I have taken pains to ensure it should not cause code to execute incorrectly. + +========== +Installing +========== + +There's basically two strategies for installing a webapp to a production server: *the single-directory strategy*, in which one runs a server against a single directory exactly like the one you created to develop the webapp, and *the two-directory strategy*, in which the static and dynamic parts of a webapp are separated into separate directory trees. + +The Single-Directory Strategy +----------------------------- + +This is arguably the simplest, since it doesn't involve breaking a webapp into two separate directory trees at install time. If you're serving a webapp for test purposes or limited-duty internal usage with the ``launch`` command, this is the only strategy supported. + +The rub comes in production use. It's unwise to have a web server run with the permission to create files in the directory trees it serves. However, TinCan needs to do just that if it needs to compile a ``.py`` file into a ``.pyc`` file. + +The solution is to run ``launch --compile`` each time you update the webapp, before serving it. This will cause all referenced Python code that needs it to be recompiled, generating fresh ``.pyc`` files. The production server will then see that the byte-compiled files are all newer than the source code they are based upon, and not attempt to compile anything. + +The single-directory strategy is what I typically use myself, and what is used in the example Apache configuration in the :ref:`example-apache-config` section above. + +The Two-Directory Strategy +-------------------------- + +This gets rid of the need to recompile things, at the expense of having to install the webapp into two separate directories. What you must do is: + +#. Create an empty directory for serving static content, and point your web server at it. This directory should *readable but not writeable* by the server. +#. Create a directory for serving the dynamic content. This will be the directory that ``tincan.launch`` gets passed, and it must be both readable and writeable by the server. +#. Place all the webapp's files in the second directory, ensuring all subdirectories created are both readable and writeable by the server. +#. Use the ``install-static`` command to copy or move the static content into the first directory.
--- a/doc/index.rst Sat Jun 08 07:43:15 2019 -0700 +++ b/doc/index.rst Sun Jun 09 10:37:45 2019 -0700 @@ -12,7 +12,6 @@ introduction philosophy - motives essential_characteristics tutorial configuration
--- a/doc/introduction.rst Sat Jun 08 07:43:15 2019 -0700 +++ b/doc/introduction.rst Sun Jun 09 10:37:45 2019 -0700 @@ -2,7 +2,7 @@ Introduction ************ -TinCan is a Python web framework, based on the Bottle micro-framework and -the Chameleon templating engine. TinCan is based on the "code-behind" +TinCan is a Python web framework, based on the `Bottle <http://bottlepy.org/>`_ micro-framework and +the `Chameleon <https://bottlepy.org/>`_ templating engine. TinCan is based on the "code-behind" paradigm popularized by ASP.Net, convention over configuration, and clear separation of presentation and back-end logic.
--- a/doc/philosophy.rst Sat Jun 08 07:43:15 2019 -0700 +++ b/doc/philosophy.rst Sun Jun 09 10:37:45 2019 -0700 @@ -24,8 +24,8 @@ I wanted to get on with the business of actually *using* TinCan to do things, so I didn't clutter it up with extra features that would be difficult to -implement. (That's why there's currently no way to choose alternate templating engines -to Chameleon. I tried to do that, and it proved overly complex. Maybe some day.) +implement. (That's why there's currently no way to choose templating engines +other than Chameleon. I tried to allow that, and it proved overly complex. Maybe some day.) =========================== Don't Be Gratuitously Bossy @@ -46,7 +46,7 @@ =========== It's maligned by many, but I personally like it, and it -does *not* necessarily mean repudiating the MVC paradigm. +does *not* necessarily mean repudiating the MVC paradigm (see :ref:`mvc-different-forms`). ===================== Don't Repeat Yourself @@ -59,21 +59,15 @@ the relationships should be obvious by virtue of the similar names. The framework should figure it out and just "do the right thing" by default. +.. _mvc-different-forms: + ============================ MVC Comes in Different Forms ============================ -If you show a little discipline, the template page will describe how data -is presented to the user, and how he or she can interact with it. A view -by any other name is still a view. The code-behind will tie that view into -the underlying business logic, and bridge the gap between the two. A controller -by any other name is still a controller. +If a programmer shows a little discipline, the template page will describe how data is presented to the user, and how he or she can interact with it. A view by any other name is still a view. The code-behind will tie that view into the underlying business logic, and bridge the gap between the two. A controller by any other name is still a controller. -If you don't show discipline, you can clutter your templates up with bits -of Python code that represent controller logic, and clutter your controller up with -bits of presentation details. Of course you can. That's not the framework's -fault; that's the programmer's fault. Bad code can be written in any language, -for any platform. +If a programmer don't show discipline, s/he can clutter templates up with bits of Python code that represent controller logic, and clutter the controller up with bits of presentation details. Of course s/he can. That's not the framework's fault; that's the programmer's fault. Bad code can be written in any language, for any platform. ================================================== The Alternative is PHP or CGI, not Django or Flask @@ -82,7 +76,7 @@ I'm not the only one out there who doesn't like all the repeating yourself that most MVC frameworks make you do. There's still *a lot* of developers out there writing CGI scripts or using PHP, because CGI and PHP don't make -you do all the busywork that frameworks do. (Can they be blamed? It really +you do all the busywork that most frameworks do. (Can they be blamed? It really feels silly when all you want is to get a quick, small site up.) That's a pity, because CGI is hideously inefficient and PHP is, well, just @@ -92,7 +86,7 @@ Hopefully, by making it easier for people to code efficient web sites using a well-designed language that's easy to learn, I'll encourage more people to create efficient sites in a well-designed language. If not, well, at least -I'll encourage *myself* to do so. +I'll enable *myself* to do so. ============================================ Meaningful Error Messages (If You Want Them) @@ -105,5 +99,4 @@ always have a log file with a traceback in it helping to pinpoint the cause. -That is, assuming you enable logging. By default, it is turned off, simply -because WSGI provides no default place for errors to go. \ No newline at end of file +(Note that WSGI provides no default means for logging errors, so if you're running TinCan via WSGI, it will be totally silent unless you pass ``tincan.launch`` a suitable logger. The simple test server created by the ``launch`` command always logs to standard error.) \ No newline at end of file