Package: simple-http-server
python-simple-http-serverDiscriptionThis is a simple http server, use MVC like design.Support Python VersionPython 3.7+Why chooseLightway.Functional programing.Filter chain support.Session support, and can support distributed session bythis extention.You can usethis extentionto supportjinjaviews.Spring MVC like request mapping.SSL support.Websocket supportEasy to use.Free style controller writing.Easily integraded with WSGI servers.Easily integraded with ASGI servers. Websocket will be supported when ASGI server enable websocket functions.Coroutine mode support.DependenciesThere are no other dependencies needed to run this project. However, if you want to run the unitests in thetestsfolder, you need to installwebsocketvia pip:python3-mpipinstallwebsocket-clientHow to useInstallpython3-mpipinstallsimple_http_serverMinimum code / component requirement setupMinimum code to get things started should have at least one controller function,using the route and server modules from simple_http_serverfromsimple_http_serverimportroute,server@route("/")defindex():return{"hello":"world"}server.start(port=9090)Write Controllersfromsimple_http_serverimportrequest_mapfromsimple_http_serverimportResponsefromsimple_http_serverimportMultipartFilefromsimple_http_serverimportParameterfromsimple_http_serverimportParametersfromsimple_http_serverimportHeaderfromsimple_http_serverimportJSONBodyfromsimple_http_serverimportHttpErrorfromsimple_http_serverimportStaticFilefromsimple_http_serverimportHeadersfromsimple_http_serverimportCookiesfromsimple_http_serverimportCookiefromsimple_http_serverimportRedirectfromsimple_http_serverimportModelDict# request_map has an alias name `route`, you can select the one you familiar with.@request_map("/index")defmy_ctrl():return{"code":0,"message":"success"}# You can return a dictionary, a string or a `simple_http_server.simple_http_server.Response` object.@route("/say_hello",method=["GET","POST"])defmy_ctrl2(name,name2=Parameter("name",default="KEIJACK"),model=ModelDict()):"""name and name2 is the same"""name==name2# Truename==model["name"]# Truereturn"<!DOCTYPE html><html><body>hello,%s,%s</body></html>"%(name,name2)@request_map("/error")defmy_ctrl3():returnResponse(status_code=500)@request_map("/exception")defexception_ctrl():raiseHttpError(400,"Exception")@request_map("/upload",method="GET")defshow_upload():root=os.path.dirname(os.path.abspath(__file__))returnStaticFile("%s/my_dev/my_test_index.html"%root,"text/html; charset=utf-8")@request_map("/upload",method="POST")defmy_upload(img=MultipartFile("img")):root=os.path.dirname(os.path.abspath(__file__))img.save_to_file(root+"/my_dev/imgs/"+img.filename)return"<!DOCTYPE html><html><body>upload ok!</body></html>"@request_map("/post_txt",method="POST")defnormal_form_post(txt):return"<!DOCTYPE html><html><body>hi,%s</body></html>"%txt@request_map("/tuple")deftuple_results():# The order here is not important, we consider the first `int` value as status code,# All `Headers` object will be sent to the response# And the first valid object whose type in (str, unicode, dict, StaticFile, bytes) will# be considered as the bodyreturn200,Headers({"my-header":"headers"}),{"success":True}"""" Cookie_sc will not be written to response. It's just some kind of default" value"""@request_map("tuple_cookie")deftuple_with_cookies(all_cookies=Cookies(),cookie_sc=Cookie("sc")):print("=====> cookies ")print(all_cookies)print("=====> cookie sc ")print(cookie_sc)print("======<")importdatetimeexpires=datetime.datetime(2018,12,31)cks=Cookies()# cks = cookies.SimpleCookie() # you could also use the build-in cookie objectscks["ck1"]="keijack"requestcks["ck1"]["path"]="/"cks["ck1"]["expires"]=expires.strftime(Cookies.EXPIRE_DATE_FORMAT)# You can ignore status code, headers, cookies even body in this tuple.returnHeader({"xx":"yyy"}),cks,"<html><body>OK</body></html>""""" If you visit /a/b/xyz/x，this controller function will be called, and `path_val` will be `xyz`"""@request_map("/a/b/{path_val}/x")defmy_path_val_ctr(path_val=PathValue()):returnf"<html><body>{path_val}</body></html>"@request_map("/star/*")# /star/c will find this controller, but /star/c/d not.@request_map("*/star")# /c/star will find this controller, but /c/d/star not.defstar_path(path_val=PathValue()):returnf"<html><body>{path_val}</body></html>"@request_map("/star/**")# Both /star/c and /star/c/d will find this controller.@request_map("**/star")# Both /c/star and /c/d/stars will find this controller.defstar_path(path_val=PathValue()):returnf"<html><body>{path_val}</body></html>"@request_map("/redirect")defredirect():returnRedirect("/index")@request_map("session")deftest_session(session=Session(),invalid=False):ins=session.get_attribute("in-session")ifnotins:session.set_attribute("in-session","Hello, Session!")__logger.info("session id:%s"%session.id)ifinvalid:__logger.info("session[%s] is being invalidated. "%session.id)session.invalidate()return"<!DOCTYPE html><html><body>%s</body></html>"%str(ins)# use coroutine, these controller functions will work both in a coroutine mode or threading mode.asyncdefsay(sth:str=""):_logger.info(f"Say:{sth}")returnf"Success!{sth}"@request_map("/中文/coroutine")asyncdefcoroutine_ctrl(hey:str="Hey!"):returnawaitsay(hey)@route("/res/write/bytes")defres_writer(response:Response):response.status_code=200response.add_header("Content-Type","application/octet-stream")response.write_bytes(b'abcd')response.write_bytes(bytearray(b'efg'))response.close()Beside using the default values, you can also use variable annotations to specify your controller function's variables.@request_map("/say_hello/to/{name}",method=["GET","POST","PUT"])defyour_ctroller_function(user_name:str,# req.parameter["user_name"]，400 error will raise when there's no such parameter in the query string.password:str,# req.parameter["password"]，400 error will raise when there's no such parameter in the query string.skills:list,# req.parameters["skills"]，400 error will raise when there's no such parameter in the query string.all_headers:Headers,# req.headersuser_token:Header,# req.headers["user_token"]，400 error will raise when there's no such parameter in the quest headers.all_cookies:Cookies,# req.cookies, return all cookiesuser_info:Cookie,# req.cookies["user_info"]，400 error will raise when there's no such parameter in the cookies.name:PathValue,# req.path_values["name"]，get the {name} value from your path.session:Session# req.getSession(True)，get the session, if there is no sessions, create one.):return"<html><body>Hello, World!</body></html>"# you can use `params` to narrow the controller mapping, the following examples shows only the `params` mapping, ignoring the# `headers` examples for the usage is almost the same as the `params`.@request("/exact_params",method="GET",params="a=b")defexact_params(a:str):print(f"{a}")# breturn{"result":"ok"}@request("/exact_params",method="GET",params="a!=b")defexact_not_params(a:str):print(f"{a}")# breturn{"result":"ok"}@request("/exact_params",method="GET",params="a^=b")defexact_startwith_params(a:str):print(f"{a}")# breturn{"result":"ok"}@request("/exact_params",method="GET",params="!a")defno_params():return{"result":"ok"}@request("/exact_params",method="GET",params="a")defmust_has_params():return{"result":"ok"}# If multiple expressions are set, all expressions must be matched to enter this controller function.@request("/exact_params",method="GET",params=["a=b","c!=d"])defmultipul_params():return{"result":"ok"}# You can set `match_all_params_expressions` to False to make that the url can enter this controller function even only one expression is matched.@request("/exact_params",method="GET",params=["a=b","c!=d"],match_all_params_expressions=False)defmultipul_params():return{"result":"ok"}We recommend using functional programing to write controller functions. but if you realy want to use Object, you can use@request_mapin a class method. For doing this, every time a new request comes, a new MyController object will be created.classMyController:def__init__(self)->None:self._name="ctr object"@request_map("/obj/say_hello",method="GET")defmy_ctrl_mth(self,name:str):return{"message":f"hello,{name},{self._name}says. "}If you want a singleton, you can add a@controllerdecorator to the class.@controllerclassMyController:def__init__(self)->None:self._name="ctr object"@request_map("/obj/say_hello",method="GET")defmy_ctrl_mth(self,name:str):return{"message":f"hello,{name},{self._name}says. "}You can also add the@request_mapto your class, this will be as the part of the url.@controller@request_map("/obj",method="GET")classMyController:def__init__(self)->None:self._name="ctr object"@request_mapdefmy_ctrl_default_mth(self,name:str):return{"message":f"hello,{name},{self._name}says. "}@request_map("/say_hello",method=("GET","POST"))defmy_ctrl_mth(self,name:str):return{"message":f"hello,{name},{self._name}says. "}You can specify theinitvariables in@controllerdecorator.@controller(args=["ctr_name"],kwargs={"desc":"this is a key word argument"})@request_map("/obj",method="GET")classMyController:def__init__(self,name,desc="")->None:self._name=f"ctr[{name}] -{desc}"@request_mapdefmy_ctrl_default_mth(self,name:str):return{"message":f"hello,{name},{self._name}says. "}@request_map("/say_hello",method=("GET","POST"))defmy_ctrl_mth(self,name:str):return{"message":f"hello,{name},{self._name}says. "}From0.7.0,@request_mapsupport regular expression mapping.# url `/reg/abcef/aref/xxx` can map the flowing controller:@route(regexp="^(reg/(.+))$",method="GET")defmy_reg_ctr(reg_groups:RegGroups,reg_group:RegGroup=RegGroup(1)):print(reg_groups)# will output ("reg/abcef/aref/xxx", "abcef/aref/xxx")print(reg_group)# will output "abcef/aref/xxx"returnf"{self._name},{reg_group.group},{reg_group}"Regular expression mapping a class:@controller(args=["ctr_name"],kwargs={"desc":"this is a key word argument"})@request_map("/obj",method="GET")# regexp do not work here, method will still availableclassMyController:def__init__(self,name,desc="")->None:self._name=f"ctr[{name}] -{desc}"@request_mapdefmy_ctrl_default_mth(self,name:str):return{"message":f"hello,{name},{self._name}says. "}@route(regexp="^(reg/(.+))$")# prefix `/obj`  from class decorator will be ignored, but `method`(GET in this example) from class decorator will still work.defmy_ctrl_mth(self,name:str):return{"message":f"hello,{name},{self._name}says. "}Model bindingIf the inner binding can not satisfied you, you can use@model_bindingand@default_model_bindingto define your own binding logic.You can use@model_bindingto define the binding of specified types.fromtypingimportAnyfromsimple_http_server.models.model_bindingsimportModelBindingfromsimple_http_serverimportmodel_bindingfromsimple_http_serverimportHttpError,routeclassPerson:def__init__(self,name:str="",sex:int="",age:int=0)->None:self.name=nameself.sex=sexself.age=age@model_binding(Person)classPersonModelBinding(ModelBinding):asyncdefbind(self)->Any:name=self.request.get_parameter("name","no-one")sex=self.request.get_parameter("sex","secret")try:age=int(self.request.get_parameter("age",""))except:raiseHttpError(400,"Age is required, and must be an integer")returnPerson(name,sex,age)# Now, you can use `Person` in your controller attributes.@route("/model_binding/person")deftest_model_binding(person:Person):return{"name":person.name,"sex":person.sex,"age":person.age,}You can also use@default_model_bindingto handle all the types that not defined in inner and the@model_bindingconfigurations.fromsimple_http_server.models.model_bindingsimportModelBindingfromsimple_http_serverimportdefault_model_bindingfromsimple_http_serverimportHttpError,routeclassDog:def__init__(self,name="a dog")->None:self.name=namedefwang(self):returnself.name@default_model_bindingclassSetAttrModelBinding(ModelBinding):defbind(self)->Any:# You can define `bind` method in normal or async ways.try:obj=self.arg_type()fork,vinself.request.parameter.items():setattr(obj,k,v)returnobjexceptExceptionase:_logger.warning(f"Cannot create Object with given type{self.arg_type}. ",stack_info=True)returnself.default_value@route("/model_binding/dog")deftest_model_binding_dog(dog:Dog):return{"name":dog.wang()}SessionDefaultly, the session is stored in local, you can extendSessionFactoryandSessionclasses to implement your own session storage requirement (like store all data in redis or memcache)fromsimple_http_serverimportSession,SessionFactory,set_session_factoryclassMySessionImpl(Session):def__init__(self):super().__init__()# your own implementation@propertydefid(self)->str:# your own implementation@propertydefcreation_time(self)->float:# your own implementation@propertydeflast_accessed_time(self)->float:# your own implementation@propertydefis_new(self)->bool:# your own implementation@propertydefattribute_names(self)->Tuple:# your own implementationdefget_attribute(self,name:str)->Any:# your own implementationdefset_attribute(self,name:str,value:Any)->None:# your own implementationdefinvalidate(self)->None:# your own implementationclassMySessionFacImpl(SessionFactory):def__init__(self):super().__init__()# your own implementationdefget_session(self,session_id:str,create:bool=False)->Session:# your own implementationreturnMySessionImpl()set_session_factory(MySessionFacImpl())There is an offical Redis implementation here:https://github.com/keijack/python-simple-http-server-redis-session.gitWebsocketTo handle a websocket session, you should handle multiple events, so it's more reasonable to use a class rather than functions to do it.In this framework, you should use@websocket_handlerto decorate the class you want to handle websocket session. Specific event listener methods should be defined in a fixed way. However, the easiest way to do it is to inheritsimple_http_server.WebsocketHandlerclass, and choose the event you want to implement. But this inheritance is not compulsory.You can configureendpoitorregexpin@websocket_handlerto setup which url the class should handle. Alongside, there is asingletonfield, which is set toTrueby default. Which means that all connections are handle by ONE object of this class. If this field is set toFalse, objects will be created when everyWebsocketSessiontry to connect.fromsimple_http_serverimportWebsocketHandler,WebsocketRequest,WebsocketSession,websocket_handler@websocket_handler(endpoint="/ws/{path_val}")classWSHandler(WebsocketHandler):defon_handshake(self,request:WebsocketRequest):""""" You can get path/headers/path_values/cookies/query_string/query_parameters from request."" You should return a tuple means (http_status_code, headers)"" If status code in (0, None, 101), the websocket will be connected, or will return the status you return."" All headers will be send to client""""_logger.info(f">>{session.id}<< open!{request.path_values}")return0,{}defon_open(self,session:WebsocketSession):""""" Will be called when the connection opened.""""_logger.info(f">>{session.id}<< open!{session.request.path_values}")defon_close(self,session:WebsocketSession,reason:str):""""" Will be called when the connection closed.""""_logger.info(f">>{session.id}<< close::{reason}")defon_ping_message(self,session:WebsocketSession=None,message:bytes=b''):""""" Will be called when receive a ping message. Will send all the message bytes back to client by default.""""session.send_pone(message)defon_pong_message(self,session:WebsocketSession=None,message:bytes=""):""""" Will be called when receive a pong message.""""passdefon_text_message(self,session:WebsocketSession,message:str):""""" Will be called when receive a text message.""""_logger.info(f">>{session.id}<< on text message:{message}")session.send(message)defon_binary_message(self,session:WebsocketSession=None,message:bytes=b''):""""" Will be called when receive a binary message if you have not consumed all the bytes in `on_binary_frame`" method.""""passdefon_binary_frame(self,session:WebsocketSession=None,fin:bool=False,frame_payload:bytes=b''):""""" If you are sending a continuation binary message to server, this will be called every time a frame is" received, you can consumed all the bytes in this method, e.g. save all bytes to a file. By doing so," you should not return and value in this method."" If you does not implement this method or return a True in this method, all the bytes will be caced in" memory and be sent to your `on_binary_message` method.""""returnTrue@websocket_handler(regexp="^/ws-reg/([a-zA-Z0-9]+)$",singleton=False)classWSHandler(WebsocketHandler):"""" You code here"""But if you want to only handle one event, you can also use a function to handle it.fromsimple_http_serverimportWebsocketCloseReason,WebsocketHandler,WebsocketRequest,WebsocketSession,websocket_message,websocket_handshake,websocket_open,websocket_close,WEBSOCKET_MESSAGE_TEXT@websocket_handshake(endpoint="/ws-fun/{path_val}")defws_handshake(request:WebsocketRequest):return0,{}@websocket_open(endpoint="/ws-fun/{path_val}")defws_open(session:WebsocketSession):_logger.info(f">>{session.id}<< open!{session.request.path_values}")@websocket_close(endpoint="/ws-fun/{path_val}")defws_close(session:WebsocketSession,reason:WebsocketCloseReason):_logger.info(f">>{session.id}<< close::{reason.message}-{reason.code}-{reason.reason}")@websocket_message(endpoint="/ws-fun/{path_val}",message_type=WEBSOCKET_MESSAGE_TEXT)# You can define a function in a sync or async way.asyncdefws_text(session:WebsocketSession,message:str):_logger.info(f">>{session.id}<< on text message:{message}")session.send(f"{session.request.path_values['path_val']}-{message}")ifmessage=="close":session.close()Error pagesYou can use@error_messageto specify your own error page. See:fromsimple_http_serverimporterror_message# map specified codes@error_message("403","404")defmy_40x_page(message:str,explain=""):returnf"""<html><head><title>发生错误！</title><head><body>message:{message}, explain:{explain}</body></html>"""# map specified code rangs@error_message("40x","50x")defmy_error_message(code,message,explain=""):returnf"{code}-{message}-{explain}"# map all error page@error_messagedefmy_error_message(code,message,explain=""):returnf"{code}-{message}-{explain}"Write filtersThis server support filters, you can userequest_filterdecorator to define your filters.fromsimple_http_serverimportrequest_filter@request_filter("/tuple/**")# use wildcard@request_filter(regexp="^/tuple")# use regular expressiondeffilter_tuple(ctx):print("---------- through filter ---------------")# add a header to request headerctx.request.headers["filter-set"]="through filter"if"user_name"notinctx.request.parameter:ctx.response.send_redirect("/index")elif"pass"notinctx.request.parameter:ctx.response.send_error(400,"pass should be passed")# you can also raise a HttpError# raise HttpError(400, "pass should be passed")else:# you should always use do_chain method to go to the nextctx.do_chain()Start your server# If you place the controllers method in the other files, you should import them here.importsimple_http_server.serverasserverimportmy_test_ctrldefmain(*args):# The following method can import several controller files once.server.scan("my_ctr_pkg",r".*controller.*")server.start()if__name__=="__main__":main()If you want to specify the host and port:server.start(host="",port=8080)If you want to specify the resources path:server.start(resources={"/path_prefix/*","/absolute/dir/root/path",# Match the files in the given folder with a special path prefix."/path_prefix/**","/absolute/dir/root/path",# Match all the files in the given folder and its sub-folders with a special path prefix."*.suffix","/absolute/dir/root/path",# Match the specific files in the given folder."**.suffix","/absolute/dir/root/path",# Match the specific files in the given folder and its sub-folders.})If you want to use ssl:server.start(host="",port=8443,ssl=True,ssl_protocol=ssl.PROTOCOL_TLS_SERVER,# Optional, default is ssl.PROTOCOL_TLS_SERVER, which will auto detect the highted protocol version that both server and client support.ssl_check_hostname=False,#Optional, if set to True, if the hostname is not match the certificat, it cannot establish the connection, default is False.keyfile="/path/to/your/keyfile.key",certfile="/path/to/your/certfile.cert",keypass="",# Optional, your private key's password)CoroutineFrom0.12.0, you can use coroutine tasks than threads to handle requests, you can set theprefer_coroutineparameter in start method to enable the coroutine mode.server.start(prefer_coroutine=True)From0.13.0, coroutine mode uses the coroutine server, that means all requests will use the async I/O rather than block I/O. So you can now useasync defto define all your controllers including the Websocket event callback methods.If you call the server starting in a async function, you can all its async version, by doing this, there sever will use the same event loop with your other async functions.awaitserver.start_async(prefer_coroutine=True)LoggerThe default logger is try to write logs to the screen, you can specify the logger handler to write it to a file.importsimple_http_server.loggerasloggerimportlogging_formatter=logging.Formatter(fmt='[%(asctime)s]-[%(name)s]-%(levelname)-4s:%(message)s')_handler=logging.TimedRotatingFileHandler("/var/log/simple_http_server.log",when="midnight",backupCount=7)_handler.setFormatter(_formatter)_handler.setLevel("INFO")logger.set_handler(_handler)If you want to add a handler rather than replace the inner one, you can use:logger.add_handler(_handler)If you want to change the logger level:logger.set_level("DEBUG")You can get a stand alone logger which is independent from the framework one via a new classlogger.LoggerFactory.importsimple_http_server.loggerasloggerlog=logger.get_logger("my_service","my_log_fac")# If you want to set a different log level to this logger factory:log_fac=logger.get_logger_factory("my_log_fac")log_fac.log_level="DEBUG"log=log_fac.get_logger("my_service")log.info(...)WSGI SupportYou can use this module in WSGI apps.importsimple_http_server.serverasserverimportosfromsimple_http_serverimportrequest_map# scan all your controllersserver.scan("tests/ctrls",r'.*controllers.*')# or define a new controller function here@request_map("/hello_wsgi")defmy_controller(name:str):return200,"Hello, WSGI!"# resources is optionalwsgi_proxy=server.init_wsgi_proxy(resources={"/public/*":f"/you/static/files/path"})# wsgi app entrance.defsimple_app(environ,start_response):returnwsgi_proxy.app_proxy(environ,start_response)# If your entrance is async:asyncdefsimple_app(envion,start_response):returnawaitwsgi_proxy.async_app_proxy(environ,start_response)ASGI SupportYou can use this module in ASGI server, takeuvicornfro example:importasyncioimportuvicornimportsimple_http_server.serverasserverfromsimple_http_server.serverimportASGIProxyasgi_proxy:ASGIProxy=Noneinit_asgi_proxy_lock:asyncio.Lock=asyncio.Lock()asyncdefinit_asgi_proxy():globalasgi_proxyifasgi_proxy==None:asyncwithinit_asgi_proxy_lock:ifasgi_proxy==None:server.scan(base_dir="tests/ctrls",regx=r'.*controllers.*')asgi_proxy=server.init_asgi_proxy(resources={"/public/*":"tests/static"})asyncdefapp(scope,receive,send):awaitinit_asgi_proxy()awaitasgi_proxy.app_proxy(scope,receive,send)defmain():config=uvicorn.Config("main:app",host="0.0.0.0",port=9090,log_level="info")asgi_server=uvicorn.Server(config)asgi_server.run()if__name__=="__main__":main()ThanksThe code that process websocket comes from the following project:https://github.com/Pithikos/python-websocket-server