├── test
├── www
│ ├── index.html
│ ├── assets
│ │ ├── js
│ │ │ └── t-t.min.js
│ │ └── index.html
│ └── 中文路径
│ │ └── index.html
└── test_server.py
├── setup.cfg
├── demo
├── www
│ ├── cat.jpg
│ ├── index.html
│ ├── test.html
│ └── jquery-3.3.1.min.js
└── main.py
├── todo.md
├── .gitignore
├── easy_py_server
├── __init__.py
├── exception.py
├── datastruct.py
└── server.py
├── README.txt
├── setup.py
├── LICENSE
└── README.md
/test/www/index.html:
--------------------------------------------------------------------------------
1 | test
--------------------------------------------------------------------------------
/test/www/assets/js/t-t.min.js:
--------------------------------------------------------------------------------
1 | const test = 0
--------------------------------------------------------------------------------
/test/www/assets/index.html:
--------------------------------------------------------------------------------
1 | test2
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | description-file = README.md
--------------------------------------------------------------------------------
/test/www/中文路径/index.html:
--------------------------------------------------------------------------------
1 | test chinese
--------------------------------------------------------------------------------
/demo/www/cat.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scientificRat/easy_py_server/HEAD/demo/www/cat.jpg
--------------------------------------------------------------------------------
/todo.md:
--------------------------------------------------------------------------------
1 | # TODO
2 | 1. 更好的log
3 | 2. session处理不当,不应该开线程
4 | 3. websocket
5 | 4. https
6 | 5. ipv6
7 | 6. forward
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | */DS_Store
3 | .idea
4 | try.*
5 | __pycache__
6 | build/
7 | dist/
8 | easy_py_server.egg-info/
9 |
--------------------------------------------------------------------------------
/easy_py_server/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = "1.2.2"
2 |
3 | from .datastruct import Request, Response, ResponseFile, MultipartFile, Method, ResponseConfig
4 | from .exception import HttpException, WarpedInternalServerException, IllegalAccessException
5 | from .server import EasyPyServer
6 |
--------------------------------------------------------------------------------
/demo/www/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Title
6 |
7 |
8 |
9 |
Hello EasyPyServer
10 |

11 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/README.txt:
--------------------------------------------------------------------------------
1 | easy_py_server
2 | A flexible microframework providing reliable HTTP service for your projects.
3 |
4 | * Easy to make HTTP services by pure python.
5 | * Flexible to integrate with your existing code **without** any configuration file or environ settings.
6 | * Spring MCV like parameter injection implemented by python decorator: `@post`, `@get` etc.
7 | * Easy to manage `static resources`,`session`, `cookies`, `path parameter`, `redirection`, `file uploading` etc.
8 | * A single process multiple threads server framework that allows you share objects in your code.
9 | * Easy to customize. `easy-py-server` is written in pure python for easy debugging and customizing.
10 |
11 |
12 | For more information:
13 | https://github.com/scientificRat/easy_py_server.git
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import re
2 | import codecs
3 | from setuptools import setup
4 |
5 | with codecs.open('README.txt', encoding='utf-8') as f:
6 | long_description = f.read()
7 |
8 | with codecs.open("easy_py_server/__init__.py", encoding="utf8") as f:
9 | version = re.search(r'__version__ = "(.*?)"', f.read()).group(1)
10 |
11 | setup(name='easy_py_server',
12 | version=version,
13 | description='A flexible microframework providing reliable HTTP service for your projects.',
14 | author='Zhengyue Huang',
15 | author_email='huangzhengyue.1996@gmail.com',
16 | url='https://github.com/scientificRat/easy_py_server.git',
17 | packages=['easy_py_server'],
18 | install_requires=['Pillow', 'termcolor'],
19 | long_description=long_description,
20 | classifiers=[
21 | "Programming Language :: Python :: 3",
22 | "Development Status :: 4 - Beta",
23 | "Topic :: Internet :: WWW/HTTP :: HTTP Servers",
24 | "License :: OSI Approved :: MIT License",
25 | "Operating System :: OS Independent",
26 | ],
27 | python_requires='>=3.5',
28 | )
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Huang Zhengyue
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/easy_py_server/exception.py:
--------------------------------------------------------------------------------
1 | from .datastruct import HTTPStatus
2 |
3 |
4 | class HttpException(Exception):
5 | def __init__(self, http_status, info=""):
6 | self.http_status = http_status
7 | self.info = info
8 |
9 | def __str__(self):
10 | return "HttpException: " + str(self.http_status) + " " + str(self.info)
11 |
12 |
13 | class IllegalAccessException(HttpException):
14 | def __init__(self, error):
15 | HttpException.__init__(self, HTTPStatus.UNPROCESSABLE_ENTITY, error)
16 |
17 |
18 | class WarpedInternalServerException(HttpException):
19 | def __init__(self, original_error, overwrite_info=None):
20 | if isinstance(original_error, WarpedInternalServerException):
21 | self.error = original_error.error
22 | else:
23 | self.error = original_error
24 | if overwrite_info is None:
25 | overwrite_info = str(self.error)
26 | self.info = overwrite_info
27 | HttpException.__init__(self, HTTPStatus.INTERNAL_SERVER_ERROR, self.info)
28 |
29 | def __str__(self):
30 | return "InternalServerException: \n" + str(self.info)
31 |
--------------------------------------------------------------------------------
/demo/www/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | test
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
56 |
--------------------------------------------------------------------------------
/demo/main.py:
--------------------------------------------------------------------------------
1 | from easy_py_server import EasyPyServer, Request, Response, MultipartFile, ResponseFile, ResponseConfig
2 |
3 | eps = EasyPyServer('0.0.0.0', 8090, static_folder="www")
4 |
5 |
6 | # method GET
7 | @eps.get("/api")
8 | def demo(a: int, b: int):
9 | return dict(success=True, content="%d + %d = %d" % (a, b, a + b))
10 |
11 |
12 | @eps.get("/test_bool")
13 | def test_bool(a: bool = False):
14 | return str(a), str(type(a))
15 |
16 |
17 | @eps.get("/test_default")
18 | def test_bool(a=10):
19 | return str(a), str(type(a))
20 |
21 |
22 | # method POST
23 | @eps.post("/post")
24 | def post(key):
25 | return str(key)
26 |
27 |
28 | # ajax json
29 | @eps.post("/ajax-json")
30 | def json_request(r: Request):
31 | print(r.params)
32 | return "Got"
33 |
34 |
35 | # 自定义header
36 | @eps.post("/cross", ResponseConfig(headers={'Access-Control-Allow-Origin': '*'}))
37 | def cross_access():
38 | return "post allow"
39 |
40 |
41 | # 自定义header
42 | @eps.get("/cross", ResponseConfig(headers={'Access-Control-Allow-Origin': '*'}))
43 | def cross_access_get():
44 | return "get allow"
45 |
46 |
47 | # uploading file
48 | @eps.post("/multipart")
49 | def post(save_name: str, file: MultipartFile):
50 | save_path = '{}.txt'.format(save_name)
51 | file.save(save_path)
52 | return dict(success=True, message="save to {}".format(save_path))
53 |
54 |
55 | # download file
56 | @eps.get("/download")
57 | def download():
58 | with open("www/cat.jpg", 'rb') as f:
59 | all_bytes = f.read()
60 | return ResponseFile(all_bytes, filename="download_cat.jpg")
61 |
62 |
63 | # path parameter
64 | @eps.get("/api/:id")
65 | def demo_path(id: int):
66 | return 'api' + str(id)
67 |
68 |
69 | @eps.get("/sum_2/:a/and/:b")
70 | def sum_2(a: int, b: int):
71 | return a + b
72 |
73 |
74 | # same path, different methods
75 | @eps.get("/sum_3")
76 | def sum_3_get(a: float, b: float, c: float):
77 | # dict object can be automatically converted to json string to response
78 | return dict(success=True, rst=(a + b + c), message="by get method")
79 |
80 |
81 | @eps.post("/sum_3")
82 | def sum_3_post(a: float, b: float, c: float):
83 | # dict object can be automatically converted to json string to response
84 | return dict(success=True, rst=(a + b + c), message="by post method")
85 |
86 |
87 | @eps.get("/sum_many")
88 | def sum_many(arr: list):
89 | return sum(arr)
90 |
91 |
92 | # set session
93 | @eps.get("/set/:data")
94 | def set(request: Request, data):
95 | request.set_session_attribute("data", data)
96 | return "set: " + str(data)
97 |
98 |
99 | # read session
100 | @eps.get("/query")
101 | def query(request: Request):
102 | data = request.get_session_attribute("data")
103 | return "get: " + str(data)
104 |
105 |
106 | # redirection
107 | @eps.get("/redirect")
108 | def redirect():
109 | resp = Response()
110 | resp.set_redirection_url("/cat.jpg")
111 | return resp
112 |
113 |
114 | if __name__ == '__main__':
115 | # start the server (default is blocking)
116 | eps.run(blocking=True)
117 |
--------------------------------------------------------------------------------
/easy_py_server/datastruct.py:
--------------------------------------------------------------------------------
1 | from http import HTTPStatus
2 | from http.client import HTTPMessage
3 | from typing import (Optional, Dict, Any)
4 | from enum import Enum
5 | from .exception import IllegalAccessException
6 |
7 |
8 | class Request:
9 | def __init__(self, params: dict, cookies: dict = None, session: dict = None, raw_headers: HTTPMessage = None):
10 | self.params = params
11 | self.cookies = cookies
12 | self.session = session
13 | self.raw_headers = raw_headers # type: HTTPMessage
14 |
15 | def get_parm(self, key: str, required=True) -> str:
16 | value = self.params.get(key, None)
17 | if required and value is None:
18 | raise IllegalAccessException("Parameter '%s' is required" % (key,))
19 | return value
20 |
21 | def get_session(self) -> Dict[Any, Any]:
22 | return self.session if self.session is not None else {}
23 |
24 | def get_session_attribute(self, key: Any) -> Optional[Any]:
25 | return self.session.get(key, None) if self.session is not None else None
26 |
27 | def remove_session(self, key: Any):
28 | if self.session is not None:
29 | self.session.pop(key, None)
30 |
31 | def set_session_attribute(self, key: Any, value: Any):
32 | if self.session is None:
33 | self.session = {}
34 | self.session[key] = value
35 |
36 | def get_cookie(self, key):
37 | if self.cookies is not None:
38 | return self.cookies[key]
39 | return None
40 |
41 |
42 | class Response:
43 |
44 | def __init__(self, content=None, content_type=None, status=HTTPStatus.OK, headers: Dict = None):
45 | self.content = content
46 | self.content_type = content_type
47 | self.status = status
48 | self.error_message = None
49 | self.set_cookie_str_list = []
50 | self.additional_headers = {}
51 | self.redirection_url = None
52 | self.headers = {} if headers is None else headers
53 |
54 | def set_header(self, key, value):
55 | self.headers[key] = value
56 |
57 | def set_content_type(self, content_type: str) -> None:
58 | self.content_type = content_type
59 |
60 | def get_content_type(self) -> str:
61 | return self.content_type
62 |
63 | def set_status(self, status: HTTPStatus):
64 | self.status = status
65 |
66 | def get_status(self):
67 | return self.status
68 |
69 | def set_status_by_code(self, code: int):
70 | self.status = HTTPStatus(code)
71 |
72 | def set_content(self, content: Any):
73 | self.content = content
74 |
75 | def get_content(self):
76 | return self.content
77 |
78 | def set_cookie_str(self, cookie_str):
79 | self.set_cookie_str_list.append(cookie_str)
80 |
81 | def set_cookie_kv(self, key, value):
82 | self.set_cookie_str("%s=%s" % (str(key), str(value)))
83 |
84 | def get_cookie_str_list(self):
85 | return self.set_cookie_str_list
86 |
87 | def add_header(self, key: str, value: str):
88 | self.additional_headers[key] = value
89 |
90 | def get_additional_headers(self):
91 | return self.additional_headers
92 |
93 | def set_redirection_url(self, url):
94 | self.redirection_url = url
95 |
96 | def get_redirection_url(self):
97 | return self.redirection_url
98 |
99 | def error(self, message: str, status=HTTPStatus.BAD_REQUEST):
100 | self.error_message = message
101 | self.status = status
102 |
103 | def get_error_message(self):
104 | return self.error_message
105 |
106 |
107 | # response file for download
108 | class ResponseFile(Response):
109 | def __init__(self, file_bytes, filename=None):
110 | headers = {'Content-Transfer-Encoding': 'binary'}
111 | if filename is not None:
112 | headers['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
113 | super().__init__(file_bytes, content_type="application/octet-stream", headers=headers)
114 |
115 |
116 | # reference from RFC 7231
117 | class Method(Enum):
118 | GET = 1
119 | HEAD = 2
120 | POST = 3
121 | PUT = 4
122 | DELETE = 5
123 | CONNECT = 6
124 | OPTIONS = 7
125 | TRACE = 8
126 | PATCH = 9
127 |
128 |
129 | # upload file
130 | class MultipartFile:
131 | def __init__(self, filename: str, content_type: str, data: bytes):
132 | self.filename = filename
133 | self.content_type = content_type
134 | self.data = data
135 | self.__data_pointer = 0
136 |
137 | def save(self, file: str):
138 | with open(file, 'wb') as f:
139 | f.write(self.data)
140 |
141 | def read(self, cnt=-1):
142 | if cnt < 0:
143 | cnt = len(self.data)
144 | end = max(self.__data_pointer + cnt, len(self.data))
145 | rst = self.data[self.__data_pointer: end]
146 | self.__data_pointer = end
147 | return rst
148 |
149 |
150 | class ResponseConfig:
151 | def __init__(self, headers=None, content_type=None):
152 | self.headers = dict() if headers is None else headers
153 | if type(self.headers) != dict:
154 | raise ValueError("headers must be a dict")
155 | self.content_type = content_type
156 | if self.content_type is not None and type(self.content_type) != str:
157 | raise ValueError("content_type must be a string")
158 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # easy\_py\_server
2 |
3 | [](https://badge.fury.io/py/easy-py-server)
4 |
5 | > A flexible microframework providing reliable HTTP service for your projects.
6 |
7 | * Easy to make HTTP services by pure python.
8 | * Flexible to integrate with your existing code **without** any configuration file or environ settings.
9 | * Spring MCV like parameter injection implemented by python decorator: `@post`, `@get` etc.
10 | * Easy to manage `static resources`,`session`, `cookies`, `path parameter`, `redirection`, `file uploading` etc.
11 | * A single process multiple threads server framework that allows you share objects in your code.
12 | * Easy to customize. `easy-py-server` is written in pure python for easy debugging and customizing.
13 |
14 | ## Get started
15 | ### Environment
16 | * python >= 3.5
17 |
18 | ### Install
19 | stable version:
20 | ```bash
21 | pip3 install easy-py-server
22 | ```
23 | working version:
24 | ```bash
25 | pip3 install git+https://github.com/scientificRat/easy_py_server.git
26 | ```
27 |
28 | ### Demo
29 |
30 | ```python
31 | from easy_py_server import EasyPyServer, Request, Response, MultipartFile, ResponseFile, ResponseConfig
32 |
33 | eps = EasyPyServer('0.0.0.0', 8090, static_folder="www")
34 |
35 |
36 | # method GET
37 | @eps.get("/api")
38 | def demo(a: int, b: int):
39 | return dict(success=True, content="%d + %d = %d" % (a, b, a + b))
40 |
41 |
42 | # method POST
43 | @eps.post("/post")
44 | def post(key):
45 | return str(key)
46 |
47 |
48 | # ajax json
49 | @eps.post("/ajax-json")
50 | def json_request(r: Request):
51 | print(r.params)
52 | return "Got"
53 |
54 |
55 | # 自定义header
56 | @eps.post("/cross", ResponseConfig(headers={'Access-Control-Allow-Origin': '*'}))
57 | def cross_access():
58 | return "post allow"
59 |
60 |
61 | # 自定义header
62 | @eps.get("/cross", ResponseConfig(headers={'Access-Control-Allow-Origin': '*'}))
63 | def cross_access_get():
64 | return "get allow"
65 |
66 |
67 | # uploading file
68 | @eps.post("/multipart")
69 | def post(save_name: str, file: MultipartFile):
70 | save_path = '{}.txt'.format(save_name)
71 | file.save(save_path)
72 | return dict(success=True, message="save to {}".format(save_path))
73 |
74 |
75 | # download file
76 | @eps.get("/download")
77 | def download():
78 | with open("www/cat.jpg", 'rb') as f:
79 | all_bytes = f.read()
80 | return ResponseFile(all_bytes, filename="download_cat.jpg")
81 |
82 |
83 | # path parameter
84 | @eps.get("/api/:id")
85 | def demo_path(id: int):
86 | return 'api' + str(id)
87 |
88 |
89 | @eps.get("/sum_2/:a/and/:b")
90 | def sum_2(a: int, b: int):
91 | return a + b
92 |
93 |
94 | # same path, different methods
95 | @eps.get("/sum_3")
96 | def sum_3_get(a: float, b: float, c: float):
97 | # dict object can be automatically converted to json string to response
98 | return dict(success=True, rst=(a + b + c), message="by get method")
99 |
100 |
101 | @eps.post("/sum_3")
102 | def sum_3_post(a: float, b: float, c: float):
103 | # dict object can be automatically converted to json string to response
104 | return dict(success=True, rst=(a + b + c), message="by post method")
105 |
106 |
107 | @eps.get("/sum_many")
108 | def sum_many(arr: list):
109 | return sum(arr)
110 |
111 |
112 | # set session
113 | @eps.get("/set/:data")
114 | def set(request: Request, data):
115 | request.set_session_attribute("data", data)
116 | return "set: " + str(data)
117 |
118 |
119 | # read session
120 | @eps.get("/query")
121 | def query(request: Request):
122 | data = request.get_session_attribute("data")
123 | return "get: " + str(data)
124 |
125 |
126 | # redirection
127 | @eps.get("/redirect")
128 | def redirect():
129 | resp = Response()
130 | resp.set_redirection_url("/cat.jpg")
131 | return resp
132 |
133 |
134 | if __name__ == '__main__':
135 | # start the server (default is blocking)
136 | eps.run(blocking=True)
137 |
138 | ```
139 |
140 | ### Create directory for static resources(Optional)
141 | ```bash
142 | mkdir www
143 | # ... add some files into this directory
144 | ```
145 |
146 | ### Run and have fun :)
147 | ```bash
148 | python3 your-source.py
149 | # your 'www' directory should be in the same directory of 'your-source.py'
150 | ```
151 |
152 | ## Documentation
153 |
154 | For normal usages, you only need to know `class EasyPyServer`, `class Method`, `class Request`,`class Response`, `class ResponseConfig`
155 | .You can easily import them by
156 | ```python
157 | from easy_py_server import EasyPyServer, Method, Request, Response, ResponseConfig
158 | ```
159 |
160 | ### Creating Server
161 | ```python
162 | from easy_py_server import EasyPyServer
163 | # create
164 | eps = EasyPyServer(listen_address="0.0.0.0", port=8090)
165 | # run
166 | eps.start_serve(blocking=True)
167 | ```
168 |
169 | ### Registering Service
170 |
171 | You can register a service by adding a decorator such as `@route`, `@get`, `@post` to your function. Feel free
172 | to use these decorators, they will register your functions as callback without changing the original code of your definition. Among them, decorator `@route` has full support for different methods:
173 |
174 | ```python
175 | @eps.route(path="/path/to/your/api", methods=[Method.GET])
176 | def f(request: Request):
177 | return Response()
178 | ```
179 | You can bind `GET`/`POST` methods with a simpler `@get`/`@post` like it's shown in the demo code:
180 | ```python
181 | @eps.get("/api")
182 | def demo(a: int, b: int):
183 | return dict(success=True, content="%d + %d = %d" % (a, b, a + b))
184 | ```
185 | You can use the decorators to specify the response's `Content-Type` or customize headers. For example, you can set `Access-Control-Allow-Origin: *` to enable cross site accessing.
186 | ```python
187 | @eps.post("/cross", ResponseConfig(headers={'Access-Control-Allow-Origin': '*'}))
188 | def cross_access():
189 | return "post allow"
190 | ```
191 |
192 | > note: Decorators are the recommended way to register your service, but you can also
193 | > use the naive .add_request_listener() to do it.
194 |
195 |
196 | ```python
197 | from easy_py_server import EasyPyServer, Method
198 | def func():
199 | pass
200 | httpd = EasyPyServer()
201 | httpd.add_request_listener("/",[Method.GET],func)
202 |
203 | ```
204 | ### Parameter Injection
205 |
206 | Parameters such as `a`, `b` and `request` shown above can be automatically injected to your service function when it is requested.
207 | These parameters will be parsed from the HTTP request depending on the names of your variables. For instance, you can
208 | access `demo` function (shown above) and inject parameters `a` and `b` by visiting the URL:
209 | ```text
210 | http://:/api?a=1&b=12
211 | ```
212 | The value of parameter `a` will be parsed as 1 and as 12 for `b`.
213 |
214 | #### Specifying Types
215 | The parameters will be interpreted as `string` object by default. You can change this behavior by specifying explict
216 | types to make them automatically converted to your desired types.
217 | ```python
218 | # post multipart file
219 | @httpd.post("/multipart")
220 | def post(save_name: str, file: MultipartFile):
221 | save_path = '{}.txt'.format(save_name)
222 | file.save(save_path)
223 | return dict(success=True, message="save to {}".format(save_path))
224 | ```
225 | #### Special types
226 | * `Request`: http request object, encapsulating `session`, `parameters` ,`cookies`
227 | * `MultipartFile`: supporting for uploading files.
228 |
229 | #### Access session and cookies
230 | They are encapsulated inside the `Request` object, so you can get them by getting a `Request` object first. It's defined in `datastruct.py`, you can check this file for more details.
231 | ```python
232 | @httpd.get("/")
233 | def index(req: Request):
234 | req.get_session()
235 | req.set_cookie()
236 | return None
237 | ```
238 |
239 | ### Automatic Response Construction
240 | You can return a python object without explicitly constructing `Response` object. They'll be automatically converted a correct HTTP response to the client. The supporting objects are listed as following:
241 |
242 | * dict -> json
243 | * string -> text
244 | * PIL -> image
245 | * bytes -> octet-stream
246 | * other -> octet-stream
247 |
248 | As an example, you can easily return a PIL image:
249 | ```python
250 | from PIL import Image
251 | @httpd.get("/show_image")
252 | def show_image(image_path: str):
253 | try:
254 | img = Image.open(image_path, mode='r')
255 | except IOError as e:
256 | return None
257 | return img
258 | ```
259 |
260 |
261 | ## More
262 | * Supporting for `ipv6` and `https` will come soon.
263 |
--------------------------------------------------------------------------------
/test/test_server.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from easy_py_server import EasyPyServer, Method, Request, ResponseConfig
3 | import requests
4 | import multiprocessing as mt
5 |
6 |
7 | def mock_func():
8 | print("ok")
9 |
10 |
11 | class TestEasyPyServer(unittest.TestCase):
12 | def test_reuse_address(self):
13 | self.assertTrue(EasyPyServer.allow_reuse_address)
14 |
15 | def test_add_listener(self):
16 | class Mock(object):
17 | def __init__(self):
18 | self.listeners_dic = {}
19 |
20 | mock = Mock()
21 | url = "/i"
22 | EasyPyServer.add_request_listener(mock, url, [Method.GET, Method.OPTIONS], mock_func)
23 | # print(mock.listeners_dic)
24 | self.assertTrue(len(mock.listeners_dic) == 1)
25 | self.assertTrue(mock.listeners_dic[url][Method.GET][0] == mock_func)
26 | self.assertTrue(Method.POST not in mock.listeners_dic[url])
27 | self.assertTrue(mock.listeners_dic[url][Method.OPTIONS][0] == mock_func)
28 |
29 |
30 | class FunctionalTest(unittest.TestCase):
31 |
32 | def setUp(self) -> None:
33 | if hasattr(self, 'server') and self.server is not None:
34 | return
35 | self.port = 8999
36 | self.addr = 'localhost'
37 | self.server = EasyPyServer(self.addr, port=self.port)
38 |
39 | def set(data, r: Request):
40 | r.set_session_attribute('data', data)
41 | return "ok"
42 |
43 | @self.server.get('/get')
44 | def get(r: Request):
45 | return r.get_session_attribute('data')
46 |
47 | @self.server.get('/sum_2/:a/and/:bb')
48 | def sum_2(a: int, bb: int):
49 | return a + bb
50 |
51 | @self.server.get('/sum_list')
52 | def sum_list_get(arr: list):
53 | return "get" + str(sum(arr))
54 |
55 | @self.server.post('/sum_list')
56 | def sum_list_post(arr: list):
57 | arr = [float(a) for a in arr]
58 | return "post" + str(sum(arr))
59 |
60 | # 自定义header
61 | @self.server.post("/cross", ResponseConfig(headers={'Access-Control-Allow-Origin': '*'}))
62 | def cross_access():
63 | return "post allow"
64 |
65 | self.server.add_request_listener('/set', [Method.GET], set)
66 | self.thread = self.server.start_serve(blocking=False)
67 |
68 | def test_static(self):
69 | base_url = f'http://{self.addr}:{self.port}'
70 |
71 | def test_file(url, expect_text, expect_content_type='text/html'):
72 | rst = requests.get(url)
73 | self.assertEqual(rst.status_code, 200)
74 | self.assertEqual(rst.headers['Content-Length'], str(len(rst.text)))
75 | self.assertEqual(rst.headers['Content-Type'], expect_content_type)
76 | self.assertEqual(rst.text, expect_text)
77 |
78 | def test_file_not_exist(url):
79 | rst = requests.get(url)
80 | self.assertEqual(rst.status_code, 404)
81 | self.assertEqual(rst.headers['Content-Type'], 'text/html; charset=utf-8')
82 | self.assertTrue(len(rst.text) > 0)
83 |
84 | def test_forbidden(url):
85 | rst = requests.get(url)
86 | self.assertEqual(rst.status_code, 403)
87 | self.assertEqual(rst.headers['Content-Type'], 'text/html; charset=utf-8')
88 | self.assertTrue(len(rst.text) > 0)
89 |
90 | test_file(f'{base_url}', 'test')
91 | test_file(f'{base_url}/', 'test')
92 | test_file(f'{base_url}/index.html', 'test')
93 | test_file(f'{base_url}?', 'test')
94 | test_file(f'{base_url}/?a=10', 'test')
95 | test_file(f'{base_url}?b=10', 'test')
96 |
97 | test_file(f'{base_url}/中文路径', 'test chinese')
98 | test_file(f'{base_url}/中文路径/index.html', 'test chinese')
99 |
100 | test_file(f'{base_url}/assets', 'test2')
101 | test_file(f'{base_url}/assets/', 'test2')
102 | test_file(f'{base_url}/assets/?', 'test2')
103 | test_file(f'{base_url}/assets/?a=10', 'test2')
104 | test_file(f'{base_url}/assets?b=10', 'test2')
105 | test_file(f'{base_url}/assets/index.html', 'test2')
106 | test_file(f'{base_url}/assets/index.html?', 'test2')
107 | test_file(f'{base_url}/assets/index.html?b=10', 'test2')
108 |
109 | test_file(f'{base_url}/assets/js/t-t.min.js', 'const test = 0', 'application/javascript')
110 | test_file(f'{base_url}/assets/js/t-t.min.js?a=10s', 'const test = 0', 'application/javascript')
111 |
112 | test_file_not_exist(f'{base_url}/assets/js/t-t.min.j')
113 | test_file_not_exist(f'{base_url}/a')
114 | test_file_not_exist(f'{base_url}/dad/adf')
115 | test_file_not_exist(f'{base_url}/ ')
116 | test_file_not_exist(f'{base_url}/中文路径 ')
117 | test_file_not_exist(f'{base_url}/ 中文路径')
118 | test_file_not_exist(f'{base_url}/中文路径/ ')
119 |
120 | test_forbidden(f'{base_url}/none')
121 | test_forbidden(f'{base_url}/assets/js/')
122 | test_forbidden(f'{base_url}/assets/js')
123 | test_forbidden(f'{base_url}/assets/js?a=12')
124 |
125 | def test_api(self):
126 | base_url = f'http://{self.addr}:{self.port}'
127 | rst = requests.get(f"{base_url}/sum_list?arr=[1.1, 2,3,4,5 ]")
128 | self.assertEqual(rst.status_code, 200)
129 | self.assertEqual(rst.headers['Content-Type'], 'text/html; charset=utf-8')
130 | self.assertEqual(rst.text, 'get15.1')
131 |
132 | # post by request type: application/x-www-form-urlencoded
133 | rst = requests.post(f"{base_url}/sum_list", data=dict(arr=[1, 2, 3, 4, 5.2]))
134 | self.assertEqual(rst.status_code, 200)
135 | self.assertEqual(rst.headers['Content-Type'], 'text/html; charset=utf-8')
136 | self.assertEqual(rst.text, 'post15.2')
137 |
138 | # post by request type: application/x-www-form-urlencoded
139 | rst = requests.post(f"{base_url}/sum_list", data=dict(arr=[1, 2, 3, 4, 5.5], none='nothing'))
140 | self.assertEqual(rst.status_code, 200)
141 | self.assertEqual(rst.headers['Content-Type'], 'text/html; charset=utf-8')
142 | self.assertEqual(rst.text, 'post15.5')
143 |
144 | # post by request type: application/json
145 | rst = requests.post(f"{base_url}/sum_list", json=dict(arr=[1, 2.2, 3, 4], none='nothing'))
146 | self.assertEqual(rst.status_code, 200)
147 | self.assertEqual(rst.headers['Content-Type'], 'text/html; charset=utf-8')
148 | self.assertEqual(rst.text, 'post10.2')
149 |
150 | def test_response_config(self):
151 | base_url = f'http://{self.addr}:{self.port}'
152 | rst = requests.post(f"{base_url}/cross")
153 | self.assertEqual(rst.status_code, 200)
154 | self.assertEqual(rst.headers['Content-Type'], 'text/html; charset=utf-8')
155 | self.assertEqual(rst.headers['Access-Control-Allow-Origin'], '*')
156 | self.assertEqual(rst.text, 'post allow')
157 |
158 | def test_session(self):
159 | s = requests.Session()
160 | base_url = f'http://{self.addr}:{self.port}'
161 | rst = s.get(f'{base_url}/get')
162 | self.assertEqual(rst.headers['Content-Type'], 'application/octet-stream')
163 | self.assertEqual(len(rst.content), 0)
164 | rst = s.get(f'{base_url}/get')
165 | self.assertEqual(rst.headers['Content-Type'], 'application/octet-stream')
166 | self.assertEqual(len(rst.content), 0)
167 | test_data = "TEST"
168 | rst = s.get(f'{base_url}/set?data={test_data}')
169 | self.assertEqual(rst.text, 'ok')
170 | rst = s.get(f'{base_url}/get')
171 | self.assertIn('text/html', rst.headers['Content-Type'])
172 | self.assertEqual(rst.text, test_data)
173 | s.close()
174 | print("\nsession test success")
175 |
176 | def test_path_param(self):
177 | base_url = f'http://{self.addr}:{self.port}'
178 | rst = requests.get(f'{base_url}/sum_2/7/and/2')
179 | self.assertEqual(rst.headers['Content-Type'], 'text/html; charset=utf-8')
180 | self.assertEqual(rst.text, '9')
181 | rst = requests.get(f'{base_url}/sum_2/02/and/2/')
182 | self.assertEqual(rst.headers['Content-Type'], 'text/html; charset=utf-8')
183 | self.assertEqual(rst.text, '4')
184 | rst = requests.get(f'{base_url}/sum_2/2/and/-3')
185 | self.assertEqual(rst.headers['Content-Type'], 'text/html; charset=utf-8')
186 | self.assertEqual(rst.text, '-1')
187 | rst = requests.get(f'{base_url}/sum_2/-201/and/-3')
188 | self.assertEqual(rst.headers['Content-Type'], 'text/html; charset=utf-8')
189 | self.assertEqual(rst.text, '-204')
190 | print("\npath_param test success")
191 |
192 | def tearDown(self) -> None:
193 | self.server.server_close()
194 | # self.process.kill()
195 | # self.process.join()
196 | pass
197 |
198 |
199 | if __name__ == '__main__':
200 | unittest.main()
201 |
--------------------------------------------------------------------------------
/easy_py_server/server.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import inspect
3 | import io
4 | import json
5 | import os
6 | import re
7 | import sys
8 | import threading
9 | import time
10 | import traceback
11 | import urllib.parse
12 | import uuid
13 | import termcolor
14 | from http.server import (HTTPServer, BaseHTTPRequestHandler)
15 | from http import HTTPStatus
16 | from typing import (Tuple, Sequence, Callable)
17 | from socketserver import ThreadingMixIn
18 | from PIL.ImageFile import ImageFile
19 | from .datastruct import *
20 | from .exception import *
21 | from easy_py_server import __version__
22 |
23 |
24 | class EasyServerHandler(BaseHTTPRequestHandler):
25 | protocol_version = "HTTP/1.1"
26 | server_name = "EasyPyServer"
27 | server_version = server_name + "/" + __version__
28 | resource_dir = None # static file folder
29 | verbose_exception = True
30 | error_content_type = "text/html; charset=utf-8"
31 | error_message_format = """
32 |
33 |
34 |
35 | Error Page
36 |
37 |
38 | EasyPyServer
39 | %(code)d %(message)s
Error code explanation: %(code)s
details:
%(explain)s
40 |
41 |
42 | """
43 | extensions_map = {
44 | 'py': 'text/plain', 'c': 'text/plain', 'h': 'text/plain', 'cpp': 'text/plain', 'hpp': 'text/plain',
45 | 'txt': 'text/plain',
46 | 'html': 'text/html', 'htm': 'text/html', 'htx': 'text/html',
47 | 'csv': 'text/csv',
48 | 'jpeg': 'image/jpeg', 'jpg': 'image/jpeg', 'jpe': 'image/jpeg', 'jfif': 'image/jpeg',
49 | 'gif': 'image/gif',
50 | 'png': 'image/png',
51 | 'svg': 'image/svg+xml',
52 | 'tif': 'image/tiff', 'tiff': "image/tiff",
53 | 'ico': 'application/x-ico',
54 | 'css': 'text/css',
55 | 'js': 'application/javascript',
56 | 'json': 'application/json',
57 | 'pdf': 'application/pdf',
58 | 'woff': 'application/font-woff',
59 | 'mp3': 'audio/mp3',
60 | 'mp4': 'audio/mp4',
61 | 'wma': 'audio/x-ms-wma',
62 | 'avi': 'video/avi',
63 | }
64 | SESSION_COOKIE_NAME = "EASY_SESSION_ID"
65 | DEFAULT_SESSION_EXPIRE_SECONDS = 12 * 3600
66 | DEFAULT_INDEX_FILES = ('index.html', 'index.htm')
67 |
68 | def __init__(self, conn_sock, client_address, server):
69 | # client_address : (ip, port)
70 | assert isinstance(server, EasyPyServer)
71 | self.server = server # type: EasyPyServer
72 | super().__init__(conn_sock, client_address, server)
73 |
74 | def version_string(self):
75 | return self.server_version
76 |
77 | @classmethod
78 | def set_server_name(cls, name):
79 | cls.server_name = name
80 |
81 | @classmethod
82 | def set_resource_dir(cls, resource_dir):
83 | cls.resource_dir = resource_dir
84 |
85 | @classmethod
86 | def set_error_format(cls, template):
87 | cls.error_message_format = template
88 |
89 | def find_listener(self, path: str, param: dict, method: Method):
90 | listeners_dic = self.server.listeners_dic
91 | entity = listeners_dic.get(path, None)
92 | # if has path parameters, the key will be the regular expression not the raw path
93 | if entity is None:
94 | for k in listeners_dic:
95 | match = re.fullmatch(k, path)
96 | if match is not None:
97 | if method not in listeners_dic[k]:
98 | raise HttpException(HTTPStatus.METHOD_NOT_ALLOWED)
99 | path_param_values = match.groups()
100 | _, params_key, _ = listeners_dic[k][method]
101 | if len(path_param_values) == len(params_key):
102 | for i in range(0, len(params_key)):
103 | param[params_key[i]] = urllib.parse.unquote(path_param_values[i])
104 | entity = listeners_dic[k]
105 | break
106 | if entity is None:
107 | return None, None
108 | if method not in entity:
109 | raise HttpException(HTTPStatus.METHOD_NOT_ALLOWED)
110 | listener, _, response_config = entity[method]
111 | return listener, response_config
112 |
113 | def call_listener(self, listener, request: Request) -> Response:
114 | pass_param_list = self.generate_listener_parameters(listener, request)
115 | request_with_session = True if request.session is not None else False
116 | try:
117 | # call the listener
118 | rtn = listener(*pass_param_list)
119 | response = self.convert_rtn(rtn)
120 | session = request.get_session()
121 | if session is not None and len(session) > 0 and not request_with_session:
122 | # set new sessions
123 | session_cookie_str = self.create_new_session(session)
124 | response.set_cookie_str(session_cookie_str)
125 | return response
126 | except HttpException as e:
127 | raise e
128 | except Exception as e:
129 | raise WarpedInternalServerException(e)
130 |
131 | def make_response(self, response: Response, response_config: ResponseConfig):
132 | """
133 | Response to client according to `Response`
134 | :param response: Response object
135 | :param response_config: ResponseConfig which is set by add_listener
136 | :return: None
137 | """
138 | # if error message is set, ignore any return content
139 | if response.get_error_message() is not None:
140 | self.send_error(response.get_status(), None, response.get_error_message())
141 | else:
142 | self.send_response(response.get_status())
143 | # send customized headers
144 | for k, v in response.headers.items():
145 | self.send_header(k, v)
146 | if response.get_status() == HTTPStatus.PERMANENT_REDIRECT:
147 | self.send_header("Location", response.get_redirection_url())
148 | self.end_headers()
149 | return
150 | # send response with content
151 | content_type = response.get_content_type()
152 | if content_type is None:
153 | content_type = self.server.default_response_type
154 | self.send_header("Content-type", content_type)
155 | content = response.get_content()
156 | if content is None:
157 | content = b""
158 | self.send_header("Content-Length", str(len(content)))
159 | for cookie_str in response.get_cookie_str_list():
160 | self.send_header("Set-Cookie", cookie_str)
161 | for key, value in response.get_additional_headers().items():
162 | self.send_header(key, value)
163 | if response_config is not None and response_config.headers is not None:
164 | for key, value in response_config.headers.items():
165 | self.send_header(key, value)
166 | self.end_headers()
167 | self.wfile.write(content)
168 |
169 | def make_response_on_exception(self, e):
170 | e = self.convert_exception(e)
171 | e_str = e.info
172 | if isinstance(e, WarpedInternalServerException):
173 | if e_str is None:
174 | e_str = ""
175 | e_str += "\n\n"
176 | if self.verbose_exception:
177 | # concat traceback error
178 | exc_type, exc_value, exc_traceback = sys.exc_info()
179 | err_list = traceback.format_exception(exc_type, exc_value, exc_traceback, limit=5)
180 | for item in err_list:
181 | e_str += str(item)
182 | self.send_error(e.http_status, None, e_str)
183 |
184 | def get_session(self, cookies) -> Optional[dict]:
185 | if cookies is not None and self.SESSION_COOKIE_NAME in cookies:
186 | return self.server.sessions.get(cookies[self.SESSION_COOKIE_NAME], None)
187 |
188 | def get_cookie(self) -> Optional[dict]:
189 | cookie_str = self.headers.get('Cookie', "")
190 | match = re.findall(r"([\S]*)=([\S]*)(;|$)", cookie_str)
191 | cookie_dict = {}
192 | for key, value, _ in match:
193 | cookie_dict[key] = value
194 | return cookie_dict
195 |
196 | def _clean_expire_session(self, session_code):
197 | time.sleep(self.DEFAULT_SESSION_EXPIRE_SECONDS)
198 | self.server.sessions.pop(session_code)
199 |
200 | def create_new_session(self, session: dict) -> str:
201 | new_session_code = str(uuid.uuid1()).replace("-", "")
202 | while new_session_code in self.server.sessions:
203 | new_session_code = str(uuid.uuid1()).replace("-", "")
204 |
205 | # fixme: 开线程清理的做法可能不得当, 可以只用一个清理线程
206 | threading.Thread(target=self._clean_expire_session, args=(new_session_code,), daemon=True).start()
207 | self.server.sessions[new_session_code] = session
208 | expire_date = self.date_time_string(int(time.time()) + self.DEFAULT_SESSION_EXPIRE_SECONDS)
209 | session_cookie_str = self.SESSION_COOKIE_NAME + "=" + new_session_code + "; path=/; expires=" + expire_date
210 | return session_cookie_str
211 |
212 | def deal_static_file_request(self, path, response_config: ResponseConfig):
213 | if self.resource_dir is None:
214 | self.send_error(HTTPStatus.FORBIDDEN)
215 | return
216 | # for security
217 | if len(re.findall(r'(/\.\./)', path)) != 0:
218 | self.send_error(HTTPStatus.FORBIDDEN)
219 | return
220 | path = os.path.join(self.resource_dir, path[1:])
221 | if not os.path.exists(path):
222 | self.send_error(HTTPStatus.NOT_FOUND)
223 | return
224 | if os.path.isdir(path):
225 | for index in self.DEFAULT_INDEX_FILES:
226 | new_path = os.path.join(path, index)
227 | if os.path.isfile(new_path) and os.path.exists(new_path):
228 | path = new_path
229 | break
230 | if os.path.isdir(path):
231 | self.send_error(HTTPStatus.FORBIDDEN)
232 | return
233 | file_basename = os.path.basename(path)
234 | postfix = file_basename.split('.')[-1].lower()
235 | default_type = 'application/octet-stream'
236 | if response_config is not None and response_config.content_type is not None:
237 | content_type = response_config.content_type
238 | else:
239 | if len(postfix) == len(file_basename):
240 | content_type = default_type
241 | else:
242 | content_type = self.extensions_map.get(postfix, default_type)
243 | # read file
244 | try:
245 | f = open(path, 'rb')
246 | except OSError:
247 | self.send_error(HTTPStatus.NOT_FOUND)
248 | return
249 | try:
250 | fs = os.fstat(f.fileno())
251 | last_modified = self.headers.get("If-Modified-Since", None)
252 | if last_modified is not None:
253 | last_modified_time = time.strptime(last_modified, '%a, %d %b %Y %H:%M:%S %Z')
254 | if time.gmtime(fs.st_mtime) == last_modified_time:
255 | self.send_response(HTTPStatus.NOT_MODIFIED)
256 | self.end_headers()
257 | return
258 | self.send_response(HTTPStatus.OK)
259 | self.send_header("Content-type", content_type)
260 | self.send_header("Content-Length", str(fs.st_size))
261 | self.send_header("Last-Modified", self.date_time_string(int(fs.st_mtime)))
262 | if response_config is not None and response_config.headers is not None:
263 | for key in response_config.headers:
264 | self.send_header(key, str(response_config.headers[key]))
265 | self.end_headers()
266 | while True:
267 | buf = f.read(1024 * 16)
268 | if not buf:
269 | break
270 | self.wfile.write(buf)
271 | return
272 | except EnvironmentError:
273 | self.send_error(HTTPStatus.INTERNAL_SERVER_ERROR)
274 | finally:
275 | f.close()
276 |
277 | def parse_request_body(self):
278 | request_len = int(self.headers.get("Content-Length", 0))
279 | request_type = self.headers.get("Content-Type", None)
280 | body = self.rfile.read(request_len) # type: bytes
281 | param_more = {}
282 | if request_type is not None:
283 | if re.match("application/x-www-form-urlencoded", request_type) is not None:
284 | # todo: 传入参数有嵌套时没能正确处理 例如k[m]=2
285 | param_more = self.parse_parameter(bytes.decode(body))
286 | elif re.match('application/json', request_type) is not None:
287 | param_more = self.parse_parameter(bytes.decode(body))
288 | elif re.match("multipart/form-data", request_type) is not None:
289 | boundary = re.findall(r'boundary=([\S]+)', request_type)
290 | if len(boundary) == 1:
291 | boundary = ("--{}".format(boundary[0])).encode()
292 | param_more_raw = self.parse_multipart_form_data(body, boundary)
293 | for name, (filename, content_type, data) in param_more_raw.items():
294 | if filename is None:
295 | param_more[name] = data
296 | else:
297 | param_more[name] = MultipartFile(filename, content_type, data)
298 | else:
299 | raise NotImplementedError("Unsupported request type: %s" % request_type)
300 | return body, param_more
301 |
302 | def construct_request_object(self, param) -> Request:
303 | cookies = self.get_cookie()
304 | session = self.get_session(cookies)
305 | raw_headers = self.headers # type: HTTPMessage
306 | return Request(param, cookies, session, raw_headers)
307 |
308 | def default_response_process(self, method):
309 | try:
310 | path, param = self.parse_url_path(self.path)
311 | body, param_more = self.parse_request_body()
312 | param.update(param_more)
313 | listener, response_config = self.find_listener(path, param, method)
314 | if listener is None:
315 | raise HttpException(HTTPStatus.NOT_FOUND)
316 | else:
317 | request = self.construct_request_object(param)
318 | response = self.call_listener(listener, request)
319 | self.make_response(response, response_config)
320 | except Exception as e:
321 | print(traceback.format_exc())
322 | print(e)
323 | self.make_response_on_exception(e)
324 |
325 | def do_GET(self):
326 | try:
327 | # parse and separate request url
328 | path, param = self.parse_url_path(self.path)
329 | # find listener. request will be regarded as static resource if no listener existed
330 | listener, response_config = self.find_listener(path, param, Method.GET)
331 | if listener is None:
332 | self.deal_static_file_request(path, response_config)
333 | else:
334 | request = self.construct_request_object(param)
335 | response = self.call_listener(listener, request)
336 | self.make_response(response, response_config)
337 | except Exception as e:
338 | print(traceback.format_exc())
339 | print(e)
340 | self.make_response_on_exception(e)
341 |
342 | def do_POST(self):
343 | self.default_response_process(Method.POST)
344 |
345 | # fixme: should not be default, especially for static files
346 | def do_HEAD(self):
347 | self.default_response_process(Method.HEAD)
348 |
349 | def do_DELETE(self):
350 | self.default_response_process(Method.DELETE)
351 |
352 | def do_PUT(self):
353 | self.default_response_process(Method.PUT)
354 |
355 | def do_CONNECT(self):
356 | self.default_response_process(Method.CONNECT)
357 |
358 | def do_OPTIONS(self):
359 | self.default_response_process(Method.OPTIONS)
360 |
361 | def do_TRACE(self):
362 | self.default_response_process(Method.TRACE)
363 |
364 | def do_PATCH(self):
365 | self.default_response_process(Method.PATCH)
366 |
367 | def log_request(self, code="-", size="-"):
368 | # copy from werkzeug
369 | try:
370 | path = urllib.parse.unquote(self.path)
371 | msg = "%s %s %s" % (self.command, path, self.request_version)
372 | except AttributeError:
373 | # path isn't set if the requestline was bad
374 | msg = self.requestline
375 | code = str(int(code))
376 | color = termcolor.colored
377 | if code[0] == "1": # 1xx - Informational
378 | msg = color(msg, attrs=["bold"])
379 | elif code[0] == "2": # 2xx - Success
380 | msg = color(msg, color="white")
381 | elif code == "304": # 304 - Resource Not Modified
382 | msg = color(msg, color="cyan")
383 | elif code[0] == "3": # 3xx - Redirection
384 | msg = color(msg, color="green")
385 | elif code == "404": # 404 - Resource Not Found
386 | msg = color(msg, color="yellow")
387 | elif code[0] == "4": # 4xx - Client Error
388 | msg = color(msg, color="red", attrs=["bold"])
389 | else: # 5xx, or any other response
390 | msg = color(msg, color="magenta", attrs=["bold"])
391 | self.log("info", '"%s" %s %s', msg, code, size)
392 |
393 | def log_error(self, *args):
394 | self.log("error", *args)
395 |
396 | def log_message(self, format, *args):
397 | self.log("info", format, *args)
398 |
399 | def address_string(self):
400 | # return the Host property in request header
401 | # the super method returns the socket client IP which is not expected under a proxy request
402 | return self.headers.get("X-Real-IP", super(EasyServerHandler, self).address_string())
403 |
404 | def log(self, type, message, *args):
405 | rst = "[%s] %s - - [%s] %s" % (type, self.address_string(), self.log_date_time_string(), message % args)
406 | if type == 'error':
407 | rst = termcolor.colored(rst, color="red", attrs=["bold"])
408 | print(rst)
409 |
410 | @staticmethod
411 | def convert_exception(e) -> HttpException:
412 | if isinstance(e, HttpException):
413 | return e
414 | if isinstance(e, NotImplementedError):
415 | return HttpException(HTTPStatus.NOT_IMPLEMENTED, info=str(e))
416 | else:
417 | return WarpedInternalServerException(e)
418 |
419 | @staticmethod
420 | def parse_parameter(src_str):
421 | param = {}
422 | match = re.findall(r'(^|&)([^=]+)=([^&]*)', src_str)
423 | if len(match) > 0:
424 | for item in re.findall(r'(^|&)([^=]+)=([^&]*)', src_str):
425 | key = urllib.parse.unquote(item[1])
426 | value = urllib.parse.unquote(item[2])
427 | enforced_list_param = False
428 | # to deal with params like arr[]=10&arr[]=12&arr[]=15
429 | if len(key) > 2 and key[-2:] == '[]':
430 | key = key[:-2]
431 | enforced_list_param = True
432 | # to deal with params like arr=10&arr=12&arr=15
433 | if key in param:
434 | if type(param[key]) == list:
435 | param[key].append(value)
436 | else:
437 | param[key] = [param[key], value]
438 | else:
439 | param[key] = value if not enforced_list_param else [value]
440 | else:
441 | try:
442 | param = json.loads(src_str)
443 | except json.JSONDecodeError as e:
444 | pass
445 | # make all the request param string
446 | for key in param:
447 | if type(param[key]) != str:
448 | param[key] = json.dumps(param[key])
449 | return param
450 |
451 | @staticmethod
452 | def parse_url_path(path) -> Tuple[str, Dict[str, str]]:
453 | row = path.split('?')
454 | path = row[0]
455 | if len(path) > 1 and path[-1] == '/':
456 | path = path[:-1]
457 | param = {} if len(row) < 2 else EasyServerHandler.parse_parameter(row[1])
458 | return urllib.parse.unquote(path), param
459 |
460 | @staticmethod
461 | def parse_multipart_form_data(body: bytes, boundary: bytes):
462 | """
463 | Parse multipart/form-data
464 | :param body: body in bytes
465 | :param boundary: boundary string in bytes
466 | :return: DICT { parm_name: (filename, content_type, data) }
467 | """
468 | end = body.rfind(b'--\r\n')
469 | if end > 0 and len(body) - end == 4:
470 | # remove tail
471 | body = body[:end]
472 | parts = body.split(boundary)
473 | rst = {}
474 | for part in parts:
475 | if len(part) == 0:
476 | continue
477 | splits = part.split(b'\r\n\r\n')
478 | assert len(splits) == 2
479 | head, data = splits
480 | data = data[:-2] # remove \r\n
481 | # parse name
482 | match = re.findall(br'name="([\S]+)"', head)
483 | name = urllib.parse.unquote(match[0].decode())
484 | # parse filename
485 | filename = None
486 | match = re.findall(br'filename="([\S ]+)"', head)
487 | if len(match) > 0:
488 | filename = urllib.parse.unquote(match[0].decode())
489 | # parse Content-Type
490 | content_type = None
491 | if filename is not None:
492 | match = re.findall(br'Content-Type: ([\S]+)$|;', head)
493 | if len(match) > 0:
494 | content_type = match[0]
495 | if filename is None:
496 | data = urllib.parse.unquote(data.decode())
497 | rst[name] = (filename, content_type, data)
498 | return rst
499 |
500 | @staticmethod
501 | def generate_listener_parameters(listener, request: Request):
502 | pass_param_list = []
503 | request_param_dic = request.params
504 | for name, parameter in inspect.signature(listener).parameters.items():
505 | # special objects
506 | # TODO: I hope to add `httpSession` `Cookies` objects, Request is not convenient
507 | if parameter.annotation == Request:
508 | pass_param_list.append(request)
509 | continue
510 | elif parameter.annotation == Response:
511 | pass_param_list.append(Response()) # empty response
512 | # get value from request parameters
513 | if name not in request_param_dic:
514 | if ":" + name in request_param_dic:
515 | value = request_param_dic[":" + name]
516 | elif parameter.default != inspect.Parameter.empty:
517 | value = parameter.default
518 | else:
519 | raise HttpException(HTTPStatus.UNPROCESSABLE_ENTITY, "parameter '%s' is required" % name)
520 | else:
521 | value = request_param_dic[name]
522 | # 根据参数annotation类型转换数据
523 | # 条件:有注解、value非None (None无法转换类型)
524 | if parameter.annotation != inspect.Parameter.empty and value is not None:
525 | tp = parameter.annotation
526 | try:
527 | if tp == MultipartFile:
528 | if not isinstance(value, MultipartFile):
529 | # 不用转换,Request中的param 已经对文件转换为了MultipartFile,这里只需要检查一下类型
530 | raise ValueError("parameter '%s' is required to be a Multipart file" % name)
531 | elif tp in (dict, list):
532 | value = json.loads(value)
533 | elif tp == tuple:
534 | value = tuple(json.loads(value))
535 | elif tp == bool:
536 | try:
537 | # may be a list/dict/bool/int, 这样做是为了正确处理传入的true/false 1,0 这种字符串的bool值
538 | possible_value = json.loads(value)
539 | except json.JSONDecodeError as e:
540 | possible_value = value
541 | value = bool(possible_value)
542 | else:
543 | value = tp(value) # force convert
544 | except Exception as e:
545 | raise WarpedInternalServerException(e, "Type converting error")
546 | pass_param_list.append(value)
547 | return pass_param_list
548 |
549 | @staticmethod
550 | def convert_rtn(rtn) -> Response:
551 | # todo: 或许应该区分一个RawResponse和便于用户使用的Response(设置header,同时自动解析content)
552 | if isinstance(rtn, Response):
553 | redirect_url = rtn.get_redirection_url()
554 | if redirect_url is not None:
555 | rtn = Response()
556 | rtn.set_status(HTTPStatus.PERMANENT_REDIRECT) # 308 PERMANENT REDIRECT
557 | rtn.set_redirection_url(redirect_url)
558 | return rtn
559 | response = Response()
560 | response.set_content(rtn)
561 | origin_content = response.get_content()
562 | # auto content type inferring and response construction
563 | # todo: This should support customization
564 | if type(origin_content) == str:
565 | response.set_content(origin_content.encode('utf-8'))
566 | response.set_content_type("text/html; charset=utf-8")
567 | elif type(origin_content) in [dict, list, tuple]:
568 | response.set_content(json.dumps(origin_content, ensure_ascii=False).encode('utf-8'))
569 | response.set_content_type('application/json; charset=utf-8')
570 | elif type(origin_content) in [int, float, complex]:
571 | origin_content = str(origin_content)
572 | response.set_content(origin_content.encode('utf-8'))
573 | response.set_content_type("text/html; charset=utf-8")
574 | elif isinstance(origin_content, ImageFile):
575 | # PIL image
576 | img_byte_array = io.BytesIO()
577 | origin_content.save(img_byte_array, format=origin_content.format)
578 | response.set_content(img_byte_array.getvalue())
579 | response.set_content_type(origin_content.get_format_mimetype())
580 | elif type(origin_content) == bytes or origin_content is None:
581 | response.set_content_type('application/octet-stream')
582 | else:
583 | response.set_content(bytes(origin_content))
584 | return response
585 |
586 |
587 | class EasyPyServer(ThreadingMixIn, HTTPServer):
588 |
589 | def __init__(self, listen_address: str = "0.0.0.0", port: int = 8090,
590 | server_app_name="EasyPyServer",
591 | static_folder="www",
592 | verbose_exception=True,
593 | default_response_type="text/html; charset=utf-8",
594 | http_request_handler=EasyServerHandler):
595 | self.server_app_name = server_app_name
596 | self.static_folder = os.path.abspath(static_folder)
597 | self.verbose_exception = verbose_exception
598 | if not os.path.exists(self.static_folder):
599 | print("[Warning] The setting static folder does not exist: {}".format(self.static_folder), file=sys.stderr)
600 | self.static_folder = None
601 |
602 | self.handler = http_request_handler # be compatible with customized handler
603 | self.handler.server_name = self.server_app_name
604 | self.handler.resource_dir = self.static_folder
605 | self.handler.verbose_exception = self.verbose_exception
606 |
607 | self.listen_address = listen_address
608 | self.port = port
609 | self.sessions = {}
610 | self.listeners_dic = {}
611 | self.default_response_type = default_response_type
612 | super().__init__((listen_address, port), self.handler)
613 |
614 | def start_serve(self, blocking=True):
615 | if not blocking:
616 | thread = threading.Thread(target=self.serve_forever)
617 | thread.setDaemon(True)
618 | thread.start()
619 | return thread
620 | else:
621 | self.serve_forever()
622 |
623 | def run(self, blocking=True):
624 | self.start_serve(blocking)
625 |
626 | def serve_forever(self, poll_interval=0.5):
627 | print("[%s] server running on http://%s:%d" % (
628 | datetime.datetime.now().ctime(), self.listen_address, self.port))
629 | super(EasyPyServer, self).serve_forever()
630 |
631 | def server_close(self) -> None:
632 | super(EasyPyServer, self).server_close()
633 | # self._BaseServer__shutdown_request = True
634 | self.shutdown()
635 |
636 | def add_request_listener(self, path: str, methods: Sequence[Method], listener: Callable,
637 | response_config: ResponseConfig = None):
638 | path_params = re.findall("(:[^/]+)", path)
639 | for parm in path_params:
640 | path = path.replace(parm, r"([\S]+)")
641 | if len(path_params) != 0:
642 | path = re.compile(path)
643 | if path in self.listeners_dic:
644 | for method in methods:
645 | self.listeners_dic[path][method] = listener, path_params, response_config
646 | else:
647 | self.listeners_dic[path] = {method: (listener, path_params, response_config) for method in methods}
648 |
649 | # decorators
650 | def route(self, path, methods=None, response_config=None):
651 | if methods is None:
652 | methods = [m for m in Method]
653 |
654 | def converter(listener):
655 | self.add_request_listener(path, methods, listener, response_config)
656 | return listener
657 |
658 | return converter
659 |
660 | def get(self, path, response_config=None):
661 | return self.route(path, [Method.GET], response_config)
662 |
663 | def post(self, path, response_config=None):
664 | return self.route(path, [Method.POST], response_config)
665 |
--------------------------------------------------------------------------------
/demo/www/jquery-3.3.1.min.js:
--------------------------------------------------------------------------------
1 | /*! jQuery v3.3.1 | (c) JS Foundation and other contributors | jquery.org/license */
2 | !function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){"use strict";var n=[],r=e.document,i=Object.getPrototypeOf,o=n.slice,a=n.concat,s=n.push,u=n.indexOf,l={},c=l.toString,f=l.hasOwnProperty,p=f.toString,d=p.call(Object),h={},g=function e(t){return"function"==typeof t&&"number"!=typeof t.nodeType},y=function e(t){return null!=t&&t===t.window},v={type:!0,src:!0,noModule:!0};function m(e,t,n){var i,o=(t=t||r).createElement("script");if(o.text=e,n)for(i in v)n[i]&&(o[i]=n[i]);t.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[c.call(e)]||"object":typeof e}var b="3.3.1",w=function(e,t){return new w.fn.init(e,t)},T=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;w.fn=w.prototype={jquery:"3.3.1",constructor:w,length:0,toArray:function(){return o.call(this)},get:function(e){return null==e?o.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=w.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return w.each(this,e)},map:function(e){return this.pushStack(w.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(o.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n0&&t-1 in e)}var E=function(e){var t,n,r,i,o,a,s,u,l,c,f,p,d,h,g,y,v,m,x,b="sizzle"+1*new Date,w=e.document,T=0,C=0,E=ae(),k=ae(),S=ae(),D=function(e,t){return e===t&&(f=!0),0},N={}.hasOwnProperty,A=[],j=A.pop,q=A.push,L=A.push,H=A.slice,O=function(e,t){for(var n=0,r=e.length;n+~]|"+M+")"+M+"*"),z=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),X=new RegExp(W),U=new RegExp("^"+R+"$"),V={ID:new RegExp("^#("+R+")"),CLASS:new RegExp("^\\.("+R+")"),TAG:new RegExp("^("+R+"|[*])"),ATTR:new RegExp("^"+I),PSEUDO:new RegExp("^"+W),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+P+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},G=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Q=/^[^{]+\{\s*\[native \w/,J=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,K=/[+~]/,Z=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ee=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},te=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ne=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},re=function(){p()},ie=me(function(e){return!0===e.disabled&&("form"in e||"label"in e)},{dir:"parentNode",next:"legend"});try{L.apply(A=H.call(w.childNodes),w.childNodes),A[w.childNodes.length].nodeType}catch(e){L={apply:A.length?function(e,t){q.apply(e,H.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function oe(e,t,r,i){var o,s,l,c,f,h,v,m=t&&t.ownerDocument,T=t?t.nodeType:9;if(r=r||[],"string"!=typeof e||!e||1!==T&&9!==T&&11!==T)return r;if(!i&&((t?t.ownerDocument||t:w)!==d&&p(t),t=t||d,g)){if(11!==T&&(f=J.exec(e)))if(o=f[1]){if(9===T){if(!(l=t.getElementById(o)))return r;if(l.id===o)return r.push(l),r}else if(m&&(l=m.getElementById(o))&&x(t,l)&&l.id===o)return r.push(l),r}else{if(f[2])return L.apply(r,t.getElementsByTagName(e)),r;if((o=f[3])&&n.getElementsByClassName&&t.getElementsByClassName)return L.apply(r,t.getElementsByClassName(o)),r}if(n.qsa&&!S[e+" "]&&(!y||!y.test(e))){if(1!==T)m=t,v=e;else if("object"!==t.nodeName.toLowerCase()){(c=t.getAttribute("id"))?c=c.replace(te,ne):t.setAttribute("id",c=b),s=(h=a(e)).length;while(s--)h[s]="#"+c+" "+ve(h[s]);v=h.join(","),m=K.test(e)&&ge(t.parentNode)||t}if(v)try{return L.apply(r,m.querySelectorAll(v)),r}catch(e){}finally{c===b&&t.removeAttribute("id")}}}return u(e.replace(B,"$1"),t,r,i)}function ae(){var e=[];function t(n,i){return e.push(n+" ")>r.cacheLength&&delete t[e.shift()],t[n+" "]=i}return t}function se(e){return e[b]=!0,e}function ue(e){var t=d.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function le(e,t){var n=e.split("|"),i=n.length;while(i--)r.attrHandle[n[i]]=t}function ce(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function fe(e){return function(t){return"input"===t.nodeName.toLowerCase()&&t.type===e}}function pe(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function de(e){return function(t){return"form"in t?t.parentNode&&!1===t.disabled?"label"in t?"label"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&ie(t)===e:t.disabled===e:"label"in t&&t.disabled===e}}function he(e){return se(function(t){return t=+t,se(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function ge(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}n=oe.support={},o=oe.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},p=oe.setDocument=function(e){var t,i,a=e?e.ownerDocument||e:w;return a!==d&&9===a.nodeType&&a.documentElement?(d=a,h=d.documentElement,g=!o(d),w!==d&&(i=d.defaultView)&&i.top!==i&&(i.addEventListener?i.addEventListener("unload",re,!1):i.attachEvent&&i.attachEvent("onunload",re)),n.attributes=ue(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=ue(function(e){return e.appendChild(d.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=Q.test(d.getElementsByClassName),n.getById=ue(function(e){return h.appendChild(e).id=b,!d.getElementsByName||!d.getElementsByName(b).length}),n.getById?(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){return e.getAttribute("id")===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n=t.getElementById(e);return n?[n]:[]}}):(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){var n="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),r.find.TAG=n.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):n.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},r.find.CLASS=n.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&g)return t.getElementsByClassName(e)},v=[],y=[],(n.qsa=Q.test(d.querySelectorAll))&&(ue(function(e){h.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+P+")"),e.querySelectorAll("[id~="+b+"-]").length||y.push("~="),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+b+"+*").length||y.push(".#.+[+~]")}),ue(function(e){e.innerHTML="";var t=d.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),h.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(n.matchesSelector=Q.test(m=h.matches||h.webkitMatchesSelector||h.mozMatchesSelector||h.oMatchesSelector||h.msMatchesSelector))&&ue(function(e){n.disconnectedMatch=m.call(e,"*"),m.call(e,"[s!='']:x"),v.push("!=",W)}),y=y.length&&new RegExp(y.join("|")),v=v.length&&new RegExp(v.join("|")),t=Q.test(h.compareDocumentPosition),x=t||Q.test(h.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return f=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r||(1&(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!n.sortDetached&&t.compareDocumentPosition(e)===r?e===d||e.ownerDocument===w&&x(w,e)?-1:t===d||t.ownerDocument===w&&x(w,t)?1:c?O(c,e)-O(c,t):0:4&r?-1:1)}:function(e,t){if(e===t)return f=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===d?-1:t===d?1:i?-1:o?1:c?O(c,e)-O(c,t):0;if(i===o)return ce(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?ce(a[r],s[r]):a[r]===w?-1:s[r]===w?1:0},d):d},oe.matches=function(e,t){return oe(e,null,null,t)},oe.matchesSelector=function(e,t){if((e.ownerDocument||e)!==d&&p(e),t=t.replace(z,"='$1']"),n.matchesSelector&&g&&!S[t+" "]&&(!v||!v.test(t))&&(!y||!y.test(t)))try{var r=m.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){}return oe(t,d,null,[e]).length>0},oe.contains=function(e,t){return(e.ownerDocument||e)!==d&&p(e),x(e,t)},oe.attr=function(e,t){(e.ownerDocument||e)!==d&&p(e);var i=r.attrHandle[t.toLowerCase()],o=i&&N.call(r.attrHandle,t.toLowerCase())?i(e,t,!g):void 0;return void 0!==o?o:n.attributes||!g?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null},oe.escape=function(e){return(e+"").replace(te,ne)},oe.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},oe.uniqueSort=function(e){var t,r=[],i=0,o=0;if(f=!n.detectDuplicates,c=!n.sortStable&&e.slice(0),e.sort(D),f){while(t=e[o++])t===e[o]&&(i=r.push(o));while(i--)e.splice(r[i],1)}return c=null,e},i=oe.getText=function(e){var t,n="",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=i(e)}else if(3===o||4===o)return e.nodeValue}else while(t=e[r++])n+=i(t);return n},(r=oe.selectors={cacheLength:50,createPseudo:se,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(Z,ee),e[3]=(e[3]||e[4]||e[5]||"").replace(Z,ee),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||oe.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&oe.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return V.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=a(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(Z,ee).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=E[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&E(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=oe.attr(r,e);return null==i?"!="===t:!t||(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i.replace($," ")+" ").indexOf(n)>-1:"|="===t&&(i===n||i.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,p,d,h,g=o!==a?"nextSibling":"previousSibling",y=t.parentNode,v=s&&t.nodeName.toLowerCase(),m=!u&&!s,x=!1;if(y){if(o){while(g){p=t;while(p=p[g])if(s?p.nodeName.toLowerCase()===v:1===p.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?y.firstChild:y.lastChild],a&&m){x=(d=(l=(c=(f=(p=y)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1])&&l[2],p=d&&y.childNodes[d];while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if(1===p.nodeType&&++x&&p===t){c[e]=[T,d,x];break}}else if(m&&(x=d=(l=(c=(f=(p=t)[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]||[])[0]===T&&l[1]),!1===x)while(p=++d&&p&&p[g]||(x=d=0)||h.pop())if((s?p.nodeName.toLowerCase()===v:1===p.nodeType)&&++x&&(m&&((c=(f=p[b]||(p[b]={}))[p.uniqueID]||(f[p.uniqueID]={}))[e]=[T,x]),p===t))break;return(x-=i)===r||x%r==0&&x/r>=0}}},PSEUDO:function(e,t){var n,i=r.pseudos[e]||r.setFilters[e.toLowerCase()]||oe.error("unsupported pseudo: "+e);return i[b]?i(t):i.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?se(function(e,n){var r,o=i(e,t),a=o.length;while(a--)e[r=O(e,o[a])]=!(n[r]=o[a])}):function(e){return i(e,0,n)}):i}},pseudos:{not:se(function(e){var t=[],n=[],r=s(e.replace(B,"$1"));return r[b]?se(function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),t[0]=null,!n.pop()}}),has:se(function(e){return function(t){return oe(e,t).length>0}}),contains:se(function(e){return e=e.replace(Z,ee),function(t){return(t.textContent||t.innerText||i(t)).indexOf(e)>-1}}),lang:se(function(e){return U.test(e||"")||oe.error("unsupported lang: "+e),e=e.replace(Z,ee).toLowerCase(),function(t){var n;do{if(n=g?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===h},focus:function(e){return e===d.activeElement&&(!d.hasFocus||d.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:de(!1),disabled:de(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return Y.test(e.nodeName)},input:function(e){return G.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:he(function(){return[0]}),last:he(function(e,t){return[t-1]}),eq:he(function(e,t,n){return[n<0?n+t:n]}),even:he(function(e,t){for(var n=0;n=0;)e.push(r);return e}),gt:he(function(e,t,n){for(var r=n<0?n+t:n;++r1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function be(e,t,n){for(var r=0,i=t.length;r-1&&(o[l]=!(a[l]=f))}}else v=we(v===a?v.splice(h,v.length):v),i?i(null,a,v,u):L.apply(a,v)})}function Ce(e){for(var t,n,i,o=e.length,a=r.relative[e[0].type],s=a||r.relative[" "],u=a?1:0,c=me(function(e){return e===t},s,!0),f=me(function(e){return O(t,e)>-1},s,!0),p=[function(e,n,r){var i=!a&&(r||n!==l)||((t=n).nodeType?c(e,n,r):f(e,n,r));return t=null,i}];u1&&xe(p),u>1&&ve(e.slice(0,u-1).concat({value:" "===e[u-2].type?"*":""})).replace(B,"$1"),n,u0,i=e.length>0,o=function(o,a,s,u,c){var f,h,y,v=0,m="0",x=o&&[],b=[],w=l,C=o||i&&r.find.TAG("*",c),E=T+=null==w?1:Math.random()||.1,k=C.length;for(c&&(l=a===d||a||c);m!==k&&null!=(f=C[m]);m++){if(i&&f){h=0,a||f.ownerDocument===d||(p(f),s=!g);while(y=e[h++])if(y(f,a||d,s)){u.push(f);break}c&&(T=E)}n&&((f=!y&&f)&&v--,o&&x.push(f))}if(v+=m,n&&m!==v){h=0;while(y=t[h++])y(x,b,a,s);if(o){if(v>0)while(m--)x[m]||b[m]||(b[m]=j.call(u));b=we(b)}L.apply(u,b),c&&!o&&b.length>0&&v+t.length>1&&oe.uniqueSort(u)}return c&&(T=E,l=w),x};return n?se(o):o}return s=oe.compile=function(e,t){var n,r=[],i=[],o=S[e+" "];if(!o){t||(t=a(e)),n=t.length;while(n--)(o=Ce(t[n]))[b]?r.push(o):i.push(o);(o=S(e,Ee(i,r))).selector=e}return o},u=oe.select=function(e,t,n,i){var o,u,l,c,f,p="function"==typeof e&&e,d=!i&&a(e=p.selector||e);if(n=n||[],1===d.length){if((u=d[0]=d[0].slice(0)).length>2&&"ID"===(l=u[0]).type&&9===t.nodeType&&g&&r.relative[u[1].type]){if(!(t=(r.find.ID(l.matches[0].replace(Z,ee),t)||[])[0]))return n;p&&(t=t.parentNode),e=e.slice(u.shift().value.length)}o=V.needsContext.test(e)?0:u.length;while(o--){if(l=u[o],r.relative[c=l.type])break;if((f=r.find[c])&&(i=f(l.matches[0].replace(Z,ee),K.test(u[0].type)&&ge(t.parentNode)||t))){if(u.splice(o,1),!(e=i.length&&ve(u)))return L.apply(n,i),n;break}}}return(p||s(e,d))(i,t,!g,n,!t||K.test(e)&&ge(t.parentNode)||t),n},n.sortStable=b.split("").sort(D).join("")===b,n.detectDuplicates=!!f,p(),n.sortDetached=ue(function(e){return 1&e.compareDocumentPosition(d.createElement("fieldset"))}),ue(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||le("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&ue(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||le("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),ue(function(e){return null==e.getAttribute("disabled")})||le(P,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),oe}(e);w.find=E,w.expr=E.selectors,w.expr[":"]=w.expr.pseudos,w.uniqueSort=w.unique=E.uniqueSort,w.text=E.getText,w.isXMLDoc=E.isXML,w.contains=E.contains,w.escapeSelector=E.escape;var k=function(e,t,n){var r=[],i=void 0!==n;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&w(e).is(n))break;r.push(e)}return r},S=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},D=w.expr.match.needsContext;function N(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var A=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,t,n){return g(t)?w.grep(e,function(e,r){return!!t.call(e,r,e)!==n}):t.nodeType?w.grep(e,function(e){return e===t!==n}):"string"!=typeof t?w.grep(e,function(e){return u.call(t,e)>-1!==n}):w.filter(t,e,n)}w.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?w.find.matchesSelector(r,e)?[r]:[]:w.find.matches(e,w.grep(t,function(e){return 1===e.nodeType}))},w.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(w(e).filter(function(){for(t=0;t1?w.uniqueSort(n):n},filter:function(e){return this.pushStack(j(this,e||[],!1))},not:function(e){return this.pushStack(j(this,e||[],!0))},is:function(e){return!!j(this,"string"==typeof e&&D.test(e)?w(e):e||[],!1).length}});var q,L=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(w.fn.init=function(e,t,n){var i,o;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(i="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:L.exec(e))||!i[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(i[1]){if(t=t instanceof w?t[0]:t,w.merge(this,w.parseHTML(i[1],t&&t.nodeType?t.ownerDocument||t:r,!0)),A.test(i[1])&&w.isPlainObject(t))for(i in t)g(this[i])?this[i](t[i]):this.attr(i,t[i]);return this}return(o=r.getElementById(i[2]))&&(this[0]=o,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):g(e)?void 0!==n.ready?n.ready(e):e(w):w.makeArray(e,this)}).prototype=w.fn,q=w(r);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};w.fn.extend({has:function(e){var t=w(e,this),n=t.length;return this.filter(function(){for(var e=0;e-1:1===n.nodeType&&w.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?w.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?u.call(w(e),this[0]):u.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(w.uniqueSort(w.merge(this.get(),w(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}w.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return k(e,"parentNode")},parentsUntil:function(e,t,n){return k(e,"parentNode",n)},next:function(e){return P(e,"nextSibling")},prev:function(e){return P(e,"previousSibling")},nextAll:function(e){return k(e,"nextSibling")},prevAll:function(e){return k(e,"previousSibling")},nextUntil:function(e,t,n){return k(e,"nextSibling",n)},prevUntil:function(e,t,n){return k(e,"previousSibling",n)},siblings:function(e){return S((e.parentNode||{}).firstChild,e)},children:function(e){return S(e.firstChild)},contents:function(e){return N(e,"iframe")?e.contentDocument:(N(e,"template")&&(e=e.content||e),w.merge([],e.childNodes))}},function(e,t){w.fn[e]=function(n,r){var i=w.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=w.filter(r,i)),this.length>1&&(O[e]||w.uniqueSort(i),H.test(e)&&i.reverse()),this.pushStack(i)}});var M=/[^\x20\t\r\n\f]+/g;function R(e){var t={};return w.each(e.match(M)||[],function(e,n){t[n]=!0}),t}w.Callbacks=function(e){e="string"==typeof e?R(e):w.extend({},e);var t,n,r,i,o=[],a=[],s=-1,u=function(){for(i=i||e.once,r=t=!0;a.length;s=-1){n=a.shift();while(++s-1)o.splice(n,1),n<=s&&s--}),this},has:function(e){return e?w.inArray(e,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return i=a=[],o=n="",this},disabled:function(){return!o},lock:function(){return i=a=[],n||t||(o=n=""),this},locked:function(){return!!i},fireWith:function(e,n){return i||(n=[e,(n=n||[]).slice?n.slice():n],a.push(n),t||u()),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!r}};return l};function I(e){return e}function W(e){throw e}function $(e,t,n,r){var i;try{e&&g(i=e.promise)?i.call(e).done(t).fail(n):e&&g(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}w.extend({Deferred:function(t){var n=[["notify","progress",w.Callbacks("memory"),w.Callbacks("memory"),2],["resolve","done",w.Callbacks("once memory"),w.Callbacks("once memory"),0,"resolved"],["reject","fail",w.Callbacks("once memory"),w.Callbacks("once memory"),1,"rejected"]],r="pending",i={state:function(){return r},always:function(){return o.done(arguments).fail(arguments),this},"catch":function(e){return i.then(null,e)},pipe:function(){var e=arguments;return w.Deferred(function(t){w.each(n,function(n,r){var i=g(e[r[4]])&&e[r[4]];o[r[1]](function(){var e=i&&i.apply(this,arguments);e&&g(e.promise)?e.promise().progress(t.notify).done(t.resolve).fail(t.reject):t[r[0]+"With"](this,i?[e]:arguments)})}),e=null}).promise()},then:function(t,r,i){var o=0;function a(t,n,r,i){return function(){var s=this,u=arguments,l=function(){var e,l;if(!(t=o&&(r!==W&&(s=void 0,u=[e]),n.rejectWith(s,u))}};t?c():(w.Deferred.getStackHook&&(c.stackTrace=w.Deferred.getStackHook()),e.setTimeout(c))}}return w.Deferred(function(e){n[0][3].add(a(0,e,g(i)?i:I,e.notifyWith)),n[1][3].add(a(0,e,g(t)?t:I)),n[2][3].add(a(0,e,g(r)?r:W))}).promise()},promise:function(e){return null!=e?w.extend(e,i):i}},o={};return w.each(n,function(e,t){var a=t[2],s=t[5];i[t[1]]=a.add,s&&a.add(function(){r=s},n[3-e][2].disable,n[3-e][3].disable,n[0][2].lock,n[0][3].lock),a.add(t[3].fire),o[t[0]]=function(){return o[t[0]+"With"](this===o?void 0:this,arguments),this},o[t[0]+"With"]=a.fireWith}),i.promise(o),t&&t.call(o,o),o},when:function(e){var t=arguments.length,n=t,r=Array(n),i=o.call(arguments),a=w.Deferred(),s=function(e){return function(n){r[e]=this,i[e]=arguments.length>1?o.call(arguments):n,--t||a.resolveWith(r,i)}};if(t<=1&&($(e,a.done(s(n)).resolve,a.reject,!t),"pending"===a.state()||g(i[n]&&i[n].then)))return a.then();while(n--)$(i[n],s(n),a.reject);return a.promise()}});var B=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;w.Deferred.exceptionHook=function(t,n){e.console&&e.console.warn&&t&&B.test(t.name)&&e.console.warn("jQuery.Deferred exception: "+t.message,t.stack,n)},w.readyException=function(t){e.setTimeout(function(){throw t})};var F=w.Deferred();w.fn.ready=function(e){return F.then(e)["catch"](function(e){w.readyException(e)}),this},w.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--w.readyWait:w.isReady)||(w.isReady=!0,!0!==e&&--w.readyWait>0||F.resolveWith(r,[w]))}}),w.ready.then=F.then;function _(){r.removeEventListener("DOMContentLoaded",_),e.removeEventListener("load",_),w.ready()}"complete"===r.readyState||"loading"!==r.readyState&&!r.documentElement.doScroll?e.setTimeout(w.ready):(r.addEventListener("DOMContentLoaded",_),e.addEventListener("load",_));var z=function(e,t,n,r,i,o,a){var s=0,u=e.length,l=null==n;if("object"===x(n)){i=!0;for(s in n)z(e,t,s,n[s],!0,o,a)}else if(void 0!==r&&(i=!0,g(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(w(e),n)})),t))for(;s1,null,!0)},removeData:function(e){return this.each(function(){K.remove(this,e)})}}),w.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=J.get(e,t),n&&(!r||Array.isArray(n)?r=J.access(e,t,w.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=w.queue(e,t),r=n.length,i=n.shift(),o=w._queueHooks(e,t),a=function(){w.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return J.get(e,n)||J.access(e,n,{empty:w.Callbacks("once memory").add(function(){J.remove(e,[t+"queue",n])})})}}),w.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length\x20\t\r\n\f]+)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,""],col:[2,""],tr:[2,""],td:[3,""],_default:[0,"",""]};ge.optgroup=ge.option,ge.tbody=ge.tfoot=ge.colgroup=ge.caption=ge.thead,ge.th=ge.td;function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&N(e,t)?w.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n-1)i&&i.push(o);else if(l=w.contains(o.ownerDocument,o),a=ye(f.appendChild(o),"script"),l&&ve(a),n){c=0;while(o=a[c++])he.test(o.type||"")&&n.push(o)}return f}!function(){var e=r.createDocumentFragment().appendChild(r.createElement("div")),t=r.createElement("input");t.setAttribute("type","radio"),t.setAttribute("checked","checked"),t.setAttribute("name","t"),e.appendChild(t),h.checkClone=e.cloneNode(!0).cloneNode(!0).lastChild.checked,e.innerHTML="",h.noCloneChecked=!!e.cloneNode(!0).lastChild.defaultValue}();var be=r.documentElement,we=/^key/,Te=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ce=/^([^.]*)(?:\.(.+)|)/;function Ee(){return!0}function ke(){return!1}function Se(){try{return r.activeElement}catch(e){}}function De(e,t,n,r,i,o){var a,s;if("object"==typeof t){"string"!=typeof n&&(r=r||n,n=void 0);for(s in t)De(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=ke;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return w().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=w.guid++)),e.each(function(){w.event.add(this,t,i,r,n)})}w.event={global:{},add:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.get(e);if(y){n.handler&&(n=(o=n).handler,i=o.selector),i&&w.find.matchesSelector(be,i),n.guid||(n.guid=w.guid++),(u=y.events)||(u=y.events={}),(a=y.handle)||(a=y.handle=function(t){return"undefined"!=typeof w&&w.event.triggered!==t.type?w.event.dispatch.apply(e,arguments):void 0}),l=(t=(t||"").match(M)||[""]).length;while(l--)d=g=(s=Ce.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=w.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=w.event.special[d]||{},c=w.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&w.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(e,r,h,a)||e.addEventListener&&e.addEventListener(d,a)),f.add&&(f.add.call(e,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),w.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,y=J.hasData(e)&&J.get(e);if(y&&(u=y.events)){l=(t=(t||"").match(M)||[""]).length;while(l--)if(s=Ce.exec(t[l])||[],d=g=s[1],h=(s[2]||"").split(".").sort(),d){f=w.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,y.handle)||w.removeEvent(e,d,y.handle),delete u[d])}else for(d in u)w.event.remove(e,d+t[l],n,r,!0);w.isEmptyObject(u)&&J.remove(e,"handle events")}},dispatch:function(e){var t=w.event.fix(e),n,r,i,o,a,s,u=new Array(arguments.length),l=(J.get(this,"events")||{})[t.type]||[],c=w.event.special[t.type]||{};for(u[0]=t,n=1;n=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n-1:w.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}return l=this,u\x20\t\r\n\f]*)[^>]*)\/>/gi,Ae=/