├── .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 | ![LOGO](/cover.png) 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 --------------------------------------------------------------------------------