一组基于标准 Python 线程模块构建的有用工具
项目描述
WebPie(web-py 的另一种拼写方式)是基于 WSGI 标准的 Python 的 Web 应用程序开发框架。WebPie 使开发线程安全的面向对象的 Web 应用程序变得简单。
WebPie 中的 Hello World
这是您可以编写的最简单的 WebPie 应用程序:
# hello_world.py
from webpie import WPApp, WPHandler
class MyHandler(WPHandler): # 1
def hello(self, request, relpath): # 2
return "Hello, World!\n" # 3
application = WPApp(MyHandler) # 4
让我们逐行查看代码:
我们创建了 MyHandler 类,它将处理 HTTP 请求。为了使用 WebPie,它必须是 WPHandler 类的子类。
我们定义了一个网络方法“hello”,当请求像http://host.org/hello这样的 URL 时会调用它。
它总是会返回文本“Hello, World!”。
最后,我们创建了一个 WSGI 应用程序作为 WPApp 类的实例,并将 MyHandler 类作为参数传递给它。
现在我们可以将我们的应用程序插入到任何 WSGI 框架中,例如 uWSGI 或 Apache httpd,例如:
$ uwsgi --http :8080 --wsgi-file hello_world.py
并尝试一下:
$ curl http://localhost:8080/hello
Hello world!
$
如果你不想使用 uWSGI 或类似的框架,你可以使用 WebPie 自己的 HTTP 服务器将你的应用程序放到 web 上:
# hello_world_server.py
from webpie import WPApp, WPHandler, run_server
import time
class MyHandler(WPHandler):
def hello(self, request, relpath):
return "Hello, World!\n"
application = WPApp(MyHandler)
application.run_server(8080) # run the server at port 8080
$ python hello_world_server.py &
$ curl http://localhost:8080/hello
Hello world!
$
网址结构
请注意,MyHandler 类只有一个方法“hello”,它映射到 URL 路径“hello”。这是 WebPie 中的一般规则 - 处理程序类的方法一对一映射到 URI 路径的元素。例如,我们可以向我们的服务器添加另一个名为“time”的方法:
from webpie import WPApp, WPHandler
import time
class MyHandler(WPHandler):
def hello(self, request, relpath):
return "Hello, World!\n"
def time(self, request, relpath):
return time.ctime()+"\n"
application = WPApp(MyHandler)
application.run_server(8080)
现在我们的处理程序可以处理两种类型的请求,它可以打招呼,它可以告诉本地时间:
$ curl http://localhost:8080/hello
Hello, World!
$ curl http://localhost:8080/time
Sun May 5 06:47:15 2019
$
请注意,处理程序方法名称自动成为 URL 路径的一部分。
如果您想将处理程序拆分为不同的类以更好地组织代码,您可以在应用程序中嵌套处理程序类。例如,我们可能希望有一个处理程序专注于报告时间,而另一个处理程序负责打招呼:
# time_hello_split.py
from webpie import WPApp, WPHandler
import time
class HelloHandler(WPHandler):
def hello(self, request, relpath):
return "Hello, World!\n"
class ClockHandler(WPHandler):
def time(self, request, relpath):
return time.ctime()+"\n", "text/plain"
class TopHandler(WPHandler):
def __init__(self, *params):
WPHandler.__init__(self, *params)
self.greet = HelloHandler(*params)
self.clock = ClockHandler(*params)
application = WPApp(TopHandler)
application.run_server(8080)
WebPie 应用程序被赋予顶级处理程序类作为参数。它将为每个 Web 请求创建一个处理程序实例。顶级处理程序可以递归地创建子处理程序。这种递归处理程序结构一对一地映射到 URL 结构。URI 只是从顶级处理程序通过其子处理程序到其中之一的方法的路径:
$ curl http://localhost:8080/greet/hello
Hello, World!
$ curl http://localhost:8080/clock/time
Sun May 5 06:49:14 2019
$
例如,要查找 URI “/greet/hello” 的方法,WebPie 从顶部处理程序开始,找到其子处理程序 Greeter 类的“greet”,然后调用其“hello”方法。
树中的非叶处理程序可以有自己的方法。例如:
# time_hello_split2.py
from webpie import WPApp, WPHandler
import time
class HelloHandler(WPHandler):
def hello(self, request, relpath):
return "Hello, World!\n"
class ClockHandler(WPHandler):
def time(self, request, relpath):
return time.ctime()+"\n", "text/plain"
class TopHandler(WPHandler):
def __init__(self, *params, **kv):
WPHandler.__init__(self, *params, **kv)
self.greet = HelloHandler(*params, **kv)
self.clock = ClockHandler(*params, **kv)
def version(self, request, relpath): # non-leaf handler can have a web method
return "1.0.3"
application = WPApp(TopHandler)
application.run_server(8080)
$ curl http://localhost:8080/version
1.0.2
应用程序和处理程序
WPApp 对象在 Web 服务器实例启动时创建一次,并且一直持续到服务器停止,而 WPHandler 对象树是从头开始为每个单独的 HTTP 请求创建的。Handler 对象的 App 成员总是指向它的 app 对象。这允许应用程序对象保留一些持久性信息并让处理程序对象访问它。例如,或时钟应用程序还可以维护它已收到的请求数:
# time_count.py
from webpie import WPApp, WPHandler
import time
class Handler(WPHandler):
def time(self, request, relpath):
self.App.Counter += 1
return time.ctime()+"\n", "text/plain"
def count(self, request, relpath):
return str(self.App.Counter)+"\n"
class App(WPApp):
def __init__(self, handler_class):
WPApp.__init__(self, handler_class)
self.Counter = 0
application = App(Handler)
application.run_server(8080)
$ curl http://localhost:8080/time
Sun May 5 08:10:12 2019
$ curl http://localhost:8080/time
Sun May 5 08:10:14 2019
$ curl http://localhost:8080/count
2
$ curl http://localhost:8080/time
Sun May 5 08:10:17 2019
$ curl http://localhost:8080/count
3
当然,按照它的编写方式,我们的应用程序不是很安全,但是有一种简单的方法可以修复它。我们稍后会谈到这个。
Web 方法详解
WebPie 服务器处理程序方法有 2 个固定参数和可选的关键字参数。
第一个参数是请求对象,它封装了有关传入 HTTP 请求的所有信息。目前 WebPie 使用 WebOb 库 Request 和 Response 类来处理 HTTP 请求和响应。
论据
大多数情况下,Web 方法如下所示:
def method(self, request, relpath, **url_args):
# ...
return response
Web 方法参数是:
要求
request 是从 WSGI 环境构建的 WebOb 请求对象。为方便起见,它也可用作处理程序的 Request 成员。
关系路径
有时,在沿着处理程序树查找处理请求的方法时,在目标处理程序方法的名称之后会有一些未使用的 URI 部分。例如,在我们的时钟示例中,我们可能希望通过以下方式构造我们的 URL 以指定我们希望查看的当前时间的字段:
http://localhost:8080/time/month # month only
http://localhost:8080/time/minute # minute only
http://localhost:8080/time # whole day/time
在这种情况下,我们希望“时间”方法处理所有类型的请求并知道要返回日期/时间的哪一部分。这是执行此操作的代码:
from webpie import WPApp, WPHandler
from datetime import datetime
class MyHandler(WPHandler):
def time(self, request, relpath):
t = datetime.now()
if not relpath:
return str(t)+"\n"
elif relpath == "year":
return str(t.year)+"\n"
elif relpath == "month":
return str(t.month)+"\n"
elif relpath == "day":
return str(t.day)+"\n"
elif relpath == "hour":
return str(t.hour)+"\n"
elif relpath == "minute":
return str(t.minute)+"\n"
elif relpath == "second":
return str(t.second)+"\n"
application = WPApp(MyHandler)
application.run_server(8080)
url_args
另一种可能更传统的方法是使用所谓的查询参数来指定日期/时间表示的格式,例如:
http://localhost:8080/time?field=minute
WebPie 总是解析查询参数并将它们传递给处理程序方法,就好像它们是关键字参数一样。例如,我们可以编写从当前时间提取字段的方法,如下所示:
# time_args.py
from webpie import WPApp, WPHandler
from datetime import datetime
class MyHandler(WPHandler):
def time(self, request, relpath, field="all"):
t = datetime.now()
if field == "all":
return str(t)+"\n"
elif field == "year":
return str(t.year)+"\n"
elif field == "month":
return str(t.month)+"\n"
elif field == "day":
return str(t.day)+"\n"
elif field == "hour":
return str(t.hour)+"\n"
elif field == "minute":
return str(t.minute)+"\n"
elif field == "second":
return str(t.second)+"\n"
WPApp(MyHandler).run_server(8080)
然后这样称呼它:
$ curl http://localhost:8080/time
2019-05-05 08:39:49.593855
$ curl "http://localhost:8080/time?field=month"
5
$ curl "http://localhost:8080/time?field=year"
2019
返回值
Web 方法的输出是一个 Response 对象。方便的是,有多种方法可以从 web 方法返回某些内容。最终,它们都用于生成和返回 Response 对象。以下是来自 Web 对象的可能返回列表以及框架如何将输出转换为 Response 对象:
返回 |
例子 |
等效响应对象 |
|---|---|---|
响应对象 |
响应(“确定”) |
相同 - 响应(“OK”) |
文本 |
“你好世界” |
响应(“你好世界”) |
文本,内容类型 |
“确定”、“文本/纯文本” |
响应(“OK”,content_type=”text/plain”) |
文本,状态 |
“错误”,500 |
响应(“错误”,状态码=500) |
文本、状态、内容类型 |
“错误”,500,“文本/纯文本” |
响应(“错误”,状态代码=500,内容类型=“文本/纯文本”) |
文字、标题 |
“确定”,{“内容类型”:“文本/纯文本”} |
响应(“OK”,标题={“Content-Type”:“text/plain”}) |
列表 |
[“你好世界”] |
响应(app_iter=[“你好”,“世界”]) |
可迭代的 |
(x 代表 [“hi”,”there”] 中的 x) |
Response(app_iter=(x for x in [“hi”,”there”])) |
可迭代的,内容类型 |
||
可迭代,状态,内容类型 |
||
可迭代,状态,标头 |
响应正文可以作为单个字符串或字节对象返回,也可以作为字符串或字节对象的列表或作为可迭代对象(生成器或迭代器)返回,从而生成字符串或字节对象的序列。如果 handler 方法返回字符串,在 Python3 下,会使用 UTF-8 转换为字节。如果要使用其他编码,则必须在从处理程序方法返回之前将字符串转换为字节。
静态内容
有时应用程序需要提供静态内容,如 HTML 文档、CSS 样式表、JavaScript 代码。WebPie App 可以配置为从文件系统中的某个目录提供静态文件。
class MyHandler(WPHandler):
#...
class MyApp(WPApp):
#...
application = MyApp(MyHandler,
static_enabled = True,
static_path = "/static",
static_location = "./scripts")
application.run_server(8002)
如果您运行这样的应用程序,对诸如“ http://…./static/code.js ”之类的 URL 的请求将导致文件本地文件 ./scripts/code.js 的传递。static_location 可以是相对于应用程序运行的工作目录,也可以是绝对路径。
因为从本地文件系统提供文件是一个潜在的安全漏洞,所以必须使用 static_enabled=True 显式启用此功能。static_path 和 static_locations 有默认值:
static_path = "/static"
static_location = "./static"
线程应用
WebPie 提供了多种机制来构建线程安全的应用程序。在多线程环境中工作时,WebPie Handler 对象在它们自己的线程中并发创建,每个请求一个,而 WebApp 对象只创建一次,并由处理请求的所有线程共享。此功能使使用 App 对象进行处理程序间同步成为可能。App 对象有自己的锁对象,线程可以通过两种不同的方式使用它:
原子装饰器
使用“原子”装饰器装饰 web 方法使 web 方法具有原子性,即如果处理程序线程进入这样的方法,同一应用程序的任何其他处理程序线程将在进入任何原子方法之前阻塞,直到第一个线程从该方法返回.
例如:
from webpie import WPApp, WPHandler, atomic
class MyApp(WPApp):
def __init__(self, root_class):
WPApp.__init__(self, root_class)
self.Memory = {}
class Handler(WPHandler):
@atomic
def set(self, req, relpath, name=None, value=None, **args):
self.App.Memory[name]=value
return "OK\n"
@atomic
def get(self, req, relpath, name=None, **args):
return self.App.Memory.get(name, "(undefined)")+"\n"
application = MyApp(Handler)
application.run_server(8002)
您还可以装饰应用程序的方法。例如:
from webpie import WPApp, WPHandler, atomic
class MyApp(WPApp):
RecordSize = 10
def __init__(self, root_class):
WPApp.__init__(self, root_class)
self.Record = []
@atomic
def add(self, value):
if value in self.Record:
self.Record.remove(value)
self.Record.insert(0, value)
if len(self.Record) > self.RecordSize:
self.Record = self.Record[:self.RecordSize]
@atomic
def find(self, value):
try: i = self.Record.index(value)
except ValueError:
return "not found"
self.Record.pop(i)
self.Record.insert(0, value)
return str(i)
class Handler(WPHandler):
def add(self, req, relpath, **args):
return self.App.add(relpath)
def find(self, req, relpath, **args):
return self.App.find(relpath)
application = MyApp(Handler)
application.run_server(8002)
应用对象作为上下文管理器
另一个实现临界区的方法是使用 App 对象作为上下文管理器:
from webpie import WPApp, WPHandler
class MyApp(WPApp):
def __init__(self, root_class):
WPApp.__init__(self, root_class)
self.Memory = {}
class Handler(WPHandler):
def set(self, req, relpath, name=None, value=None, **args):
with self.App:
self.App.Memory[name]=value
return "OK\n"
def get(self, req, relpath, name=None, **args):
with self.App:
return self.App.Memory.get(name, "(undefined)") + "\n"
application = MyApp(Handler)
application.run_server(8002)
会话管理
Jinja2 环境
WebPie 了解 Jinja2 模板库并提供了一些使用它的快捷方式。
要使您的应用程序与 Jinja2 一起工作,您需要先初始化 Jinja2 环境:
from webpie import WPApp, WPHandler
class MyHandler(WPHandler):
# ...
class MyApp(WPApp):
# ...
application = MyApp(MyHandler)
application.initJinjaEnvironment(
tempdirs = [...],
filters = {...},
globals = {...}
)
initJinjaEnvironment 方法接受 3 个参数:
tempdirs - 查找 Jinja2 模板的目录列表,
过滤器 - 带有过滤器名称和过滤器功能的字典以添加到环境中,
globals - 具有“全局”变量的字典,在渲染模板时将添加到变量列表中
以下是此类应用程序和相应模板的示例:
# templates.py
from webpie import WPApp, WPHandler
import time
Version = "1.3"
def format_time(t):
return time.ctime(t)
class MyHandler(WPHandler):
def time(self, request, relpath):
return self.render_to_response("time.html", t=time.time())
application = WPApp(MyHandler)
application.initJinjaEnvironment(
["samples"],
filters={ "format": format_time },
globals={ "version": Version }
)
application.run_server(8080)
模板 samples/time.html 是:
<html>
<body>
<p>Current time is {{t|format}}</p>
<p style=<s>"float:right"</s>><i>Version: {{version}}</i></p>
</body>
</html>
在此示例中,应用程序使用“samples”作为模板位置初始化 Jinja2 环境,函数“format_time”成为用于将数字时间显示为日期/时间字符串的过滤器,并且“全局”变量“version”设置为编码。
然后,处理程序调用从 WPHandler 继承的“render_to_response”方法,以将当前时间作为“t”参数传递给模板“time.html”,并将“版本”作为全局变量隐式传递给渲染。“render_to_response”方法渲染模板并返回正确构造的响应对象,内容类型设置为“text/html”。