Package: quick-server
quick_serveris a quick to use and easy to set up server
implementation. It has the following goals / features and is primarily
meant to speed up back end implementation / iteration:serve local files as is with basic black- and white-listingprovide functionality for dynamic requestsprovide easy access to worker threads (and caching)provide a basic command interpret loop for server commandsUsageYou can installquick_serverwith pip:pipinstallquick_serverImport it in python via:fromquick_serverimportcreate_server,msg,setup_restartNote that python 2 support is discontinued. Use version0.6.x:pipinstallquick_server<0.7Note that python 3.9 and lower support is discontinued. Use version0.7.x:pipinstallquick_server<0.8Setting up a basic file serverFollowing we will set up a basicquick_server. Please refer to theinline documentationof the methods for
full information.setup_restart()# sets up restart functionality (if not called the `restart` command of the server needs external help to work)# should be the first real executed command in the script# some services, like heroku, don't play well with this command and it should not be called if in such an environmentaddr=""# empty address is equivalent to "localhost"port=8080server=create_server((addr,port),parallel=True)# parallel is recommended unless your code is not thread-safeserver.bind_path("/","www")# binds the "www" directory to the server's rootserver.add_default_white_list()# adds typical file types to the list of files that will be served; you can use server.add_file_patterns to add more file typesserver.favicon_fallback="favicon.ico"# sets the default favicon file to the given file on disk (you'll need a file called "favicon.ico")# you can also use server.link_empty_favicon_fallback()server.suppress_noise=True# don't report successful requests (turn off if you want to measure performance)server.report_slow_requests=True# reports requests that take longer than 5sStarting the actual server:msg(f"{' '.join(sys.argv)}")# prints how the script was startedmsg(f"starting server at{addrifaddrelse'localhost'}:{port}")try:server.serve_forever()# starts the server -- only returns when the server stops (e.g., by typing `quit`, `restart`, or `CTRL-C`)finally:msg("shutting down..")msg(f"{' '.join(sys.argv)}")# print how the script was called before exit -- this way you don't have to scroll up to remember when the server was running for a whileserver.server_close()# make sure to clean up all resourcesAdding dynamic requestsDynamic requests can be set up by annotating a function. The annotation
consists ofreturn-typeandhttp-method.APOSTrequest inJSONformat:fromquick_serverimportQuickServerRequestHandler,ReqArgs@server.json_post("/json_request",0)# creates a request at http://localhost:8080/json_request -- 0 additional path segments are alloweddef_json_request(req:QuickServerRequestHandler,args:ReqArgs)->dict:return{"post":args["post"],}AGETrequest asplain text:@server.text_get("/text_request")# creates a request at http://localhost:8080/text_request -- additional path segments are alloweddef_text_request(req:QuickServerRequestHandler,args:ReqArgs)->str:return"plain text"Other forms of requests are also supported, namelyDELETEandPUT.argsis an object holding all request arguments.args["query"]contains URL query arguments.args["fragment"]contains the URL fragment part.args["paths"]contains the remaining path segments.args["post"]contains the posted content.args["files"]contains uploaded files.args["meta"]starts as empty dict but allows to add additional
info to a request without conflicting with the other fields.Adding MiddlewareMiddleware can be added for common operations that apply for many endpoints
such as, e.g., login token verification. The request and argument objects are
passed through the middleware and can be modified by it.fromquick_serverimportPreventDefaultResponse,Response,ReqNextdefcheck_login(req:QuickServerRequestHandler,args:ReqArgs,okay:ReqNext)->ReqNext|dict:ifis_valid(args["post"].get("token")):args["meta"]["username"]=...# we can manipulate the args objectreturnokay# proceed with the next middleware / main request# Response allows to return non-default status codes.# It can be used in normal request functions as well.returnResponse("Authentication Required",401)# Alternatively we could just return a normal response with details herereturn{"success":False,"msg":...,}# If a non-control flow response is needed the PreventDefaultResponse# exception allows to return non-default status codes from anywhere.# This also works in normal request functions as well.raisePreventDefaultResponse(401,"Authentication Required")@server.json_post("/user_details")@server.middleware(check_login)def_user_details(req:QuickServerRequestHandler,args:ReqArgs)->dict:return{"success":True,"username":args["meta"]["username"],}Worker threads and cachingWorker threads are long running server side computations.
The client can start a request, gets an immediate response,
and will check periodically if the computation has finished.
From the client’s perspective it looks like a normal request.Worker threads require support from the client side.First, provide the necessary JavaScript file viaserver.link_worker_js("/js/worker.js")(useserver.link_legacy_worker_js("/js/worker.js")if you arenotusing a transpiler)and load it on the client side:<scriptsrc="js/worker.js"charset="utf-8"></script>A worker request can be set up on the server side withfromquick_serverimportWorkerArgs@server.json_worker("/json_worker")def_json_worker(post:WorkerArgs)->dict:# post contains all post arguments# ...# long, slow computationreturnmyresult# myresult must be JSON convertibleand accessed from the client. An instance of theWorkerclass is
needed:varwork=newquick_server.Worker();work.status((req)=>{...// req contains the number of currently active requests (-1 indicates an error state)// it can be used to tell the user that something is happening});Accessing the worker:// the first argument identifies worker jobs// jobs with the same name get replaced when a new one has been started// the second argument is the URLwork.post('worker_name','json_worker',{...// this object will appear as args on the server side},(data)=>{...// data is the result of the worker function of the server side// this function is only called if the request was successful});A worker can be cancelled using its name:work.cancel('worker_name');Note that all running workers are cancelled when the page is unloaded.Workers can automatically cache the server response usingquick_cache. The
server needs to be set up for this:cache=QuickCache(base_file,quota=500,ram_quota=100,warnings=msg)server.cache=cacheThen caching can be used for workers:@server.json_worker("/json_worker",cache_id=lambdaargs:{...# uniquely identify the task from its arguments (must be JSON convertible)})def_json_worker(post:WorkerArgs)->dict:# ...# long, slow computationreturnmyresult# myresult must be JSON convertibleNote that caching can also be used for other types of requests.Using workers with babel or reactIf you’re usingbabel(e.g., withreact) you can also
mirror the file into your source folder:server.mirror_worker_js("src/worker.js")and then import it:import'./worker.js';constWORKER=newwindow.quick_server.Worker();exportfunctionregisterStatus(cb){WORKER.status(cb);}exportfunctionfetchWorker(ref,url,post,cb){WORKER.post(ref,url,post,cb);}exportfunctioncancelWorker(ref){WORKER.cancel(ref);}Note that for a build you need to actually copyworker.jsinto you source folder since the build
system gets confused with filesystem links.
To usequick_serverwith a build bind the build folder:server.bind_path("/","build/")During development it is recommended to forward
requests from thereactserver toquick_server.
For this add the following line to yourpackage.json:"proxy":"http://localhost:8080"where the proxy field redirects to thequick_server.TokensTokens are means to store client information on the server.
For that the server must send the token-id to the client:server.create_token()# creates a new token -- send this to the clientThe server can now access (read / write) data associated with this token:@server.json_post("/json_request",0)def_json_request(req:QuickServerRequestHandler,args:ReqArgs)->...:# assuming the token-id was sent via post# expire can be the expiration time in seconds of a token,# None for no expiration, or be omitted for the default expiration (1h)withserver.get_token_obj(args["post"]["token"],expire=None)asobj:...# do stuff with objCORS and proxyingCORS can be activated with:server.cross_origin=Trueand requests can be redirected via proxy (if you want to avoid CORS):server.bind_proxy("/foo/","http://localhost:12345")redirects every request that begins with/foo/and
has not been handled byquick_servertohttp://localhost:12345.Custom server commandsBy defaultquick_serverprovides the commandshelp(list of
available commands),restart(restart the server), andquit(terminates the server). You can add own commands via@server.cmd()defname(args:list[str])->None:# creates the command nameifnotargs:msg("hello")else:msg(f"hi{' '.join(args)}")# words typed after name are printed hereA common command to add when having caching functionality (e.g.,
provided byquick_cache) is to
clear caches. This show-cases also auto-complete functionality:defcomplete_cache_clear(args:list[str],text:str)->list[str]:# args contains already completed arguments; text the currently started oneifargs:# we only allow up to one argumentreturn[]return[sectionforsectionincache.list_sections()ifsection.startswith(text)]# cache is the quick_cache object@server.cmd(complete=complete_cache_clear)defcache_clear(args:list[str])->None:iflen(args)>1:# we only allow up to one argumentmsg(f"too many extra arguments! expected one got{' '.join(args)}")returnmsg(f"clear{''ifargselse'all '}cache{' 'ifargselse's'}{args[0]ifargselse''}")cache.clean_cache(args[0]ifargselseNone)Server without command loopThe easiest way to start the server without a command loop (e.g., when
started as service) is to stop the loop with an EOF by calling the
script like this:cat/dev/null|pythonyourscript.pyor use theno_command_loopflag and run the script normally:server.no_command_loop=TrueHTTPSYou can wrap the server socket to support HTTPS:importssladdr=""# empty address is equivalent to "localhost"port=443# the HTTPS default port 443 might require root privilegesserver=create_server((addr,port),parallel=True)server.socket=ssl.wrap_socket(server.socket,certfile="path/to/localhost.pem",server_side=True)# setup your servertry:server.serve_forever()finally:server.server_close()More examplesexample.pyandexample2.pyalso contain minimal example
servers. You can run them with./example.pyand./example2.pyrespectively from the examples directory. Then you can browse tohttp://localhost:8000/example/.ContributingPull requests are highly appreciated :) Also, feel free to openissuesfor any
questions or bugs you may encounter.