├── .gitattributes
├── .gitignore
├── .gitmodules
├── LICENSE
├── MANIFEST.in
├── README.md
├── auto_build.py
├── cover.png
├── setup.py
├── tools
├── bench_max_connections_client.py
└── bench_max_connections_server.py
└── urouter
├── __init__.py
├── config.py
├── consts.py
├── context
├── request.py
├── response.py
└── session.py
├── logger.py
├── mimetypes.py
├── pool
├── __init__.py
└── queue.py
├── regexutil.py
├── router.py
├── ruleutil.py
├── typeutil.py
└── util.py
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 | MANIFEST
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .nox/
42 | .coverage
43 | .coverage.*
44 | .cache
45 | nosetests.xml
46 | coverage.xml
47 | *.cover
48 | .hypothesis/
49 | .pytest_cache/
50 |
51 | # Translations
52 | *.mo
53 | *.pot
54 |
55 | # Django stuff:
56 | *.log
57 | local_settings.py
58 | db.sqlite3
59 |
60 | # Flask stuff:
61 | instance/
62 | .webassets-cache
63 |
64 | # Scrapy stuff:
65 | .scrapy
66 |
67 | # Sphinx documentation
68 | docs/_build/
69 |
70 | # PyBuilder
71 | target/
72 |
73 | # Jupyter Notebook
74 | .ipynb_checkpoints
75 |
76 | # IPython
77 | profile_default/
78 | ipython_config.py
79 |
80 | # pyenv
81 | .python-version
82 |
83 | # celery beat schedule file
84 | celerybeat-schedule
85 |
86 | # SageMath parsed files
87 | *.sage.py
88 |
89 | # Environments
90 | .env
91 | .venv
92 | env/
93 | venv/
94 | ENV/
95 | env.bak/
96 | venv.bak/
97 |
98 | # Spyder project settings
99 | .spyderproject
100 | .spyproject
101 |
102 | # Rope project settings
103 | .ropeproject
104 |
105 | # mkdocs documentation
106 | /site
107 |
108 | # mypy
109 | .mypy_cache/
110 | .dmypy.json
111 | dmypy.json
112 |
113 | # Pyre type checker
114 | .pyre/
115 |
116 |
117 | # custom
118 | .mpyproject.json
119 | .vscode/
120 | .idea/
121 | www/
122 | boot.py
123 | test.py
124 | main.py
125 | resources/index.html
126 | resources/style.css
127 | resources/micro-route-poster.psd
128 | resources/.vscode
129 |
130 | .vscode/
131 |
132 | *.mpy
133 | test.py
134 | main.py
135 | test/
136 | test*
137 | boot.py
138 | www/
139 | iperf3.py
140 |
141 | cross/
142 | src/
143 | across/
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include *.mpy
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # micropython-uRouter 开发文档
2 |
3 | 
4 |
5 | **注意:** 本项目目前处于缓慢或者暂停更新状态,因为作者高三了,欢迎志同道合的朋友提交 `pull request` 。 同时,我需要有朋友帮我翻译和完善下文档,有意者可以提交issue或者pr哦~
6 |
7 | ## 阅读 [开发文档](https://urouter.m-jay.cn) 以获得更多信息
8 |
9 | ## 注意: 本框架目前正处于预览开发阶段, 如果在使用过程中遇到任何问题, 请前往提交 [issue](https://github.com/Li-Lian1069/micropython-urouter/issue)
10 |
11 |
12 | 如果您喜欢本项目, 或者本项目帮到了您, 麻烦给本项目点个 `star` 吧~
13 |
14 |
15 | ## I need help
16 | I am not expert at English, so i need someone who is good at English helping me to translate the document into English.
17 |
18 | ## 这是什么
19 |
20 | 这是一个工作在 `micropython` 上的一个轻量, 简单, 快速的WEB框架.
21 |
22 | **为什么要开发这个框架?**
23 |
24 | 据我所知, 目前已知的可以在 `micropython` 上运行的 web 框架有 [microWebSrv](https://github.com/jczic/MicroWebSrv) , 但是这个框架需要 `_thread` 模块的支持
25 |
26 | 但, 很遗憾的是, 在类似于 `esp8266` 这样的板子上并没有该模块的支持.
27 |
28 | 同时, 这个模块的运行需要阻塞主线程, 这就意味着你的开发版只能一味地处理HTTP请求, 而不能处理其他的事情.
29 |
30 | 因此, 我们需要一个支持单线程运行, 而且不会阻塞主线程的web框架, 于是这个框架应运而生.
31 |
32 | ## 特色功能:
33 | 本框架的开发以 `简洁`, `轻巧`, `易用`, `灵活`, `高效` 作为主旨.
34 |
35 | 本模块有两种独特的工作模式:
36 | - **normal-mode 普通工作模式**
37 | 在此模式下本框架的处理是阻塞的, 即一切请求都会被阻塞并且等待
38 |
39 | - **dynamic-mode 动态工作模式**
40 | 在此模式下本框架的处理是非阻塞的, 即检查浏览器请求的操作会立即被返回(理论上), 只有在处理HTTP请求的情况下会阻塞线程.
41 |
42 | ### flask 风格的context上下文管理
43 | 本模块拥有类似于 `flask` 框架的 `context` 管理对象(即`request`, `response`, `session`), 减少学习成本, 简化开发流程
44 |
45 | ### 魔法路径变量
46 | 本框架支持与Flask类似的魔法路径变量, 支持使用一个规则字符串匹配多个路径.
47 |
48 | ## 与 microWebSrv 的对比
49 |
50 | | 特性/框架 | mpy_uRouter | microWebSrv |
51 | | -------------- | ----------- | ----------- |
52 | | python习惯命名 | * | |
53 | | 单线程支持 | * | * |
54 | | 多线程支持 | | * |
55 | | 非阻塞支持 | * | |
56 | | 多对象支持 | * | |
57 | | 学习成本 | 低 | 高 |
58 | | 复杂程度 | 简单 | 复杂 |
59 |
60 | ## 如何使用?
61 | ### 通过pypi安装
62 | ```python
63 | # on mpy repl.
64 | import upip
65 |
66 | upip.install('micropython-uRouter')
67 | upip.install('micropython-ulogger')
68 | ```
69 |
70 | ### 手动安装(推荐)
71 | 通过 `pypi` 下载的包是未经过编译的 `python` 源代码文件, `micropython` 支持将源代码文件进行编译以获得执行速度上的提升和体积上的缩小. 如果你的设备(例如`esp8266`) 在导入模块的过程中遇到内存分配失败的错误, 可以尝试使用此方式安装.
72 |
73 | 步骤:
74 | 1. 先去本项目的 [release](https://github.com/Li-Lian1069/micropython-urouter/releases) 页面下载一个最新版本的打包版本
75 | 2. 将下载到的文件上传到开发版的 `/lib` 目录中. (可以使用 `thonny` ide)
76 | 3. 安装类似的步骤安装本框架的依赖库: https://github.com/Li-Lian1069/micropython-ulogger
77 |
78 | ### 快速开始
79 | 我们先来建立一个最简单的处理路由:
80 | ```python
81 | # connect to network...
82 |
83 | from urouter import uRouter
84 | app = uRouter()
85 |
86 | @app.route("/")
87 | def index():
88 | return "
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/majoson-chen/micropython-urouter/2363db996282f6e734a13e22b6cf130cee9b1adf/cover.png
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 | from pathlib import Path
3 |
4 | __version__: str
5 | # get version
6 | with (Path('.') / 'urouter' / '__init__.py').open('r') as fp:
7 | while True:
8 | line = fp.readline()
9 | if line.startswith("__version__"):
10 | exec(line)
11 | break
12 |
13 | with open("README.md", "r", encoding="utf-8") as fh:
14 | long_description = fh.read()
15 |
16 | setup(
17 | name="micropython-urouter",
18 | version=__version__,
19 |
20 | packages=['urouter'],
21 | install_requires=['micropython-ulogger'],
22 | # requirements=['micropython-ulogger'],
23 | include_package_data=True,
24 |
25 | author="Youkii-Chen",
26 | author_email="youkii-chen@qq.com",
27 | description="A simple, lightweight, fast, and flexible WEB framework designed for embedded devices.",
28 | long_description=long_description,
29 | long_description_content_type="text/markdown",
30 | url="https://github.com/Youkii-Chen/micropython-urouter",
31 | project_urls={
32 | "Bug Tracker": "https://github.com/Youkii-Chen/micropython-urouter/issues",
33 | },
34 | classifiers=[
35 | "Programming Language :: Python :: 3",
36 | "License :: OSI Approved :: MIT License",
37 | "Operating System :: OS Independent",
38 | ],
39 | keywords="micropython",
40 |
41 | )
42 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.
5 | # Everyone is permitted to copy and distribute verbatim copies
6 | # of this license document, but changing it is not allowed.
7 | # @author m-jay
8 | # @E-mail m-jay-1376@qq.com
9 | """
10 | A lightweight HTTP request routing processing support library based on micropython.
11 | The previous name was [micro-route](https://github.com/Li-Lian1069/micro_route)
12 | """
13 |
14 | __version__ = 'v0.1.2 alpha'
15 |
16 |
17 | from .consts import *
18 | from .router import uRouter
19 | from .config import CONFIG
20 | from .mimetypes import get as get_mime_type
21 | from .logger import get as get_logger
22 |
23 |
24 |
25 | __all__ = (
26 | uRouter,
27 | CONFIG,
28 |
29 | __version__,
30 |
31 | GET,
32 | POST,
33 | HEAD,
34 | PUT,
35 | OPINOS,
36 | DELETE,
37 | TRACE,
38 | CONNECT,
39 | NORMAL_MODE,
40 | DYNAMIC_MODE,
41 |
42 | get_mime_type,
43 | get_logger
44 | )
45 |
--------------------------------------------------------------------------------
/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/consts.py:
--------------------------------------------------------------------------------
1 | try:
2 | from micropython import const
3 | except:
4 | const = lambda x:x
5 |
6 | GET = "GET"
7 | POST = "POST"
8 | HEAD = "HEAD"
9 | PUT = "PUT"
10 | OPINOS = "OPINOS"
11 | DELETE = "DELETE"
12 | TRACE = "TRACE"
13 | CONNECT = "CONNECT"
14 |
15 | NORMAL_MODE = const(100)
16 | DYNAMIC_MODE = const(200)
17 |
18 | HTML_ESCAPE_CHARS = {
19 | "&" : "&",
20 | """ : '"',
21 | "'" : "'",
22 | ">" : ">",
23 | "<" : "<",
24 | " " : " "
25 | }
26 |
27 | STATU_CODES:dict = {
28 | 200 : 'OK', # 客户端请求成功
29 | 201 : 'Created', # 请求已经被实现,而且有一个新的资源已经依据请求的需要而创建,且其URI已经随Location头信息返回。
30 | 301 : 'Moved Permanently', # 被请求的资源已永久移动到新位置,并且将来任何对此资源的引用都应该使用本响应返回的若干个URI之一
31 | 302 : 'Found', # 在响应报文中使用首部“Location: URL”指定临时资源位置
32 | 304 : 'Not Modified', # 条件式请求中使用
33 | 403 : 'Forbidden', # 请求被服务器拒绝
34 | 404 : 'Not Found', # 服务器无法找到请求的URL
35 | 405 : 'Method Not Allowed', # 不允许使用此方法请求相应的URL
36 | 500 : 'Internal Server Error', # 服务器内部错误
37 | 502 : 'Bad Gateway', # 代理服务器从上游收到了一条伪响应
38 | 503 : 'Service Unavailable', # 服务器此时无法提供服务,但将来可能可用
39 | 505 : 'HTTP Version Not Supported' # 服务器不支持,或者拒绝支持在请求中使用的HTTP版本。这暗示着服务器不能或不愿使用与客户端相同的版本。响应中应当包含一个描述了为何版本不被支持以及服务器支持哪些协议的实体。
40 | }
41 |
42 | empty_dict = {}
43 | def placeholder_func():
44 | ...
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/urouter/context/response.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- encoding: utf-8 -*-
3 | '''
4 | @File : response.py
5 | @Time : 2021/07/10 23:40:35
6 | @Author : M-Jay
7 | @Contact : m-jay-1376@qq.com
8 |
9 | Used to respond to HTTP requests.
10 | '''
11 | import socket
12 | import json
13 |
14 | from ..util import dump_headers_generator, is_html
15 | from ..config import CONFIG
16 | from ..consts import STATU_CODES
17 | from ..mimetypes import get as getmtp
18 | from io import BytesIO
19 | from os import stat
20 |
21 | from .. import logger
22 | logger = logger.get("uRouter.response")
23 |
24 |
25 | class Response():
26 | _client: socket.socket
27 | _responsed: bool
28 | _header_sended: bool
29 | _buf: bytearray
30 | root_path: str
31 |
32 | headers: dict
33 | statu_code: int
34 | mime_type: str
35 |
36 | def __init__(self):
37 | self._buf = bytearray(1024)
38 | self.root_path = ""
39 |
40 | def init(
41 | self,
42 | client: socket.socket,
43 | ):
44 | # logger.debug("init response")
45 |
46 | self._client = client
47 | self._stream_mode = False
48 | self._responsed = False
49 | self._header_sended = False
50 |
51 | self.statu_code = 200
52 | # self.mime_type = "text/html; charset=%s" % CONFIG.charset
53 | self._mime_type = ""
54 | self.headers = {
55 | "Server": "uRouter on micropython",
56 | }
57 |
58 | @property
59 | def mime_type(self):
60 | return self._mime_type
61 |
62 | @mime_type.setter
63 | def mime_type(self, value: str):
64 | # mime_type can only be set once
65 | if self._mime_type:
66 | # set already.
67 | return
68 | else:
69 | self._mime_type = value
70 |
71 | def _flush(self):
72 | """
73 | Flush the data.
74 | """
75 | self._client.settimeout(0)
76 | while True:
77 | # flush
78 | try:
79 | if self._client.readinto(self._buf):
80 | continue
81 | else:
82 | # read-len == 0, flush over.
83 | break
84 | except OSError:
85 | # have no data
86 | break
87 |
88 | self._client.settimeout(CONFIG.request_timeout)
89 |
90 |
91 | def _send_headers(self):
92 | """
93 | Send the headers, than you can send the data.
94 | It must be called after you use `redirect` or `setheader` or `set_statu_code`
95 | """
96 |
97 | if self._header_sended:
98 | return # don not repeaty send header.
99 |
100 | # send head line.
101 | self._client.send(
102 | # HTTP/1.1 200 OK
103 | ("%s %s %s\r\n" % (
104 | "HTTP/1.1",
105 | self.statu_code,
106 | STATU_CODES.get(self.statu_code)
107 | )).encode(CONFIG.charset)
108 | )
109 |
110 | if self.mime_type:
111 | self.headers["Content-Type"] = self.mime_type
112 | else:
113 | self.headers["Content-Type"] = "application/octet-stream"
114 |
115 | # send headers
116 | for header_line in dump_headers_generator(self.headers):
117 | self._client.send(header_line.encode(CONFIG.charset))
118 | # end headers
119 |
120 | self._client.send(b"\r\n")
121 | self._header_sended = True
122 | # logger.debug("header sended: ", self.headers)
123 | # content start.
124 |
125 | def abort(
126 | self,
127 | statu_code: int = 500,
128 | ):
129 | self.mime_type = "text/html"
130 | self.statu_code = statu_code
131 | self._send_headers()
132 |
133 | def redirect(
134 | self,
135 | location: str,
136 | statu_code: int = 302
137 | ):
138 | """
139 | Redirect the request.
140 | """
141 | assert not self._responsed, "Do not send response repeatily."
142 |
143 | self.headers["Location"] = location
144 | self.statu_code = statu_code
145 | self._send_headers()
146 | self._responsed = True
147 |
148 | def make_response(self, data: any) -> bool:
149 | """
150 | Send the response data, and close the response.
151 | * If your data length is specific, use it to make a response.
152 | * If not, please use stream-mode.
153 | """
154 | assert not self._responsed, "Do not send response repeatily."
155 |
156 | data = self.parse_data(data)
157 | self.headers["Content-Length"] = "%d" % len(data)
158 | self._send_headers()
159 |
160 |
161 | self._client.send(data)
162 | self._responsed = True
163 |
164 | # logger.debug("make response: ", data)
165 | return True
166 |
167 | def make_stream(self):
168 | """
169 | use stream-mode to send data.
170 | it can send the uncertain size data.
171 | """
172 | assert not self._responsed, "Do not send response repeatily."
173 |
174 | self.headers["Transfer-Encoding"] = "chunked"
175 | self._stream_mode = True
176 | self._stream_finish = False
177 | self._send_headers()
178 | self._responsed = True
179 |
180 | # logger.debug("make stream.")
181 |
182 | def finish_stream(self):
183 | """
184 | Finish the streaming and close the request.
185 | """
186 | self._client.send(b"0\r\n\r\n")
187 | # self._close()
188 | # logger.debug("finish stream.")
189 | self._stream_finish = True
190 |
191 |
192 | def send_data(self, data: any) -> int:
193 | """
194 | Send the data if you enabled the stream-mode
195 | """
196 | assert self._stream_mode, "This method just suit for stream-mode."
197 |
198 | data = self.parse_data(data)
199 |
200 | self._client.send(b"%x\r\n" % len(data))
201 | self._client.send(data)
202 |
203 | # logger.debug("send stream data: ", data)
204 | return self._client.send("\r\n")
205 |
206 | def send_file(self, path: str) -> bool:
207 | """
208 | Send the local file which in root_path.
209 | if file not-found in root-path, it will send 404 statu automaticly
210 | if send failed, it will return False
211 |
212 | :param path: the file name(reletive path suggested)
213 | """
214 | if path[0] != "/": # must start with '/'
215 | path = "/%s" % path
216 |
217 | path = "%s%s" % (self.root_path, path)
218 |
219 | self._responsed = True
220 |
221 | try:
222 | file = stat(path)
223 | except: # file not found.
224 | self.abort(404)
225 | # logger.debug("local file not found: ", path)
226 | return True
227 |
228 |
229 | if file[0] == 16384: # If it is a folder, give up
230 | # TODO: send default file in this floder.
231 | self.abort(404)
232 | # logger.debug("local file not found: ", path)
233 | return True
234 |
235 | file_size = file[6]
236 | # 没报错就是找到文件了
237 | suffix = path[path.rfind('.'):]
238 | # 设定文档类型
239 | self.mime_type = getmtp(suffix.lower(), "application/octet-stream")
240 | self.headers["Content-Length"] = "%s" % file_size
241 |
242 | self._send_headers()
243 | # 分片传输文件
244 | with open(path, 'rb') as file:
245 | try:
246 | while file_size > 0:
247 | x = file.readinto(self._buf)
248 | if not self._client.write((
249 | self._buf
250 | if
251 | x == len(self._buf)
252 | else
253 | memoryview(self._buf)[:x]
254 | )) == x:
255 | logger.warn("The size of the sent data does not match: ", path)
256 | return False
257 | file_size -= x
258 | except OSError:
259 | # timeout
260 | logger.debug("Send local file timeout.")
261 | except Exception as e:
262 | logger.debug("Faild to send local file: ", e)
263 | return False
264 |
265 | logger.debug("send local file: ", path)
266 | return True
267 |
268 | def parse_data(self, data) -> bytes:
269 | """
270 | pass in a str, bytes, bytearray , number, BytesIO obj.
271 | return a buffer to send data.
272 |
273 | :param arg: [description]
274 | :type arg: [type]
275 | """
276 |
277 |
278 | # if isinstance(data, str):
279 | # if data.find(""):
280 | # self.mime_type = "text/html; charset=%s" % CONFIG.charset
281 | # else:
282 | # self.mime_type = "text/plain; charset=%s" % CONFIG.charset
283 | # elif isinstance(data, (bytes, bytearray, BytesIO)):
284 | # self.mime_type = "application/octet-stream"
285 |
286 | if isinstance(data, str):
287 | self.mime_type = (
288 | "text/html; charset=%s" % CONFIG.charset
289 | if is_html(data)
290 | else "text/plain; charset=%s" % CONFIG.charset
291 | )
292 | data = data.encode(CONFIG.charset)
293 | elif isinstance(data, (bytes, bytearray)):
294 | pass # Acceptable type
295 | elif isinstance(data, (tuple, list, dict)):
296 | self.mime_type = "application/json"
297 | data = json.dumps(data).encode(CONFIG.charset)
298 | elif isinstance(data, int, float):
299 | self.mime_type = "text/plain"
300 | data = b"%s" % data
301 | elif isinstance(data, BytesIO):
302 | data: BytesIO
303 | self.mime_type = "application/octet-stream"
304 | data = memoryview(data.getvalue()) # Avoid additional memory allocation.
305 | else:
306 | raise TypeError("Unknown response type.")
307 |
308 | return data
309 |
--------------------------------------------------------------------------------
/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/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/pool/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- encoding: utf-8 -*-
3 | '''
4 | @File : __init__.py
5 | @Time : 2021/07/23 16:21:55
6 | @Author : M-Jay
7 | @Contact : m-jay-1376@qq.com
8 |
9 | Manage connections and http requests.
10 | '''
11 |
12 | from gc import collect
13 | import socket
14 | import select
15 | from ..config import CONFIG
16 | from ..consts import DYNAMIC_MODE, placeholder_func
17 | from .queue import Queue
18 | from ..ruleutil import RuleTree
19 |
20 | from ..regexutil import comp_head
21 | from ..typeutil import httphead, routetask
22 |
23 | from ..context.request import Request
24 | from ..context.response import Response
25 | from ..context.session import Session
26 |
27 | from .. import logger
28 | logger = logger.get("uRouter.poll")
29 |
30 |
31 | class Poll:
32 | _host: socket.socket
33 | _poll: select.poll
34 | _conns: list
35 | _queue: Queue
36 | _rlt: RuleTree
37 | # _conns = [
38 | # "socket clients"
39 | # (client1, addr),
40 | # (client2, addr)
41 | # ]
42 |
43 | _mode: int
44 | max: int
45 |
46 | request: Request
47 | response: Response
48 | session: Session
49 |
50 | def __init__(
51 | self,
52 | mode: int,
53 | host: socket.socket,
54 | request,
55 | response,
56 | session,
57 | keep_alive
58 | ):
59 |
60 | self.max = CONFIG.max_connections
61 | # Make sure that there is always a place available
62 |
63 | self._rlt = RuleTree()
64 | self._mode = mode
65 | self._conns = []
66 | self._host = host
67 | self._poll = select.poll()
68 | if not hasattr(self._poll, "ipoll"):
69 | self._poll.ipoll = self._poll.poll # for pc debug.
70 |
71 | self._poll.register(host, select.POLLIN | select.POLLERR)
72 | self._queue = Queue()
73 |
74 | if mode == DYNAMIC_MODE:
75 | host.setblocking(False)
76 |
77 | self.request = request
78 | self.response = response
79 | self.session = session
80 | self._keep_alv = keep_alive
81 |
82 | def close(self):
83 | for client, _ in self._conns:
84 | self._poll.unregister(client)
85 | try:
86 | client.close()
87 | except:
88 | ...
89 |
90 | self._poll.unregister(self._host)
91 |
92 | self._queue._heap.clear()
93 |
94 | def _find_addr(self, client: socket.socket):
95 | """
96 | Find the addr of the client in conns
97 |
98 | :return: if not found, return None.
99 | """
100 | n = 0
101 | while n < len(self._conns):
102 | client_, addr = self._conns[n]
103 | if client_ == client:
104 | return addr
105 | n += 1
106 | return None # Not found.
107 |
108 | def _append_conn(self, client, addr):
109 | if (client, addr) in self._conns:
110 | # exist already.
111 | return
112 | else:
113 | client.settimeout(CONFIG.request_timeout)
114 | self._conns.insert(0, (client, addr))
115 | self._poll.register(client, select.POLLIN | select.POLLERR)
116 | return
117 |
118 | def _pop_conn(self, client: socket.socket = None):
119 | """
120 | Pop the connetion from _conns.
121 | * if pass nothing, it will pop the earliest conn.
122 | if exist, pop it , if not, just close it.
123 | """
124 | if len(self._conns) == 0:
125 | self._poll.unregister(client)
126 | elif client:
127 | # pop the appointed conn.
128 | for idx, (sock, _) in enumerate(self._conns):
129 | if sock == client:
130 | self._conns.pop(idx)
131 | break
132 | else:
133 | # pop the earlist conn.
134 | client, _ = self._conns.pop()
135 |
136 | self._poll.unregister(client)
137 | try:
138 | client.close()
139 | except:
140 | ...
141 |
142 | def _append_to_queue(self, client: socket.socket, addr: tuple):
143 | """
144 | Read the http-head and try to match the handler,
145 | finally append it to the queue.
146 |
147 | * This function will not try to catch exceptions.
148 | """
149 | client.settimeout(CONFIG.request_timeout)
150 | try:
151 | line = client.readline().decode().strip()
152 | except OSError as e:
153 | # timeout
154 | logger.debug("Http head read timeout.")
155 | self._pop_conn(client)
156 | return None
157 | head: httphead = comp_head(line)
158 | if not head:
159 | # head not matched.
160 | self._pop_conn(client)
161 | logger.debug("http head-line do not matched.")
162 | return None
163 |
164 | # head matched.
165 | # handler = self._rlt.match(head.uri, head.method)
166 |
167 | (weight, func, kwargs) = self._rlt.match(head.uri, head.method)
168 |
169 | self._queue.push(
170 | weight, routetask(client, addr, head, func, kwargs)
171 | )
172 | # ("weight", "client", "addr", "http_head", "func", "url_vars")
173 |
174 | def check(self, timeout: int) -> int:
175 | """
176 | Check the new request or new connetcions, and append them into Queue.
177 |
178 | :return: the quantity of waiting tasks.
179 | """
180 |
181 | # if self._mode == NORMAL_MODE:
182 | # clients = self._poll.ipoll(timeout)
183 | # elif self._mode == DYNAMIC_MODE:
184 | # clients = self._poll.ipoll(0)
185 | if self._queue._heap:
186 | # task waiting
187 | # return len(self._queue._heap)
188 | logger.debug("task exist, skip check.")
189 | return len(self._queue._heap)
190 |
191 | # have no wating tasks, try to accept any.
192 |
193 | logger.debug("check the new requests, timeout: ", timeout)
194 |
195 | clients = (
196 | self._poll.poll(0) # non-block
197 | if
198 | self._mode == DYNAMIC_MODE
199 | else
200 | # self._poll.poll(timeout)
201 | self._poll.poll(timeout)
202 | )
203 |
204 | if not clients:
205 | return 0
206 |
207 | client: socket.socket
208 | for client, event in clients:
209 | if client == self._host and event == select.POLLIN: # new connect.
210 | while len(self._conns) >= self.max:
211 | # if connections fulfill
212 | # pop the earliest conn
213 | self._pop_conn()
214 | logger.debug("Conns fulfill, close one.")
215 |
216 | # append new to conns.
217 | try:
218 | client, addr = self._host.accept()
219 | # self._conns.insert(0, (client, addr)) # Append to conns.
220 | # self._poll.register(client)
221 | self._append_to_queue(client, addr)
222 | logger.debug("New conn: ", addr)
223 | continue
224 | except OSError as e:
225 | if e.args == (23,):
226 | # The number of connections exceeds the system limit
227 | # reduce the max quantity.
228 | self.max == len(self._conns)
229 | logger.warn(
230 | "The maximum number of connections you set exceeds the system limit. The max quantity is: ",
231 | len(self._conns)
232 | )
233 | continue
234 | else:
235 | logger.warn(
236 | "Unknown error when accept a new connection: ", e)
237 | # if CONFIG.debug: raise e # debug
238 | else: # Existing connections.
239 | if event == select.POLLIN: # New request.
240 | addr = self._find_addr(client)
241 | # try:
242 | self._append_to_queue(client, addr)
243 | continue
244 | elif event == select.POLLERR: # ERR happend, close it.
245 | self._pop_conn(client)
246 | logger.warn("A socket obj error, close it.")
247 |
248 | collect()
249 | return len(self._queue._heap)
250 |
251 | def process_once(self):
252 | """
253 | * In nromal-mode, it will wait a request and handle it befor timeout.
254 | * In dynamic-mode, it will check whether there is a new request, \
255 | if there is, it will process it, if not, it will return directly(non-blocking).
256 | """
257 | # task: routetask = self._queue.pop()
258 | # routetask: ("weight", "client", "addr", "http_head", "func", "url_vars")
259 |
260 | client: socket.socket
261 | head: httphead
262 |
263 | task: routetask = self._queue.pop_task()
264 |
265 | if task == None:
266 | # have no task.
267 | logger.debug("have no new request.")
268 | return
269 | else:
270 | client, addr, head, func, kwargs = task
271 |
272 | request = self.request
273 | response = self.response
274 | session = self.session
275 |
276 | request.init(client, addr, head)
277 | response.init(client)
278 | session.init(request, response)
279 |
280 | if self._keep_alv:
281 | if 'keep-alive' in request.headers.get("Connection", ""):
282 | keep = True
283 | response.headers["Connection"] = "keep-alive"
284 | else:
285 | keep = False
286 | response.headers["Connection"] = "close"
287 | else:
288 | keep = False
289 | response.headers["Connection"] = "close"
290 |
291 | if func != placeholder_func:
292 | # rule hited
293 | # logger.debug("rule hited: ", func)
294 | try:
295 | rlt = func(**kwargs)
296 | # After processing, flush the data-stream and close the connection.
297 | response._flush()
298 |
299 | if response._stream_mode:
300 | if not response._stream_finish:
301 | response.finish_stream()
302 | elif not response._responsed:
303 | # 还未响应过
304 | if rlt != None:
305 | # 有内容
306 | response.make_response(rlt)
307 | else:
308 | # 无内容
309 | response.statu_code = 500
310 | response.make_response(
311 | "The processing function did not return any data")
312 | except OSError:
313 | # Timeout
314 | # skip this request
315 | logger.debug("process timeout: ", request.url)
316 | self._pop_conn(client)
317 | return
318 | except Exception as e:
319 | # 处理错误, 500 状态码安排上
320 | logger.error("router function error happended: ", e)
321 | response.abort()
322 | if CONFIG.debug:
323 | raise e
324 | else:
325 | # rule not hited, try to send local file.
326 | # logger.debug("rule not hited, try to send local-file")
327 | try:
328 | # After processing, flush the data-stream and close the connection.
329 | response._flush()
330 | response.send_file(request.url)
331 | # 已经发送文件 | 发送 404
332 | except OSError:
333 | # Timeout
334 | # skip this request
335 | logger.debug("process timeout: ", request.url)
336 | self._pop_conn(client)
337 | return
338 | except Exception as e:
339 | logger.error("failed to send local file: ", e)
340 | # 处理错误, 500 状态码安排上
341 | response.abort()
342 | if CONFIG.debug:
343 | raise e
344 |
345 | if keep:
346 | # keep-alive
347 | # client.setblocking(False)
348 | self._append_conn(client, addr)
349 | # logger.debug("Responsed, keep alive.")
350 | else:
351 | # close
352 | self._pop_conn(client)
353 | # logger.debug("Responsed, close it.")
354 |
355 | logger.info(response.statu_code, ": ", addr, " > ", request.url)
356 | collect()
357 |
--------------------------------------------------------------------------------
/urouter/pool/queue.py:
--------------------------------------------------------------------------------
1 | from ..typeutil import routetask
2 |
3 |
4 | class Queue():
5 | _heap: list
6 |
7 | def __init__(self):
8 | """
9 | Initial a queue.
10 | """
11 | self._heap = []
12 |
13 | def push(self, weight: int, task: routetask):
14 |
15 | # Put higher weights at the last of the list
16 | # If the same weight appears,
17 | # the item to be added will be before the existing item
18 |
19 | # No sorting algorithm is needed here.
20 |
21 | length = len(self._heap)
22 |
23 | if length == 0:
24 | self._heap.append((weight, task))
25 | return
26 |
27 | for idx, (w, _) in enumerate(self._heap):
28 | if w > weight:
29 | # 你权重比我高, 排在你后面
30 | self._heap.insert(idx, (weight, task))
31 | return
32 | elif w == weight:
33 | # 和你权重一样高, 排在你后面
34 | self._heap.insert(idx, (weight, task))
35 | return
36 | elif idx == length - 1:
37 | # 已经到最后边了
38 | self._heap.append((weight, task))
39 | return
40 |
41 | def pop_task(self) -> routetask:
42 | """
43 | Pop the route-task, if have nothing, return None
44 | """
45 | # The higher value and the oldest connection will be at the end
46 | # so just pop it.
47 |
48 | return self._heap.pop()[1] if len(self._heap) else None
49 |
50 |
--------------------------------------------------------------------------------
/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/router.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- encoding: utf-8 -*-
3 | '''
4 | @File : router.py
5 | @Time : 2021/07/10 17:35:51
6 | @Author : M-Jay
7 | @Contact : m-jay-1376@qq.com
8 |
9 | The entry for this framework.
10 | '''
11 |
12 | import socket
13 |
14 | from gc import collect
15 | from .config import CONFIG
16 |
17 | from .context.session import Session
18 | from .context.response import Response
19 | from .context.request import Request
20 | from .consts import *
21 | from . import logger
22 | from .pool import Poll
23 |
24 | # =============================
25 | # These will pass by __init__.py, Don't change them (session = xxx),
26 | # To update these, pleause call xxx.close(), and xxx.init()
27 |
28 | # =============================
29 | logger = logger.get("router.main")
30 | collect()
31 |
32 |
33 | class uRouter():
34 | # ========vars==========
35 | _sock: socket.socket
36 | _stop: bool
37 | _poll: Poll
38 |
39 | _mode: int
40 | _root_path: str
41 |
42 | def __init__(
43 | self,
44 | host: str = "0.0.0.0",
45 | port: int = 80,
46 | root_path: str = "/www",
47 | mode: int = NORMAL_MODE,
48 | keep_alive=False,
49 | backlog: int = 5,
50 | sock_family: int = socket.AF_INET,
51 | ):
52 | """
53 | Create a router, and then you can add some rule for it.
54 |
55 | :param host: The bound IP, default is "0.0.0.0"
56 | :param port: The bound port, default is 80
57 | :param root_path: The storage path of static files, default is "/www"
58 | :param backlog: appoint the max number of connections at the same time.
59 | :param mode: appoint the work method, optional `NORMAL_MODE` or `DYNAMIC_MODE`
60 | :param sock_family: Appoint a sock-family to bind, such as ipv4(socket.AF_INET) or ipv6(socket.AF_INET6). micropython seems that have no ipv6 support, but i provide this param to use in future. defaults to ipv4(AF_INET)
61 | :type sock_family: Socket.CONSTS, optional
62 | """
63 | self._stop = False
64 | self._session = Session()
65 | self._response = Response()
66 | self._request = Request()
67 |
68 | self._mode = mode
69 |
70 | self._init_sock(host, port, sock_family, backlog)
71 | self._poll = Poll(
72 | mode,
73 | self._sock,
74 | self.request,
75 | self.response,
76 | self.session,
77 | keep_alive
78 | )
79 |
80 | # 格式化路径, 使其规范, root_path 后不能带 /
81 | if root_path.endswith("/"):
82 | root_path = root_path[:-1] # 过滤掉 /
83 | self._root_path = root_path
84 | self.response.root_path = root_path
85 |
86 | @property
87 | def request(self):
88 | return self._request
89 |
90 | @request.setter
91 | def request(self, arg):
92 | raise EOFError("Do not change the request obj.")
93 |
94 | @property
95 | def response(self):
96 | return self._response
97 |
98 | @response.setter
99 | def response(self, arg):
100 | raise EOFError("Do not change the response obj.")
101 |
102 | @property
103 | def session(self):
104 | return self._session
105 |
106 | @session.setter
107 | def session(self, arg):
108 | raise EOFError("Do not change the session obj.")
109 |
110 | def _init_sock(
111 | self,
112 | host: str,
113 | port: int,
114 | family: int,
115 | backlog: int
116 | ):
117 | # init the SOCKET
118 |
119 | self._sock = socket.socket(
120 | family, socket.SOCK_STREAM) # SOCK_STREAM (TCP)
121 |
122 | self._sock.setsockopt(
123 | socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # Fastely reuse-tcp
124 |
125 | try:
126 | self._sock.bind(socket.getaddrinfo(
127 | host, port)[0][-1])
128 | except:
129 | # there may raise a error on esp8266, do not know why.
130 | self._sock.bind((host, port))
131 |
132 | self._sock.listen(backlog)
133 | logger.info("Server listening on %s:%s" % (host, port))
134 |
135 | def serve_forever(self):
136 | """
137 | Auto-run the web servies, if you want to accept the request in manual by your self, use the function `accept` instead of me.
138 | When you call me, I'll run all the time after you close the server.
139 | * This method is only work in NORMAL-MODE
140 | * This method will block your thread.
141 |
142 | :param timeout: The timeout(ms), if this time is exceeded, we will return. Set a negative number to wait forever, defaults to -1
143 | :type timeout: int, optional
144 | """
145 | assert self._mode != DYNAMIC_MODE, TypeError(
146 | "This method isn't work in DYNAMIC-MODE")
147 |
148 | logger.info("Start to listen the http requests.")
149 | while self._sock: # loop until stop. when stop, _sock will be None
150 | try:
151 | self.serve_once()
152 | if self._stop:
153 | logger.info("Stop serving.")
154 | return
155 | except KeyboardInterrupt:
156 | break
157 | except Exception as e:
158 | logger.critical("serve error: ", e)
159 | if CONFIG.debug:
160 | raise e
161 |
162 | def serve_once(self, timeout: int = -1) -> bool:
163 | """
164 | Wait to accept a request from client in manual, it is disposable, if you need a auto acceptor, pleasue use the function `serve`
165 | * This method is only work in NORMAL-MODE
166 |
167 | :param timeout: The timeout(ms), if this time is exceeded, we will return. Set a negative number to wait forever, defaults to -1
168 | :type timeout: int, optional
169 | :return: If success, return True, failure to False
170 | """
171 | try:
172 | self._poll.check(timeout)
173 | self._poll.process_once()
174 | except Exception as e:
175 | logger.critical("Serve critical: ", e)
176 | if CONFIG.debug: raise e
177 |
178 | def check(self, timeout: int = -1):
179 | """
180 | Check the new request or new connetcions, and append them into Queue.
181 |
182 | :param timeout: int, The timeout(ms), if this time is exceeded, we will return. Set a negative number to wait forever, defaults to -1
183 | :return: the quantity of waiting tasks.
184 | """
185 | try:
186 | return self._poll.check(timeout)
187 | except Exception as e:
188 | logger.critical("Check critical: ", e)
189 | if CONFIG.debug: raise e
190 | return 0
191 | # =========
192 | # METHODS
193 | # ↓↓↓↓↓↓↓↓↓
194 |
195 | def stop(self):
196 | """
197 | When you call `serve_forever`, you can use this method to stop the server when the last request was handler out.
198 | """
199 | self._stop = True
200 |
201 | def close(self):
202 | """
203 | Stop the server. If you had been called the func `serve_forever`, it will be return at the last affiar was done.
204 | """
205 | self._poll.close()
206 |
207 | self._sock.close()
208 | self._sock = None
209 | # del self
210 |
211 | def route(
212 | self,
213 | rule: str,
214 | methods: iter = (GET, ),
215 | weight: int = 0
216 | ):
217 | """
218 | Append a rule to the router. For example:
219 | ```
220 | @app.route ("/")
221 | def index ():
222 | return "
hello world!
"
223 | ```
224 | now when you visit the root directory(/), you will see `Hello world`.
225 | * NOTICE:
226 | If you append several same rule in a same catching method, we will catch the initial defind.
227 |
228 | :param rule: The url rule.
229 | :param methods: The method you will catch, you can defind many same rule but in a difference catching-method to achieve different functions, in optional such as `GET`\`POST`\`PUT`\`HEAD`, must be a capital word, defaults to "GET"
230 | :type methods: iter, optional
231 | """
232 | def decorater(func):
233 | # TODO
234 | self._poll._rlt.append(rule, func, weight, methods)
235 | return func
236 | return decorater
237 |
238 | def websocket(self):
239 | assert False, NotImplementedError("This is developping")
240 | # TODO
241 |
--------------------------------------------------------------------------------
/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/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 | )
--------------------------------------------------------------------------------
/urouter/util.py:
--------------------------------------------------------------------------------
1 | from .config import CONFIG
2 | from .consts import *
3 |
4 |
5 | def catch_chars(string: str) -> str:
6 | """
7 | 将转化过的html还原
8 | :param string: 可以是一个 str, 也可以是一个 list[str]
9 | :return 输入 str 类型返回 str 类型 , 输入 list 类型返回 list
10 | 例如:
11 | escape_chars ("hello world") -> "hello world"
12 | """
13 |
14 | if type(string) == str:
15 | for k, v in HTML_ESCAPE_CHARS.items():
16 | string = string.replace(k, v)
17 | elif type(string) == list:
18 | for k, v in HTML_ESCAPE_CHARS.items():
19 | for idx in range(len(string)):
20 | string[idx] = string[idx].replace(k, v)
21 | return string
22 |
23 |
24 | def load_form_data(data: str, obj: dict):
25 | """
26 | 传入一个bytes或者str, 将其解析成Python的dict对象, 用于解析HTML的表单数据
27 | 例如:
28 | load_form_data("user_name=abc&user_passwd=123456")
29 | -> {
30 | "user_name" : "abc",
31 | "user_passwd" : "123456"
32 | }
33 | """
34 | if type(data) == bytes:
35 | data = data.decode(CONFIG.charset)
36 |
37 | data: list = data.split("&")
38 |
39 | if not data == ['']: # data 有数据时再进行解析
40 | data = catch_chars(data)
41 | for line in data:
42 | idx = line.find("=")
43 | # arg_name : line [:idx]
44 | # arg_value : line [idx+1:]
45 | obj[line[:idx]] = line[idx+1:]
46 | return obj
47 |
48 |
49 | def dump_headers_generator(headers: dict):
50 | """
51 | Create a generator from headers.
52 | when you generated it , it will return a header line.
53 | """
54 | for k, v in headers.items():
55 | if isinstance(v, (tuple, list)):
56 | # transform the tuple to str.
57 | yield "%s%s%s%s" % (k, ": ", '; '.join(v), "\r\n")
58 | else:
59 | yield "%s%s%s%s" % (k, ": ", v, "\r\n")
60 |
61 | return
62 |
63 |
64 | def is_html(string: str) -> bool:
65 | """
66 | Check whether if it was a html type.
67 | """
68 |
69 | if string.find(""):
70 | return True
71 | elif string.find(""):
72 | return True
73 | elif string.find("
"):
74 | return True
75 | elif string.find(""):
76 | return True
77 | elif string.find(""):
78 | return True
79 |
80 | return False
--------------------------------------------------------------------------------