├── MANIFEST.in
├── .gitattributes
├── cover.png
├── .gitmodules
├── urouter
├── context
│ ├── session.py
│ ├── request.py
│ └── response.py
├── logger.py
├── typeutil.py
├── mimetypes.py
├── config.py
├── regexutil.py
├── __init__.py
├── consts.py
├── pool
│ ├── queue.py
│ └── __init__.py
├── util.py
├── ruleutil.py
└── router.py
├── tools
├── bench_max_connections_server.py
└── bench_max_connections_client.py
├── auto_build.py
├── setup.py
├── .gitignore
├── README.md
└── LICENSE
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include *.mpy
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/majoson-chen/micropython-urouter/HEAD/cover.png
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "docs"]
2 | path = docs
3 | url = https://github.com/Li-Lian1069/micropython-urouter-docs
4 | branch = v0.1-alpha-simplified-chinese
5 |
--------------------------------------------------------------------------------
/urouter/context/session.py:
--------------------------------------------------------------------------------
1 | from .response import Response
2 | from .request import Request
3 |
4 |
5 | class Session():
6 | def init(self, resquets: Request, response: Response):
7 | pass
8 |
9 | def close(self):
10 | pass
11 |
--------------------------------------------------------------------------------
/urouter/logger.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- encoding: utf-8 -*-
3 | '''
4 | @File : logger.py
5 | @Time : 2021/07/10 17:36:04
6 | @Author : M-Jay
7 | @Contact : m-jay-1376@qq.com
8 | '''
9 | # the logger mg.
10 |
11 | import ulogger
12 | from .config import CONFIG
13 |
14 | handler = ulogger.Handler(CONFIG.logger_level)
15 |
16 |
17 | def get(name: str) -> ulogger.Logger:
18 | return ulogger.Logger(
19 | name=name,
20 | handlers=(handler,)
21 | )
22 |
--------------------------------------------------------------------------------
/urouter/typeutil.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- encoding: utf-8 -*-
3 | '''
4 | @File : typeutil.py
5 | @Time : 2021/07/10 17:36:31
6 | @Author : M-Jay
7 | @Contact : m-jay-1376@qq.com
8 |
9 | some custom type here.
10 | '''
11 |
12 | import collections
13 |
14 | ruleitem = collections.namedtuple(
15 | "rule_item",
16 | ("weight", "comper", "func", "methods", "url_vars")
17 | )
18 |
19 | routetask = collections.namedtuple(
20 | "route_task",
21 | ("client", "addr", "http_head", "func", "url_vars")
22 | )
23 |
24 | httphead = collections.namedtuple(
25 | "http_head",
26 | ("method", "uri", "version")
27 | )
28 |
29 | headeritem = collections.namedtuple(
30 | "header_item",
31 | ("key", "value")
32 | )
--------------------------------------------------------------------------------
/tools/bench_max_connections_server.py:
--------------------------------------------------------------------------------
1 | if __name__ == "__main__":
2 |
3 | host = input("[i] Input your board host: ")
4 |
5 | import socket
6 | clients = []
7 | import time
8 | while True:
9 | client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
10 | client.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # Fastely reuse-tcp
11 | client.settimeout(10)
12 | try:
13 | client.connect((host, 8848))
14 | clients.append(client)
15 | time.sleep(0.3)
16 | except:
17 | print("test over.")
18 | for sock in clients:
19 | try:
20 | sock.close()
21 | except:
22 | pass
23 | break
24 |
25 |
--------------------------------------------------------------------------------
/tools/bench_max_connections_client.py:
--------------------------------------------------------------------------------
1 | import socket
2 |
3 |
4 | def test():
5 | clients = []
6 | host = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
7 | host.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # Fastely reuse-tcp
8 | host.bind(("0.0.0.0", 8848))
9 | host.listen(0)
10 |
11 | while True:
12 | try:
13 | client, addr = host.accept()
14 | clients.append(client)
15 | print("Accept: ", addr)
16 | except OSError:
17 | # max connect.
18 | print("test over, The max connection quantity is: ", len(clients))
19 | host.close()
20 | for sock in clients:
21 | try:
22 | sock.close()
23 | except:
24 | pass
25 | return
26 |
--------------------------------------------------------------------------------
/auto_build.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # Build this project automaticly.
3 | import os
4 | from pathlib import Path
5 | import shutil
6 | import mpy_cross
7 |
8 | cwd = Path('.')
9 |
10 | # check mpy-corss
11 | assert not os.system('mpy-cross'), "mpy-cross not found!"
12 |
13 | # backup source
14 | shutil.copy(
15 | src = cwd / 'urouter',
16 | dst = cwd / 'urouter_bak'
17 | )
18 |
19 | # compile code
20 | for file in (cwd / 'urouter').iterdir():
21 | file: Path
22 |
23 | if file.name == '__init__.py':
24 | continue
25 | # skip
26 | else:
27 | new_file: Path = (file.parent / (file.stem() + ".mpy"))
28 |
29 | cmd = f'mpy-cross -s {file.absolute()}'
30 | os.system(cmd)
31 |
32 | assert new_file.exists(), f"Compile Failed: {new_file}"
33 | os.remove(file)
34 |
35 | # build dist
36 |
37 |
38 |
--------------------------------------------------------------------------------
/urouter/mimetypes.py:
--------------------------------------------------------------------------------
1 | # TODO: local-file database
2 | MAP: dict = {
3 | ".txt": "text/plain",
4 | ".htm": "text/html",
5 | ".html": "text/html",
6 | ".css": "text/css",
7 | ".csv": "text/csv",
8 | ".js": "application/javascript",
9 | ".xml": "application/xml",
10 | ".xhtml": "application/xhtml+xml",
11 | ".json": "application/json",
12 | ".zip": "application/zip",
13 | ".pdf": "application/pdf",
14 | ".ts": "application/typescript",
15 | ".woff": "font/woff",
16 | ".woff2": "font/woff2",
17 | ".ttf": "font/ttf",
18 | ".otf": "font/otf",
19 | ".jpg": "image/jpeg",
20 | ".jpeg": "image/jpeg",
21 | ".png": "image/png",
22 | ".gif": "image/gif",
23 | ".svg": "image/svg+xml",
24 | ".ico": "image/x-icon"
25 | # others : "application/octet-stream"
26 | }
27 |
28 |
29 | def get(suf: str, default: str = "application/octet-stream") -> str:
30 | """
31 | Pass in a file suffix, return its immetype
32 | """
33 | return MAP.get(suf, default)
34 |
--------------------------------------------------------------------------------
/urouter/config.py:
--------------------------------------------------------------------------------
1 | from ulogger import INFO
2 |
3 | class _CONFIG:
4 | def __init__(self):
5 | self.charset = 'utf-8'
6 | self._buff_size = 1024
7 | self._logger_level = INFO
8 | self.request_timeout = 7
9 | self.max_connections = 5
10 | self.debug = False
11 |
12 |
13 | def buff_size(self, app, value: int = None):
14 | """
15 | Set or get the buffer size of the response, the larger the value, the faster the processing speed.
16 | """
17 | if value:
18 | # set
19 | app.response._buf = bytearray(value)
20 | else:
21 | return len(app.response._buf)
22 |
23 | # =====================================
24 |
25 | @property
26 | def logger_level(self):
27 | return self._logger_level
28 |
29 | @logger_level.setter
30 | def logger_level(self, level):
31 | from .logger import handler
32 | self.set_logger_level = level
33 | handler.level = level
34 |
35 |
36 |
37 | CONFIG = _CONFIG()
--------------------------------------------------------------------------------
/urouter/regexutil.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- encoding: utf-8 -*-
3 | '''
4 | @File : regexutil.py
5 | @Time : 2021/07/10 17:37:02
6 | @Author : M-Jay
7 | @Contact : m-jay-1376@qq.com
8 | '''
9 | # some regex comper and template.
10 | import re
11 | from .typeutil import httphead
12 |
13 |
14 | # ==============================
15 | MATCH_STRING = "([^\d][^/|.]*)"
16 | MATCH_INT = "(\d*)"
17 | MATCH_FLOAT = "(\d*\.?\d*)"
18 | MATCH_PATH = "(.*)"
19 | VAR_VERI = "<(string|int|float|custom=.*):(\w+)>" # 匹配URL的规则是否为变量
20 | # ==============================
21 |
22 | FIRSTLINE_AGREEMENT = "(GET|POST|HEAD|PUT|DELETE|CONNECT|OPTIONS|TRACE|PATCH) (.*) (.*)"
23 | COMP_HTTP_FIRSTLINE = re.compile(FIRSTLINE_AGREEMENT)
24 |
25 | def comp_head(string: str) -> httphead:
26 | """Match the http firstline.
27 |
28 | :return: httphead(method, uri, http_version), if not matched, return None.
29 | """
30 | m = COMP_HTTP_FIRSTLINE.search(string)
31 |
32 | if m:
33 | return httphead(
34 | m.group(1),
35 | m.group(2),
36 | m.group(3)
37 | )
38 | else:
39 | return None
--------------------------------------------------------------------------------
/urouter/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright 2021 github@Li-Lian1069 m-jay.cn
2 | # GNU LESSER GENERAL PUBLIC LICENSE
3 | # Version 3, 29 June 2007
4 | # Copyright (C) 2007 Free Software Foundation, Inc.
"): 72 | return True 73 | elif string.find("
Hello World!
" 89 | 90 | app.serve_forever() 91 | ``` 92 | 这样, 当你在浏览器地址栏输入开发板的局域网ip并按下回车时, 你的浏览器就会显示出 `Hello World!` 了. 93 | 94 | #### 监听其他访问方式 95 | 有时候你可能需要使用 `POST` 方式获取一些数据, 我们可以设置让本框架监听 `POST` 的访问方式: 96 | ```python 97 | from urouter import uRouter, POST 98 | app = uRouter() 99 | 100 | @app.router("/login", methods=(POST,)) 101 | def login(): 102 | request = app.request 103 | form = request.form 104 | if form.get("user_name") == 'admin' and form.get("passwd") == "admin": 105 | return "Login succeed!" 106 | else: 107 | return "Login failed, check your username or you password!" 108 | ``` 109 | 在上面的例子中, 我们使用了 `request` 对象, 这个对象用来获取关于浏览器请求的一些信息, 例如 headers, 客户端地址, 访问方式等等. 我们使用的 `form` 对象就是从中获取的, `form` 对象是一个字典, 可以使用 `get` 方式来获取相应的数据. 110 | 111 | 注意我们使用了 `methods=(POST,)` 来指定监听方式为 `POST`, 这个 `POST` 对象是我们从 `urouter` 中导入的一个常量, 需要注意的是: `methods` 参数必须传入一个可迭代的对象(`list` 或者 `tuple`), 一般的情况下推荐使用 `tuple`(如果您不需要动态修改监听方式), 因为在成员数量固定的情况下, `tuple` 比 `list` 更加节省内存, 在嵌入式设备中内存是非常有限的, 我们要在最大程度上节省不必要的内存开支. 112 | 113 | 114 | ### 获取更多开发信息, 详见 [开发文档](https://urouter.m-jay.cn) 115 | 116 | ## 注意: 117 | 在使用本框架时, 应注意: 118 | - 请不要在让本模块的监听和处理函数在多个线程中运行(即保持本框架工作在单线程模式), 你可以专门申请一个线程让他工作, 但是不要让他同时处理多件事情, 这会造成 `context` 对象的混乱. 119 | 例子: 120 | ```python 121 | # connect to network... 122 | 123 | from _thread import start_new_thread 124 | from urouter import uRouter 125 | app = uRouter() 126 | 127 | @app.route("/") 128 | def index(): 129 | return "
Hello World!
" 130 | 131 | start_new_thread(app.serve_forever) 132 | # that is ok. 133 | 134 | start_new_thread(app.serve_forever) 135 | # Do not start two server. 136 | ``` 137 | 138 | ```python 139 | # connect to network... 140 | 141 | from _thread import start_new_thread 142 | from urouter import uRouter 143 | app = uRouter() 144 | 145 | @app.route("/") 146 | def index(): 147 | return "
Hello World!
"
148 |
149 | while True:
150 | if app.check():
151 | start_new_thread(app.serve_once)
152 | # Don't do that.
153 | ```
154 | - 本模块可以同时拥有多个app实例, 可以同时工作(未经充分测试)
155 | - 不要随意修改本框架的 `context` 对象(例如 `request` 和 `response`, `session`), 理论上, 您不应该修改任何未声明可以被修改的内容.
156 |
157 |
--------------------------------------------------------------------------------
/urouter/ruleutil.py:
--------------------------------------------------------------------------------
1 | import re
2 | from urouter.consts import placeholder_func, empty_dict
3 |
4 | from . import regexutil
5 |
6 | from .typeutil import ruleitem
7 |
8 | def make_path(paths: list) -> str:
9 | """
10 | 把一个str的list合并在一起,并用 '/' 分割
11 | -> ['api','goods']
12 | <- "/api/goods"
13 | """
14 | if not paths:
15 | return '/'
16 |
17 | s = ''
18 | for i in paths:
19 | if i == '':
20 | continue # 过滤空项
21 | s = '%s%s%s' % (s, '/', i)
22 |
23 | return s
24 |
25 | def split_url(url: str) -> list:
26 | """
27 | 将字符串URL分割成一个LIST
28 |
29 | -> '/hello/world'
30 | <- ['hello', 'world']
31 | """
32 | return [x for x in url.split("/") if x != ""] # 去除数组首尾空字符串
33 |
34 | def parse_url(url: str) -> str:
35 | """
36 | 规范化 URL
37 |
38 | -> hello/world
39 | <- /hello/world
40 | """
41 | if url == "":
42 | url = "/"
43 | if not url.startswith('/'):
44 | url = "/%s" % url # 添加开头斜杠
45 | # if not url.endswith ("/"): url += "/" # 添加末尾斜杠
46 | return url
47 |
48 | def _translate_rule(rule: str) -> tuple:
49 | """
50 | 将一个普通的路由字符串转化成正则表达式路由
51 | :param rule: 欲转化的规则文本
52 | :return (rule:str, url_vars:list)
53 |
54 | url_vars = [
55 | (var_name:str, vartype: class)
56 | ]
57 | 例子:
58 | => '/'
59 | <= ('^//?(\\?.*)?$', [])
60 | """
61 | rule: list = split_url(parse_url(rule))
62 | url_vars: list = [] # 存放变量名称
63 |
64 | for i in rule: # 对其进行解析
65 | m = re.match(regexutil.VAR_VERI, i)
66 | # m.group (1) -> string | float ...
67 | # m.group (2) -> var_name
68 | if m:
69 | # 如果匹配到了,说明这是一个变量参数
70 | var_type = m.group(1)
71 | if var_type == "string":
72 | # rule.index (i) 获取 i 在 l_rule 中的下标
73 | rule[rule.index(i)] = regexutil.MATCH_STRING
74 | url_vars.append((m.group(2),))
75 | elif var_type == "float":
76 | rule[rule.index(i)] = regexutil.MATCH_FLOAT
77 | url_vars.append((m.group(2), float))
78 | elif var_type == "int":
79 | rule[rule.index(i)] = regexutil.MATCH_INT
80 | url_vars.append((m.group(2), int))
81 | elif var_type == "path":
82 | rule[rule.index(i)] = regexutil.MATCH_PATH
83 | url_vars.append((m.group(2),))
84 | elif var_type.startswith("custom="):
85 | rule[rule.index(i)] = m.group(1)[7:]
86 | url_vars.append((m.group(2),))
87 | else:
88 | raise TypeError(
89 | "Cannot resolving this variable: {0}".format(i))
90 |
91 | rule = "^" + make_path(rule) + "/?(\?.*)?$"
92 |
93 | return (rule, tuple(url_vars))
94 |
95 | class RuleTree():
96 | tree: list # [ruletasks]
97 |
98 | def __init__(self):
99 | """Create a rule-tree"""
100 | self.tree = []
101 |
102 | def append(
103 | self,
104 | rule: str,
105 | func: callable,
106 | weight: int,
107 | methods: iter
108 | ):
109 | """Append a item to rule-tree"""
110 | rule, url_vars = _translate_rule(rule)
111 |
112 |
113 | item = ruleitem(weight, re.compile(rule), func, methods, url_vars)
114 | self.tree.append(item)
115 | # ruleitem: "rule", "func", "weight", "methods", "url_vars"
116 |
117 | def match(self, url: str, method: int) -> tuple:
118 | """
119 | Search for the relevant rule.
120 | if hit, will return a tuple: (weight, func, {vars: value})
121 | if not, return None
122 | """
123 | result = None
124 | kw_args = {}
125 | item: ruleitem
126 | for item in self.tree:
127 |
128 | if method not in item.methods:
129 | # 访问方式不匹配,跳过
130 | continue
131 |
132 |
133 | result = item.comper.match(url)
134 | if result:
135 | # 有结果代表匹配到了
136 |
137 | # 检测是否有变量列表
138 | if item.url_vars:
139 | try:
140 | # 获取 result 中的所的组
141 | idx = 1
142 | while True:
143 | var_tp = item.url_vars[idx-1]
144 | # var_tp = (var_name, var_type)
145 | # var_name = var_tp[0]
146 |
147 | # 有类型则进行转化,无类型则跳过
148 | if len(var_tp) == 2: # 有类型
149 | # 有类型则转化
150 | value = var_tp[1](result.group(idx))
151 | else:
152 | # 无类型说明默认为str
153 | value = result.group(idx)
154 |
155 | # var_tp[0] 变量名
156 | kw_args[var_tp[0]] = value
157 | # 按顺序取出变量,放入kw_args中
158 | idx += 1
159 | except:
160 | # 报错说明没了
161 | ...
162 |
163 | return (item.weight, item.func, kw_args)
164 | # 没有被截胡说明没有被匹配到
165 | return (0 , placeholder_func , empty_dict)
166 |
--------------------------------------------------------------------------------
/urouter/context/request.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- encoding: utf-8 -*-
3 | '''
4 | @File : request.py
5 | @Time : 2021/07/23 17:06:57
6 | @Author : M-Jay
7 | @Contact : m-jay-1376@qq.com
8 |
9 | Used to obtain connection information.
10 | '''
11 |
12 | import socket
13 |
14 | from ..consts import *
15 | from ..util import *
16 | from ..config import CONFIG
17 | from ..typeutil import headeritem, httphead
18 | from json import loads as json_loads
19 |
20 | from gc import collect
21 | from .. import logger
22 | logger = logger.get("uRouter.request")
23 |
24 |
25 | class Header(dict):
26 | def __init__(self, gen, *args, **kwargs):
27 | self.gen = gen
28 | super().__init__(*args, **kwargs)
29 |
30 | def __getitem__(self, key):
31 | if key not in self:
32 | # key not found.
33 | item: headeritem
34 | for item in self.gen:
35 | if item.key == key:
36 | return item.value
37 |
38 | return super().__getitem__(key)
39 |
40 | def get(self, key, *args):
41 | if key not in self:
42 | # key not found.
43 | for item in self.gen:
44 | if item.key == key:
45 | return item.value
46 |
47 | return super().get(key, *args)
48 |
49 |
50 | class Request():
51 | host: str
52 | port: int
53 | uri: str
54 | url: str
55 | method: int
56 | http_version: str
57 | args: dict
58 |
59 | _client: socket.socket
60 | _headers: dict
61 | _form: dict
62 |
63 | def init(
64 | self,
65 | client: socket.socket,
66 | addr: tuple,
67 | head: httphead
68 | ):
69 | self._client = client
70 | # comp http first line.
71 | self.host, self.port = addr
72 |
73 | self._form = None
74 |
75 | self.method, self.uri, self.http_version = head
76 |
77 | self._hdgen = self._header_generate()
78 | self._headers = Header(self._hdgen)
79 | self.args = {}
80 | # Parsing uri to url
81 |
82 | pst = self.uri.find("?")
83 | if pst > -1:
84 | # 解析参数
85 | self.args = load_form_data(self.uri[pst+1:], self.args)
86 | self.url = self.uri[:pst]
87 | else:
88 | self.url = self.uri
89 |
90 | def _flush_header(self):
91 | """
92 | Flush header data.
93 | """
94 | try:
95 | while True:
96 | next(self._hdgen)
97 | except StopIteration:
98 | return # flush over
99 |
100 |
101 | def _header_generate(self) -> str:
102 | """
103 | Generate headers lazily, reducing resource waste
104 | it will return a header-item at once and append it to `_headers` automaticly.
105 | """
106 | line: str
107 | while True:
108 | line = self._client.readline()
109 | # filter the empty line
110 | if line:
111 | line = line.decode(CONFIG.charset).strip()
112 | # filter the \r\n already.
113 | if line:
114 | # if it not None, it will be a header line.
115 | pst = line.find(':')
116 | key: str = line[:pst]
117 | # content = line[pst+2:]
118 | # tuple is more memory-saved
119 | value = line[pst+2:].strip()
120 | self._headers[key] = value
121 | yield headeritem(key, value)
122 | else:
123 | # if it is a empty line(just \r\n), it means that header line was generated out.
124 | return
125 | else:
126 | return # have no header, just return.
127 |
128 | @property
129 | def headers(self) -> dict:
130 | return self._headers
131 |
132 | def _load_form(self, buffsize=4096):
133 | # Content-Type: application/x-www-form-urlencoded
134 | # file1=EpicGamesLauncher.exe&file2=api-ms-win-core-heap-l1-1-0.dll
135 |
136 | # Content-Type: multipart/form-data:
137 | # POST / HTTP/1.1
138 | # Host: 127.0.0.1
139 | # Connection: keep-alive
140 | # Content-Length: 348
141 | # Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryOM7deWP2QaJYb9LE
142 | # Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
143 | # Accept-Encoding: gzip, deflate, br
144 | # Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
145 |
146 | # ------WebKitFormBoundaryOM7deWP2QaJYb9LE
147 | # Content-Disposition: form-data; name="file1"; filename="fl1.txt"
148 | # Content-Type: text/plain
149 |
150 | # This is file 1
151 | # ------WebKitFormBoundaryOM7deWP2QaJYb9LE
152 | # Content-Disposition: form-data; name="file2"; filename="fl2.txt"
153 | # Content-Type: text/plain
154 |
155 | # This is file 2
156 | # ------WebKitFormBoundaryOM7deWP2QaJYb9LE--
157 |
158 | if self._form:
159 | return
160 |
161 | content_type = self.headers.get(
162 | "Content-Type",
163 | "application/x-www-form-urlencoded"
164 | )
165 |
166 | if "multipart/form-data" in content_type:
167 | raise NotImplementedError("multipart/form-data was not implemented, \
168 | please use application/x-www-form-urlencoded method.")
169 |
170 | # flush the header content.
171 | self._flush_header()
172 |
173 | content = self._client.recv(buffsize).decode(CONFIG.charset)
174 |
175 | if "application/x-www-form-urlencoded" in content_type:
176 | # application/x-www-form-urlencoded content like this
177 | # username=123456&passwd=admin
178 |
179 | # data = self._client.recv()
180 | # content = data.decode(CONFIG.charset)
181 | # items = content.split("&")
182 | # try to recv all data.
183 | items: list = content.split("&")
184 |
185 | for item in items:
186 | k, v = item.split("=")
187 | self._form[k] = v
188 |
189 | elif "application/json" in content_type:
190 | self._form = json_loads()
191 |
192 | collect()
193 |
194 | @property
195 | def form(self) -> dict:
196 | # 惰性加载 form, 只要你不用, 我就不加载.
197 | if not self._form:
198 | self._load_form()
199 |
200 | return self._form
201 |
202 | @property
203 | def client(self) -> socket.socket:
204 | self._flush_header()
205 | return self._client
206 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.