Skip to main content

A web server framework written on top of Flask

Project description

Nasse

A web framework built on top of Flask

Stop spending time making the docs, verify the request, compress or format the response, and focus on making your next cool app!



PyPI version PyPI - Downloads PyPI - Python Version PyPI - Status GitHub - License GitHub top language CodeQL Checks Badge Pytest Code Size Repo Size Issues

Getting Started

These 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.

Prerequisites

You will need Python 3 to use this module

# vermin output
Minimum required versions: 3.4
Incompatible versions:     2

According to Vermin (--backport typing), Python 3.4 is needed for pathlib.

Always check if your Python version works with Nasse before using it in production

Installing

Option 1: From PyPI

pip install nasse

Option 2: From Git

pip install git+https://github.com/Animenosekai/nasse

You can check if you successfully installed it by printing out its version:

$ python -c "import nasse; print(nasse.__version__)"
# output:
Nasse v1.1

Purpose

This web server framework aims to bring a powerful tool to make your web application development easier.

It will bring type safety, along with automatic documentation to force you write clean and safe code along with avoiding unnecessary checks and data validation.

Usage

Creating a new app

Creating a new app is dead simple.

Just import the Nasse object and create a new instance of it

>>> from nasse import Nasse
>>> 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(name="My App", cors_headers=["https://myapp.com"])

In this case, the server ID will be generated automatically, by removing the spaces, etc. but you can also specify one directly Nasse(name="My App", id="coolapp").

The account_managemenet parameter should be of instance models.AccountManagement and is used to manage the user accounts (login, etc.).

Example

>>> from yuno.security.token import TokenManagement
>>> from account_management import get_account_by_id
>>> from nasse.models import AccountManagement
>>> tm = TokenManagement()
>>> class NewAccountManagement(AccountManagement):
...     def retrieve_type(self, account):
...         return "admin" if account.id == "123abc" else "user"
...     def retrieve_account(self, token: str):
...         return get_account_by_id(tm.decode(token))
...     def verify_token(self, token: str):
...         try:
...             tm.decode(token)
...             return True
...         except Exception:
...             return False
...
>>> app = Nasse(name="My App", cors_headers=["https://myapp.com"], account_management=NewAccountManagement())

There is also some self explanatory parameters:

  • max_request_size: limits the size of the request body.
  • compress: if we need to compress the response

Other parameters will be passed to Flask when creating the instance.

Registering new endpoints

You can register new endpoints using the route decorator, just like with Flask!

>>> @app.route("/hello")
>>> def hello():
...     return "Hello World!"

But did you know that this worked too?

>>> @app.route()
>>> def hello():
...     return "Hello World!"

Where's the "/hello" part?

If you don't specify it, it will be automatically generated from the function name.

There is a specific syntax for your functions name:

To create a hyphen "-", use an upper case letter, kinda like when you use camelCase.

def myRoute(): --> /my-route

To create a new slash "/", use an underscore "_".

def my_route(): --> /my/route

To 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/<name>/

Also, the directory the function is in will be used to determine the route, this behavior can be changed with the base_dir parameter of the endpoint.

You can then use parameters to configure and document the endpoint.

Documenting your endpoints

You can configure the endpoint by passint it a nasse.models.Endpoint instance.

It accepts a lot of parameters:

  • path: str = Default("")

The path of the endpoint, overwriting the function name as defined before

  • methods: list[str] = Default("GET")

The HTTP methods that can be used to access the endpoint

  • json: bool = Default(True)

If the response should be JSON

  • name: str = Default("")

The name of the endpoint, used for documentation purposes

  • description: str | dict[method<str>:str] = Default("")

The description of the endpoint, used for documentation purposes

  • section: str = Default("Other")

The section/category of the endpoint, used for documentation purposes

  • returning: models.Return | list[models.Return] = Default([])

What the endpoint returns, used for documentation purposes

  • login: models.Login | dict[method<str>:models.Login] = Default(models.Login(required=False))

The login which can be used to access the endpoint

  • headers: models.Header | list[models.Header] = Default([])

The headers which can be used to access the endpoint

  • cookies: models.Cookie | list[models.Cookie] = Default([])

The cookies which can be used to access the endpoint

  • params: models.Param | list[models.Param] = Default([])

The parameters which can be used to access the endpoint

  • dynamics: models.Dynamic | list[models.Dynamic] = Default([])

The dynamic route parameters which can be used to access the endpoint

  • errors: models.Error | list[models.Error] = Default([])

The errors which can be returned by the endpoint

  • base_dir: str = Default(None)

The base directory of the endpoint, used to determine the route

Everything is meant to be reusable to write less and more readble 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 tweakings for each endpoint.

To inherit the configuration from another endpoint, you just need to pass the endpoint to the endpoint parameter of the new Endpoint.

>>> from nasse.models import Endpoint
>>> from config import BASE_ENDPOINT
>>> from account_management import all_accounts
>>> ACCOUNT_ENDPOINTS = Endpoint(
    endpoint=BASE_ENDPOINT,
    section="Account Management",
)
>>> @app.route("/accounts", endpoint=Endpoint(
    endpoint=ACCOUNT_ENDPOINTS,
    name="Accounts",
    description="Get all of the accounts"
))
>>> def accounts():
...     return all_accounts()

It is very important to rightfully document your endpoints because it will be used to process the requests and validate stuff.

Documentation Values

Return

The models.Return model is used to document what the endpoint returns.

Here are its parameters:

  • name: the name of the returned value
  • example: an example of value it could return
  • description: a description of the returned value
  • methods: the HTTP methods where the value is returned
  • type: the type of the returned value
  • children: any children returned values
  • nullable: if the value can be null

Login

The models.Login model is used to document how a user can authenticate its request with this endpoint.

Here are its parameters:

  • required: if the login is required
  • types: the type of account that are allowed to access this endpoint
  • no_login: if no login is required
  • verification_only: if it is only required to verify the login token but not to get the account (this will avoid retrieving the account on each request but still validate the token)

UserSent

The models.UserSent model is used to document what the user sent to the endpoint.

Here are its parameters:

  • name: the name of the sent value
  • description: a description of the sent value
  • required: if the value is required
  • methods: the HTTP methods where the value is sent
  • type: the type of the sent value

models.Dynamic, models.Param, models.Header and models.Cookie are all subclasses of models.UserSent.

Error

The models.Error model is used to document what errors can be returned by the endpoint.

Here are its parameters:

  • name: the name of the error
  • description: a description of the error
  • code: the code of the error
  • methods: the HTTP methods where the error is returned

Context

The context values will be type casted with the provided endpoint documentation.

There is multiple ways you can access a request context.

You can import the request global variable from nasse

>>> from nasse import request
>>> request.values

But a better way would be to directly ask for it inside your endpoint function parameter.

You can ask whatever you want from their.

>>> @app.route()
>>> def hello(request): # this will ask Nasse for the `request` object
...     return request.values
>>> @app.route()
>>> def hello(headers): # this will ask Nasse for the request `headers`
...     return request.values

Here is a list of the parameters you can ask for:

  • app: The current Nasse app instance
  • nasse: An alias for app
  • endpoint: The current Nasse endpoint
  • nasse_endpoint: An alias for endpoint
  • request: The current request context
  • method: The HTTP method of the request
  • values: The URL/form values of the request
  • params: An alias for values
  • args: The URL arguments of the request
  • form: The form values of the request
  • headers: The headers of the request
  • account: The authenticated account for the request
  • dynamics: The dynamic route parameters of the request

Any other requested parameter will be either a dynamic route parameter or a URL/form parameter.

Returned Values

You can return any kind of value from your endpoint function.

There is multiple ways to return values:

  • Using the response.Response class
... return Response(
    data=data,
    code=200,
    headers={
        "X-ANISE-CACHE": "HIT"
    },
    ...
)
  • Using only the data
... return "Hello World"
# or
... return binary_data # of instance `bytes`
  • Using a dictionary

The dictionary will be automatically passed to response.Response

... return {
    "data": data,
    "code": 200,
    "headers": {
        "X-ANISE-CACHE": "HIT"
    },
    ...
}
  • Using an iterable, like a tuple
... return 200, "Hello World"
# or
... return ("Hello World", 200)

On debug mode, a set of timing header headers will be returned.

Error handling

Even if your application encounters an error/exception, it will be automatically catched by Nasse and correctly formatted to be returned to the client.

You should use the nasse.exceptions.NasseException class to inherit your own exceptions.

This way, you will be able to fully customize the error response.

But even with regular Exception exceptions, Nasse will try to generate an error name and a description without leaking too much information.

JSON

If 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": {}
}

Also, on debug mode, the response will have an additional debug field containing the debug information.

If the format parameter is set to xml or html when making the request, the response will be automatically converted to an XML format.

Utilities

Nasse is shipped with a set of utilities that you can use inside your application.

These are some helpful ones:

  • nasse.logging.log: To log messages to the console using Nasse's logging system

  • nasse.utils.ip.get_ip: To get the client's IP address

  • nasse.utils.regex.is_email: To check if a string is an email

  • nasse.timer.Timer: To measure the time taken by a block of code

...and so on.

They are mostly located inside the utils module.

Running the server

Gunicorn is used to run the server, which is a production-ready WSGI server.

To run the server, simply call app.run().

You can here specify the host and port to run the server on.

If not specified, the port and host specified in the program arguments/flags --host something --port 4000 will be used, or if none found, 127.0.0.1:5000 will be used.

On debug mode, Nasse will reload on any file change.

You can use the include and exclude parameters to specify which files to watch for.

By default, Nasse will watch all files in the current directory and subdirectories.

The other keyword arguments will be passed to the Gunicorn configuration.

Generate documentation

With the data you provided to the endpoints, Nasse is able to generate markdown and postman documentation for you.

Use the make_docs method inside your application to generate the documentation.

>>> app.make_docs()

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.

It will create a docs directory in the current directory to put both documentations.

The Postamn documentation is a set of JSON files, one for each category, that you can import inside Postman to test your API.

Deployment

This module is currently in development and might contain bugs.

Feel free to use it in production if you feel like it is suitable for your production even if you may encounter issues.

Contributing

Pull requests are welcome. For major changes, please open a discussion first to discuss what you would like to change.

Please make sure to update the tests as appropriate.

Built With

Authors

Acknowledgments

Thanks to CherryPieWithPoison for the Statue of the Seven model.

© 2021 - 2022 CherryPieWithPoison

The Dictionary to XML conversion is heavily inspired by dict2xml by delfick.

Licensed under the MIT License. More info in the head of the xml.py file.

License

This project is licensed under the MIT License - see the LICENSE file for more details

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

Nasse-1.1.tar.gz (47.1 kB view details)

Uploaded Source

Built Distribution

Nasse-1.1-py3-none-any.whl (57.2 kB view details)

Uploaded Python 3

File details

Details for the file Nasse-1.1.tar.gz.

File metadata

  • Download URL: Nasse-1.1.tar.gz
  • Upload date:
  • Size: 47.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.0 CPython/3.9.12

File hashes

Hashes for Nasse-1.1.tar.gz
Algorithm Hash digest
SHA256 98639b52b2211ce771f1155c528ec9fa2eac87cb52af19ee453a4a85b0f7d3fb
MD5 1ca5181801264d223c0a196d03bd7e23
BLAKE2b-256 1c71515a6797a7d761cba527c1ceaf5e9bf0f710bdd5140435f65cf3a10ac113

See more details on using hashes here.

File details

Details for the file Nasse-1.1-py3-none-any.whl.

File metadata

  • Download URL: Nasse-1.1-py3-none-any.whl
  • Upload date:
  • Size: 57.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.0 CPython/3.9.12

File hashes

Hashes for Nasse-1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 ccf4609c4683745c1010744cdce6814f2b153f82139bbe3ada682eb26003e3c3
MD5 212aa69d34511fa8b9f0f81e222c8ad6
BLAKE2b-256 8b9600c366a13e51fc6e7d9d7150fcda7bbae26343e8e998fa80b689d728119f

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page