Package: nasse
nasseA web server framework written on top of Flask which lets you focus on your ideas 🍡Actually spend time making your next app, we will do the annoying stuff for you!IndexIndexGetting StartedPrerequisitesInstallingOption 1: From PyPIOption 2: From GitPurposeComparisonBeforeAfterUsageCreating a new appFlask integrationFlask ConfigurationExampleIn the routesExampleRegistering new endpointsDocumenting your endpointsDocumentation ValuesReturnLoginUserSentErrorContextReturned ValuesError handlingJSONUtilitiesLoggingString FormattingRunning the serverGenerate documentationLocalizationCLIRunnerDocsHTTP AppRequestResultHistoryExplorerOptionsFile browserDeploymentContributingBuilt WithAuthorsAcknowledgmentsLicenseGetting StartedThese instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See deployment for notes on how to deploy the project on a live system.PrerequisitesYou will need Python 3 to use this module# vermin outputMinimumrequiredversions:3.8
Incompatibleversions:2Always check if your Python version works withNassebefore using it in production.InstallingOption 1: From PyPIpipinstall--upgradenasseThis will install the latest stable version from PyPIOption 2: From Gitpipinstall--upgradegit+https://github.com/Animenosekai/nasse.gitThis will install the latest development version from the git repositoryYou can check if you successfully installed it by printing out its version:$nasse--version2.0.2PurposeThis web server framework aims at bringing a new powerful tool to make your web application development easier.It brings type safety, along with automatic documentation to force you write clean and safe code while avoiding unnecessary checks and data validation.ComparisonBefore# my_project/api/v1/accounts.pyimportjsonfromflaskimportFlask,requestfromflask_compressimportCompressfromflask_corsimportCORSfromflask_talismanimportTalismanapp=Flask(__name__)Compress(app)CORS(app)Talisman(app)# after a quick StackOverflow searchEMAIL_REGEX=re.compile(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b')@app.route("/api/v1/accounts/profile",methods=["GET","POST"])defv1_accounts_profile():username=request.params.get("username",None)ifnotusername:return400,json.dumps({"error":"MISSING_ARG","message":"The `username` argument is missing from the request"})password=request.params.get("password",None)ifnotpassword:return400,json.dumps({"error":"MISSING_ARG","message":"The `password` argument is missing from the request"})[...]# ...password validation and all...ifstr(request.method).upper()=="POST":email=request.params.get("email",None)ifemailandnotEMAIL_REGEX.fullmatch(email):return400,json.dumps({"error":"WRONG_EMAIL","message":"The `email` argument doesn't seem to be a valid email address"})[...]return200,json.dumps({"error":None,"message":"Account successfully created"})return200,json.dumps({"error":None,"message":"Welcome back!"})<!-- my_project/docs/accounts.md --># AccountsThis file documents the correct way of using the accounts management endpoints for my project.## v1/accountsHere is an explanation...After# my_project/api/v1/accounts.pyimportrefromnasseimportNasse,Endpoint,Param,Loginfromnasse.utils.typesimportEmail,LimitedStringapp=Nasse()classUsername(LimitedString):LIMIT=200# only allowing 200 charactersclassPassword(LimitedString):LIMIT=100REGEX=re.compile(r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$")THROW=TrueACCOUNTS_MANAGEMENT=Endpoint(category="Accounts Management",login=Login(required=True))@app.route(Endpoint(endpoint=ACCOUNTS_MANAGEMENT,# this will take ACCOUNTS_MANAGEMENT as the basemethods=["GET","POST"],login=Login(no_login=True),params=[Param("username",{"GET":"The username of the user to check the authentication","POST":"A new username"},type=Username),Param("email","The user's email address",methods="POST",required=False,type=Email),Param("password","The user's password",type=Password),]))defprofile(method:str,username:str,password:str,email:str=None):# all of this comes from the request params and are type safe/validatedifmethod=="POST":return200,"Account created successfully"return200,"Welcome back"# This will actually generate all of the docs for you !app.make_docs()app.run()NoteTip: You can also usenasse my_project/api/v1/accounts.pyto run without callingapp.run()!But it could even look like this:# my_project/api/v1/accounts.pyimportrefromnasseimportNasse,Endpoint,Loginfromnasse.utils.typesimportEmail,LimitedStringapp=Nasse()classUsername(LimitedString):LIMIT=200# only allowing 200 charactersclassPassword(LimitedString):LIMIT=100REGEX=re.compile(r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$")THROW=True@app.route(category="Accounts Management",methods=["GET","POST"],login=Login(no_login=True))defprofile(method:str,username:Username,password:Password,email:Email=None):ifmethod=="POST":return200,"Account created successfully"return200,"Welcome back"app.make_docs()app.run()UsageCreating a new appCreating a new app is dead simple.Just import the Nasse object and create a new instance of it>>>fromnasseimportNasse>>>app=Nasse()This is the bare minimum, but you should of course configure it.You should at least give it a name and configure the CORS domains.>>>app=Nasse("My App",cors="https://myapp.com")Lots of things will be initialized with their default configuration.If you want to customize them, you might want to take a look at thenasse.config.NasseConfigobject, which has all the Nasse configuration.NoteSome strings support theNasse string formatting.
For example:NasseConfig.server_headersupports it, and the default value is :nasse/{version} ({name}). The{version}and{name}parts will be filled automatically.Theaccount_managementparameter should be of instancemodels.AccountManagementand is used to manage the users authentications.Example>>>fromyuno.security.tokenimportTokenManagement>>>fromaccount_managementimportget_account_by_id>>>fromnasse.modelsimportAccountManagement>>>tm=TokenManagement()>>>classNewAccountManagement(AccountManagement):...defretrieve_type(self,account):...return"admin"ifaccount.id=="123abc"else"user"...defretrieve_account(self,token:str):...returnget_account_by_id(tm.decode(token))...defverify_token(self,token:str):...try:...tm.decode(token)...returnTrue...exceptException:...returnFalse...>>>app=Nasse(account_management=NewAccountManagement())You can also specify specific Flask parameters using theflask_optionsparameter if needed.Flask integrationNasse being built on top of Flask, it is fully compatible with its ecosystem.As said before, lots of Flask settings are available and exposed to you.Flask ConfigurationYou can configure the Flask app using theflask_optionsparameter when instantiating a new Nasse instance.But you can also use theNasse.flaskattribute, which exposes the underlying Flask app to use your favorite plugins !ExampleIf for whatever reason you need to add a Flask plugin :fromnasseimportNassefromflask_cool_plugin_yayimportCoolPluginapp=Nasse()CoolPlugin(app.flask)or if a service expect a flask app, you can trick it to expose the Flask app :fromnasseimportNassenasse_app=Nasse()app=nasse_app.flask# this is the Flask appIn the routesMost of your Flask compatible code should probably already be compatible with Nasse.For example, therouteandmethodsparts of theapp.routefunction work the same way as Flask.ExampleWithFlaskThis is a very basic way of using Flask.fromflaskimportFlask,requestapp=Flask(__name__)@app.route("/hello",methods=["GET","POST"])defhello():return200,"Hello{}".format(request.params.get("name","world"))WithNasseWell this already works with Nasse !fromnasseimportNasse,requestapp=Nasse(__name__)@app.route("/hello",methods=["GET","POST"])defhello():return200,"Hello{}".format(request.params.get("name","world"))But can also be changed like this :fromnasseimportNasse,Paramapp=Nasse()@app.route(methods=["GET","POST"],params=Param("name",required=False))defhello(name:str="World"):return200,"Hello{}".format(name)Registering new endpointsYou can register new endpoints using theroutedecorator, just like Flask!>>>@app.route("/hello")>>>defhello():...return"Hello World!"But did you know that this worked too?>>>@app.route()>>>defhello():...return"Hello World!"Where's the"/hello"part?If you don't specify it, it will be automatically generated from the function name.You can follow a specific syntax for your functions name to create rich routes:To create a hyphen "-", use an upper case letter, kinda like when you usecamelCase.def myRoute()⇒ /my-routeTo create a new slash "/", use an underscore "_".def my_route()⇒ /my/routeTo create a new dynamic parameter, use a double underscore "__".def my__route__(route)⇒ /my/<route>/You can mix everything up as you wish.def my_helloWorld_route__name__(name)⇒ /my/hello-world/route/<name>/Also, the directory the function is in will be used to determine the route, this behavior can be changed with thebase_dirparameter of the endpoint.You can then use parameters to configure and document the endpoint.Documenting your endpointsYou can configure the endpoint by passing it anasse.models.Endpointinstance.Everything is meant to be reusable to write less and more readable code.For example, you could define a basic and global endpoint configuration for all your endpoints at the top level.Then configure a general endpoint configuration for the endpoint file.And then make specific tweaking for each endpoint.To inherit the configuration from another endpoint, you just need to pass the endpoint to theendpointparameter of the newEndpoint.>>>fromnasse.modelsimportEndpoint>>>fromconfigimportBASE_ENDPOINT>>>fromaccount_managementimportall_accounts>>>ACCOUNT_ENDPOINTS=Endpoint(endpoint=BASE_ENDPOINT,category="Account Management",)>>>@app.route("/accounts",endpoint=Endpoint(endpoint=ACCOUNT_ENDPOINTS,name="Accounts",description="Get all of the accounts"))>>>defaccounts():...returnall_accounts()But did you know that defining your functions the right way (giving a good name, type-hinting them and addingdoc-strings) would already give Nasse enough information to build a nicely documented endpoint.For example>>>fromnasseimportParameter>>>@app.route(path="/accounts",name="accounts_endpoint",category="test",parameters=[Parameter("username"),Parameter("limit",type=int,required=False)],description="This returns all accounts")...defaccounts_endpoint(username:str,limit=100):...returnget_accounts(username)Could be rewritten as:>>>@app.route...defaccounts(username:str,limit:int=100):..."""This returns all accounts"""...returnget_accounts()This is the most natural way of writing your endpoints, almost as if your function never leaved Python.And by using themikodocumentation style, you can even add descriptions to your parameters:>>>@app.route...defaccounts(username:str,limit:int=100):..."""...     This returns all accounts......     Parameters...     ----------...     limit: int...         An upper boundary for the number of accounts to return...     """...returnget_accounts()NoteThe name of the endpoint will be the name of the function and its category will be the name of the file you defined the function in.We understand that it is not required to list all parameters in the doc-string.Note that Nasse will automatically detect the different dynamic parts of the path:>>>@app.route...defhello__someone__(someone:str):...returnf"Hello{someone}"...>>>@app.route...defrepeat__msg__(msg:str,number:int=10):..."""...     Repeats the given element `number` times......     Parameters...     ----------...     msg: str...         The element to repeat...     number...         The number of times to repeat it...     """...return[msg]*numberYou can also specify the return value from within the function definition:>>>fromnasseimportNasse,Response,Return>>>app=Nasse()>>>>>># Basic example>>>@app.route...defhello(username:str="someone")->Response[Return("hello",example="someone")]:..."""A hello world"""...returnResponse({"hello":username})...>>># A bit more complex>>>@app.route...defhello(method:str,username:str="someone")->Response[{"GET":Return("hello"),"POST":Return("hi")}]:..."""A hello world"""...ifmethod=="POST":...returnResponse({"hi":username})...returnResponse({"hello":username})You can then use the same syntax as when documentingreturningin theEndpointclass.Refer toReturnfor theReturnclass explanation.NoteType checkers won't complain because of the use ofResponse, which technically your function returns.It is very important to rightfully document your endpoints because it will be used to process the requests and validate the inputs.Documentation ValuesReturnThemodels.Returnmodel is used to document what the endpoint returns.Here are its parameters:name: The name of the fieldexample: An example of returned valuedescription: A description of the returned valuetype: The type of returned valuechildren: The different children valuesnullable: If this value is can benull(None)LoginThemodels.Loginmodel is used to document how a user can authenticate its request with this endpoint.Here are its parameters:required: If the login is required or not. The user may still authenticate.types: Accepted types of accountsskip: Whether to completely skip or not the authentication stepskip_fetch: Whether to skip fetching the account or not. This effectively only checks if the provided token is correct or not.UserSentThemodels.UserSentmodel is used to document what the user can send to the endpoint.Here are its parameters:name: The name of the value sentdescription: A description of the value sentrequired: If the value is required or nottype: The type of value sent by the userNotemodels.Dynamic,models.Parameter,models.Headerandmodels.Cookieare all aliases ofmodels.UserSentErrorThemodels.Errormodel is used to document what errors can be returned by the endpoint.Here are its parameters:name: The name of the errordescription: A description of a situation where this error might be raisedcode: The status code of the response sent along this errorContextThe context values will be of the type you provided in the endpoint definition.There are multiple ways you can access a request context.You can import therequestglobal variable fromnasse>>>fromnasseimportrequest>>>request.valuesBut a better way would be to directly ask for it inside your endpoint function parameter.You can ask whatever you want from there.>>>@app.route()>>>defhello(request):# this will ask Nasse for the `request` object...returnrequest.values>>>@app.route>>>defhello(headers):# this will ask Nasse for the request `headers`...returnheadersHere is a list of parameters you can ask for:app: The current Nasse app instanceendpoint: The current Nasse endpointrequest: The current request contextmethod: The HTTP method of the requestvalues: The URL/form values of the requestargs: The URL arguments of the requestform: The form values of the requestheaders: The headers of the requestaccount: The authenticated account for the requestdynamics: The dynamic route parameters of the requestAnd their aliasesnasse: An alias forappnasse_endpoint: An alias forendpointparams: An alias forvaluesAny other requested parameter will be either adynamic route parameteror aURL/form parameter.NoteThose request parameters can also be used to document your endpoints, please look at theDocumenting your endpointssection for more information.Returned ValuesYou can return any kind of value from your endpoint function.There are multiple ways to return values:Using theresponse.Responseclass...returnResponse(data=data,code=200,headers={"X-ANISE-CACHE":"HIT"},...)Using only the data...return"Hello World"# or...returnbinary_data# of instance `bytes`Using a dictionaryThe dictionary will be automatically passed toresponse.Response...return{"data":data,"code":200,"headers":{"X-ANISE-CACHE":"HIT"},...}If the dictionary can't be passed toresponse.Response, it will be treated as part of thedatato send back....return{"greeting":"Hello World"}Using an iterable, like a tuple...return200,"Hello World"# or...return("Hello World",200)NoteOn debug mode, a set of timing header headers will automatically be returned.Error handlingEven if your application encounters an error/exception, it will be automatically caught by Nasse and correctly formatted to be safely returned to the client.NoteTip: You should use thenasse.exceptions.NasseExceptionclass to create your own exceptions.
This way, you will be able to fully customize the error response.But even with regularExceptionexceptions, Nasse will attempt to generate an error name and a description without leaking too much information.JSONIf the endpoint is configured as a JSON endpoint, it will be formatted using the following schema, and will have some features added.{"success":true,"error":null,"message":"","data":{}}Nasse uses a custom JSON encoder to fully customize it and avoiding bugs.NoteAlso, on debug mode, the response will have an additionaldebugfield containing the debug information.NoteIf theformatparameter is set toxmlorhtmlwhen making the request, the response will be automatically converted to anXMLformat.
Example:/hello?format=xmlwill produce aXMLformatted output.UtilitiesNasse is shipped with a set of utilities that you can use inside your application.They are mostly located inside theutilsmodule.LoggingYou should use Nasse's logging system to log stuff on your console to avoid any issue withrich.You can access the logging instance as a global variable :fromnasse.utils.loggingimportloggerlogger.log("Hello")logger.debug("Hello")logger.warn("Hello")...Or use the one on your Nasse instance :fromnasseimportNasseapp=Nasse()app.log("Hello")app.logger.info("Hello")app.logger.error("Hello")You can actually decide to only include the message to the log file and not print it on the console by using theHIDDENlogging level :logger.hidden("Hello")# this will add the hello message to the log file but not STDOUTThe logger supportsNasse's string formatting.String FormattingString formatting is a way of making template strings which will dynamically change its value with variables.In python, theformatfunction allows strings to use variables to create strings :name="world"# are all equivalent"Hello{}".format(name)"Hello{name}".format(name=name)f"Hello{name}"With Nasse, you can use this string formatting syntax, but with default values which can be automatically filled :fromnasse.utilsimportformatterformatter.format("Hello{name}from process{pid}",name="world")Most of the time, this is used when logging stuff to the console :logger.print("Hello{user}from process{pid}",user="world")Here is a list of default variables you can use with Nasse's string formattingnormalgreygrayredgreenbluecyanturquoisewhiteyellowpurplepinkmagentanameappidhostportdebugversionbase_dirtimecallerthreadpidcwdRunning the serverRunning the server is as easy as callingapp.run()NoteYou can also use the terminal to directly run the server. Head over toRunnerfor further information.Whereappis your Nasse instance.You can specify the backend you want to use to run the server using one of the already definednasse.servers...objects are a custom-made one, following thenasse.servers.ServerBackendclass.You can here specify the host and port to run the server on.If not specified, the host and port127.0.0.1:5005will be used.On debug mode, Nasse will reload on any file change.NoteTip: You can use theincludeandexcludeparameters to specify which files to watch for.Generate documentationWith the data you provided to the endpoints, Nasse is able to generate markdown and postman documentation for you.Use themake_docsmethod inside your application to generate the documentation.>>>app.make_docs()NoteYou can also use the terminal to generate the docs. Head over toDocsfor further information.It will generate the examples, usage and explanation for each endpoint, along with an index of the endpoints and a general explanation at the top.The Postman documentation is a set of JSON files, one for each category, that you can import inside Postman to test your API.It will create adocsdirectory in the current directory to put both documentations.LocalizationYou can now use the docs' localization feature which lets you use a different language for your docs !fromnasseimportNassefromnasse.localizationimportJapaneseLocalizationapp=Nasse()app.make_docs("./docs/jpa",localization=JapaneseLocalization,javascript=False)Here are the built-in languages :EnglishFrenchJapaneseBut you can create your own translation by sub-classingnasse.localization.LocalizationCLIRunnerYou can directly run your Nasse apps using the CLI.To do so, head over to your terminal and enter:nasse<your_app.py>This should run the app with the default parameters.If you want to customize the running behaviors, you can use the arguments with a(server)note prepended to their explanation when you run the--helpcommand:$nasse--help
usage:nasse[-h][--version][--hostHOST][--portPORT][--serverSERVER][--watch[WATCH...]][--ignore[IGNORE...]][--debug][--configCONFIG][input]AwebserverframeworkwrittenontopofFlaskwhichletsyoufocusonyourideas🍡

positionalarguments:inputThefileorURLtousewithnasse

options:-h,--helpshowthishelpmessageandexit--version,-vshowprogram'sversionnumberandexit--hostHOST(server)Thehosttobindto--portPORT,-pPORT(server)Theporttobindto--serverSERVER,-sSERVER(server)Theservertouse(accepts:flask,gunicorn)--watch[WATCH...],-w[WATCH...](server)Filestowatchchangesondebugmode--ignore[IGNORE...],-i[IGNORE...](server)Filestoignorewhenwatchingforfilechangesondebugmode--debug,-d(server)Torunwithdebugmodeenabled--configCONFIG,-cCONFIG(server)AconfigurationfileforextraargumentspassedtotheserverThe--configargument takes in a JSON file containing the different configurations you would like to pass in to therunfunction of yourNasseinstance.DocsYou can use the CLI to directly generate the docs:nasse--make-docs<your_app>.pyYou can also customize the docs' generation using arguments:$nasse--help
usage:nasse[-h][--version][--make-docs][--languageLANGUAGE][--outputOUTPUT][--docs-curl][--docs-javascript][--docs-python][input]AwebserverframeworkwrittenontopofFlaskwhichletsyoufocusonyourideas🍡

positionalarguments:inputThefileorURLtousewithnasse

options:-h,--helpshowthishelpmessageandexit--version,-vshowprogram'sversionnumberandexit--make-docs,-md,--docs,--generate-docs(docs)Generatesthedocsandexits--languageLANGUAGE,--localizationLANGUAGE(docs)Thedocslanguage--outputOUTPUT,-oOUTPUT,--docs-dirOUTPUT,--docs_dirOUTPUT(docs)Thedirectorytooutputthedocs--docs-curl(docs)Ifweneedtorenderthecurlexamples--docs-javascript(docs)Ifweneedtorenderthejavascriptexamples--docs-python(docs)IfweneedtorenderthepythonexamplesHere are the languages you can use to generate the docs:eng(default)frajpnNoteThose codes are the name of the file theLocalizationobject was created inHTTP AppYou can also use the built-in HTTP app to test your endpoints.This app might be familiar to those using the Postman software.To open it, just enternasseThis should open the app, and you should land on this page:RequestTheRequesttab is the main tab to prepare your requests.The first two inputs lets you choose the HTTP method to use and the URL to submit the requests to.The inputted URL can be an absolute URL or a relative path. In the latter case, the URL provided when launching the app or the one inputted in theOptionstab will be used to complete it.ResultWhen you submit a request, by pressingSor clicking theSubmitbutton on the footer, theResulttab should open automatically.Click on it to expand it.You can here see the details of your request, or the details of the error if the request errored out.HistoryThe history tab keeps a history of your requests.You can click on any request to see its details in theResulttab.The bottom part of the history tab shows a graph with the time it took to make each request.ExplorerThe explorer can be used when you are testing aNasseserver inDEBUG MODE(--debugenabled)If I go intoplayground/readmeand I run the server:🧃❯pythonrun.py--debug2023/08/11,18:55:59|[WARNING](yay)DEBUGMODEISENABLED2023/08/11,18:55:59|[INFO](yay)🎏PressCtrl+Ctoquit2023/08/11,18:55:59|[INFO](yay)🌍Bindingto127.0.0.1:5005
🍡yayisrunningonhttp://127.0.0.1:5005—0:00:04I can then run the HTTP app:nassehttp://127.0.0.1:5005And will see the different endpoints in the explorer:OptionsThe options screen will let you easily configure the app:All the modifications will be stored in the current directory and be restored on the next launch.File browserThe file browser is used throughout the app whenever it needs a file input.You can filter the different files and folders by starting to type your keyword.You can then navigate using the arrows on your keyboard and select your file by pressingENTER.DeploymentThis module is currently in development and might contain bugs.Please verify and test your endpoints thoroughly before releasing anything at a production stage.Feel free to report any issue you might encounter on Nasse's GitHub page.ContributingPull requests are welcome. For major changes, please open a discussion first to discuss what you would like to change.Built WithFlask- Nasse is built on top of flask to provide the interfacewatchdog- To watch for file changesWerkzeug- Flask's corebleach- To sanitize inputsrich- To provide good-looking console outputstextual- To build the HTTP TUIFlask-Compress- To compress the responsesAuthorsAnimenosekai-Initial work-AnimenosekaiAcknowledgmentsThanks toCherryPieWithPoisonfor the Statue of the Seven model.© 2021 - 2022 CherryPieWithPoisonThe Dictionary to XML conversion is heavily inspired by dict2xml by delfick.Licensed under the MIT License. More information in the header of thexml.pyfile.LicenseThis project is licensed under the MIT License - see theLICENSEfile for more details