Chapter 2: Organizing a Project

It is Day 0. You have a project in hand. You are fired up and ready to build a new web application. Ideas are swirling in your head, and your fingers are itching to start punching the keyboard. Time to sit down and start coding!

Or is it? It is tempting to start building an application as soon as the ideas about what we want to build begin to formulate in our heads. Before doing that, we should think about setting ourselves up for success. Having a solid foundation for the building will make the process much easier, reduce bugs, and result in a cleaner application.

The three foundations for beginning any Python web application project are as follows:

  • Your IDE/coding editor
  • An environment for running your development application
  • A project application structure

These three elements take into account a lot of personal tastes. There are so many good tools and approaches. There is no way a single book could cover them all. If you are a more seasoned developer and already have a set of preferences, great, run with that and skip ahead to the next chapter.

In this chapter, we will explore a couple of modern options to get you up and running. The focus will be on foundation #2 (the environment) and foundation #3 (the application structure). We skip #1 and assume you are using a modern IDE of your own choosing. Popular choices in the Python world include VS Code, PyCharm, and Sublime Text. If you are not using one of these or something similar, go look them up and find one that works for you.

After we have set up our environment, we will explore some patterns to be implemented in Sanic that will help define your application architecture. This is not a software architecture book. I highly recommend you learn about approaches such as "Domain-Driven Design" and "Clean Architecture." This book is focused much more on the practical aspects and decisions of building a web application in Sanic, so feel free to adjust the patterns as you feel necessary.

In this chapter, we'll go through the following topics:

  • Setting up an environment and directory
  • Using blueprints effectively
  • Wiring it all up
  • Running our application

Technical requirements

Before we begin, we will assume that you have the following already set up on your computer:

  • A modern Python installation (Python 3.8 or newer)
  • A terminal (and basic knowledge of how to execute programs)
  • An IDE (as discussed above)

Setting up an environment and directory

The first few steps that you take when starting any project have a monumental impact on the entirety of the project. Whether you are embarking on a multi-year project—or one that will be complete in a couple of hours—these early decisions will shape how you and others work on the project. But, even though these are important choices, do not fall into the trap of thinking that you need to find the perfect solution. There is no single "right way" to set up an environment or project directory. Remember our discussion from the previous chapter: we want to make the choices that fit the project at hand.

Environment

A good practice for Python development is to isolate its running environment from other projects. This is typically accomplished with virtual environments. In its most basic understanding, a virtual environment is a tool that allows you to install Python dependencies in isolation. This is important so that when we begin to develop our application, we have control of the requirements and dependencies in use. In its absence, we might mistakenly run our application and have requirements from other projects bleed into the application, thereby causing bugs and unintended behaviors.

The use of a virtual environment is so foundational in the Python development world that it has become the expected "norm" when creating a Python script or application. The first step you should always take when starting a new project is making a new virtual environment for it. The alternative to them is to run your application with your operating system's installation of Python. Do not do this. It may be fine for a while, but eventually, you will come across conflicting requirements, naming collisions, or other difficulties that all stem from a lack of isolation. The first step to becoming a better Python developer is to use virtual environments, if you are not doing so already.

It is also extremely helpful to acquaint yourself with the different tools that IDEs provide in hooking up to your virtual environment. These tools will often include things such as code completion and guide you as you start using features of your dependencies.

We do eventually want to run our application using containers. Being able to run our application inside a Docker container will greatly reduce the complexity associated with deploying our application down the road. This will be discussed further in Chapter 9, Best Practices to Improve Your Web Applications. However, I also believe that our application should be runnable (and therefore testable) from multiple environments. Even if we intend to use Docker down the road, we first need our application running locally without it. Debugging becomes much easier when our application does not rely upon an overly complex set of requirements just to run. Therefore, let's spend some time thinking about how to set up a virtual environment.

Many great tutorials and resources are available regarding how to use virtual environments. There are also many tools out there that are created to help manage the process. While I am a fan of the simple, tried and tested method of virtualenv, plus virtuanenvwrapper, many people are fans of pipenv, or poetry. These latter tools are meant to be a more "complete" encapsulation of your running environment. If they work for you, great. You are encouraged to spend some time to see what strikes a chord and resonates with your development pattern and needs.

We will leave virtual environments aside for now and briefly explore the usage of a relatively new pattern in Python. In Python 3.8, Python adopted a new pattern in PEP 582 that formalizes the inclusion of requirements in an isolated environment in a special __pypackages__ directory that lives inside the project. While the concept is similar to virtual environments, it works a little differently.

In order to implement __pypackages__, we are making it mandatory for our fictitious development team to use pdm. This is a relatively new tool that makes it super simple to adhere to some of the latest practices in modern Python development. If this approach interests you, take some time to read PEP 582 (https://www.python.org/dev/peps/pep-0582/) and look at pdm (https://pdm.fming.dev/).

You can get started by installing it with pip:

$ pip install --user pdm

Refer to the installation instructions on their website for more details: https://pdm.fming.dev/#installation. Pay particular attention to useful features such as shell completion and IDE integrations.

Now, let's proceed with setting up:

  1. To get started, we create a new directory for our application and, from that directory, run the following and follow the prompts to set up a basic structure:

    $ mkdir booktracker

    $ cd booktracker

    $ pdm init

  2. Now we will install Sanic:

    $ pdm add sanic

  3. We now have access to Sanic. Just to confirm in our heads that we are indeed in an isolated environment, let's quickly jump into the Python REPL and check the location of Sanic using sanic.__file__:

    $ python

    >>> import sanic

    >>> sanic.__file__

    '/path/to/booktracker/__pypackages__/3.9/lib/sanic/__init__.py'

Sanic CLI

As discussed in Chapter 8, Running a Sanic Server, many considerations go into how to deploy and run Sanic. Unless we are specifically looking into one of these alternatives, you can assume in this book that we are running Sanic using the Sanic CLI. This will stand up our application using the integrated Sanic web server.

First, we will check to see what version we are running:

$ sanic -v

Sanic 21.3.4

And then we will check to see what options we can use with the CLI:

$ sanic -h

usage: sanic [-h] [-H HOST] [-p PORT] [-u UNIX] [--cert CERT] [--key KEY] [-w WORKERS] [--debug] [--access-logs | --no-access-logs] [-v] module

                 Sanic

         Build Fast. Run Fast.

positional arguments:

  module                path to your Sanic app. Example: path.to.server:app

optional arguments:

  -h, --help            show this help message and exit

  -H HOST, --host HOST  host address [default 127.0.0.1]

  -p PORT, --port PORT  port to serve on [default 8000]

  -u UNIX, --unix UNIX  location of unix socket

  --cert CERT           location of certificate for SSL

  --key KEY             location of keyfile for SSL.

  -w WORKERS, --workers WORKERS

                        number of worker processes [default 1]

  --debug

  --access-logs         display access logs

  --no-access-logs      no display access logs

  -v, --version         show program's version number and exit

Our standard form for running our applications right now will be as follows:

$ sanic src.server:app -p 7777 --debug --workers=2

What thought went into the decision behind using this command? Let's take a look.

Why src.server:app?

First, we are going to run this from the ./booktracker directory. All of our code will be nested in an src directory.

Second, it is somewhat standard practice that our application creates a single Sanic() instance and assigns it to a variable called app:

app = Sanic("BookTracker")

If we were to place that in a file called app.py, then our module and variable would start to get confused:

from app import app

The preceding import statement is, well, ugly. It is beneficial to avoid naming conflicts between modules and the contents of that module as much as possible.

A bad example of this exists in the standard library. Have you ever done this one by accident?

>>> import datetime

>>> datetime(2021, 1, 1)

Traceback (most recent call last):

  File "<stdin>", line 1, in <module>

TypeError: 'module' object is not callable

Oops, we should have used from datetime import datetime. We want to minimize the replication of module names and properties, and to make our imports easy to remember and intuitive to look at.

Therefore, we will place our global app variable in a file called server.py. Sanic will look for our app instance when you pass in the <module>:<variable> form.

Why -p 7777?

We, of course, could choose any arbitrary port here. Many web servers will use port 8000 and that is the Sanic default if we just left it out completely. However, precisely because it is standard, we want to choose something else. Often, it is beneficial to choose a port that is less likely to collide with other ports that might be running on your machine. The more we can preserve common ports, the less likely we will run into collisions.

Why --debug?

While developing, having DEBUG mode enabled provides a more verbose output from Sanic, and an auto-reloading server. It can be helpful to see more logs, but make sure you turn this off in production.

The auto-reloading feature is particularly beneficial because you can start writing your app in one window, and have it running in a separate terminal session. Then, every time that you make a change and save the application, Sanic will restart the server, and your new code is immediately available for testing.

If you want auto-reloading but not all the extra verbosity, consider using --auto-reload instead.

Why --workers=2?

It is not an uncommon problem where someone begins to build an application and then realizes down the road that they have made a mistake by not preparing for horizontal scaling. Perhaps they added a global state that cannot be accessed outside of a single process:

sessions = set()

@app.route("/login")

async def login(request):

    new_session = await create_session(request)

    sessions.add(new_session)

Oops, now that person needs to go back and re-engineer the solution if they want to scale up the application. This could be a costly endeavor. Luckily, we are smarter than that.

By forcing our development pattern to include multiple workers from the beginning, it will help remind us as we are solving problems that our application must account for scaling. Even if our ultimate deployment does not use multiple Sanic workers per instance (and instead, for example, uses multiple Kubernetes pods with single worker instances; see Chapter 9, Best Practices to Improve Your Web Applications), this constant safeguard is a helpful way to keep the ultimate goal integral to the design process.

Directory structure

There are many different patterns you can follow for organizing a web application. Perhaps the simplest would be the single server.py file, where all of your logic exists together. For obvious reasons, this is not a practical solution for larger, real-world projects. So we will ignore that one.

What types of solutions are there? Perhaps we could use the "apps" structure that Django prefers, where discrete portions of our application are grouped into a single module. Or, perhaps you prefer to group by type, for example, by keeping all of your view controllers together. We make no judgments here about what is better for your needs, but we need to understand some consequences of our decisions.

When making a decision, you might want to learn some common practices. This might be a good opportunity to go and look up some of the following patterns:

  • Model View Controller (MVC)
  • Model View ViewModel (MVVM)
  • Domain-Driven Design (DDD)
  • Clean Architecture (CA)

Just to give you a flavor of the differences (or at least my interpretation of them), you might structure your project in one of the following ways:

You might use MVC:

./booktracker

├── controllers

│   ├── book.py

│   └── author.py

├── models

│   ├── book.py

│   └── author.py

├── views

│   ├── static

│   └── templates

└── services

Or you might use DDD:

./booktracker

└── domains

    ├── author

    │   ├── view.py

    │   └── model.py

    ├── book

    │   ├── view.py

    │   └── model.py

    └── universal

        └── middleware.py

In this book, we are going to adopt something that approximates to a hybrid approach. There is a time and place for applying these theoretical constructs. I urge you to learn them. The information is useful. But we are here to learn how to practically go about building an application with Sanic.

Here's the modified structure:

./booktracker

├── blueprints

│   ├── author

│   │   ├── view.py

│   │   └── model.py

│   └── book

│       ├── view.py

│       └── model.py

├── middleware

│   └── thing.py

├── common

│   ├── utilities

│   └── base

└── server.py

Let's break down each of these to see what they might look like and understand the thought process behind this application design.

./blueprints

This might strike you as odd since ultimately, this directory looks like it contains more than just blueprints. And, you would be right. Looking at the tree, you see that "blueprints" include both view.py and model.py. The goal of this directory is to separate your application into logical components, or domains. It functions much the same way as an apps directory might in a Django application. If you can isolate some construct or portion of your application as being a distinct entity, it should probably have a subfolder here.

A single module in this directory might contain models for validating incoming requests, utilities for fetching data from a database, and blueprints with attached route handlers. This keeps related code close together.

But why call it blueprints? Each subdirectory will contain much more than a single Blueprint object. The point is to reinforce the idea that everything in this directory resolves around one of these discrete components. The standard method for organization a so-called component in Sanic is the Blueprint method (which we will learn more about in the next section). Therefore, each subdirectory will have one, and only one, Blueprint object.

Another important rule is as follows: nothing inside the ./bluprints directory will reference our Sanic application. This means that both Sanic.get_app() and from server import app are forbidden inside this directory.

It is generally helpful to think of a blueprint as corresponding to a portion of your API design pattern:

  • example.com/auth -> ./blueprints/auth
  • example.com/cake -> ./blueprints/cake
  • example.com/pie -> ./blueprints/pie
  • example.com/user -> ./blueprints/user

./middleware

This directory should contain any middleware that is meant to be global in scope:

@app.on_request

async def extract_user(request):

    user = await get_user_from_request(request)

    request.ctx.user = user

As discussed later in this chapter and in Chapter 6, Operating Outside the Response Handler, as well as in the Sanic user guide (https://sanic.dev/en/guide/best-practices/blueprints.html#middleware), middleware can be global or attached to blueprints. If you need to apply middleware to specific routes, perhaps blueprint-based middleware makes sense. In this case, you should nest them in the appropriate ./blueprints directory and not here.

./common

This module is meant to be a place for storing class definitions and functions that will be used to build your application. It is for everything that will span your blueprints and be pervasive within your application.

Tip

Try to expand upon the directory structure here to meet your needs. However, try not to add too many top-level directories. If you start cluttering up your folders, think about how you might be able to nest directories inside one another. Usually, you will find that this leads to a cleaner architecture. There is also such a thing as going too far with nesting. For example, if you need to navigate ten levels deep in your application code, perhaps you should dial it back.

It's still Day 0. You still have a lot of great ideas in your head about what you want to build. And thanks to some thoughtful pre-planning, we now have an effective setup for building application locally. At this point, we should know how the application will run locally, and generally how the project will be organized. What we will learn next is the transition step from application structure to business logic.

Using blueprints effectively

If you already know what a blueprint is, imagine for a moment that you do not. As we are building out our application and trying to structure our code base in a logical and maintainable pattern, we realize that we need to constantly pass around our app object:

from some.location import app

@app.route("/my/stuff")

async def stuff_handler(...):

    ...

@app.route("/my/profile")

async def profile_handler(...):

    ...

This can become very tedious if we need to make changes to our endpoints. You can imagine a scenario where we would need to update a bunch of separate files to duplicate the same change over and over again.

Perhaps more frustratingly, we might end up in a scenario where we have circular imports:

# server.py

from user import *

app = Sanic(...)

# user.py

from server import app

@app.route("/user")

...

Blueprints solve both of these problems and allow us to abstract away some content so that the component can stand on its own. Returning to the preceding example, we take the common part of the endpoints (/my) and add it to the Blueprint definition:

from sanic import Blueprint

bp = Blueprint("MyInfo", url_prefix="/my")

@bp.route("/stuff")

async def stuff_handler(...):

    ...

@bp.route("/profile")

async def profile_handler(...):

    ...

In this example, we were able to group these routes together into a single blueprint. Importantly, this allows us to pull common parts of the URL path (/my) to the Blueprint, which gives us the flexibility to make changes in the future.

No matter how you decide to organize your file structure, you probably should always use blueprints. They make organization easier, and can even be nested. Personally, I will only ever use @app.route in the most simple of web applications. For any real projects, I always attach routes to blueprints.

Blueprint registration

Just creating our blueprints is not enough. Python would have no way to know they exist. We need to import our blueprints and attach them to our application. This is done through a simple registration method: app.blueprint():

# server.py

from user import bp as user_bp

app = Sanic(...)

app.blueprint(user_bp)

A common "gotcha" is misunderstanding what blueprint is doing. Something like this will not work as expected:

from sanic import Sanic, Blueprint

app = Sanic("MyApp")

bp = Blueprint("MyBrokenBp")

app.blueprint(bp)

@bp.route("/oops")

At the instant that we register a blueprint, everything that was attached to it will reattach to the application. This means that anything added to the blueprint after the call to app.blueprint() will not be applied. In the preceding example, /oops will not exist on the application. Therefore, you should try and register your blueprints as late as possible.

Tip

I think it is super convenient to always name blueprint variables bp. When I open a file, I automatically know what bp means. Some people may find it helpful to give their variable a more meaningful name: user_bp or auth_bp. For me, I would rather keep them consistent in the files I am always looking at, and just rename them at import: from user import bp as user_bp.

Blueprint versioning

A very powerful and common construct in API design is versioning. Let's imagine that we are developing our book API that will be consumed by customers. They have already created their integrations, and perhaps they have been using the API for some time already.

You have some new business requirements, or new features you want to support. The only way to accomplish that is to change how a particular endpoint works. However, this will break backward compatibility for users. This is a dilemma.

API designers often solve this problem by versioning their routes. Sanic makes this easy by adding a keyword argument to a route definition, or (perhaps more usefully) a blueprint.

You can learn more about versioning in the user guide (https://sanic.dev/en/guide/advanced/versioning.html) and we will discuss it in more depth in Chapter 3, Routing and Intaking HTTP Requests. For now, we will have to be content with knowing that our original API design needs a modification, and we will see how we can achieve that in the next section.

Grouping blueprints

As you begin to develop your applications, you might start to see similarities between blueprints. Just like we saw that we could pull common parts of routes out to Blueprint, we can pull common parts of Blueprint out into BlueprintGroup. This serves the same purpose:

from myinfo import bp as myinfo_bp

from somethingelse import bp as somethingelse_bp

from sanic import Blueprint

bp = Blueprint.group(myinfo_bp, somethingelse_bp, url_prefix="/api")

We have now added /api to the beginning of every route path defined inside myinfo and somethingelse.

By grouping blueprints, we are condensing our logic and becoming less repetitive. In the above example, by adding a prefix to the whole group, we no longer need to manage individual endpoints or even blueprints. We really need to keep the nesting possibilities in mind as we design the layout of our endpoints and our project structure.

In the last section, we mentioned using versions to provide an easy path to flexibly upgrade our API. Let's go back to our book tracking application and see what this might look like. If you recall, our application looked like this:

./booktracker

└── blueprints

    ├── author

    │   └── view.py

    └── book

        └── view.py

And we also have the view.py files:

# ./blueprints/book/view.py

bp = Blueprint("book", url_prefix="/book")

# ./blueprints/author/view.py

bp = Blueprint("author", url_prefix="/author")

Let's imagine the scenario where this API is already deployed and in use by customers when our new business requirements come in for a /v2/books route.

We add it to our existing architecture, and immediately it is starting to look ugly and messy:

└── blueprints

    ├── author

    │   └── view.py

    ├── book

    │   └── view.py

    └── book_v2

        └── view.py

Let's refactor this. We will not change ./blueprints/author or ./blueprints/book, just nest them a little deeper. That part of the application is already built and we do not want to touch it. However, now that we have learned from our mistake, we want to revise our strategy for /v2 endpoints to look like this:

└── blueprints

    ├── v1

    │   ├── author

    │   │   └── view.py

    │   ├── book

    │   │   └── view.py

    │   └── group.py

    └── v2

        ├── book

        │   └── view.py

        └── group.py

We just created a new file, group.py:

# ./blueprints/v2/group.py

from .book.view import bp as book_bp

from sanic import Blueprint

group = Blueprint.group(book_bp, version=2)

Grouping blueprints is a powerful concept when building complex APIs. It allows us to nest blueprints as deep as we need to while providing us with both routing and organizational control. In this example, notice how we were able to assign version=2 to the group. This means now that every route attached to a blueprint in this group will have a /v2 path prefix.

Wiring it all up

As we have learned, creating a pragmatic directory structure leads to predictable and easy-to-navigate source code. Because it is predictable to us as developers, it is also predictable for computers to run. Perhaps we can use this to our advantage.

Earlier, we discussed one of the problems we often encounter when trying to expand our application from the single file structure: circular imports. We can solve this well with our blueprints, but it still leaves us wondering about what to do with things that we might want to attach at the application level (such as middleware, listeners, and signals). Let's take a look at those use cases now.

Controlled imports

It is generally preferred to break code up into modules using nested directories and files that help us both logically think about our code, but also navigate to it. This does not come without a cost. What happens when two modules are interdependent? This will cause a circular import exception, and our Python application will crash. We need to not only think about how to logically organize our code but also how different parts of the code can be imported and used in other locations.

Consider the following example. First, create a file called ./server.py like this:

app = Sanic(__file__)

Second, create a second file called ./services/db.py:

app = Sanic.get_app()

@app.before_server_start

async def setup_db_pool(app, _):

    ...

This example illustrates the problem. When we run our application, we need Sanic(__file__) to run before Sanic.get_app(). But, we need to import .services.db so that it can attach to our application. Which file evaluates first? Since the Python interpreter will run instructions sequentially, we need to make sure that we instantiate the Sanic() object before importing the db module.

This will work:

app = Sanic(__file__)

from .services.db import *

However, it sort of looks ugly and non-Pythonic. Indeed, if you run tools such as flake8, you will start to notice that your environment does not really like this pattern so much either. It breaks the normal practice of placing imports at the top of the file. Learn more about this anti-pattern here: https://www.flake8rules.com/rules/E402.html.

You may decide that you do not care, and that is perfectly okay. Remember, we are in this to find the solution that works for your application. Before we make a decision, however, let's look at some other alternatives.

We could have a single startup file that will be a controlled set of import ordering:

# ./startup.py

from .server import app

from .services.db import *

Now, instead of running sanic server:app, we want to point our server to the new startup.py file:

sanic startup:app

Let's keep looking for an alternative.

Tip

The Sanic.get_app() construct is a very useful pattern for gaining access to your app instance without having to pass it around by import. This is a very helpful step in the right direction, and you can learn more about it in the user guide: https://sanic.dev/en/guide/basics/app.html#app-registry.

Factory pattern

We are going to move our application creation into a factory pattern. You may be familiar with this if you come from Flask as many examples and tutorials use a similar construct. The main reason for doing this here is that we want to set up our application for good development practices in the future. It will also ultimately solve the circular import problem. Later on down the line in Chapter 9, Best Practices to Improve Your Web Applications, we will talk about testing. In the absence of a nice factory, testing will become much more difficult.

We need to create a new file, ./utilities/app_factory.py, and redo our ./server.py file:

# ./utilities/app_factory.py

from typing import Optional, Sequence

from sanic import Sanic

from importlib import import_module

DEFAULT_BLUEPRINTS = [

    "src.blueprints.v1.book.view",

    "src.blueprints.v1.author.view",

    "src.blueprints.v2.group",

]

def create_app(

    init_blueprints: Optional[Sequence[str]] = None,

) -> Sanic:

    app = Sanic("BookTracker")

    if not init_blueprints:

        init_blueprints = DEFAULT_BLUEPRINTS

    for module_name in init_blueprints:

        module = import_module(module_name)

        app.blueprint(getattr(module, "bp"))

    return app

from .utilities.app_factory import create_app

app = create_app()

As you can see, our new factory will create the app instance, and attach some blueprints to it. We specifically are allowing for the factory to override the blueprints that it will use. Perhaps this is unnecessary and we could instead hardcode them all the time. However, I like the flexibility that this provides us, and find it helpful later on down the road when I want to start testing my application.

One problem that might jump out at you is that it requires our modules to have a global bp variable. While I mentioned that this is standard practice for me, it might not work in all scenarios.

Autodiscovery

The Sanic user guide gives us another idea in the How to… section. See https://sanic.dev/en/guide/how-to/autodiscovery.html. It suggests that we create an autodiscover utility that will handle some of the importing for us, and also have the benefit of automatically attaching blueprints. Remember how I said I like predictable folder structures? We are about to take advantage of this pattern.

Let's create ./utilities/autodiscovery.py:

# ./utilities/autodiscovery.py

from importlib import import_module

from inspect import getmembers

from types import ModuleType

from typing import Union

from sanic.blueprints import Blueprint

def autodiscover(app, *module_names: Union[str, ModuleType]) -> None:

    mod = app.__module__

    blueprints = set()

    def _find_bps(module: ModuleType) -> None:

        nonlocal blueprints

        for _, member in getmembers(module):

            if isinstance(member, Blueprint):

                blueprints.add(member)

    for module in module_names:

        if isinstance(module, str):

            module = import_module(module, mod)

        _find_bps(module)

    for bp in blueprints:

        app.blueprint(bp)

This file closely matches what the user guide suggests (https://sanic.dev/en/guide/how-to/autodiscovery.html#utility.py). Noticeably absent from the code presented there is the idea of recursion. If you look up the function in the user guide, you will see that it includes the ability to recursively search through our source code looking for Blueprint instances. While convenient, in the application that we are building, we want the express control provided by having to declare every blueprint's location. Quoting Tim Peters, The Zen of Python, again:

Explicit is better than implicit.

What the autodiscover tool does is allow us to pass locations to modules and hands the task of importing them over to the application. After loading the module, it will inspect any blueprints. The last thing it will handle is automatically registering the discovered blueprints to our application instance.

Now, our server.py file looks like this:

from typing import Optional, Sequence

from sanic import Sanic

from .autodiscovery import autodiscover

DEFAULT_BLUEPRINTS = [

    "src.blueprints.v1.book.view",

    "src.blueprints.v1.author.view",

    "src.blueprints.v2.group",

]

def create_app(

    init_blueprints: Optional[Sequence[str]] = None,

) -> Sanic:

    app = Sanic("BookTracker")

    if not init_blueprints:

        init_blueprints = DEFAULT_BLUEPRINTS

    autodiscover(app, *init_blueprints)

    return app

Tip

In this example, we are using the import paths as strings. We could just as easily import the modules here and pass those objects since the autodiscover utility works with both module objects and strings. We prefer strings though since it will keep the annoying circular import exceptions away.

Another thing to keep in mind is that this autodiscover tool could be used for a module containing middleware or listeners. The given example is still fairly simplistic, and will not cover all use cases. How, for example, should we handle deeply nested blueprint groups? This is a great opportunity for you to experiment, and I highly encourage you to spend some time playing with the application structure and the autodiscover tool to figure out what works best for you.

Running our application

Now that we have laid our application foundations, we are almost ready to run our server. We are going to make one small change to server.py to include a small little utility to run at startup to show us what routes are registered:

from .utilities.app_factory import create_app

from sanic.log import logger

app = create_app()

@app.main_process_start

def display_routes(app, _):

    logger.info("Registered routes:")

    for route in app.router.routes:

        logger.info(f"> /{route.path}")

You can head over to the GitHub repository, https://github.com/PacktPublishing/Python-Web-Development-with-Sanic/tree/main/Chapter02, to see the full source code.

We can now start our application for the first time. Remember, this is going to be our pattern:

$ sanic src.server:app -p 7777 --debug --workers=2

We should see something like this:

[2021-05-30 11:34:54 +0300] [36571] [INFO] Goin' Fast @ http://127.0.0.1:7777

[2021-05-30 11:34:54 +0300] [36571] [INFO] Registered routes:

[2021-05-30 11:34:54 +0300] [36571] [INFO] > /v2/book

[2021-05-30 11:34:54 +0300] [36571] [INFO] > /book

[2021-05-30 11:34:54 +0300] [36571] [INFO] > /author

[2021-05-30 11:34:54 +0300] [36572] [INFO] Starting worker [36572]

[2021-05-30 11:34:54 +0300] [36573] [INFO] Starting worker [36573]

Hooray!

And now, for the tempting part. What does our code actually do? Head over to your favorite web browser and open http://127.0.0.1:7777/book. It might not be much to look at yet, but you should see some JSON data. Next, try going to /author and /v2/book. You should now see the content that we created above. Feel free to play around with these routes by adding to them. Every time you do, you should see your changes reflected in the web browser.

Our journey into web application development has officially begun.

Summary

We have looked at the important impact of some of the early decisions we make about setting up our environment and project organization. We can—and should—constantly adapt our environment and application to meet changing needs. We used pdm to leverage some of the newest tools to run our server in a well-defined and isolated environment.

In our example, we then started to build our application. Perhaps we were too hasty when we added our /book route because we quickly realized that we needed the endpoint to perform differently. Rather than breaking the application for existing users, we simply created a new group of blueprints that will be the beginning of /v2 of our API. By nesting and grouping blueprints, we are setting the application up for future flexibility and development maintainability. Going forward, let's stick to this pattern as much as possible.

We also examined a few alternative approaches for organizing our application logic. These early decisions will impact the import ordering and shape the look of the application. We decided to adopt a factory method that will help us in the future when we start to test the application.

With the basic application structure decided, in the next chapter, we will begin to explore the most important aspect of a web server and framework: handling the request/response cycle. We know that we will use blueprints, but it is time to dive in and look more closely at what we can do with Sanic routing and handlers. In this chapter, there was a taste of it with API versioning. In the next chapter, we will also look at routing more generally and try to understand some strategies for designing application logic within a web API.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.216.220.21