# HG changeset patch # User David Barts # Date 1562255535 25200 # Node ID d59811b95a62576d2e89255975d708cf7bb4c6c0 # Parent 25fdd985d0462e43086c1e35fcdd41798dfde68e Package-ize it. diff -r 25fdd985d046 -r d59811b95a62 LICENSE.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/LICENSE.txt Thu Jul 04 08:52:15 2019 -0700 @@ -0,0 +1,21 @@ +MIT License + +Copyright © 2019 David W. Barts + +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. diff -r 25fdd985d046 -r d59811b95a62 MANIFEST.in --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MANIFEST.in Thu Jul 04 08:52:15 2019 -0700 @@ -0,0 +1,5 @@ +include tincan.py +include setup.py +include README.txt +include LICENSE.txt +include bin/* diff -r 25fdd985d046 -r d59811b95a62 README.html --- a/README.html Mon Jul 01 09:44:51 2019 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,93 +0,0 @@ - - - - - Introducing TinCan - - - -

Introducing TinCan, a “Code-Behind” MVC Framework for Bottle

-

Introduction

-

TinCan is a Python 3 code-behind web framework implemented in the Bottle - microframework. As with Bottle, all the code is in one module, and there - is no direct dependency on anything that is not part of the standard - Python library (except of course for Bottle itself).

-

The default templating engine for TinCan is Chameleon. - TinCan adds Chameleon as a fully-supported templating engine for Bottle. - Any template engine supported by Bottle can be used to render TinCan - Pages.

-

Why Do This?

-

In short, there is too much repeating oneself in most all Python web - frameworks (and this includes Bottle). One is always saying “this is - controller foo, whose view is in the template foo.pt, at route /foo.”

-

That’s a lot more busywork than just writing foo.php - or foo.cgi, so many simple webapps end up being - implemented via the latter means. That’s unfortunate, as CGI isn’t very - resource-efficient, and there’s much nicer languages to code in than PHP - (such as Python :-) ). Worst of all, you now have logic and presentation - all scrambled together, never a good idea.

-

What if, instead, you could write foo.pspx and - foo.py, and a framework would automatically - create the /foo.pspx route for you, much like - ASP.NET or JSP would for a .aspx or .jsp - file? The matching code-behind in the .py file - would be easily detected (same name, different extension) and - automatically associated with the template, of course. You could focus on - writing pages instead of repeating yourself saying the obvious over and - over again.

-

This is what TinCan lets you do.

-

Hang On, Code-Behind Isn’t MVC!

-

Why isn’t it? The model, as always, is the data and containing - core business logic. The template file defines the view presented to the - user, and the code-behind is the intermediary between the two. A - controller by any other name…

-

How Can There Be Multiple Views for One Controller?

-

Easily. Take a look at the #python header directive.

-

Multiple Controllers for One View?

-

Personally, I don’t think this is the best of ideas. Just because two - controllers might be able to share a view now does not mean they - will continue to in the future. Then you change one (controller, view) - pair and another controller someplace else breaks!

-

However, if you insist, check out the #template and #hidden - header directives.

-

But This Causes Less SEO-Friendly Routes!

-

First, this is not always important. Sometimes, all you want to do is get - a small, simple, special-purpose, site up and running with a minimum of - busywork. Why should you be forced to do more work just because that extra - work benefits someone else?

-

Second, when it is, you can always use RewriteRule (Apache) - rewrite (nginx), or the equivalent in your favorite Web - server, and code your templates to use the SEO-friendly version of your - URL’s. With TinCan sitting behind a good, production-grade web server, you - get the best of both worlds: fast, simple deployment when you want it, and - SEO-friendly URL’s when you want it.

-

But What about Routing Things Based on Both Path and Method?

-

That’s easy enough to do, as TinCan is implemented on top of Bottle. You - can add your Bottle routes, using the @route decorator on - your controller methods, same as always. Just stick them in the same - start-up script you use to launch your TinCan files.

-

If for some reason you don’t want to mess with manually creating routes - and associating them with controllers in Bottle (even in cases like this - where it arguably makes sense), and want to do everything the - TinCan way, you can create a set of hidden (using the #hidden - directive) pages and a main dummy page whose code-behind forwards (page.request.app.forward) - to the appropriate hidden page depending on request method.

-

What about Launching Multiple TinCan Webapps?

-

It works just as well (and just as poorly) as launching multiple Bottle - webapps. Note that the big limitation here is Python’s module subsystem; - there is only one. Thus, all webapps share the same module path. There is - no way to have one webapp using an older version of a given module served - by the same server as another using a newer version, save renaming one of - the modules. This is a Python issue, not a Bottle issue or a TinCan issue.

-

Note that TinCan bypasses the Python module cache and manages its own - importing of code-behind files, so there is no problem if you have - multiple webapps using the same relative URL paths. TinCan will keep all - those code-behind files straight; it will not confuse one webapp’s /index.py with another’s.

-

What about Bottle Plugins?

-

I am working on adding support for these.

- - diff -r 25fdd985d046 -r d59811b95a62 README.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/README.txt Thu Jul 04 08:52:15 2019 -0700 @@ -0,0 +1,4 @@ +This is the TinCan web framework. + +For more information, see the TinCan home page: +http://bartsent.com/tincan.pspx \ No newline at end of file diff -r 25fdd985d046 -r d59811b95a62 bin/install-static --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/install-static Thu Jul 04 08:52:15 2019 -0700 @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# Install the static files from the source webapp tree into the target +# directory. + +# I m p o r t s + +import os, sys +from argparse import ArgumentParser +import shutil +from stat import S_ISDIR, S_ISREG +from tincan import _casef + +# V a r i a b l e s + +NOT_STATIC = set([".pspx", ".pt", ".py", ".pyc"]) +MYNAME = os.path.basename(sys.argv[0]) +WINF = "WEB-INF" +copyfile = shutil.copy2 + +# F u n c t i o n s + +def dir_must_exist(d): + if not os.path.isdir(d): + sys.stderr.write( + "{0}: {1!r} - no such directory\n".format(MYNAME, d)) + sys.exit(2) + +def copydir(source, target, exclude=True): + for entry in os.listdir(source): + if exclude and _casef(entry, "upper") == WINF: + continue + if entry.startswith(".") or _casef(os.path.splitext(entry)[1]) in NOT_STATIC: + continue + spath = os.path.join(source, entry) + tpath = os.path.join(target, entry) + stype = os.stat(spath).st_mode + try: + ttype = os.stat(tpath).st_mode + except FileNotFoundError: + ttype = None + if S_ISREG(stype): + if ttype is not None and not S_ISREG(ttype): + sys.stderr.write( + "{0}: {1!r} not a file\n".format(MYNAME, tpath)) + sys.exit(1) + if args.move: + print("mv", repr(spath), repr(tpath)) + shutil.move(spath, tpath) + else: + print("cp", repr(spath), repr(tpath)) + copyfile(spath, tpath) + elif S_ISDIR(stype): + if ttype is None: + print("mkdir", repr(tpath)) + os.mkdir(tpath) + elif not S_ISDIR(ttype): + sys.stderr.write( + "{0}: {1!r} not a directory\n".format(MYNAME, tpath)) + sys.exit(1) + copydir(spath, tpath, False) + else: + sys.stderr.write( + "{0}: warning - {1!r} not a file or directory\n".format(spath)) + +# M a i n P r o g r a m + +parser = ArgumentParser(prog=sys.argv[0], usage="%(prog)s [options] source target") +group = parser.add_mutually_exclusive_group() +group.add_argument("-m", "--move", + action="store_true", help="move files instead of copying them") +group.add_argument("-n", "--no-preserve", + action="store_true", help="DO NOT preserve modes, times, etc.") +parser.add_argument("source", nargs=1) +parser.add_argument("target", nargs=1) +args = parser.parse_args(sys.argv[1:]) +source = args.source[0] +target = args.target[0] + +if args.no_preserve: + copyfile = shutil.copyfile + +dir_must_exist(source) +dir_must_exist(os.path.join(source, WINF)) + +try: + if not os.path.isdir(target): + print("mkdir", repr(target)) + os.mkdir(target) + copydir(source, target) +except (OSError, shutil.Error) as e: + sys.stderr.write("{0}: {1!s}\n".format(MYNAME, e)) + sys.exit(1) + +sys.exit(0) diff -r 25fdd985d046 -r d59811b95a62 bin/launch --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/launch Thu Jul 04 08:52:15 2019 -0700 @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +# XXX - This code must not be in tincan.py, because the built-in class +# loader will then confuse __main__.Page and tincan.Page, and fail to +# locate the code-behind. + +# I m p o r t s + +import os, sys +from argparse import ArgumentParser +import logging +from tincan import launch, ENCODING + +# V a r i a b l e s + +MYNAME = os.path.basename(sys.argv[0]) + +# M a i n P r o g r a m + +parser = ArgumentParser(prog=sys.argv[0], usage="%(prog)s [options] [directory [path]]") +opt = parser.add_argument +opt("-b", "--bind", default="localhost", help="address to bind to (default: localhost)") +opt("-c", "--compile", action="store_true", help="compile .py files only; do not run a server") +opt("-d", "--debug", action="store_true", help="enable debug mode") +opt("-e", "--encoding", default=ENCODING, help="encoding to use (default {0})".format(ENCODING)) +opt("-f", "--force", action="store_true", help="do not abort on errors") +opt("-p", "--port", default=8080, help="port to listen on (default: 8080)") +opt("-s", "--static", action="store_true", help="serve static files") +opt("directory", default=".", help="directory to serve", nargs='?') +opt("path", default="/", help="URL path to serve", nargs='?') +args = parser.parse_args(sys.argv[1:]) + +mylog = logging.getLogger(MYNAME) +mylog.addHandler(logging.StreamHandler()) +mylog.setLevel(logging.DEBUG if args.debug else logging.INFO) + +app, errors = launch(fsroot=args.directory, urlroot=args.path, logger=mylog, + encoding=args.encoding, static=args.static) +if errors: + action = "continuing" if args.force else "aborting" + sys.stderr.write("{0}: {1} error{2} detected, {3}\n".format( + MYNAME, errors, "" if errors == 1 else "s", action)) + if not args.force: sys.exit(1) + +if not args.compile: + app.run(host=args.bind, port=args.port) + +sys.exit(1 if errors else 0) diff -r 25fdd985d046 -r d59811b95a62 install-static --- a/install-static Mon Jul 01 09:44:51 2019 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,94 +0,0 @@ -#!/usr/bin/env python3 -# Install the static files from the source webapp tree into the target -# directory. - -# I m p o r t s - -import os, sys -from argparse import ArgumentParser -import shutil -from stat import S_ISDIR, S_ISREG -from tincan import _casef - -# V a r i a b l e s - -NOT_STATIC = set([".pspx", ".pt", ".py", ".pyc"]) -MYNAME = os.path.basename(sys.argv[0]) -WINF = "WEB-INF" -copyfile = shutil.copy2 - -# F u n c t i o n s - -def dir_must_exist(d): - if not os.path.isdir(d): - sys.stderr.write( - "{0}: {1!r} - no such directory\n".format(MYNAME, d)) - sys.exit(2) - -def copydir(source, target, exclude=True): - for entry in os.listdir(source): - if exclude and _casef(entry, "upper") == WINF: - continue - if entry.startswith(".") or _casef(os.path.splitext(entry)[1]) in NOT_STATIC: - continue - spath = os.path.join(source, entry) - tpath = os.path.join(target, entry) - stype = os.stat(spath).st_mode - try: - ttype = os.stat(tpath).st_mode - except FileNotFoundError: - ttype = None - if S_ISREG(stype): - if ttype is not None and not S_ISREG(ttype): - sys.stderr.write( - "{0}: {1!r} not a file\n".format(MYNAME, tpath)) - sys.exit(1) - if args.move: - print("mv", repr(spath), repr(tpath)) - shutil.move(spath, tpath) - else: - print("cp", repr(spath), repr(tpath)) - copyfile(spath, tpath) - elif S_ISDIR(stype): - if ttype is None: - print("mkdir", repr(tpath)) - os.mkdir(tpath) - elif not S_ISDIR(ttype): - sys.stderr.write( - "{0}: {1!r} not a directory\n".format(MYNAME, tpath)) - sys.exit(1) - copydir(spath, tpath, False) - else: - sys.stderr.write( - "{0}: warning - {1!r} not a file or directory\n".format(spath)) - -# M a i n P r o g r a m - -parser = ArgumentParser(prog=sys.argv[0], usage="%(prog)s [options] source target") -group = parser.add_mutually_exclusive_group() -group.add_argument("-m", "--move", - action="store_true", help="move files instead of copying them") -group.add_argument("-n", "--no-preserve", - action="store_true", help="DO NOT preserve modes, times, etc.") -parser.add_argument("source", nargs=1) -parser.add_argument("target", nargs=1) -args = parser.parse_args(sys.argv[1:]) -source = args.source[0] -target = args.target[0] - -if args.no_preserve: - copyfile = shutil.copyfile - -dir_must_exist(source) -dir_must_exist(os.path.join(source, WINF)) - -try: - if not os.path.isdir(target): - print("mkdir", repr(target)) - os.mkdir(target) - copydir(source, target) -except (OSError, shutil.Error) as e: - sys.stderr.write("{0}: {1!s}\n".format(MYNAME, e)) - sys.exit(1) - -sys.exit(0) diff -r 25fdd985d046 -r d59811b95a62 launch --- a/launch Mon Jul 01 09:44:51 2019 -0700 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,47 +0,0 @@ -#!/usr/bin/env python3 -# XXX - This code must not be in tincan.py, because the built-in class -# loader will then confuse __main__.Page and tincan.Page, and fail to -# locate the code-behind. - -# I m p o r t s - -import os, sys -from argparse import ArgumentParser -import logging -from tincan import launch, ENCODING - -# V a r i a b l e s - -MYNAME = os.path.basename(sys.argv[0]) - -# M a i n P r o g r a m - -parser = ArgumentParser(prog=sys.argv[0], usage="%(prog)s [options] [directory [path]]") -opt = parser.add_argument -opt("-b", "--bind", default="localhost", help="address to bind to (default: localhost)") -opt("-c", "--compile", action="store_true", help="compile .py files only; do not run a server") -opt("-d", "--debug", action="store_true", help="enable debug mode") -opt("-e", "--encoding", default=ENCODING, help="encoding to use (default {0})".format(ENCODING)) -opt("-f", "--force", action="store_true", help="do not abort on errors") -opt("-p", "--port", default=8080, help="port to listen on (default: 8080)") -opt("-s", "--static", action="store_true", help="serve static files") -opt("directory", default=".", help="directory to serve", nargs='?') -opt("path", default="/", help="URL path to serve", nargs='?') -args = parser.parse_args(sys.argv[1:]) - -mylog = logging.getLogger(MYNAME) -mylog.addHandler(logging.StreamHandler()) -mylog.setLevel(logging.DEBUG if args.debug else logging.INFO) - -app, errors = launch(fsroot=args.directory, urlroot=args.path, logger=mylog, - encoding=args.encoding, static=args.static) -if errors: - action = "continuing" if args.force else "aborting" - sys.stderr.write("{0}: {1} error{2} detected, {3}\n".format( - MYNAME, errors, "" if errors == 1 else "s", action)) - if not args.force: sys.exit(1) - -if not args.compile: - app.run(host=args.bind, port=args.port) - -sys.exit(1 if errors else 0) diff -r 25fdd985d046 -r d59811b95a62 setup.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/setup.py Thu Jul 04 08:52:15 2019 -0700 @@ -0,0 +1,39 @@ +#!/usr/bin/env python + +import sys +from setuptools import setup + +if sys.version_info < (3, 4): + raise NotImplementedError("Sorry, you need at least Python 3.4 TinCan.") + +setup(name='TinCanFramework', + version='0.1.0', + description='Simple code-behind WSGI framework for small web-applications, implemented on top of bottle.', + author="David W. Barts", + author_email="tincan@bartsent.com", + url='http://bartsent.com/tincan.pspx', + py_modules=['tincan'], + scripts=['bin/install-static', 'bin/launch'], + license='MIT', + platforms='any', + install_requires=['bottle>=0.12.0'], + classifiers=['Development Status :: 4 - Beta', + "Operating System :: OS Independent", + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries', + 'Topic :: Internet :: WWW/HTTP :: HTTP Servers', + 'Topic :: Internet :: WWW/HTTP :: WSGI', + 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', + 'Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware', + 'Topic :: Internet :: WWW/HTTP :: WSGI :: Server', + 'Topic :: Software Development :: Libraries :: Application Frameworks', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + ], + ) +