changeset 65:d59811b95a62 draft

Package-ize it.
author David Barts <n5jrn@me.com>
date Thu, 04 Jul 2019 08:52:15 -0700
parents 25fdd985d046
children f33cb3e93473
files LICENSE.txt MANIFEST.in README.html README.txt bin/install-static bin/launch install-static launch setup.py
diffstat 9 files changed, 210 insertions(+), 234 deletions(-) [+]
line wrap: on
line diff
--- /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.
--- /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/*
--- 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 @@
-<!DOCTYPE html>
-<html>
-  <head>
-    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
-    <title>Introducing TinCan</title>
-    <style type="text/css">
-      .kbd { font-family: monospace; }
-    </style>
-  </head>
-  <body>
-    <h1>Introducing TinCan, a “Code-Behind” MVC Framework for Bottle</h1>
-    <h2>Introduction</h2>
-    <p>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).</p>
-    <p>The default templating engine for TinCan is <a href="https://chameleon.readthedocs.io/en/latest/">Chameleon</a>.
-      TinCan adds Chameleon as a fully-supported templating engine for Bottle.
-      Any template engine supported by Bottle can be used to render TinCan
-      Pages.</p>
-    <h2>Why Do This?</h2>
-    <p>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 <span class="kbd">foo</span>, whose view is in the template <span
-        class="kbd">foo.pt</span>, at route <span class="kbd">/foo</span>.”</p>
-    <p>That’s a lot more busywork than just writing <span class="kbd">foo.php</span>
-      or <span class="kbd">foo.cgi</span>, 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.</p>
-    <p>What if, instead, you could write <span class="kbd">foo.pspx</span> and
-      <span class="kbd">foo.py</span>, and a framework would automatically
-      create the <span class="kbd">/foo.pspx</span> route for you, much like
-      ASP.NET or JSP would for a <span class="kbd">.aspx</span> or <span class="kbd">.jsp</span>
-      file? The matching code-behind in the <span class="kbd">.py</span> 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. </p>
-    <p>This is what TinCan lets you do.</p>
-    <h2>Hang On, Code-Behind Isn’t MVC!</h2>
-    <p>Why <em>isn’t</em> 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…</p>
-    <h2>How Can There Be Multiple Views for One Controller?</h2>
-    <p>Easily. Take a look at the <code>#python</code> header directive. </p>
-    <h2>Multiple Controllers for One View?</h2>
-    <p>Personally, I don’t think this is the best of ideas. Just because two
-      controllers might be able to share a view <em>now</em> does not mean they
-      will continue to in the future. Then you change one (controller, view)
-      pair and another controller someplace else breaks!</p>
-    <p>However, if you insist, check out the <code>#template</code> and <code>#hidden</code>
-      header directives. </p>
-    <h2>But This Causes Less SEO-Friendly Routes!</h2>
-    <p>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 <em>else</em>?</p>
-    <p>Second, when it is, you can always use <code>RewriteRule</code> (Apache)
-      <code>rewrite</code> (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. </p>
-    <h2>But What about Routing Things Based on Both Path and Method?</h2>
-    <p>That’s easy enough to do, as TinCan is implemented on top of Bottle. You
-      can add your Bottle routes, using the <code>@route</code> decorator on
-      your controller methods, same as always. Just stick them in the same
-      start-up script you use to launch your TinCan files.</p>
-    <p>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 <em>everything</em> the
-      TinCan way, you can create a set of hidden (using the <code>#hidden</code>
-      directive) pages and a main dummy page whose code-behind forwards (<code>page.request.app.forward</code>)
-      to the appropriate hidden page depending on request method. </p>
-    <h2>What about Launching Multiple TinCan Webapps?</h2>
-    <p>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.</p>
-    <p>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 <span
-        class="kbd">/index.py</span> with another’s.</p>
-    <h2>What about Bottle Plugins?</h2>
-    <p>I am working on adding support for these.</p>
-  </body>
-</html>
--- /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
--- /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)
--- /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)
--- 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)
--- 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)
--- /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',
+                   ],
+      )
+