├── .gitignore ├── Chapter01 ├── .gitkeep └── server.py ├── Chapter02 ├── .gitkeep └── booktracker │ ├── .pdm.toml │ ├── alt1 │ ├── myapp │ │ ├── __init__.py │ │ ├── bootstrap.py │ │ ├── listener.py │ │ └── server.py │ └── run.py │ ├── pdm.lock │ ├── pyproject.toml │ └── src │ ├── blueprints │ ├── __init__.py │ ├── v1 │ │ ├── __init__.py │ │ ├── author │ │ │ ├── __init__.py │ │ │ └── view.py │ │ └── book │ │ │ ├── __init__.py │ │ │ └── view.py │ └── v2 │ │ ├── __init__.py │ │ ├── book │ │ ├── __init__.py │ │ └── view.py │ │ └── group.py │ ├── server.py │ └── utilities │ ├── app_factory.py │ └── autodiscovery.py ├── Chapter03 ├── ip-address-pattern.py └── serving-static-content-with-nginx │ ├── docker-compose.yml │ ├── nginx │ └── default.conf │ └── static │ └── foo.txt ├── Chapter04 ├── .gitkeep ├── requeststream │ ├── client.py │ ├── server.py │ ├── somefile.txt │ └── upload.py ├── validation_dataclasses │ └── server.py └── validation_pydantic │ └── server.py ├── Chapter05 ├── .gitkeep ├── html │ ├── server.py │ └── templates │ │ └── index.html ├── sse │ ├── index.html │ └── server.py └── websockets │ ├── index.html │ └── server.py ├── Chapter06 ├── .gitkeep ├── inprocess-queue │ ├── db │ ├── job │ │ ├── __init__.py │ │ ├── backend.py │ │ ├── blueprint.py │ │ ├── job.py │ │ ├── model.py │ │ ├── operations │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── hello.py │ │ │ └── registry.py │ │ ├── startup.py │ │ ├── view.py │ │ └── worker.py │ └── server.py ├── request-middleware │ └── server.py └── response-middleware │ └── server.py ├── Chapter07 ├── .gitkeep ├── accesstoken │ ├── auth │ │ ├── __init__.py │ │ ├── access_token.py │ │ ├── cookie.py │ │ ├── decorator.py │ │ ├── refresh_token.py │ │ └── user.py │ ├── index-refresh-on-exception.html │ ├── index-refresh-periodically.html │ └── server.py ├── apitoken │ └── server.py ├── corsissue │ ├── server1 │ │ └── server.py │ └── server2 │ │ ├── favicon.ico │ │ └── index.html ├── corsresponse │ ├── access-control-allow-methods │ │ └── server.py │ ├── access-control-expose-headers │ │ ├── server.py │ │ └── test.html │ ├── server.py │ └── server2.py └── csrf │ └── server.py ├── Chapter08 ├── .gitkeep ├── k8s │ ├── Dockerfile │ ├── app.yml │ ├── ingress.yml │ ├── issuer.yml │ ├── load-balancer.yml │ └── server.py └── paas │ ├── Procfile │ ├── requirements.txt │ └── server.py ├── Chapter09 ├── .gitkeep ├── hikingapp │ ├── README.md │ ├── application │ │ ├── Dockerfile │ │ ├── hiking │ │ │ ├── __init__.py │ │ │ ├── blueprints │ │ │ │ ├── __init__.py │ │ │ │ ├── hikes │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── executor.py │ │ │ │ │ ├── hydrator.py │ │ │ │ │ ├── models.py │ │ │ │ │ └── queries │ │ │ │ │ │ ├── get_hikes_by_user.sql │ │ │ │ │ │ └── get_hikes_by_user_by_name.sql │ │ │ │ ├── slow │ │ │ │ │ ├── __init__.py │ │ │ │ │ └── view.py │ │ │ │ ├── trails │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── executor.py │ │ │ │ │ ├── models.py │ │ │ │ │ ├── queries │ │ │ │ │ │ ├── get_all_trails.sql │ │ │ │ │ │ └── get_trail_by_name.sql │ │ │ │ │ └── view.py │ │ │ │ ├── users │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── executor.py │ │ │ │ │ ├── models.py │ │ │ │ │ ├── queries │ │ │ │ │ │ ├── get_all_users.sql │ │ │ │ │ │ └── get_user_by_name.sql │ │ │ │ │ └── view.py │ │ │ │ └── view.py │ │ │ ├── common │ │ │ │ ├── __init__.py │ │ │ │ ├── base_model.py │ │ │ │ ├── cache.py │ │ │ │ ├── dao │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── decorator.py │ │ │ │ │ ├── executor.py │ │ │ │ │ └── hydrator.py │ │ │ │ └── log.py │ │ │ ├── middleware │ │ │ │ └── request_context.py │ │ │ ├── server.py │ │ │ └── worker │ │ │ │ ├── __init__.py │ │ │ │ ├── postgres.py │ │ │ │ └── redis.py │ │ └── requirements.txt │ ├── docker-compose.yml │ └── postgres │ │ ├── Dockerfile │ │ └── initial.sql ├── loggingapp0 │ ├── Dockerfile │ ├── myapp │ │ ├── common │ │ │ ├── __init__.py │ │ │ └── log.py │ │ └── server.py │ └── tests │ │ └── .gitkeep ├── loggingapp1 │ ├── Dockerfile │ ├── myapp │ │ ├── common │ │ │ ├── __init__.py │ │ │ └── log.py │ │ └── server.py │ └── tests │ │ └── .gitkeep ├── testing0 │ ├── conftest.py │ ├── server.py │ └── test_sample.py ├── testing1 │ ├── conftest.py │ ├── server.py │ └── test_sample.py ├── testing2 │ ├── __init__.py │ ├── path │ │ ├── __init__.py │ │ └── to │ │ │ ├── __init__.py │ │ │ └── some_blueprint.py │ └── tests │ │ ├── __init__.py │ │ ├── conftest.py │ │ └── test_some_blueprint.py ├── testing3 │ ├── __init__.py │ ├── path │ │ ├── __init__.py │ │ └── to │ │ │ ├── __init__.py │ │ │ ├── some_blueprint.py │ │ │ ├── some_db_connection.py │ │ │ ├── some_registration_service.py │ │ │ └── some_startup.py │ └── tests │ │ ├── __init__.py │ │ ├── conftest.py │ │ └── test_some_blueprint.py ├── testing4 │ ├── __init__.py │ ├── server.py │ └── tests │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── test_1.py │ │ └── test_2.py └── tracing │ └── myapp │ ├── __init__.py │ ├── common │ ├── __init__.py │ └── log.py │ ├── middleware │ └── request_context.py │ └── server.py ├── Chapter10 ├── .gitkeep ├── graphql │ ├── application │ │ ├── Dockerfile │ │ ├── requirements.txt │ │ └── world │ │ │ ├── __init__.py │ │ │ ├── blueprints │ │ │ ├── __init__.py │ │ │ ├── cities │ │ │ │ ├── __init__.py │ │ │ │ ├── executor.py │ │ │ │ ├── integrator.py │ │ │ │ ├── models.py │ │ │ │ ├── queries │ │ │ │ │ ├── get_all_cities.sql │ │ │ │ │ ├── get_cities_by_country_code.sql │ │ │ │ │ ├── get_city_by_id.sql │ │ │ │ │ └── get_city_by_name.sql │ │ │ │ └── schema.gql │ │ │ ├── countries │ │ │ │ ├── __init__.py │ │ │ │ ├── executor.py │ │ │ │ ├── integrator.py │ │ │ │ ├── models.py │ │ │ │ ├── queries │ │ │ │ │ ├── get_all_countries.sql │ │ │ │ │ └── get_country_by_name.sql │ │ │ │ └── schema.gql │ │ │ ├── graphql │ │ │ │ ├── __init__.py │ │ │ │ ├── query.py │ │ │ │ └── view.py │ │ │ ├── languages │ │ │ │ ├── __init__.py │ │ │ │ ├── executor.py │ │ │ │ ├── integrator.py │ │ │ │ ├── models.py │ │ │ │ ├── queries │ │ │ │ │ └── get_by_country_code.sql │ │ │ │ └── schema.gql │ │ │ └── view.py │ │ │ ├── common │ │ │ ├── __init__.py │ │ │ ├── base_model.py │ │ │ ├── dao │ │ │ │ ├── __init__.py │ │ │ │ ├── decorator.py │ │ │ │ ├── executor.py │ │ │ │ ├── hydrator.py │ │ │ │ └── integrator.py │ │ │ └── log.py │ │ │ ├── middleware │ │ │ ├── __init__.py │ │ │ └── request_context.py │ │ │ ├── server.py │ │ │ └── worker │ │ │ ├── __init__.py │ │ │ └── postgres.py │ └── docker-compose.yml ├── httpredirect │ └── wadsworth │ │ ├── __init__.py │ │ ├── applications │ │ ├── __init__.py │ │ ├── redirect.py │ │ └── server.py │ │ ├── blueprints │ │ ├── __init__.py │ │ ├── hello │ │ │ ├── __init__.py │ │ │ └── view.py │ │ ├── info │ │ │ ├── __init__.py │ │ │ └── view.py │ │ ├── redirect │ │ │ ├── __init__.py │ │ │ └── view.py │ │ └── view.py │ │ └── certs │ │ ├── cert.pem │ │ └── key.pem ├── pwa │ ├── my-svelte-project │ │ ├── .gitignore │ │ ├── README.md │ │ ├── livereload.js │ │ ├── package.json │ │ ├── public │ │ │ ├── favicon.png │ │ │ ├── global.css │ │ │ └── index.html │ │ ├── rollup.config.js │ │ ├── scripts │ │ │ └── setupTypeScript.js │ │ ├── src │ │ │ ├── App.svelte │ │ │ ├── dummy.txt │ │ │ ├── foo │ │ │ └── main.js │ │ └── yarn.lock │ └── server.py ├── wdsbot │ ├── from_bot │ │ ├── bot.py │ │ └── server.py │ └── from_sanic │ │ ├── bot.py │ │ └── server.py └── wsfeed │ ├── README.md │ ├── application │ ├── Dockerfile │ ├── feeder │ │ ├── __init__.py │ │ ├── blueprints │ │ │ ├── __init__.py │ │ │ ├── feed │ │ │ │ ├── __init__.py │ │ │ │ ├── channel.py │ │ │ │ ├── client.py │ │ │ │ └── view.py │ │ │ └── view.py │ │ ├── common │ │ │ ├── __init__.py │ │ │ └── log.py │ │ ├── middleware │ │ │ ├── __init__.py │ │ │ └── request_context.py │ │ ├── server.py │ │ └── worker │ │ │ ├── __init__.py │ │ │ └── redis.py │ └── requirements.txt │ └── docker-compose.yml ├── Chapter11 ├── .gitkeep └── booktracker │ ├── README.md │ ├── application │ ├── Dockerfile │ ├── Makefile │ ├── booktracker │ │ ├── __init__.py │ │ ├── blueprints │ │ │ ├── __init__.py │ │ │ ├── author │ │ │ │ ├── __init__.py │ │ │ │ ├── executor.py │ │ │ │ ├── model.py │ │ │ │ ├── queries │ │ │ │ │ ├── create_author.sql │ │ │ │ │ ├── get_all_authors.sql │ │ │ │ │ ├── get_author_by_eid.sql │ │ │ │ │ └── get_authors_by_name.sql │ │ │ │ └── view.py │ │ │ ├── book │ │ │ │ ├── __init__.py │ │ │ │ ├── executor.py │ │ │ │ ├── hydrator.py │ │ │ │ ├── model.py │ │ │ │ ├── queries │ │ │ │ │ ├── create_book.sql │ │ │ │ │ ├── create_book_series.sql │ │ │ │ │ ├── create_book_to_user.sql │ │ │ │ │ ├── get_all_books.sql │ │ │ │ │ ├── get_all_books_for_user.sql │ │ │ │ │ ├── get_all_series.sql │ │ │ │ │ ├── get_book_by_eid.sql │ │ │ │ │ ├── get_book_by_eid_for_user.sql │ │ │ │ │ ├── get_book_series_by_eid.sql │ │ │ │ │ ├── get_books_by_title.sql │ │ │ │ │ ├── get_series_by_name.sql │ │ │ │ │ ├── update_book_state.sql │ │ │ │ │ └── update_toggle_book_is_loved.sql │ │ │ │ └── view.py │ │ │ ├── frontend │ │ │ │ ├── __init__.py │ │ │ │ ├── reload.py │ │ │ │ └── view.py │ │ │ ├── user │ │ │ │ ├── __init__.py │ │ │ │ ├── executor.py │ │ │ │ ├── model.py │ │ │ │ └── queries │ │ │ │ │ ├── create_user.sql │ │ │ │ │ ├── get_by_eid.sql │ │ │ │ │ └── get_by_login.sql │ │ │ └── view.py │ │ ├── common │ │ │ ├── __init__.py │ │ │ ├── auth │ │ │ │ ├── __init__.py │ │ │ │ ├── endpoint.py │ │ │ │ ├── handler.py │ │ │ │ ├── model.py │ │ │ │ └── startup.py │ │ │ ├── base_model.py │ │ │ ├── cache.py │ │ │ ├── cookie.py │ │ │ ├── csrf.py │ │ │ ├── dao │ │ │ │ ├── __init__.py │ │ │ │ ├── decorator.py │ │ │ │ ├── executor.py │ │ │ │ └── hydrator.py │ │ │ ├── eid.py │ │ │ ├── log.py │ │ │ └── pagination.py │ │ ├── middleware │ │ │ ├── __init__.py │ │ │ └── request_context.py │ │ ├── server.py │ │ └── worker │ │ │ ├── __init__.py │ │ │ ├── module.py │ │ │ ├── postgres.py │ │ │ ├── redis.py │ │ │ └── request.py │ ├── package.json │ ├── requirements.txt │ ├── rollup.config.js │ ├── ui │ │ ├── .prettierignore │ │ ├── .prettierrc.json │ │ ├── public │ │ │ ├── favicon.png │ │ │ ├── global.css │ │ │ ├── index.html │ │ │ └── livereload.js │ │ ├── scripts │ │ │ └── setupTypeScript.js │ │ └── src │ │ │ ├── App.svelte │ │ │ ├── components │ │ │ ├── AddBook.svelte │ │ │ ├── AddPyWebDev.svelte │ │ │ ├── AuthCode.svelte │ │ │ ├── Autocomplete.svelte │ │ │ ├── BookState.svelte │ │ │ ├── Clipboard.svelte │ │ │ ├── Love.svelte │ │ │ └── Reading.svelte │ │ │ ├── layouts │ │ │ ├── Footer.svelte │ │ │ ├── Header.svelte │ │ │ ├── Library.svelte │ │ │ ├── Main.svelte │ │ │ └── Nav.svelte │ │ │ ├── main.js │ │ │ ├── stores │ │ │ ├── book.js │ │ │ └── user.js │ │ │ └── utils │ │ │ ├── account.js │ │ │ ├── actions.js │ │ │ ├── cookie.js │ │ │ ├── debounce.js │ │ │ └── time.js │ └── yarn.lock │ ├── docker-compose.yml │ └── postgres │ ├── Dockerfile │ └── initial.sql ├── LICENSE └── README.md /Chapter01/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter01/.gitkeep -------------------------------------------------------------------------------- /Chapter01/server.py: -------------------------------------------------------------------------------- 1 | from sanic import Sanic, text, Request 2 | 3 | app = Sanic(__name__) 4 | 5 | @app.post("/") 6 | async def handler(request: Request): 7 | message = ( 8 | request.head + b"\n\n" + request.body 9 | ).decode("utf-8") 10 | print(message) 11 | return text("Done") 12 | 13 | app.run(port=9999, debug=True) -------------------------------------------------------------------------------- /Chapter02/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter02/.gitkeep -------------------------------------------------------------------------------- /Chapter02/booktracker/.pdm.toml: -------------------------------------------------------------------------------- 1 | [python] 2 | path = "/usr/bin/python" 3 | -------------------------------------------------------------------------------- /Chapter02/booktracker/alt1/myapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter02/booktracker/alt1/myapp/__init__.py -------------------------------------------------------------------------------- /Chapter02/booktracker/alt1/myapp/bootstrap.py: -------------------------------------------------------------------------------- 1 | from .server import * # noqa 2 | from .listener import * # noqa 3 | -------------------------------------------------------------------------------- /Chapter02/booktracker/alt1/myapp/listener.py: -------------------------------------------------------------------------------- 1 | from sanic import Sanic 2 | 3 | app = Sanic.get_app() 4 | 5 | 6 | @app.before_server_start 7 | def _(*__): 8 | print("...") 9 | -------------------------------------------------------------------------------- /Chapter02/booktracker/alt1/myapp/server.py: -------------------------------------------------------------------------------- 1 | from sanic import Sanic 2 | 3 | app = Sanic(__file__) 4 | 5 | 6 | app.route("/")(lambda x: None) 7 | -------------------------------------------------------------------------------- /Chapter02/booktracker/alt1/run.py: -------------------------------------------------------------------------------- 1 | from myapp.bootstrap import app 2 | 3 | 4 | app.run() 5 | -------------------------------------------------------------------------------- /Chapter02/booktracker/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "" 3 | version = "" 4 | description = "" 5 | authors = [ 6 | {name = "Adam Hopkins", email = "adam@amhopkins.com"}, 7 | ] 8 | dependencies = [ 9 | "sanic~=21.3", 10 | ] 11 | requires-python = ">=3.9" 12 | dynamic = ["classifiers"] 13 | license = {text = "NOLICENSE"} 14 | 15 | [project.urls] 16 | homepage = "" 17 | 18 | [build-system] 19 | requires = ["pdm-pep517"] 20 | build-backend = "pdm.pep517.api" 21 | 22 | [tool] 23 | [tool.pdm] 24 | -------------------------------------------------------------------------------- /Chapter02/booktracker/src/blueprints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter02/booktracker/src/blueprints/__init__.py -------------------------------------------------------------------------------- /Chapter02/booktracker/src/blueprints/v1/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter02/booktracker/src/blueprints/v1/__init__.py -------------------------------------------------------------------------------- /Chapter02/booktracker/src/blueprints/v1/author/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter02/booktracker/src/blueprints/v1/author/__init__.py -------------------------------------------------------------------------------- /Chapter02/booktracker/src/blueprints/v1/author/view.py: -------------------------------------------------------------------------------- 1 | from sanic import Blueprint, Request, HTTPResponse, json 2 | 3 | bp = Blueprint("Author", url_prefix="/author") 4 | 5 | 6 | @bp.get("/") 7 | async def get_all_books(request: Request) -> HTTPResponse: 8 | return json(["Mark Twain", "Robert Louis Stevenson"]) 9 | -------------------------------------------------------------------------------- /Chapter02/booktracker/src/blueprints/v1/book/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter02/booktracker/src/blueprints/v1/book/__init__.py -------------------------------------------------------------------------------- /Chapter02/booktracker/src/blueprints/v1/book/view.py: -------------------------------------------------------------------------------- 1 | from sanic import Blueprint, Request, HTTPResponse, json 2 | 3 | bp = Blueprint("Book", url_prefix="/book") 4 | 5 | 6 | @bp.get("/") 7 | async def get_all_books(request: Request) -> HTTPResponse: 8 | return json(["The Adventures of Tom Sawyer", "Treasure Island"]) 9 | -------------------------------------------------------------------------------- /Chapter02/booktracker/src/blueprints/v2/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter02/booktracker/src/blueprints/v2/__init__.py -------------------------------------------------------------------------------- /Chapter02/booktracker/src/blueprints/v2/book/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter02/booktracker/src/blueprints/v2/book/__init__.py -------------------------------------------------------------------------------- /Chapter02/booktracker/src/blueprints/v2/book/view.py: -------------------------------------------------------------------------------- 1 | from sanic import Blueprint, Request, HTTPResponse, json 2 | 3 | bp = Blueprint("Bookv2", url_prefix="/book") 4 | 5 | 6 | @bp.get("/") 7 | async def get_all_books(request: Request) -> HTTPResponse: 8 | return json({"books": ["The Adventures of Tom Sawyer", "Treasure Island"]}) 9 | -------------------------------------------------------------------------------- /Chapter02/booktracker/src/blueprints/v2/group.py: -------------------------------------------------------------------------------- 1 | from .book.view import bp as book_bp 2 | from sanic import Blueprint 3 | 4 | group = Blueprint.group(book_bp, version=2) 5 | -------------------------------------------------------------------------------- /Chapter02/booktracker/src/server.py: -------------------------------------------------------------------------------- 1 | from .utilities.app_factory import create_app 2 | from sanic.log import logger 3 | 4 | app = create_app() 5 | 6 | 7 | @app.main_process_start 8 | def display_routes(app, _): 9 | logger.info("Registered routes:") 10 | for route in app.router.routes: 11 | logger.info(f"> /{route.path}") 12 | -------------------------------------------------------------------------------- /Chapter02/booktracker/src/utilities/app_factory.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Sequence 2 | from sanic import Sanic 3 | 4 | 5 | from .autodiscovery import autodiscover 6 | 7 | DEFAULT_BLUEPRINTS = [ 8 | "src.blueprints.v1.book.view", 9 | "src.blueprints.v1.author.view", 10 | "src.blueprints.v2.group", 11 | ] 12 | 13 | 14 | def create_app( 15 | init_blueprints: Optional[Sequence[str]] = None, 16 | ) -> Sanic: 17 | app = Sanic("BookTracker") 18 | 19 | if not init_blueprints: 20 | init_blueprints = DEFAULT_BLUEPRINTS 21 | 22 | autodiscover(app, *init_blueprints) 23 | 24 | return app 25 | -------------------------------------------------------------------------------- /Chapter02/booktracker/src/utilities/autodiscovery.py: -------------------------------------------------------------------------------- 1 | from importlib import import_module 2 | from inspect import getmembers 3 | from types import ModuleType 4 | from typing import Union 5 | 6 | from sanic.blueprints import Blueprint, BlueprintGroup 7 | 8 | 9 | def autodiscover(app, *module_names: Union[str, ModuleType]): 10 | mod = app.__module__ 11 | blueprints = set() 12 | 13 | def _find_bps(module): 14 | nonlocal blueprints 15 | found_blueprints = set() 16 | found_blueprint_groups = set() 17 | 18 | for _, member in getmembers(module): 19 | if isinstance(member, Blueprint): 20 | found_blueprints.add(member) 21 | elif isinstance(member, BlueprintGroup): 22 | found_blueprint_groups.add(member) 23 | 24 | # If a module imports a bp in the same file as a BlueprintGroup is 25 | # created, then we want to remove the Bluesprints in that group 26 | # so we subtract any blueprints that are grouped from those found 27 | # in the module. 28 | blueprints.update( 29 | found_blueprints 30 | - { 31 | bp 32 | for group in found_blueprint_groups 33 | for bp in group.blueprints 34 | } 35 | ) 36 | 37 | # Add in any blueprint groups 38 | blueprints.update(found_blueprint_groups) 39 | 40 | for module in module_names: 41 | if isinstance(module, str): 42 | module = import_module(module, mod) 43 | _find_bps(module) 44 | 45 | for bp in blueprints: 46 | app.blueprint(bp) 47 | -------------------------------------------------------------------------------- /Chapter03/ip-address-pattern.py: -------------------------------------------------------------------------------- 1 | IP_ADDRESS_PATTERN = ( 2 | r"(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}" 3 | r"(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" 4 | ) 5 | 6 | 7 | @app.get(f"/") 8 | async def get_ip_details(request: Request, ip: str): 9 | return text(f"type={type(ip)} {ip=}") 10 | -------------------------------------------------------------------------------- /Chapter03/serving-static-content-with-nginx/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | client: 5 | image: nginx:alpine 6 | ports: 7 | - 8888:80 8 | volumes: 9 | - ./nginx/default.conf:/etc/nginx/conf.d/default.conf 10 | - ./static:/var/www 11 | -------------------------------------------------------------------------------- /Chapter03/serving-static-content-with-nginx/nginx/default.conf: -------------------------------------------------------------------------------- 1 | upstream example.com { 2 | keepalive 100; 3 | server 1.2.3.4:9999; 4 | } 5 | 6 | server { 7 | server_name example.com; 8 | root /var/www; 9 | 10 | location / { 11 | try_files $uri @sanic; 12 | } 13 | location @sanic { 14 | proxy_pass http://$server_name; 15 | proxy_set_header Host $host; 16 | proxy_set_header X-Forwarded-Proto $scheme; 17 | proxy_set_header X-Real-IP $remote_addr; 18 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 19 | } 20 | location ~* \.(jpg|jpeg|png|gif|ico|css|js|txt)$ { 21 | expires max; 22 | log_not_found off; 23 | access_log off; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Chapter03/serving-static-content-with-nginx/static/foo.txt: -------------------------------------------------------------------------------- 1 | hello... 2 | -------------------------------------------------------------------------------- /Chapter04/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter04/.gitkeep -------------------------------------------------------------------------------- /Chapter04/requeststream/client.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import httpx 4 | 5 | 6 | def gen(): 7 | for _ in range(1): 8 | print("waiting") 9 | time.sleep(1) 10 | yield b'{"foo": "bar"}' 11 | 12 | 13 | r = httpx.post("http://localhost:7777/transaction", data=gen()) 14 | print(r.status_code) 15 | print(r.content) 16 | -------------------------------------------------------------------------------- /Chapter04/requeststream/server.py: -------------------------------------------------------------------------------- 1 | import aiofiles 2 | from sanic import Request, Sanic, text 3 | from sanic.views import stream 4 | 5 | app = Sanic(__name__) 6 | app.config.FALLBACK_ERROR_FORMAT = "text" 7 | 8 | 9 | @app.post("/transaction") 10 | @stream 11 | async def transaction(request: Request): 12 | result = "" 13 | while True: 14 | body = await request.stream.read() # type: ignore 15 | if body is None: 16 | break 17 | result += body.decode("utf-8") 18 | return text(result, status=201) 19 | 20 | 21 | @app.post("/upload") 22 | @stream 23 | async def upload(request: Request): 24 | filename = await request.stream.read() # type: ignore 25 | async with aiofiles.open(filename.decode("utf-8"), mode="w") as f: 26 | while True: 27 | body = await request.stream.read() # type: ignore 28 | if body is None: 29 | break 30 | await f.write(body.decode("utf-8")) 31 | return text("Done", status=201) 32 | -------------------------------------------------------------------------------- /Chapter04/requeststream/somefile.txt: -------------------------------------------------------------------------------- 1 | 1111222233334444555566667777888899990000 2 | -------------------------------------------------------------------------------- /Chapter04/requeststream/upload.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | 3 | 4 | def gen(filename, f, size=4): 5 | yield filename 6 | while True: 7 | data = f.read(size) 8 | if not data: 9 | break 10 | yield data.encode("utf-8") 11 | 12 | 13 | with open("./file.txt", "r") as f: 14 | r = httpx.post("http://localhost:7777/upload", data=gen(b"somefile.txt", f)) 15 | print(r.status_code) 16 | for line in r.text.split("\n"): 17 | print(line) 18 | -------------------------------------------------------------------------------- /Chapter05/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter05/.gitkeep -------------------------------------------------------------------------------- /Chapter05/html/server.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from jinja2.loaders import FileSystemLoader 3 | from sanic import Sanic, html 4 | from jinja2 import Environment 5 | 6 | app = Sanic(__name__) 7 | 8 | 9 | @app.before_server_start 10 | def setup_template_env(app, _): 11 | app.ctx.env = Environment( 12 | loader=FileSystemLoader(Path(__file__).parent / "templates"), 13 | autoescape=True, 14 | ) 15 | 16 | 17 | @app.get("/") 18 | async def handler(request): 19 | template = request.app.ctx.env.get_template("index.html") 20 | output = template.render( 21 | songs=[ 22 | "Stairway to Heaven", 23 | "Kashmir", 24 | "All along the Watchtower", 25 | "Black Hole Sun", 26 | "Under the Bridge", 27 | ] 28 | ) 29 | return html(output) 30 | -------------------------------------------------------------------------------- /Chapter05/html/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Adam's Top Songs 5 | 6 | 7 |

Adam's Top Songs

8 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Chapter05/sse/index.html: -------------------------------------------------------------------------------- 1 | 2 | 68 | 69 | 70 | 71 |

72 | 


--------------------------------------------------------------------------------
/Chapter05/websockets/index.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
26 | 
39 | 
40 | 41 | 42 | -------------------------------------------------------------------------------- /Chapter05/websockets/server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Set, final 3 | from uuid import UUID, uuid4 4 | from sanic import Sanic, Request, text 5 | from sanic.response import redirect 6 | 7 | app = Sanic(__name__) 8 | app.config.KEEP_ALIVE = False 9 | app.static("/index.html", "./index.html", name="index") 10 | 11 | 12 | @app.route("/") 13 | def home(request: Request): 14 | return redirect(request.app.url_for("index")) 15 | 16 | 17 | @app.after_server_start 18 | async def setup_chatroom(app: Sanic, loop): 19 | app.ctx.chatroom = ChatRoom(loop) 20 | 21 | 22 | @app.websocket("/chat") 23 | async def feed(request, ws): 24 | try: 25 | client = Client(ws.send) 26 | request.app.ctx.chatroom.enter(client) 27 | 28 | while True: 29 | message = await ws.recv() 30 | if not message: 31 | break 32 | await request.app.ctx.chatroom.push(message, client.uid) 33 | 34 | finally: 35 | request.app.ctx.chatroom.exit(client) 36 | 37 | 38 | class Client: 39 | def __init__(self, send) -> None: 40 | self.uid = uuid4() 41 | self.send = send 42 | 43 | def __hash__(self) -> int: 44 | return self.uid.int 45 | 46 | 47 | class ChatRoom: 48 | def __init__(self, loop) -> None: 49 | self.clients: Set[Client] = set() 50 | self.loop = loop 51 | 52 | def enter(self, client: Client): 53 | self.clients.add(client) 54 | 55 | def exit(self, client: Client): 56 | self.clients.remove(client) 57 | 58 | async def push(self, message: str, sender: UUID): 59 | recipients = (client for client in self.clients if client.uid != sender) 60 | await asyncio.gather(*[client.send(message) for client in recipients]) 61 | 62 | 63 | app.run(port=9999, debug=True) 64 | -------------------------------------------------------------------------------- /Chapter06/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter06/.gitkeep -------------------------------------------------------------------------------- /Chapter06/inprocess-queue/db: -------------------------------------------------------------------------------- 1 | 187b765c-bd3e-49b8-b5e3-3efe79e24f55|hello|0|2022-01-02T09:11:35.158717|{}|null 2 | 187b765c-bd3e-49b8-b5e3-3efe79e24f55|hello|1|2022-01-02T09:11:45.166766|{}|"Hello, world" 3 | 910376d7-2eb7-44c9-9fdf-d8ccf9c9e53e|hello|0|2022-01-02T09:12:27.614149|{"name":"Adam"}|null 4 | 910376d7-2eb7-44c9-9fdf-d8ccf9c9e53e|hello|1|2022-01-02T09:12:37.617913|{"name":"Adam"}|"Hello, Adam" 5 | -------------------------------------------------------------------------------- /Chapter06/inprocess-queue/job/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter06/inprocess-queue/job/__init__.py -------------------------------------------------------------------------------- /Chapter06/inprocess-queue/job/blueprint.py: -------------------------------------------------------------------------------- 1 | from sanic import Blueprint 2 | from job.startup import ( 3 | setup_task_executor, 4 | setup_job_fetch, 5 | register_operations, 6 | ) 7 | from job.view import JobListView, JobDetailView 8 | 9 | bp = Blueprint("JobQueue", url_prefix="/job") 10 | 11 | bp.after_server_start(setup_job_fetch) 12 | bp.after_server_start(setup_task_executor) 13 | bp.after_server_start(register_operations) 14 | bp.add_route(JobListView.as_view(), "") 15 | bp.add_route(JobDetailView.as_view(), "/") 16 | -------------------------------------------------------------------------------- /Chapter06/inprocess-queue/job/job.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Any, Dict 3 | 4 | from uuid import uuid4 5 | 6 | from .operations.base import Operation 7 | from .operations.registry import OperationRegistry 8 | 9 | # from .task import Task 10 | 11 | 12 | class Job: 13 | def __init__(self, operation: str, backend, uid=None, kwargs=None) -> None: 14 | self.operation = operation 15 | self.uid = uid or uuid4() 16 | self.backend = backend 17 | self.kwargs = kwargs or {} 18 | self.retval = None 19 | 20 | async def execute(self, operation: Operation): 21 | print(f"Executing {self.operation}") 22 | self.retval = await operation.run(**self.kwargs) 23 | 24 | async def __aenter__(self): 25 | operation_class = OperationRegistry().get(self.operation) 26 | 27 | if operation_class: 28 | operation = operation_class() 29 | await self.backend.start(self) 30 | return operation 31 | else: 32 | raise Exception(f"No operation named {self.operation}") 33 | 34 | async def __aexit__(self, *_): 35 | await self.backend.stop(self) 36 | 37 | @classmethod 38 | async def create(cls, job: Dict[str, Any], backend): 39 | operation = job["operation"] 40 | uid = job["uid"] 41 | return cls(operation, backend, uid=uid, kwargs=job["kwargs"]) 42 | -------------------------------------------------------------------------------- /Chapter06/inprocess-queue/job/model.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import asdict, dataclass 4 | from datetime import datetime 5 | from typing import Any, Dict 6 | from uuid import UUID 7 | 8 | import ujson as json 9 | 10 | 11 | @dataclass 12 | class JobDetails: 13 | uid: UUID 14 | name: str 15 | complete: bool 16 | timestamp: datetime 17 | kwargs: Dict[str, Any] 18 | return_value: Any 19 | 20 | def __json__(self): 21 | output = asdict(self) 22 | output["uid"] = str(self.uid) 23 | output["timestamp"] = self.timestamp.isoformat() 24 | return json.dumps(output) 25 | -------------------------------------------------------------------------------- /Chapter06/inprocess-queue/job/operations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter06/inprocess-queue/job/operations/__init__.py -------------------------------------------------------------------------------- /Chapter06/inprocess-queue/job/operations/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC, abstractmethod 4 | 5 | 6 | class Operation(ABC): 7 | @abstractmethod 8 | async def run(self, **_): 9 | raise NotImplementedError 10 | -------------------------------------------------------------------------------- /Chapter06/inprocess-queue/job/operations/hello.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from .base import Operation 3 | 4 | 5 | class Hello(Operation): 6 | async def run(self, name="world"): 7 | message = f"Hello, {name}" 8 | print(message) 9 | await asyncio.sleep(10) 10 | print("Done.") 11 | return message 12 | -------------------------------------------------------------------------------- /Chapter06/inprocess-queue/job/operations/registry.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Type 2 | from .base import Operation 3 | 4 | 5 | class OperationRegistry: 6 | _singleton = None 7 | operations: Dict[str, Type[Operation]] 8 | 9 | def __new__(cls, *args, **kwargs): 10 | if cls._singleton is None: 11 | cls._singleton = super().__new__(cls) 12 | cls._singleton.operations = {} 13 | 14 | return cls._singleton 15 | 16 | def __init__(self, *operations: Type[Operation]) -> None: 17 | for operation in operations: 18 | self.register(operation) 19 | 20 | def register(self, operation: Type[Operation]) -> None: 21 | name = operation.__name__.lower() 22 | self.operations[name] = operation 23 | 24 | def get(self, name: str) -> Type[Operation]: 25 | return self.operations[name] 26 | -------------------------------------------------------------------------------- /Chapter06/inprocess-queue/job/startup.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from job.worker import worker 3 | from .backend import FileBackend 4 | from .operations.hello import Hello 5 | from .operations.registry import OperationRegistry 6 | 7 | 8 | async def setup_job_fetch(app, _): 9 | app.ctx.jobs = FileBackend("./db") 10 | 11 | 12 | async def setup_task_executor(app, _): 13 | app.ctx.queue = asyncio.Queue(maxsize=64) 14 | for x in range(app.config.NUM_TASK_WORKERS): 15 | name = f"Worker-{x}" 16 | print(f"Starting up executor: {name}") 17 | app.add_task(worker(name, app.ctx.queue, app.ctx.jobs)) 18 | 19 | 20 | async def register_operations(app, _): 21 | app.ctx.registry = OperationRegistry(Hello) 22 | -------------------------------------------------------------------------------- /Chapter06/inprocess-queue/job/view.py: -------------------------------------------------------------------------------- 1 | from sanic import json 2 | from sanic.views import HTTPMethodView 3 | from sanic.exceptions import InvalidUsage 4 | import uuid 5 | 6 | 7 | class JobListView(HTTPMethodView): 8 | async def post(self, request): 9 | operation = request.json.get("operation") 10 | kwargs = request.json.get("kwargs", {}) 11 | if not operation: 12 | raise InvalidUsage("Missing operation") 13 | 14 | uid = uuid.uuid4() 15 | await request.app.ctx.queue.put( 16 | { 17 | "operation": operation, 18 | "uid": uid, 19 | "kwargs": kwargs, 20 | } 21 | ) 22 | return json({"uid": str(uid)}, status=202) 23 | 24 | 25 | class JobDetailView(HTTPMethodView): 26 | async def get(self, request, uid: uuid.UUID): 27 | data = await request.app.ctx.jobs.fetch(uid) 28 | return json(data) 29 | -------------------------------------------------------------------------------- /Chapter06/inprocess-queue/job/worker.py: -------------------------------------------------------------------------------- 1 | from .job import Job 2 | 3 | 4 | async def worker(name, queue, backend): 5 | while True: 6 | job = await queue.get() 7 | if not job: 8 | break 9 | 10 | size = queue.qsize() 11 | print(f"[{name}] Running {job}. {size} in queue.") 12 | 13 | job_instance = await Job.create(job, backend) 14 | 15 | async with job_instance as operation: 16 | await job_instance.execute(operation) 17 | -------------------------------------------------------------------------------- /Chapter06/inprocess-queue/server.py: -------------------------------------------------------------------------------- 1 | from sanic import Sanic 2 | from job.blueprint import bp 3 | 4 | app = Sanic(__name__) 5 | app.config.NUM_TASK_WORKERS = 3 6 | app.blueprint(bp) 7 | -------------------------------------------------------------------------------- /Chapter06/request-middleware/server.py: -------------------------------------------------------------------------------- 1 | from sanic import Blueprint, Sanic, json 2 | 3 | app = Sanic(__name__) 4 | 5 | 6 | bp = Blueprint("Six", url_prefix="/six") 7 | 8 | 9 | @app.on_request 10 | async def one(request): 11 | request.ctx.numbers = [] 12 | request.ctx.numbers.append(1) 13 | 14 | 15 | @bp.on_request 16 | async def two(request): 17 | request.ctx.numbers.append(2) 18 | 19 | 20 | @app.on_request 21 | async def three(request): 22 | request.ctx.numbers.append(3) 23 | 24 | 25 | @bp.on_request 26 | async def four(request): 27 | request.ctx.numbers.append(4) 28 | 29 | 30 | @app.on_request 31 | async def five(request): 32 | request.ctx.numbers.append(5) 33 | 34 | 35 | @bp.on_request 36 | async def six(request): 37 | request.ctx.numbers.append(6) 38 | 39 | 40 | @app.get("/") 41 | async def app_handler(request): 42 | return json(request.ctx.numbers) 43 | 44 | 45 | @bp.get("/") 46 | async def bp_handler(request): 47 | return json(request.ctx.numbers) 48 | 49 | 50 | app.blueprint(bp) 51 | -------------------------------------------------------------------------------- /Chapter06/response-middleware/server.py: -------------------------------------------------------------------------------- 1 | from sanic import Blueprint, Sanic, json 2 | 3 | app = Sanic(__name__) 4 | 5 | bp = Blueprint("Six", url_prefix="/six") 6 | 7 | 8 | @bp.on_response 9 | async def complete(request, response): 10 | return json(request.ctx.numbers) 11 | 12 | 13 | @app.on_request 14 | async def zero(request): 15 | request.ctx.numbers = [] 16 | 17 | 18 | @app.on_response 19 | async def one(request, response): 20 | request.ctx.numbers.append(1) 21 | 22 | 23 | @bp.on_response 24 | async def two(request, response): 25 | request.ctx.numbers.append(2) 26 | 27 | 28 | @app.on_response 29 | async def three(request, response): 30 | request.ctx.numbers.append(3) 31 | 32 | 33 | @bp.on_response 34 | async def four(request, response): 35 | request.ctx.numbers.append(4) 36 | 37 | 38 | @app.on_response 39 | async def five(request, response): 40 | request.ctx.numbers.append(5) 41 | 42 | 43 | @bp.on_response 44 | async def six(request, response): 45 | request.ctx.numbers.append(6) 46 | 47 | 48 | @bp.get("/") 49 | async def bp_handler(request): 50 | request.ctx.numbers = [] 51 | return json("blah blah blah") 52 | 53 | 54 | app.blueprint(bp) 55 | -------------------------------------------------------------------------------- /Chapter07/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter07/.gitkeep -------------------------------------------------------------------------------- /Chapter07/accesstoken/auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter07/accesstoken/auth/__init__.py -------------------------------------------------------------------------------- /Chapter07/accesstoken/auth/access_token.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Any, Dict 3 | 4 | import jwt 5 | from sanic import Request 6 | from sanic.log import error_logger 7 | 8 | from .user import User 9 | 10 | 11 | @dataclass 12 | class AccessToken: 13 | payload: Dict[str, Any] 14 | token: str 15 | 16 | def __str__(self) -> str: 17 | return self.token 18 | 19 | @property 20 | def header_payload(self): 21 | return self._parts[0] 22 | 23 | @property 24 | def signature(self): 25 | return self._parts[1] 26 | 27 | @property 28 | def _parts(self): 29 | return self.token.rsplit(".", maxsplit=1) 30 | 31 | 32 | def generate_access_token(user: User, secret: str, exp: int) -> AccessToken: 33 | payload = { 34 | "whatever": "youwant", 35 | "exp": exp, 36 | } 37 | raw_token = jwt.encode(payload, secret, algorithm="HS256") 38 | access_token = AccessToken(payload, raw_token) 39 | return access_token 40 | 41 | 42 | def check_access_token( 43 | access_token: str, secret: str, allow_expired: bool = False 44 | ) -> bool: 45 | try: 46 | jwt.decode( 47 | access_token, 48 | secret, 49 | algorithms=["HS256"], 50 | require=["exp"], 51 | verify_exp=(not allow_expired), 52 | ) 53 | except jwt.exceptions.InvalidTokenError as e: 54 | error_logger.exception(e) 55 | return False 56 | 57 | return True 58 | 59 | 60 | def get_token_from_request(request: Request) -> str: 61 | access_token = request.cookies.get("access_token", "") 62 | access_token_signature = request.cookies.get("access_token_signature", "") 63 | token = ".".join([access_token, access_token_signature]) 64 | return token 65 | -------------------------------------------------------------------------------- /Chapter07/accesstoken/auth/cookie.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Optional 3 | 4 | 5 | def set_cookie( 6 | response, 7 | key, 8 | value, 9 | httponly=False, 10 | samesite="lax", 11 | domain: Optional[str] = None, 12 | exp: Optional[datetime] = None, 13 | ): 14 | response.cookies[key] = value 15 | response.cookies[key]["httponly"] = httponly 16 | response.cookies[key]["path"] = "/" 17 | response.cookies[key]["secure"] = True 18 | response.cookies[key]["samesite"] = samesite 19 | 20 | if domain: 21 | response.cookies[key]["domain"] = domain 22 | 23 | if exp: 24 | response.cookies[key]["expires"] = exp 25 | -------------------------------------------------------------------------------- /Chapter07/accesstoken/auth/decorator.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from inspect import isawaitable 3 | 4 | from sanic import Request 5 | from sanic.exceptions import Unauthorized 6 | 7 | from auth.access_token import check_access_token, get_token_from_request 8 | 9 | 10 | def protected(maybe_func=None, *, arg1=None, arg2=None): 11 | def decorator(f): 12 | @wraps(f) 13 | async def decorated_function(request: Request, *args, **kwargs): 14 | token = get_token_from_request(request) 15 | if not check_access_token(token, request.app.config.JWT_SECRET): 16 | raise Unauthorized("Unauthorized access") 17 | 18 | response = f(request, *args, **kwargs) 19 | if isawaitable(response): 20 | response = await response 21 | 22 | return response 23 | 24 | return decorated_function 25 | 26 | return decorator(maybe_func) if maybe_func else decorator 27 | -------------------------------------------------------------------------------- /Chapter07/accesstoken/auth/refresh_token.py: -------------------------------------------------------------------------------- 1 | from secrets import token_urlsafe 2 | from bcrypt import hashpw, gensalt 3 | from .user import User 4 | 5 | 6 | def generate_token(): 7 | api_key = token_urlsafe() 8 | hashed_key = hashpw(api_key.encode("utf-8"), gensalt()) 9 | return api_key, hashed_key 10 | 11 | 12 | async def store_refresh_token(user: User, hashed_key: str) -> None: 13 | user.refresh_hash = hashed_key 14 | -------------------------------------------------------------------------------- /Chapter07/accesstoken/auth/user.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from typing import Optional 3 | 4 | from sanic import Request 5 | 6 | 7 | @dataclass 8 | class User: 9 | user_id: int 10 | refresh_hash: Optional[str] = field(default=None) 11 | 12 | 13 | fake_database = { 14 | 1: User(1), 15 | } 16 | 17 | 18 | async def authenticate_login_credentials(username: str, password: str) -> User: 19 | # Do some fancy logic to validate the username and password 20 | return fake_database[1] 21 | 22 | 23 | async def get_user_from_request(request: Request) -> User: 24 | return fake_database[1] 25 | -------------------------------------------------------------------------------- /Chapter07/apitoken/server.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from inspect import isawaitable 3 | from secrets import token_urlsafe 4 | 5 | from bcrypt import checkpw, gensalt, hashpw 6 | from sanic import Request, Sanic, json, text 7 | from sanic.exceptions import Unauthorized 8 | 9 | app = Sanic(__name__) 10 | app.ctx.global_store = b"" 11 | 12 | 13 | def api_key_required( 14 | maybe_func=None, 15 | *, 16 | exception=Unauthorized, 17 | message="Invalid or unknown API key", 18 | ): 19 | def decorator(f): 20 | @wraps(f) 21 | async def decorated_function(request: Request, *args, **kwargs): 22 | try: 23 | is_valid = checkpw( 24 | request.token.encode("utf-8"), request.app.ctx.global_store 25 | ) 26 | 27 | if not is_valid: 28 | raise ValueError("Bad token") 29 | except ValueError as e: 30 | raise exception(message) from e 31 | 32 | response = f(request, *args, **kwargs) 33 | if isawaitable(response): 34 | response = await response 35 | 36 | return response 37 | 38 | return decorated_function 39 | 40 | return decorator(maybe_func) if maybe_func else decorator 41 | 42 | 43 | @app.post("/apikey") 44 | async def gen_handler(request: Request): 45 | api_key, hashed_key = generate_token() 46 | request.app.ctx.global_store = hashed_key 47 | print(f"{api_key=}") 48 | print(f"{hashed_key=}") 49 | return json({"api_key": api_key}) 50 | 51 | 52 | @app.get("/protected") 53 | @api_key_required 54 | async def protected_handler(request): 55 | return text("hi") 56 | 57 | 58 | def generate_token(): 59 | api_key = token_urlsafe() 60 | hashed_key = hashpw(api_key.encode("utf-8"), gensalt()) 61 | return api_key, hashed_key 62 | -------------------------------------------------------------------------------- /Chapter07/corsissue/server1/server.py: -------------------------------------------------------------------------------- 1 | from sanic import HTTPResponse, Request, Sanic, text 2 | 3 | app = Sanic(__name__) 4 | 5 | 6 | @app.get("/") 7 | async def handler(request: Request, name: str) -> HTTPResponse: 8 | return text(f"Hi {name}") 9 | 10 | 11 | # DO NOT DO THIS 12 | # @app.on_response 13 | # async def cors(_, resp): 14 | # resp.headers["Access-Control-Allow-Origin"] = "*" 15 | -------------------------------------------------------------------------------- /Chapter07/corsissue/server2/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter07/corsissue/server2/favicon.ico -------------------------------------------------------------------------------- /Chapter07/corsissue/server2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | CORS issue 8 | 9 | 10 |

Loading...

11 | 18 | 19 | -------------------------------------------------------------------------------- /Chapter07/corsresponse/access-control-allow-methods/server.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from sanic import Request, Sanic, response, text 4 | 5 | app = Sanic(__name__) 6 | app.config.ALLOWED_ORIGINS = [ 7 | "http://mysite.com", 8 | "http://127.0.0.1:8888", 9 | "http://127.0.0.1:7777", 10 | ] 11 | 12 | 13 | @app.get("/") 14 | async def handler(request: Request): 15 | return text("Hi") 16 | 17 | 18 | def is_preflight(request: Request) -> bool: 19 | return ( 20 | request.method == "OPTIONS" 21 | and "access-control-request-method" in request.headers 22 | ) 23 | 24 | 25 | async def options_handler(request, methods): 26 | resp = response.empty() 27 | if request.ctx.preflight: 28 | resp.headers["access-control-allow-credentials"] = "true" 29 | resp.headers["access-control-allow-methods"] = ",".join(methods) 30 | resp.headers["vary"] = "origin" 31 | origin = request.headers.get("origin") 32 | if not origin or origin not in request.app.config.ALLOWED_ORIGINS: 33 | return 34 | 35 | resp.headers["access-control-allow-origin"] = origin 36 | return resp 37 | 38 | 39 | @app.on_request 40 | async def check_preflight(request: Request) -> None: 41 | request.ctx.preflight = is_preflight(request) 42 | 43 | 44 | @app.before_server_start 45 | def add_info_handlers(app: Sanic, _): 46 | app.router.reset() 47 | for group in app.router.groups.values(): 48 | if "OPTIONS" not in group.methods: 49 | app.add_route( 50 | handler=partial(options_handler, methods=group.methods), 51 | uri=group.uri, 52 | methods=["OPTIONS"], 53 | strict_slashes=group.strict, 54 | name="options_handler", 55 | ) 56 | app.router.finalize() 57 | -------------------------------------------------------------------------------- /Chapter07/corsresponse/access-control-expose-headers/server.py: -------------------------------------------------------------------------------- 1 | from sanic import HTTPResponse, Request, Sanic, text 2 | 3 | app = Sanic(__name__) 4 | app.config.ALLOWED_ORIGINS = ["http://mysite.com", "http://localhost:8000"] 5 | 6 | 7 | @app.get("/") 8 | async def handler(request: Request): 9 | response = text("Hi") 10 | response.headers["foobar"] = "hello, 123" 11 | return response 12 | 13 | 14 | app.static("/test", "./test.html") 15 | 16 | 17 | def is_preflight(request: Request) -> bool: 18 | return ( 19 | request.method == "OPTIONS" 20 | and "access-control-request-method" in request.headers 21 | ) 22 | 23 | 24 | @app.on_response 25 | async def add_cors_headers(request: Request, response: HTTPResponse) -> None: 26 | # Add headers here on all requests 27 | 28 | origin = request.headers.get("origin") 29 | if not origin or origin not in request.app.config.ALLOWED_ORIGINS: 30 | return 31 | 32 | response.headers["access-control-allow-origin"] = origin 33 | # Uncomment this line to show the headers 34 | # response.headers["access-control-expose-headers"] = "foobar" 35 | -------------------------------------------------------------------------------- /Chapter07/corsresponse/access-control-expose-headers/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | CORS issue 8 | 9 | 10 |

CORS Testing

11 |

Loading...

12 | 20 | 21 | -------------------------------------------------------------------------------- /Chapter07/corsresponse/server2.py: -------------------------------------------------------------------------------- 1 | from http.client import responses 2 | from sanic import Sanic, text, HTTPResponse, Request 3 | from sanic import response 4 | 5 | app = Sanic(__name__) 6 | app.config.ALLOWED_ORIGINS = ["http://127.0.0.1:8888", "http://127.0.0.1:7777"] 7 | 8 | 9 | @app.get("/") 10 | async def handler(request: Request) -> HTTPResponse: 11 | response = text("Hi") 12 | response.headers["foobar"] = "hello, 123" 13 | return response 14 | 15 | 16 | app.static("/test", "./test.html") 17 | 18 | 19 | async def options_handler(request: Request) -> HTTPResponse: 20 | return response.empty() 21 | 22 | 23 | @app.before_server_start 24 | def add_info_handlers(app: Sanic, _) -> None: 25 | app.router.reset() 26 | for group in app.router.groups.values(): 27 | if "OPTIONS" not in group.methods: 28 | app.add_route( 29 | handler=options_handler, 30 | uri=group.uri, 31 | methods=["OPTIONS"], 32 | strict_slashes=group.strict, 33 | ) 34 | app.router.finalize() 35 | 36 | 37 | def is_preflight(request: Request) -> bool: 38 | return ( 39 | request.method == "OPTIONS" 40 | and "access-control-request-method" in request.headers 41 | ) 42 | 43 | 44 | @app.on_response 45 | async def add_cors_headers(request: Request, response: HTTPResponse) -> None: 46 | # Add headers here on all requests 47 | 48 | origin = request.headers.get("origin") 49 | if not origin or origin not in request.app.config.ALLOWED_ORIGINS: 50 | return 51 | 52 | response.headers["vary"] = "origin" 53 | response.headers["access-control-allow-origin"] = origin 54 | response.headers["access-control-expose-headers"] = "foobar" 55 | # response.headers["access-control-max-age"] = 60 * 10 56 | 57 | if is_preflight(request): 58 | response.headers["access-control-allow-credentials"] = "true" 59 | response.headers["access-control-allow-headers"] = "preflight" 60 | # response.headers["access-control-allow-methods"] = "options,post" 61 | -------------------------------------------------------------------------------- /Chapter08/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter08/.gitkeep -------------------------------------------------------------------------------- /Chapter08/k8s/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM sanicframework/sanic:3.9-latest 2 | 3 | COPY . /srv 4 | WORKDIR /srv 5 | EXPOSE 7777 6 | 7 | ENTRYPOINT ["sanic", "server:app", "--port=7777", "--host=0.0.0.0"] 8 | -------------------------------------------------------------------------------- /Chapter08/k8s/app.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: ch08-k8s-app 5 | spec: 6 | ports: 7 | - port: 80 8 | targetPort: 7777 9 | selector: 10 | app: ch08-k8s-app 11 | --- 12 | apiVersion: apps/v1 13 | kind: Deployment 14 | metadata: 15 | name: ch08-k8s-app 16 | spec: 17 | selector: 18 | matchLabels: 19 | app: ch08-k8s-app 20 | replicas: 2 21 | template: 22 | metadata: 23 | labels: 24 | app: ch08-k8s-app 25 | spec: 26 | containers: 27 | - name: ch08-k8s-app 28 | image: admhpkns/my-sanic-example-app:2 29 | ports: 30 | - containerPort: 7777 31 | -------------------------------------------------------------------------------- /Chapter08/k8s/ingress.yml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: ch08-k8s-app-ingress 5 | annotations: 6 | kubernetes.io/ingress.class: nginx 7 | # cert-manager.io/cluster-issuer: letsencrypt-tls 8 | # certmanager.k8s.io/acme-challenge-type: http01 9 | spec: 10 | # tls: 11 | # - hosts: 12 | # - example.com 13 | # secretName: ch08-k8s-tls 14 | rules: 15 | - host: "example.com" 16 | http: 17 | paths: 18 | - pathType: Prefix 19 | path: / 20 | backend: 21 | service: 22 | name: ch08-k8s-app 23 | port: 24 | number: 80 25 | -------------------------------------------------------------------------------- /Chapter08/k8s/issuer.yml: -------------------------------------------------------------------------------- 1 | apiVersion: cert-manager.io/v1 2 | kind: ClusterIssuer 3 | metadata: 4 | name: letsencrypt-tls 5 | spec: 6 | acme: 7 | email: fake@email.com 8 | server: https://acme-v02.api.letsencrypt.org/directory 9 | privateKeySecretRef: 10 | name: letsencrypt-tls-private-key 11 | solvers: 12 | - http01: 13 | ingress: 14 | class: nginx 15 | -------------------------------------------------------------------------------- /Chapter08/k8s/load-balancer.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | annotations: 5 | service.beta.kubernetes.io/do-loadbalancer-hostname: example.com 6 | name: ingress-nginx-controller 7 | namespace: ingress-nginx 8 | spec: 9 | type: LoadBalancer 10 | externalTrafficPolicy: Local 11 | ports: 12 | - name: http 13 | port: 80 14 | protocol: TCP 15 | targetPort: http 16 | - name: https 17 | port: 443 18 | protocol: TCP 19 | targetPort: https 20 | selector: 21 | app.kubernetes.io/name: ingress-nginx 22 | app.kubernetes.io/instance: ingress-nginx 23 | app.kubernetes.io/component: controller 24 | -------------------------------------------------------------------------------- /Chapter08/k8s/server.py: -------------------------------------------------------------------------------- 1 | from sanic import HTTPResponse, Request, Sanic, text 2 | from sanic.log import logger 3 | 4 | app = Sanic(__name__) 5 | app.config.REAL_IP_HEADER = "x-real-ip" 6 | 7 | 8 | @app.get("/") 9 | async def handler(request: Request) -> HTTPResponse: 10 | logger.info(request.headers) 11 | return text(f"Hello from {request.remote_addr}") 12 | 13 | 14 | @app.get("/healthz") 15 | async def healthz(request: Request) -> HTTPResponse: 16 | return text("OK") 17 | -------------------------------------------------------------------------------- /Chapter08/paas/Procfile: -------------------------------------------------------------------------------- 1 | web: sanic server:app --host=0.0.0.0 --port=$PORT 2 | -------------------------------------------------------------------------------- /Chapter08/paas/requirements.txt: -------------------------------------------------------------------------------- 1 | sanic 2 | -------------------------------------------------------------------------------- /Chapter08/paas/server.py: -------------------------------------------------------------------------------- 1 | from sanic import HTTPResponse, Request, Sanic, text 2 | from sanic.log import logger 3 | 4 | app = Sanic(__name__) 5 | app.config.REAL_IP_HEADER = "do-connecting-ip" 6 | 7 | 8 | @app.get("/") 9 | async def handler(request: Request) -> HTTPResponse: 10 | logger.info(request.headers) 11 | return text(f"Hello from {request.remote_addr}") 12 | 13 | 14 | @app.get("/healthz") 15 | async def healthz(request: Request) -> HTTPResponse: 16 | return text("OK") 17 | -------------------------------------------------------------------------------- /Chapter09/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter09/.gitkeep -------------------------------------------------------------------------------- /Chapter09/hikingapp/README.md: -------------------------------------------------------------------------------- 1 | # Chapter 9 - Full Hiking App 2 | 3 | ## Getting started 4 | 5 | ``` 6 | docker-compose build 7 | ``` 8 | 9 | ``` 10 | docker-compose up 11 | ``` 12 | 13 | ## Checkout some endpoints: 14 | 15 | ``` 16 | curl localhost:7777/v1/trails 17 | curl localhost:7777/v1/users/alice/hikes 18 | curl localhost:7777/v1/slow 19 | ``` 20 | 21 | _Make sure to hit `curl localhost:7777/v1/slow` multiple times to see the difference_ 22 | -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9 2 | 3 | RUN mkdir /app 4 | WORKDIR /app 5 | 6 | COPY ./requirements.txt /app/requirements.txt 7 | RUN pip install -r requirements.txt 8 | 9 | COPY ./hiking /app/hiking 10 | -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter09/hikingapp/application/hiking/__init__.py -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/blueprints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter09/hikingapp/application/hiking/blueprints/__init__.py -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/blueprints/hikes/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter09/hikingapp/application/hiking/blueprints/hikes/__init__.py -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/blueprints/hikes/executor.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from hiking.common.dao.executor import BaseExecutor 3 | from .models import Hike 4 | 5 | from hiking.blueprints.users.models import User 6 | 7 | 8 | class HikeExecutor(BaseExecutor): 9 | async def get_hikes_by_user(self, user: User) -> List[Hike]: 10 | query = self._queries["get_hikes_by_user"] 11 | records = await self.db.fetch_all(query, values={"user_id": user.user_id}) 12 | hikes = self.hydrator.hydrate(records, Hike, True) 13 | 14 | return hikes 15 | 16 | async def get_hikes_by_user_by_name(self, name: str) -> List[Hike]: 17 | ... 18 | -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/blueprints/hikes/hydrator.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Mapping, Optional, Type 2 | from hiking.common.dao.hydrator import Hydrator 3 | from hiking.common.base_model import BaseModel 4 | from hiking.blueprints.hikes.models import Hike 5 | from hiking.blueprints.trails.models import Trail 6 | 7 | 8 | class HikeHydrator(Hydrator): 9 | def do_hydration( 10 | self, 11 | record: Mapping[str, Any], 12 | model: Type[BaseModel], 13 | exclude: Optional[List[str]] = None, 14 | ): 15 | values = dict(record) 16 | date = values.pop("date") 17 | return Hike(Trail(**values), date) 18 | -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/blueprints/hikes/models.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from hiking.common.base_model import BaseModel 3 | from datetime import date 4 | 5 | from hiking.blueprints.trails.models import Trail 6 | 7 | 8 | @dataclass 9 | class Hike(BaseModel): 10 | trail: Trail 11 | date: date 12 | -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/blueprints/hikes/queries/get_hikes_by_user.sql: -------------------------------------------------------------------------------- 1 | SELECT t.*, h.date 2 | FROM hikes h 3 | JOIN trails t ON h.trail_id = t.trail_id 4 | WHERE h.user_id = :user_id 5 | -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/blueprints/hikes/queries/get_hikes_by_user_by_name.sql: -------------------------------------------------------------------------------- 1 | SELECT t.*, h.date 2 | FROM hikes h 3 | JOIN trails t ON h.trail_id = t.trail_id 4 | JOIN users u ON h.user_id = u.user_id 5 | WHERE LOWER(u.name) = LOWER(:name); 6 | -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/blueprints/slow/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter09/hikingapp/application/hiking/blueprints/slow/__init__.py -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/blueprints/slow/view.py: -------------------------------------------------------------------------------- 1 | from sanic import Blueprint, HTTPResponse, Request, text 2 | import random 3 | import asyncio 4 | from hiking.common.cache import cache_response 5 | 6 | 7 | bp = Blueprint("Slow", url_prefix="/slow") 8 | 9 | 10 | @bp.get("") 11 | @cache_response("tortoise") 12 | async def wow_super_slow(request: Request) -> HTTPResponse: 13 | wait_time = 0 14 | for _ in range(10): 15 | t = random.random() 16 | await asyncio.sleep(t) 17 | wait_time += t 18 | return text(f"Wow, that took {wait_time:.2f}s!") 19 | -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/blueprints/trails/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter09/hikingapp/application/hiking/blueprints/trails/__init__.py -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/blueprints/trails/executor.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | from hiking.blueprints.trails.models import Trail 3 | from hiking.common.dao.executor import BaseExecutor 4 | 5 | 6 | class TrailExecutor(BaseExecutor): 7 | async def get_all_trails( 8 | self, *, exclude: Optional[List[str]] = None 9 | ) -> List[Trail]: 10 | ... 11 | 12 | async def get_trail_by_name( 13 | self, name: str, *, exclude: Optional[List[str]] = None 14 | ) -> Trail: 15 | ... 16 | -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/blueprints/trails/models.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from hiking.common.base_model import BaseModel 3 | from decimal import Decimal 4 | 5 | 6 | @dataclass 7 | class Trail(BaseModel): 8 | trail_id: int 9 | name: str 10 | distance: Decimal 11 | -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/blueprints/trails/queries/get_all_trails.sql: -------------------------------------------------------------------------------- 1 | SELECT * 2 | FROM trails; 3 | -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/blueprints/trails/queries/get_trail_by_name.sql: -------------------------------------------------------------------------------- 1 | SELECT * 2 | FROM trails 3 | WHERE LOWER(name) = LOWER(:name); 4 | -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/blueprints/trails/view.py: -------------------------------------------------------------------------------- 1 | from sanic import Blueprint, HTTPResponse, json, Request 2 | from sanic.views import HTTPMethodView 3 | from .executor import TrailExecutor 4 | 5 | bp = Blueprint("Trails", url_prefix="/trails") 6 | 7 | 8 | class TrailListView(HTTPMethodView, attach=bp): 9 | async def get(self, request: Request) -> HTTPResponse: 10 | executor = TrailExecutor(request.app.ctx.postgres) 11 | trails = await executor.get_all_trails() 12 | return json({"trails": trails}) 13 | 14 | 15 | class TrailDetailView(HTTPMethodView, attach=bp, uri="/"): 16 | async def get(self, request: Request, name: str) -> HTTPResponse: 17 | executor = TrailExecutor(request.app.ctx.postgres) 18 | trail = await executor.get_trail_by_name(name) 19 | return json({"trail": trail}) 20 | -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/blueprints/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter09/hikingapp/application/hiking/blueprints/users/__init__.py -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/blueprints/users/executor.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | from hiking.blueprints.users.models import User 3 | from hiking.common.dao.executor import BaseExecutor 4 | 5 | 6 | class UserExecutor(BaseExecutor): 7 | async def get_all_users(self, *, exclude: Optional[List[str]] = None) -> List[User]: 8 | ... 9 | 10 | async def get_user_by_name( 11 | self, name: str, *, exclude: Optional[List[str]] = None 12 | ) -> User: 13 | ... 14 | -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/blueprints/users/models.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from hiking.common.base_model import BaseModel 3 | 4 | 5 | @dataclass 6 | class User(BaseModel): 7 | user_id: int 8 | name: str 9 | total_distance_hiked: float = field(default=0) 10 | -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/blueprints/users/queries/get_all_users.sql: -------------------------------------------------------------------------------- 1 | SELECT * 2 | FROM users; 3 | -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/blueprints/users/queries/get_user_by_name.sql: -------------------------------------------------------------------------------- 1 | SELECT *, 2 | ( 3 | SELECT SUM(distance) 4 | FROM trails t 5 | JOIN hikes h on t.trail_id = h.trail_id 6 | WHERE h.user_id = u.user_id 7 | ) total_distance_hiked 8 | FROM users u 9 | WHERE LOWER(name) = LOWER(:name); 10 | -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/blueprints/users/view.py: -------------------------------------------------------------------------------- 1 | from sanic import Blueprint, HTTPResponse, json, Request 2 | from sanic.views import HTTPMethodView 3 | from .executor import UserExecutor 4 | from ..hikes.executor import HikeExecutor 5 | from ..hikes.hydrator import HikeHydrator 6 | 7 | bp = Blueprint("Users", url_prefix="/users") 8 | 9 | 10 | class UserListView(HTTPMethodView, attach=bp): 11 | async def get(self, request: Request) -> HTTPResponse: 12 | executor = UserExecutor(request.app.ctx.postgres) 13 | users = await executor.get_all_users(exclude=["total_distance_hiked"]) 14 | return json({"users": users}) 15 | 16 | 17 | class UserDetailView(HTTPMethodView, attach=bp, uri="/"): 18 | async def get(self, request: Request, name: str) -> HTTPResponse: 19 | executor = UserExecutor(request.app.ctx.postgres) 20 | user = await executor.get_user_by_name(name) 21 | return json({"user": user}) 22 | 23 | 24 | class UserHikeDetailsView(HTTPMethodView, attach=bp, uri="//hikes"): 25 | async def get(self, request: Request, name: str) -> HTTPResponse: 26 | user_executor = UserExecutor(request.app.ctx.postgres) 27 | hike_executor = HikeExecutor(request.app.ctx.postgres, HikeHydrator()) 28 | user = await user_executor.get_user_by_name(name) 29 | hikes = await hike_executor.get_hikes_by_user(user) 30 | return json({"user": user, "hikes": hikes}) 31 | 32 | 33 | class UserHikeDetailsViewV2(HTTPMethodView, attach=bp, uri="//hikes", version=2): 34 | async def get(self, request: Request, name: str) -> HTTPResponse: 35 | hike_executor = HikeExecutor(request.app.ctx.postgres, HikeHydrator()) 36 | hikes = await hike_executor.get_hikes_by_user_by_name(name) 37 | return json({"user": {"name": name.title()}, "hikes": hikes}) 38 | -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/blueprints/view.py: -------------------------------------------------------------------------------- 1 | from .users.view import bp as users_bp 2 | from .trails.view import bp as trails_bp 3 | from .slow.view import bp as slow_bp 4 | from sanic import Blueprint 5 | 6 | bp = Blueprint.group(users_bp, trails_bp, slow_bp, version=1) 7 | -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter09/hikingapp/application/hiking/common/__init__.py -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/common/base_model.py: -------------------------------------------------------------------------------- 1 | from dataclasses import asdict, dataclass, field 2 | from datetime import date, datetime 3 | from typing import List 4 | import ujson 5 | from uuid import UUID 6 | 7 | 8 | @dataclass 9 | class MetaState: 10 | exclude: List[str] = field(default_factory=list) 11 | 12 | 13 | class BaseModel: 14 | __state__: MetaState 15 | 16 | def __post_init__(self) -> None: 17 | self.__state__ = MetaState() 18 | 19 | def __json__(self): 20 | return ujson.dumps( 21 | { 22 | k: self._clean(v) 23 | for k, v in asdict(self).items() 24 | if k not in self.__state__.exclude 25 | } 26 | ) 27 | 28 | @staticmethod 29 | def _clean(value): 30 | if isinstance(value, (date, datetime)): 31 | return value.isoformat() 32 | elif isinstance(value, UUID): 33 | return str(value) 34 | return value 35 | -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/common/cache.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from typing import Dict, Union 3 | from aioredis.client import Redis 4 | from sanic.response import HTTPResponse, raw 5 | from sanic import Request 6 | 7 | 8 | def make_key(build_key, request): 9 | return ":".join(["cached-response", build_key, request.name]) 10 | 11 | 12 | async def set_cached_response( 13 | response: HTTPResponse, 14 | redis: Redis, 15 | key: str, 16 | exp: int, 17 | ): 18 | await redis.hset( 19 | key, 20 | mapping={ 21 | b"body": response.body, 22 | b"status": str(response.status).encode("utf-8"), 23 | b"content_type": response.content_type.encode("utf-8"), 24 | }, 25 | ) 26 | await redis.expire(key, exp) 27 | 28 | 29 | async def get_cached_response( 30 | request: Request, redis: Redis, key: str 31 | ) -> Dict[str, Union[str, int]]: 32 | exists = await redis.hgetall(key) 33 | if exists and not request.args.get("refresh"): 34 | cached_response = { 35 | k.decode("utf-8"): v.decode("utf-8") for k, v in exists.items() 36 | } 37 | cached_response["status"] = int(cached_response["status"]) 38 | return cached_response 39 | 40 | return {} 41 | 42 | 43 | def cache_response(build_key, exp: int = 60 * 60 * 72): 44 | def decorator(f): 45 | @wraps(f) 46 | async def decorated_function(request, *handler_args, **handler_kwargs): 47 | cache: Redis = request.app.ctx.redis 48 | key = make_key(build_key, request) 49 | 50 | if cached_response := await get_cached_response(request, cache, key): 51 | response = raw(**cached_response) 52 | else: 53 | response = await f(request, *handler_args, **handler_kwargs) 54 | await set_cached_response(response, cache, key, exp) 55 | 56 | return response 57 | 58 | return decorated_function 59 | 60 | return decorator 61 | -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/common/dao/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter09/hikingapp/application/hiking/common/dao/__init__.py -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/common/dao/decorator.py: -------------------------------------------------------------------------------- 1 | from inspect import getsourcelines, signature 2 | from functools import wraps 3 | from typing import get_origin, get_args 4 | from sanic.exceptions import SanicException, NotFound 5 | 6 | 7 | def execute(func): 8 | sig = signature(func) 9 | src = getsourcelines(func) 10 | auto_exec = src[0][-1].strip() in ("...", "pass") 11 | model = sig.return_annotation 12 | as_list = False 13 | 14 | if origin := get_origin(model): 15 | as_list = bool(origin is list) 16 | if not as_list: 17 | return SanicException( 18 | f"{func} must return either a model or a list of models. " 19 | "eg. -> Foo or List[Foo]" 20 | ) 21 | model = get_args(model)[0] 22 | 23 | name = func.__name__ 24 | 25 | def decorator(f): 26 | @wraps(f) 27 | async def decorated_function(*args, **kwargs): 28 | if auto_exec: 29 | self = args[0] 30 | query = self._queries[name] 31 | method_name = "fetch_all" if as_list else "fetch_one" 32 | bound = sig.bind(*args, **kwargs) 33 | bound.apply_defaults() 34 | values = {**bound.arguments} 35 | values.pop("self") 36 | exclude = values.pop("exclude", None) 37 | results = await getattr(self.db, method_name)( 38 | query=query, values=values 39 | ) 40 | 41 | if not results: 42 | raise NotFound(f"Did not find {model.__name__}") 43 | return self.hydrator.hydrate(results, model, as_list, exclude) 44 | 45 | return await f(*args, **kwargs) 46 | 47 | return decorated_function 48 | 49 | return decorator(func) 50 | -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/common/dao/executor.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Dict, Optional, Set, Type 4 | from inspect import getmembers, isfunction, getmodule 5 | from pathlib import Path 6 | from databases import Database 7 | from sanic.exceptions import SanicException 8 | from .decorator import execute 9 | from .hydrator import Hydrator 10 | 11 | 12 | class BaseExecutor: 13 | _registry: Set[Type[BaseExecutor]] = set() 14 | _queries: Dict[str, str] = {} 15 | _fallback_hydrator: Hydrator 16 | db: Database 17 | 18 | def __init__(self, db: Database, hydrator: Optional[Hydrator] = None) -> None: 19 | self.db = db 20 | self._hydrator = hydrator 21 | 22 | def __init_subclass__(cls) -> None: 23 | BaseExecutor._registry.add(cls) 24 | 25 | @property 26 | def hydrator(self) -> Hydrator: 27 | if self._hydrator: 28 | return self._hydrator 29 | return self._fallback_hydrator 30 | 31 | @classmethod 32 | def load(cls, hydrator: Hydrator) -> None: 33 | cls._fallback_hydrator = hydrator 34 | for executor in cls._registry: 35 | module = getmodule(executor) 36 | if not module: 37 | raise SanicException(f"Could not locate module for {executor}") 38 | 39 | base = Path(module.__file__).parent 40 | for name, func in getmembers(executor, cls.isgetter): 41 | path = base / "queries" / f"{name}.sql" 42 | cls._queries[name] = cls.load_sql(path) 43 | setattr(executor, name, execute(func)) 44 | 45 | @staticmethod 46 | def isgetter(obj) -> bool: 47 | """Check if the object is a method that starts with get_""" 48 | if isfunction(obj): 49 | return obj.__name__.startswith("get_") 50 | return False 51 | 52 | @staticmethod 53 | def load_sql(path: Path) -> str: 54 | with open(path, "r") as f: 55 | return f.read() 56 | -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/common/dao/hydrator.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Literal, Mapping, Optional, Type, TypeVar, Union, overload 2 | from hiking.common.base_model import BaseModel 3 | 4 | Model = TypeVar("Model", bound=BaseModel) 5 | RecordT = Mapping[str, Any] 6 | ExcludeT = Optional[List[str]] 7 | 8 | 9 | class Hydrator: 10 | @overload 11 | def hydrate( 12 | self, 13 | record: RecordT, 14 | model: Type[Model], 15 | as_list: Literal[False], 16 | exclude: ExcludeT = None, 17 | ) -> Model: 18 | ... 19 | 20 | @overload 21 | def hydrate( 22 | self, 23 | record: List[RecordT], 24 | model: Type[Model], 25 | as_list: Literal[True], 26 | exclude: ExcludeT = None, 27 | ) -> List[Model]: 28 | ... 29 | 30 | def hydrate( 31 | self, 32 | record: Union[RecordT, List[RecordT]], 33 | model: Type[Model], 34 | as_list: bool, 35 | exclude: ExcludeT = None, 36 | ) -> Union[Model, List[Model]]: 37 | 38 | if as_list: 39 | record = [record] if not isinstance(record, list) else record 40 | return [self.do_hydration(r, model, exclude) for r in record] 41 | if isinstance(record, list): 42 | raise TypeError("Unexpectedly found multiple records while hydrating") 43 | return self.do_hydration(record, model, exclude) 44 | 45 | def do_hydration( 46 | self, 47 | record: RecordT, 48 | model: Type[Model], 49 | exclude: ExcludeT = None, 50 | ) -> Model: 51 | obj = model(**record) 52 | if exclude: 53 | obj.__state__.exclude = exclude 54 | return obj 55 | -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/middleware/request_context.py: -------------------------------------------------------------------------------- 1 | from contextvars import ContextVar 2 | 3 | from sanic import Request, Sanic 4 | 5 | app = Sanic.get_app() 6 | 7 | 8 | @app.after_server_start 9 | async def setup_request_context(app, _): 10 | app.ctx.request = ContextVar("request") 11 | 12 | 13 | @app.on_request 14 | async def attach_request(request: Request): 15 | request.app.ctx.request.set(request) 16 | -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/server.py: -------------------------------------------------------------------------------- 1 | from sanic import Sanic 2 | 3 | from hiking.common.log import setup_logging 4 | 5 | 6 | def create_app(): 7 | app = Sanic("HikingApp") 8 | setup_logging(app) 9 | 10 | from hiking.blueprints.view import bp # noqa 11 | from hiking.middleware import request_context # noqa 12 | from hiking.worker import postgres # noqa 13 | from hiking.worker import redis # noqa 14 | 15 | app.blueprint(bp) 16 | 17 | return app 18 | -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/worker/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter09/hikingapp/application/hiking/worker/__init__.py -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/worker/postgres.py: -------------------------------------------------------------------------------- 1 | from sanic import Sanic 2 | from databases import Database 3 | from hiking.common.dao.executor import BaseExecutor 4 | from hiking.common.dao.hydrator import Hydrator 5 | 6 | app = Sanic.get_app() 7 | 8 | 9 | @app.before_server_start 10 | async def setup_postgres(app: Sanic, _) -> None: 11 | app.ctx.postgres = Database( 12 | app.config.POSTGRES_DSN, 13 | min_size=app.config.POSTGRES_MIN, 14 | max_size=app.config.POSTGRES_MAX, 15 | ) 16 | 17 | 18 | @app.after_server_start 19 | async def connect_postgres(app: Sanic, _) -> None: 20 | await app.ctx.postgres.connect() 21 | 22 | 23 | @app.after_server_start 24 | async def load_sql(app: Sanic, _) -> None: 25 | BaseExecutor.load(Hydrator()) 26 | 27 | 28 | @app.after_server_stop 29 | async def shutdown_postgres(app: Sanic, _) -> None: 30 | await app.ctx.postgres.disconnect() 31 | -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/hiking/worker/redis.py: -------------------------------------------------------------------------------- 1 | from sanic import Sanic 2 | import aioredis 3 | 4 | app = Sanic.get_app() 5 | 6 | 7 | @app.before_server_start 8 | async def setup_redis(app, _): 9 | app.ctx.redis_pool = aioredis.BlockingConnectionPool.from_url( 10 | app.config.REDIS_DSN, max_connections=app.config.REDIS_MAX 11 | ) 12 | app.ctx.redis = aioredis.Redis(connection_pool=app.ctx.redis_pool) 13 | 14 | 15 | @app.after_server_stop 16 | async def shutdown_redis(app, _): 17 | await app.ctx.redis_pool.disconnect() 18 | -------------------------------------------------------------------------------- /Chapter09/hikingapp/application/requirements.txt: -------------------------------------------------------------------------------- 1 | aioredis 2 | databases[postgresql] 3 | sanic 4 | -------------------------------------------------------------------------------- /Chapter09/hikingapp/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | build: ./application 4 | ports: 5 | - 7777:7777 6 | depends_on: 7 | - db-postgres 8 | - db-redis 9 | command: 10 | sanic 11 | --port=7777 12 | --host=0.0.0.0 13 | --workers=2 14 | --factory 15 | --debug 16 | hiking.server:create_app 17 | volumes: 18 | - ./application/hiking:/app/hiking 19 | environment: 20 | SANIC_POSTGRES_DSN: "postgres://postgres:foobar@db-postgres:5432" 21 | SANIC_POSTGRES_MIN: 6 22 | SANIC_POSTGRES_MAX: 12 23 | SANIC_REDIS_DSN: "redis://db-redis:6379" 24 | SANIC_REDIS_MAX: 12 25 | SANIC_KEEP_ALIVE_TIMEOUT: 1 26 | db-postgres: 27 | build: ./postgres 28 | ports: 29 | - 5432:5432 30 | environment: 31 | - POSTGRES_PASSWORD=foobar 32 | volumes: 33 | - ./postgres/initial.sql:/docker-entrypoint-initdb.d/initial.sql 34 | db-redis: 35 | image: redis:alpine 36 | ports: 37 | - 6379:6379 38 | -------------------------------------------------------------------------------- /Chapter09/hikingapp/postgres/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres 2 | WORKDIR /docker-entrypoint-initdb.d 3 | ADD initial.sql /docker-entrypoint-initdb.d 4 | EXPOSE 5432 5 | CMD ["postgres", "-c", "log_statement=all", "-c", "log_destination=stderr"] 6 | -------------------------------------------------------------------------------- /Chapter09/hikingapp/postgres/initial.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | user_id SERIAL PRIMARY KEY, 3 | name CHARACTER VARYING(256) NOT NULL 4 | ); 5 | 6 | CREATE TABLE trails ( 7 | trail_id SERIAL PRIMARY KEY, 8 | name CHARACTER VARYING(256) NOT NULL, 9 | distance NUMERIC(6,2) NOT NULL 10 | ); 11 | 12 | CREATE TABLE hikes ( 13 | trail_id INTEGER REFERENCES trails (trail_id), 14 | user_id INTEGER REFERENCES users (user_id), 15 | date DATE, 16 | PRIMARY KEY (trail_id, user_id) 17 | ); 18 | 19 | 20 | INSERT INTO users (name) VALUES 21 | ('Alice'), 22 | ('Bob'), 23 | ('Carol'); 24 | 25 | INSERT INTO trails (name, distance) VALUES 26 | ('Green', 7.7), 27 | ('Red', 22.4), 28 | ('Blue', 1025), 29 | ('White', 613), 30 | ('Black', 54.1); 31 | 32 | INSERT INTO hikes (user_id, trail_id, date) VALUES 33 | (1, 1, '2021-01-01'), 34 | (1, 2, '2021-02-01'), 35 | (2, 5, '2021-05-02'), 36 | (3, 1, '2021-01-03'), 37 | (3, 5, '2021-05-03'); 38 | -------------------------------------------------------------------------------- /Chapter09/loggingapp0/Dockerfile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter09/loggingapp0/Dockerfile -------------------------------------------------------------------------------- /Chapter09/loggingapp0/myapp/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter09/loggingapp0/myapp/common/__init__.py -------------------------------------------------------------------------------- /Chapter09/loggingapp0/myapp/common/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from sanic import Sanic 4 | from sanic.log import error_logger, logger 5 | 6 | app_logger = logging.getLogger("myapplogger") 7 | 8 | 9 | DEFAULT_LOGGING_FORMAT = ( 10 | "[%(asctime)s] [%(levelname)s] [%(filename)s:%(lineno)s] %(message)s" 11 | ) 12 | 13 | 14 | def setup_logging(app: Sanic): 15 | formatter = logging.Formatter( 16 | fmt=app.config.get("LOGGING_FORMAT", DEFAULT_LOGGING_FORMAT), 17 | datefmt="%Y-%m-%d %H:%M:%S %z", 18 | ) 19 | 20 | # Setup application logger 21 | handler = logging.StreamHandler() 22 | handler.setFormatter(formatter) 23 | app_logger.addHandler(handler) 24 | 25 | # Output logs to file in production 26 | if app.config.get("ENVIRONMENT", "local") == "production": 27 | file_handler = logging.FileHandler("output.log") 28 | file_handler.setFormatter(formatter) 29 | app_logger.addHandler(file_handler) 30 | 31 | # Apply the same logging handlers to Sanic's logging instances 32 | logger.handlers = app_logger.handlers 33 | error_logger.handlers = app_logger.handlers 34 | -------------------------------------------------------------------------------- /Chapter09/loggingapp0/myapp/server.py: -------------------------------------------------------------------------------- 1 | from sanic import Sanic, text 2 | 3 | from myapp.common.log import app_logger, setup_logging 4 | 5 | 6 | def create_app(): 7 | app = Sanic("logging_app") 8 | setup_logging(app) 9 | 10 | @app.route("") 11 | async def dummy(_): 12 | app_logger.debug("This is a DEBUG message") 13 | app_logger.info("This is a INFO message") 14 | app_logger.warning("This is a WARNING message") 15 | app_logger.error("This is a ERROR message") 16 | app_logger.critical("This is a CRITICAL message") 17 | return text("") 18 | 19 | return app 20 | -------------------------------------------------------------------------------- /Chapter09/loggingapp0/tests/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter09/loggingapp0/tests/.gitkeep -------------------------------------------------------------------------------- /Chapter09/loggingapp1/Dockerfile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter09/loggingapp1/Dockerfile -------------------------------------------------------------------------------- /Chapter09/loggingapp1/myapp/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter09/loggingapp1/myapp/common/__init__.py -------------------------------------------------------------------------------- /Chapter09/loggingapp1/myapp/common/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | 4 | from sanic import Sanic 5 | from sanic.log import error_logger, logger 6 | 7 | app_logger = logging.getLogger("myapplogger") 8 | 9 | 10 | DEFAULT_LOGGING_FORMAT = ( 11 | "[%(asctime)s] [%(levelname)s] [%(filename)s:%(lineno)s] %(message)s" 12 | ) 13 | DEFAULT_LOGGING_DATEFORMAT = "%Y-%m-%d %H:%M:%S %z" 14 | 15 | 16 | class ColorFormatter(logging.Formatter): 17 | COLORS = { 18 | "DEBUG": "\033[34m", 19 | "WARNING": "\033[01;33m", 20 | "ERROR": "\033[01;31m", 21 | "CRITICAL": "\033[02;47m\033[01;31m", 22 | } 23 | 24 | def format(self, record) -> str: 25 | prefix = self.COLORS.get(record.levelname) 26 | message = super().format(record) 27 | 28 | if prefix: 29 | message = f"{prefix}{message}\033[0m" 30 | 31 | return message 32 | 33 | 34 | def setup_logging(app: Sanic): 35 | environment = app.config.get("ENVIRONMENT", "local") 36 | logging_level = app.config.get( 37 | "LOGGING_LEVEL", 38 | logging.DEBUG if environment == "local" else logging.INFO, 39 | ) 40 | fmt = app.config.get("LOGGING_FORMAT", DEFAULT_LOGGING_FORMAT) 41 | datefmt = app.config.get("LOGGING_DATEFORMAT", DEFAULT_LOGGING_DATEFORMAT) 42 | formatter = _get_formatter(environment == "local", fmt, datefmt) 43 | 44 | # Setup application logger 45 | handler = logging.StreamHandler() 46 | handler.setFormatter(formatter) 47 | app_logger.addHandler(handler) 48 | app_logger.setLevel(logging_level) 49 | 50 | # Output logs to file in production 51 | if app.config.get("ENVIRONMENT", "local") == "production": 52 | file_handler = logging.FileHandler("output.log") 53 | file_handler.setFormatter(formatter) 54 | app_logger.addHandler(file_handler) 55 | 56 | # Apply the same logging handlers to Sanic's logging instances 57 | logger.handlers = app_logger.handlers 58 | error_logger.handlers = app_logger.handlers 59 | 60 | 61 | def _get_formatter(is_local, fmt, datefmt): 62 | formatter_type = logging.Formatter 63 | if is_local and sys.stdout.isatty(): 64 | formatter_type = ColorFormatter 65 | 66 | return formatter_type( 67 | fmt=fmt, 68 | datefmt=datefmt, 69 | ) 70 | -------------------------------------------------------------------------------- /Chapter09/loggingapp1/myapp/server.py: -------------------------------------------------------------------------------- 1 | from sanic import Sanic, text 2 | 3 | from myapp.common.log import app_logger, setup_logging 4 | 5 | 6 | def create_app(): 7 | app = Sanic("logging_app") 8 | setup_logging(app) 9 | 10 | @app.route("") 11 | async def dummy(_): 12 | app_logger.debug("This is a DEBUG message") 13 | app_logger.info("This is a INFO message") 14 | app_logger.warning("This is a WARNING message") 15 | app_logger.error("This is a ERROR message") 16 | app_logger.critical("This is a CRITICAL message") 17 | return text("") 18 | 19 | return app 20 | -------------------------------------------------------------------------------- /Chapter09/loggingapp1/tests/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter09/loggingapp1/tests/.gitkeep -------------------------------------------------------------------------------- /Chapter09/testing0/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sanic_testing.testing import SanicTestClient 3 | 4 | from server import app 5 | 6 | 7 | @pytest.fixture 8 | def test_client(): 9 | return SanicTestClient(app) 10 | -------------------------------------------------------------------------------- /Chapter09/testing0/server.py: -------------------------------------------------------------------------------- 1 | from sanic import Sanic, text 2 | 3 | app = Sanic(__name__) 4 | 5 | 6 | @app.get("/") 7 | async def handler(request): 8 | return text("...") 9 | -------------------------------------------------------------------------------- /Chapter09/testing0/test_sample.py: -------------------------------------------------------------------------------- 1 | def test_sample(test_client): 2 | request, response = test_client.get("/") 3 | 4 | assert response.status == 200 5 | -------------------------------------------------------------------------------- /Chapter09/testing1/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from server import app as application_instance 4 | 5 | 6 | @pytest.fixture 7 | def app(): 8 | return application_instance 9 | -------------------------------------------------------------------------------- /Chapter09/testing1/server.py: -------------------------------------------------------------------------------- 1 | from sanic import Sanic, text 2 | 3 | app = Sanic(__name__) 4 | 5 | 6 | @app.get("/") 7 | async def handler(request): 8 | return text("...") 9 | -------------------------------------------------------------------------------- /Chapter09/testing1/test_sample.py: -------------------------------------------------------------------------------- 1 | from sanic import Sanic 2 | 3 | 4 | def test_sample(app: Sanic): 5 | request, response = app.test_client.get("/") 6 | 7 | assert response.status == 200 8 | -------------------------------------------------------------------------------- /Chapter09/testing2/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter09/testing2/__init__.py -------------------------------------------------------------------------------- /Chapter09/testing2/path/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter09/testing2/path/__init__.py -------------------------------------------------------------------------------- /Chapter09/testing2/path/to/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter09/testing2/path/to/__init__.py -------------------------------------------------------------------------------- /Chapter09/testing2/path/to/some_blueprint.py: -------------------------------------------------------------------------------- 1 | from typing import Any, NamedTuple, Type 2 | from sanic import Blueprint, Request, json 3 | 4 | bp = Blueprint("Something", url_prefix="/some") 5 | 6 | 7 | class ExpectedTypes(NamedTuple): 8 | a_string: str 9 | an_int: int 10 | 11 | 12 | def _check(exists: bool, value: Any, expected: Type[object]): 13 | if not exists: 14 | return "MISSING" 15 | return "OK" if type(value) is expected else "WRONG" 16 | 17 | 18 | @bp.post("/validation") 19 | async def check_types(request: Request): 20 | valid = { 21 | field_name: _check( 22 | field_name in request.json, request.json.get(field_name), field_type 23 | ) 24 | for field_name, field_type in ExpectedTypes.__annotations__.items() 25 | } 26 | status = ( 27 | 200 28 | if all(value == "OK" for value in valid.values()) 29 | and len(request.json) == len(ExpectedTypes.__annotations__) 30 | else 400 31 | ) 32 | return json(valid, status=status) 33 | -------------------------------------------------------------------------------- /Chapter09/testing2/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter09/testing2/tests/__init__.py -------------------------------------------------------------------------------- /Chapter09/testing2/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sanic import Sanic 3 | 4 | 5 | @pytest.fixture 6 | def dummy_app(): 7 | return Sanic("DummyApp") 8 | -------------------------------------------------------------------------------- /Chapter09/testing3/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter09/testing3/__init__.py -------------------------------------------------------------------------------- /Chapter09/testing3/path/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter09/testing3/path/__init__.py -------------------------------------------------------------------------------- /Chapter09/testing3/path/to/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter09/testing3/path/to/__init__.py -------------------------------------------------------------------------------- /Chapter09/testing3/path/to/some_blueprint.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, NamedTuple, Type 2 | from sanic import Blueprint, Request, json 3 | from sanic.exceptions import InvalidUsage 4 | from .some_db_connection import FakeDBConnection 5 | from .some_registration_service import RegistrationService 6 | 7 | bp = Blueprint("Registration", url_prefix="/registration") 8 | 9 | 10 | class RegistrationSchema(NamedTuple): 11 | username: str 12 | email: str 13 | 14 | 15 | def _is_okay(field: str, exists: bool, value: Any, expected: Type[object]) -> bool: 16 | if not exists: 17 | raise InvalidUsage(f"Missing required field: {field}") 18 | return type(value) is expected 19 | 20 | 21 | def _validate(input: Dict[str, Any], schema: Type[tuple]) -> None: 22 | for field_name, field_type in schema.__annotations__.items(): 23 | if not _is_okay( 24 | field_name, field_name in input, input.get(field_name), field_type 25 | ): 26 | raise InvalidUsage( 27 | f"Unexpected value '{field_type}' for field '{field_name}'" 28 | ) 29 | 30 | if len(input) != len(schema.__annotations__): 31 | fields = ", ".join(schema.__annotations__.keys()) 32 | raise InvalidUsage(f"Unknown fields, please only send: {fields}") 33 | 34 | 35 | @bp.post("/") 36 | async def check_types(request: Request): 37 | _validate(request.json, RegistrationSchema) 38 | connection: FakeDBConnection = request.app.ctx.db 39 | service = RegistrationService(connection) 40 | await service.register_user(request.json["username"], request.json["email"]) 41 | return json(True, status=201) 42 | -------------------------------------------------------------------------------- /Chapter09/testing3/path/to/some_db_connection.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | 4 | class FakeDBConnection: 5 | async def execute(self, query: str, *params: Any): 6 | ... 7 | -------------------------------------------------------------------------------- /Chapter09/testing3/path/to/some_registration_service.py: -------------------------------------------------------------------------------- 1 | from .some_db_connection import FakeDBConnection 2 | 3 | 4 | class RegistrationService: 5 | def __init__(self, connection: FakeDBConnection) -> None: 6 | self.connection = connection 7 | 8 | async def register_user(self, username: str, email: str) -> None: 9 | query = "INSERT INTO users VALUES ($1, $2);" 10 | await self.connection.execute(query, username, email) 11 | -------------------------------------------------------------------------------- /Chapter09/testing3/path/to/some_startup.py: -------------------------------------------------------------------------------- 1 | from sanic import Sanic 2 | from .some_db_connection import FakeDBConnection 3 | 4 | app = Sanic.get_app() 5 | 6 | 7 | @app.before_server_start 8 | async def setup_db_connection(app, _): 9 | app.ctx.db = FakeDBConnection() 10 | -------------------------------------------------------------------------------- /Chapter09/testing3/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter09/testing3/tests/__init__.py -------------------------------------------------------------------------------- /Chapter09/testing3/tests/conftest.py: -------------------------------------------------------------------------------- 1 | from importlib import import_module 2 | from unittest.mock import AsyncMock 3 | import pytest 4 | from sanic import Sanic 5 | import testing3.path.to.some_db_connection 6 | 7 | 8 | @pytest.fixture 9 | def mocked_execute(monkeypatch): 10 | execute = AsyncMock() 11 | monkeypatch.setattr( 12 | testing3.path.to.some_db_connection.FakeDBConnection, "execute", execute 13 | ) 14 | return execute 15 | 16 | 17 | @pytest.fixture 18 | def dummy_app(): 19 | app = Sanic("DummyApp") 20 | 21 | import_module("testing3.path.to.some_startup") 22 | return app 23 | -------------------------------------------------------------------------------- /Chapter09/testing3/tests/test_some_blueprint.py: -------------------------------------------------------------------------------- 1 | # test_some_blueprint.py 2 | import pytest 3 | from testing3.path.to.some_blueprint import bp 4 | 5 | 6 | @pytest.fixture 7 | def app_with_bp(dummy_app): 8 | dummy_app.blueprint(bp) 9 | return dummy_app 10 | 11 | 12 | @pytest.mark.parametrize( 13 | "input,expected_status", 14 | ( 15 | ( 16 | { 17 | "username": "Alice", 18 | "email": "alice@bob.com", 19 | }, 20 | 201, 21 | ), 22 | ), 23 | ) 24 | def test_some_blueprint_data_validation( 25 | app_with_bp, 26 | mocked_execute, 27 | input, 28 | expected_status, 29 | ): 30 | _, response = app_with_bp.test_client.post( 31 | "/registration", 32 | json=input, 33 | ) 34 | 35 | assert response.status == expected_status 36 | 37 | if expected_status == 201: 38 | mocked_execute.assert_awaited_with( 39 | "INSERT INTO users VALUES ($1, $2);", input["username"], input["email"] 40 | ) 41 | -------------------------------------------------------------------------------- /Chapter09/testing4/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter09/testing4/__init__.py -------------------------------------------------------------------------------- /Chapter09/testing4/server.py: -------------------------------------------------------------------------------- 1 | from sanic import Sanic, Request, json 2 | from itertools import count 3 | 4 | 5 | app = Sanic("test") 6 | 7 | 8 | @app.before_server_start 9 | def setup(app, _): 10 | app.ctx.counter = count() 11 | 12 | 13 | @app.get("") 14 | async def handler(request: Request): 15 | return json(next(request.app.ctx.counter)) 16 | -------------------------------------------------------------------------------- /Chapter09/testing4/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter09/testing4/tests/__init__.py -------------------------------------------------------------------------------- /Chapter09/testing4/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sanic_testing.reusable import ReusableClient 3 | 4 | from ..server import app 5 | 6 | 7 | @pytest.fixture(scope="session") 8 | def test_client(): 9 | client = ReusableClient(app, host="localhost", port=1234) 10 | client.run() 11 | yield client 12 | client.stop() 13 | -------------------------------------------------------------------------------- /Chapter09/testing4/tests/test_1.py: -------------------------------------------------------------------------------- 1 | from sanic_testing.reusable import ReusableClient 2 | 3 | from ..server import app 4 | 5 | 6 | def test_reusable_context(): 7 | client = ReusableClient(app, host="localhost", port=4567) 8 | 9 | with client: 10 | _, response = client.get("/") 11 | assert response.json == 0 12 | 13 | _, response = client.get("/") 14 | assert response.json == 1 15 | 16 | _, response = client.get("/") 17 | assert response.json == 2 18 | 19 | 20 | def test_reusable_fixture(test_client): 21 | _, response = test_client.get("/") 22 | assert response.json == 0 23 | 24 | _, response = test_client.get("/") 25 | assert response.json == 1 26 | 27 | _, response = test_client.get("/") 28 | assert response.json == 2 29 | -------------------------------------------------------------------------------- /Chapter09/testing4/tests/test_2.py: -------------------------------------------------------------------------------- 1 | def test_reusable_fixture(test_client): 2 | _, response = test_client.get("/") 3 | assert response.json == 3 4 | 5 | _, response = test_client.get("/") 6 | assert response.json == 4 7 | 8 | _, response = test_client.get("/") 9 | assert response.json == 5 10 | -------------------------------------------------------------------------------- /Chapter09/tracing/myapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter09/tracing/myapp/__init__.py -------------------------------------------------------------------------------- /Chapter09/tracing/myapp/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter09/tracing/myapp/common/__init__.py -------------------------------------------------------------------------------- /Chapter09/tracing/myapp/middleware/request_context.py: -------------------------------------------------------------------------------- 1 | from contextvars import ContextVar 2 | 3 | from sanic import Request, Sanic 4 | 5 | app = Sanic.get_app() 6 | 7 | 8 | @app.after_server_start 9 | async def setup_request_context(app, _): 10 | app.ctx.request = ContextVar("request") 11 | 12 | 13 | @app.on_request 14 | async def attach_request(request: Request): 15 | request.app.ctx.request.set(request) 16 | -------------------------------------------------------------------------------- /Chapter09/tracing/myapp/server.py: -------------------------------------------------------------------------------- 1 | from sanic import Request, Sanic, json 2 | 3 | from myapp.common.log import app_logger, setup_logging 4 | 5 | 6 | def create_app(): 7 | app = Sanic("TracingApp") 8 | setup_logging(app) 9 | 10 | @app.route("") 11 | async def dummy(request: Request): 12 | app_logger.debug("This is a DEBUG message") 13 | app_logger.info("This is a INFO message") 14 | app_logger.warning("This is a WARNING message") 15 | app_logger.error("This is a ERROR message") 16 | app_logger.critical("This is a CRITICAL message") 17 | return json( 18 | { 19 | "requests_count": request.protocol.state["requests_count"], 20 | "request_id": str(request.id), 21 | "conn_id": id(request.conn_info), 22 | "email": request.headers.get("x-goog-authenticated-user-email"), 23 | } 24 | ) 25 | 26 | from myapp.middleware import request_context # noqa 27 | 28 | return app 29 | -------------------------------------------------------------------------------- /Chapter10/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter10/.gitkeep -------------------------------------------------------------------------------- /Chapter10/graphql/application/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9 2 | 3 | WORKDIR /app 4 | 5 | COPY ./requirements.txt /app/requirements.txt 6 | RUN pip install -r requirements.txt 7 | 8 | COPY ./world /app/world 9 | -------------------------------------------------------------------------------- /Chapter10/graphql/application/requirements.txt: -------------------------------------------------------------------------------- 1 | ariadne 2 | databases[postgresql] 3 | sanic 4 | -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter10/graphql/application/world/__init__.py -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/blueprints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter10/graphql/application/world/blueprints/__init__.py -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/blueprints/cities/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter10/graphql/application/world/blueprints/cities/__init__.py -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/blueprints/cities/executor.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from world.blueprints.cities.models import City 4 | from world.common.dao.executor import BaseExecutor 5 | 6 | 7 | class CityExecutor(BaseExecutor): 8 | async def get_all_cities( 9 | self, 10 | *, 11 | exclude: Optional[List[str]] = None, 12 | limit: int = 15, 13 | offset: int = 0, 14 | ) -> List[City]: 15 | ... 16 | 17 | async def get_cities_by_country_code( 18 | self, 19 | *, 20 | code: str, 21 | exclude: Optional[List[str]] = None, 22 | limit: int = 15, 23 | offset: int = 0, 24 | ) -> List[City]: 25 | ... 26 | 27 | async def get_city_by_name( 28 | self, name: str, *, exclude: Optional[List[str]] = None 29 | ) -> City: 30 | ... 31 | 32 | async def get_city_by_id( 33 | self, city_id: int, *, exclude: Optional[List[str]] = None 34 | ) -> City: 35 | ... 36 | -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/blueprints/cities/integrator.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any, Dict, List, Optional, TYPE_CHECKING 4 | 5 | from ariadne import ObjectType 6 | from graphql.type import GraphQLResolveInfo 7 | 8 | from world.blueprints.languages.executor import LanguageExecutor 9 | from world.common.dao.integrator import BaseIntegrator 10 | from .executor import CityExecutor 11 | from .models import City 12 | 13 | if TYPE_CHECKING: 14 | from world.blueprints.languages.models import Language 15 | 16 | 17 | class CityIntegrator(BaseIntegrator): 18 | name = "city" 19 | 20 | def make_query_def(self) -> List[str]: 21 | return [ 22 | "city(name: String!): City", 23 | "cities(country: String, limit: Int, offset: Int): [City]", 24 | ] 25 | 26 | def make_additional_schema(self) -> ObjectType: 27 | city = ObjectType("City") 28 | city.set_field("languages", self.resolve_languages) 29 | return city 30 | 31 | async def query_city(self, _: Any, info: GraphQLResolveInfo, *, name: str) -> City: 32 | executor = CityExecutor(info.context.app.ctx.postgres) 33 | return await executor.get_city_by_name(name=name) 34 | 35 | async def query_cities( 36 | self, 37 | _: Any, 38 | info: GraphQLResolveInfo, 39 | *, 40 | country: Optional[str] = None, 41 | limit: Optional[int] = None, 42 | offset: Optional[int] = None, 43 | ) -> List[City]: 44 | executor = CityExecutor(info.context.app.ctx.postgres) 45 | kwargs: Dict[str, Any] = {} 46 | if limit: 47 | kwargs["limit"] = limit 48 | if offset: 49 | kwargs["offset"] = offset 50 | if country: 51 | cities = await executor.get_cities_by_country_code( 52 | code=country, 53 | **kwargs, 54 | ) 55 | else: 56 | cities = await executor.get_all_cities(**kwargs) 57 | return cities 58 | 59 | async def resolve_languages( 60 | self, city: City, info: GraphQLResolveInfo 61 | ) -> List[Language]: 62 | executor = LanguageExecutor(info.context.app.ctx.postgres) 63 | return await executor.get_by_country_code(country_code=city.countrycode) 64 | -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/blueprints/cities/models.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from world.common.base_model import BaseModel 4 | 5 | 6 | @dataclass 7 | class City(BaseModel): 8 | id: int 9 | name: str 10 | countrycode: str 11 | district: str 12 | population: int 13 | -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/blueprints/cities/queries/get_all_cities.sql: -------------------------------------------------------------------------------- 1 | SELECT * 2 | FROM city 3 | LIMIT :limit 4 | OFFSET :offset; 5 | -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/blueprints/cities/queries/get_cities_by_country_code.sql: -------------------------------------------------------------------------------- 1 | SELECT * 2 | FROM city 3 | WHERE countrycode = :code 4 | LIMIT :limit 5 | OFFSET :offset; 6 | -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/blueprints/cities/queries/get_city_by_id.sql: -------------------------------------------------------------------------------- 1 | SELECT * 2 | FROM city c 3 | WHERE id = :city_id; 4 | -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/blueprints/cities/queries/get_city_by_name.sql: -------------------------------------------------------------------------------- 1 | SELECT * 2 | FROM city c 3 | WHERE LOWER(name) = LOWER(:name); 4 | -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/blueprints/cities/schema.gql: -------------------------------------------------------------------------------- 1 | type City { 2 | id: Int 3 | name: String 4 | countrycode: String 5 | district: String 6 | population: Int 7 | languages: [Language] 8 | } 9 | -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/blueprints/countries/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter10/graphql/application/world/blueprints/countries/__init__.py -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/blueprints/countries/executor.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from world.blueprints.countries.models import Country 4 | from world.common.dao.executor import BaseExecutor 5 | 6 | 7 | class CountryExecutor(BaseExecutor): 8 | async def get_all_countries( 9 | self, 10 | *, 11 | exclude: Optional[List[str]] = None, 12 | limit: int = 15, 13 | offset: int = 0, 14 | ) -> List[Country]: 15 | ... 16 | 17 | async def get_country_by_name( 18 | self, name: str, *, exclude: Optional[List[str]] = None 19 | ) -> Country: 20 | ... 21 | -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/blueprints/countries/models.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from world.common.base_model import BaseModel 4 | 5 | 6 | @dataclass 7 | class Country(BaseModel): 8 | code: str 9 | name: str 10 | continent: str 11 | region: str 12 | surfacearea: int 13 | indepyear: int 14 | population: int 15 | lifeexpectancy: int 16 | gnp: float 17 | gnpold: float 18 | localname: str 19 | governmentform: str 20 | headofstate: str 21 | capital: int 22 | code2: str 23 | -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/blueprints/countries/queries/get_all_countries.sql: -------------------------------------------------------------------------------- 1 | SELECT * 2 | FROM country 3 | LIMIT :limit 4 | OFFSET :offset; 5 | -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/blueprints/countries/queries/get_country_by_name.sql: -------------------------------------------------------------------------------- 1 | SELECT * 2 | FROM country c 3 | WHERE LOWER(name) = LOWER(:name); 4 | -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/blueprints/countries/schema.gql: -------------------------------------------------------------------------------- 1 | type Country { 2 | code: String 3 | name: String 4 | continent: String 5 | region: String 6 | surfacearea: Int 7 | indepyear: Int 8 | population: Int 9 | lifeexpectancy: Int 10 | gnp: Float 11 | gnpold: Float 12 | localname: String 13 | governmentform: String 14 | headofstate: String 15 | capital: City 16 | code2: String 17 | languages: [Language] 18 | } 19 | -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/blueprints/graphql/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter10/graphql/application/world/blueprints/graphql/__init__.py -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/blueprints/graphql/query.py: -------------------------------------------------------------------------------- 1 | from ariadne import QueryType 2 | 3 | query = QueryType() 4 | -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/blueprints/graphql/view.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from ariadne import graphql, make_executable_schema 4 | from ariadne.constants import PLAYGROUND_HTML 5 | from sanic import Blueprint, HTTPResponse, Request, Sanic, html, json 6 | from sanic.views import HTTPMethodView 7 | 8 | from world.blueprints.cities.integrator import CityIntegrator 9 | from world.blueprints.countries.integrator import CountryIntegrator 10 | from world.blueprints.languages.integrator import LanguageIntegrator 11 | from world.common.dao.integrator import RootIntegrator 12 | from .query import query 13 | 14 | bp = Blueprint("GraphQL", url_prefix="/graphql") 15 | 16 | 17 | class GraphQLView(HTTPMethodView, attach=bp, uri=""): 18 | async def get(self, request: Request) -> HTTPResponse: 19 | return html(PLAYGROUND_HTML) 20 | 21 | async def post(self, request: Request) -> HTTPResponse: 22 | success, result = await graphql( 23 | request.app.ctx.schema, 24 | request.json, 25 | context_value=request, 26 | debug=request.app.debug, 27 | ) 28 | 29 | status_code = 200 if success else 400 30 | return json(result, status=status_code) 31 | 32 | 33 | @bp.before_server_start 34 | async def setup_graphql(app: Sanic, _: Any) -> None: 35 | integrator = RootIntegrator.create( 36 | CityIntegrator, 37 | CountryIntegrator, 38 | LanguageIntegrator, 39 | query=query, 40 | ) 41 | integrator.load() 42 | integrator.attach_resolvers() 43 | defs = integrator.generate_query_defs() 44 | print(defs) 45 | additional = integrator.generate_additional_schemas() 46 | app.ctx.schema = make_executable_schema(defs, query, *additional) 47 | -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/blueprints/languages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter10/graphql/application/world/blueprints/languages/__init__.py -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/blueprints/languages/executor.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from world.blueprints.languages.models import Language 4 | from world.common.dao.executor import BaseExecutor 5 | 6 | 7 | class LanguageExecutor(BaseExecutor): 8 | async def get_by_country_code( 9 | self, 10 | *, 11 | country_code: str, 12 | exclude: Optional[List[str]] = None, 13 | limit: int = 15, 14 | offset: int = 0, 15 | ) -> List[Language]: 16 | ... 17 | -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/blueprints/languages/integrator.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from world.common.dao.integrator import BaseIntegrator 4 | 5 | 6 | class LanguageIntegrator(BaseIntegrator): 7 | name = "language" 8 | -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/blueprints/languages/models.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from world.common.base_model import BaseModel 4 | 5 | 6 | @dataclass 7 | class Language(BaseModel): 8 | countrycode: str 9 | language: str 10 | isofficial: bool 11 | percentage: float 12 | -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/blueprints/languages/queries/get_by_country_code.sql: -------------------------------------------------------------------------------- 1 | SELECT * 2 | FROM countrylanguage 3 | WHERE countrycode = :country_code 4 | LIMIT :limit 5 | OFFSET :offset; 6 | -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/blueprints/languages/schema.gql: -------------------------------------------------------------------------------- 1 | type Language { 2 | countrycode: String 3 | language: String 4 | isofficial: Boolean 5 | percentage: Float 6 | } 7 | -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/blueprints/view.py: -------------------------------------------------------------------------------- 1 | from sanic import Blueprint 2 | 3 | from .graphql.view import bp as graphql_bp 4 | 5 | bp = Blueprint.group(graphql_bp, version=1) 6 | -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter10/graphql/application/world/common/__init__.py -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/common/base_model.py: -------------------------------------------------------------------------------- 1 | from dataclasses import asdict, dataclass, field 2 | from datetime import date, datetime 3 | from typing import List, TypeVar, Union 4 | from uuid import UUID 5 | 6 | import ujson 7 | 8 | T = TypeVar("T") 9 | 10 | 11 | @dataclass 12 | class MetaState: 13 | exclude: List[str] = field(default_factory=list) 14 | 15 | 16 | class BaseModel: 17 | __state__: MetaState 18 | 19 | def __post_init__(self) -> None: 20 | self.__state__ = MetaState() 21 | 22 | def __json__(self) -> str: 23 | return ujson.dumps( 24 | { 25 | k: self._clean(v) 26 | for k, v in asdict(self).items() 27 | if k not in self.__state__.exclude 28 | } 29 | ) 30 | 31 | @staticmethod 32 | def _clean(value: T) -> Union[str, T]: 33 | if isinstance(value, (date, datetime)): 34 | return value.isoformat() 35 | elif isinstance(value, UUID): 36 | return str(value) 37 | return value 38 | -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/common/dao/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter10/graphql/application/world/common/dao/__init__.py -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/common/dao/executor.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from inspect import getmembers, getmodule, isfunction 4 | from pathlib import Path 5 | from typing import Any, Dict, Optional, Set, Type, cast 6 | 7 | from databases import Database 8 | from sanic.exceptions import SanicException 9 | 10 | from .decorator import execute 11 | from .hydrator import Hydrator 12 | 13 | 14 | class BaseExecutor: 15 | _registry: Set[Type[BaseExecutor]] = set() 16 | _queries: Dict[str, str] = {} 17 | _fallback_hydrator: Hydrator 18 | db: Database 19 | 20 | def __init__(self, db: Database, hydrator: Optional[Hydrator] = None) -> None: 21 | self.db = db 22 | self._hydrator = hydrator 23 | 24 | def __init_subclass__(cls) -> None: 25 | BaseExecutor._registry.add(cls) 26 | 27 | @property 28 | def hydrator(self) -> Hydrator: 29 | if self._hydrator: 30 | return self._hydrator 31 | return self._fallback_hydrator 32 | 33 | @classmethod 34 | def load(cls, hydrator: Hydrator) -> None: 35 | cls._fallback_hydrator = hydrator 36 | for executor in cls._registry: 37 | module = getmodule(executor) 38 | if not module: 39 | raise SanicException(f"Could not locate module for {executor}") 40 | 41 | base = Path(module.__file__).parent 42 | for name, func in getmembers(executor, cls.isgetter): 43 | path = base / "queries" / f"{name}.sql" 44 | cls._queries[name] = cls.load_sql(path) 45 | setattr(executor, name, execute(func)) 46 | 47 | @staticmethod 48 | def isgetter(obj: Any) -> bool: 49 | """Check if the object is a method that starts with get_""" 50 | if isfunction(obj): 51 | return obj.__name__.startswith("get_") 52 | return False 53 | 54 | @staticmethod 55 | def load_sql(path: Path) -> str: 56 | with open(path, "r") as f: 57 | return f.read() 58 | -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/common/dao/hydrator.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Literal, Mapping, Optional, Type, TypeVar, Union, overload 2 | 3 | from world.common.base_model import BaseModel 4 | 5 | ModelT = TypeVar("ModelT", bound=BaseModel) 6 | RecordT = Mapping[str, Any] 7 | ExcludeT = Optional[List[str]] 8 | 9 | 10 | class Hydrator: 11 | @overload 12 | def hydrate( 13 | self, 14 | record: RecordT, 15 | model: Type[ModelT], 16 | as_list: Literal[False], 17 | exclude: ExcludeT = None, 18 | ) -> ModelT: 19 | ... 20 | 21 | @overload 22 | def hydrate( 23 | self, 24 | record: List[RecordT], 25 | model: Type[ModelT], 26 | as_list: Literal[True], 27 | exclude: ExcludeT = None, 28 | ) -> List[ModelT]: 29 | ... 30 | 31 | @overload 32 | def hydrate( 33 | self, 34 | record: Union[RecordT, List[RecordT]], 35 | model: Type[ModelT], 36 | as_list: bool, 37 | exclude: ExcludeT = None, 38 | ) -> Union[ModelT, List[ModelT]]: 39 | ... 40 | 41 | def hydrate( 42 | self, 43 | record: Union[RecordT, List[RecordT]], 44 | model: Type[ModelT], 45 | as_list: bool, 46 | exclude: ExcludeT = None, 47 | ) -> Union[ModelT, List[ModelT]]: 48 | if as_list: 49 | record = [record] if not isinstance(record, list) else record 50 | return [self.do_hydration(r, model, exclude) for r in record] 51 | if isinstance(record, list): 52 | raise TypeError("Unexpectedly found multiple records while hydrating") 53 | return self.do_hydration(record, model, exclude) 54 | 55 | def do_hydration( 56 | self, 57 | record: RecordT, 58 | model: Type[ModelT], 59 | exclude: ExcludeT = None, 60 | ) -> ModelT: 61 | obj = model(**record) 62 | if exclude: 63 | obj.__state__.exclude = exclude 64 | return obj 65 | -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/middleware/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter10/graphql/application/world/middleware/__init__.py -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/middleware/request_context.py: -------------------------------------------------------------------------------- 1 | from contextvars import ContextVar 2 | from typing import Any 3 | 4 | from sanic import Request, Sanic 5 | 6 | app = Sanic.get_app() 7 | 8 | 9 | @app.after_server_start 10 | async def setup_request_context(app: Sanic, _: Any) -> None: 11 | app.ctx.request = ContextVar("request") 12 | 13 | 14 | @app.on_request 15 | async def attach_request(request: Request) -> None: 16 | request.app.ctx.request.set(request) 17 | -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/server.py: -------------------------------------------------------------------------------- 1 | from sanic import Sanic 2 | 3 | from world.common.log import setup_logging 4 | 5 | 6 | def create_app() -> Sanic: 7 | app = Sanic("GraphQLApp") 8 | setup_logging(app) 9 | 10 | from world.blueprints.view import bp # noqa 11 | from world.middleware import request_context # noqa 12 | from world.worker import postgres # noqa 13 | 14 | app.blueprint(bp) 15 | 16 | return app 17 | -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/worker/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter10/graphql/application/world/worker/__init__.py -------------------------------------------------------------------------------- /Chapter10/graphql/application/world/worker/postgres.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from databases import Database 4 | from sanic import Sanic 5 | 6 | from world.common.dao.executor import BaseExecutor 7 | from world.common.dao.hydrator import Hydrator 8 | 9 | app = Sanic.get_app() 10 | 11 | 12 | @app.before_server_start 13 | async def setup_postgres(app: Sanic, _: Any) -> None: 14 | app.ctx.postgres = Database( 15 | app.config.POSTGRES_DSN, 16 | min_size=app.config.POSTGRES_MIN, 17 | max_size=app.config.POSTGRES_MAX, 18 | ) 19 | 20 | 21 | @app.after_server_start 22 | async def connect_postgres(app: Sanic, _: Any) -> None: 23 | await app.ctx.postgres.connect() 24 | 25 | 26 | @app.after_server_start 27 | async def load_sql(app: Sanic, _: Any) -> None: 28 | BaseExecutor.load(Hydrator()) 29 | 30 | 31 | @app.after_server_stop 32 | async def shutdown_postgres(app: Sanic, _: Any) -> None: 33 | await app.ctx.postgres.disconnect() 34 | -------------------------------------------------------------------------------- /Chapter10/graphql/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | app: 4 | build: ./application 5 | ports: 6 | - 7777:7777 7 | depends_on: 8 | - db-postgres 9 | command: 10 | sanic 11 | --port=7777 12 | --host=0.0.0.0 13 | --factory 14 | --auto-reload 15 | world.server:create_app 16 | volumes: 17 | - ./application/world:/app/world 18 | environment: 19 | SANIC_POSTGRES_DSN: "postgres://postgres:postgres@db-postgres:5432/world" 20 | SANIC_POSTGRES_MIN: 6 21 | SANIC_POSTGRES_MAX: 12 22 | SANIC_KEEP_ALIVE_TIMEOUT: 1 23 | db-postgres: 24 | image: aa8y/postgres-dataset:world 25 | ports: 26 | - 5432:5432 27 | command: ["postgres", "-c", "log_statement=all"] 28 | -------------------------------------------------------------------------------- /Chapter10/httpredirect/wadsworth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter10/httpredirect/wadsworth/__init__.py -------------------------------------------------------------------------------- /Chapter10/httpredirect/wadsworth/applications/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter10/httpredirect/wadsworth/applications/__init__.py -------------------------------------------------------------------------------- /Chapter10/httpredirect/wadsworth/applications/redirect.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from sanic import Sanic 4 | from sanic.exceptions import ServerError 5 | from sanic.handlers import ErrorHandler 6 | from sanic.server.async_server import AsyncioServer 7 | 8 | from wadsworth.blueprints.info.view import bp as info_view 9 | from wadsworth.blueprints.redirect.view import bp as redirect_view 10 | 11 | 12 | def attach_redirect_app(main_app: Sanic) -> Sanic: 13 | redirect_app = Sanic("RedirectApp") 14 | redirect_app.blueprint(info_view) 15 | redirect_app.blueprint(redirect_view) 16 | redirect_app.ctx.main_app = main_app 17 | 18 | @main_app.before_server_start 19 | async def startup_redirect_app(main: Sanic, _: Any) -> None: 20 | app_server = await redirect_app.create_server( 21 | port=8080, return_asyncio_server=True 22 | ) 23 | if not app_server: 24 | raise ServerError("Failed to create redirect server") 25 | main_app.ctx.redirect = app_server 26 | main_app.add_task(runner(redirect_app, app_server)) 27 | 28 | @main_app.after_server_stop 29 | async def shutdown_redirect_app(main: Sanic, _: Any) -> None: 30 | await main.ctx.redirect.before_stop() 31 | await main.ctx.redirect.close() 32 | for connection in main.ctx.redirect.connections: 33 | connection.close_if_idle() 34 | await main.ctx.redirect.after_stop() 35 | redirect_app.is_stopping = False 36 | 37 | @redirect_app.before_server_start 38 | async def before_server_start(*_: Any) -> None: 39 | print("before_server_start") 40 | 41 | @redirect_app.after_server_stop 42 | async def after_server_stop(*_: Any) -> None: 43 | print("after_server_stop") 44 | 45 | return redirect_app 46 | 47 | 48 | async def runner(app: Sanic, app_server: AsyncioServer) -> None: 49 | app.is_running = True 50 | try: 51 | app.signalize() 52 | app.finalize() 53 | ErrorHandler.finalize(app.error_handler) 54 | app_server.init = True 55 | 56 | await app_server.before_start() 57 | await app_server.after_start() 58 | await app_server.serve_forever() 59 | finally: 60 | app.is_running = False 61 | app.is_stopping = True 62 | -------------------------------------------------------------------------------- /Chapter10/httpredirect/wadsworth/applications/server.py: -------------------------------------------------------------------------------- 1 | from sanic import Sanic 2 | 3 | from wadsworth.applications.redirect import attach_redirect_app 4 | from wadsworth.blueprints.info.view import bp as info_view 5 | from wadsworth.blueprints.view import bp 6 | 7 | 8 | def create_app(): 9 | app = Sanic("MainApp") 10 | app.config.SERVER_NAME = "localhost:8443" 11 | app.blueprint(bp) 12 | app.blueprint(info_view) 13 | 14 | attach_redirect_app(app) 15 | 16 | return app 17 | -------------------------------------------------------------------------------- /Chapter10/httpredirect/wadsworth/blueprints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter10/httpredirect/wadsworth/blueprints/__init__.py -------------------------------------------------------------------------------- /Chapter10/httpredirect/wadsworth/blueprints/hello/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter10/httpredirect/wadsworth/blueprints/hello/__init__.py -------------------------------------------------------------------------------- /Chapter10/httpredirect/wadsworth/blueprints/hello/view.py: -------------------------------------------------------------------------------- 1 | from sanic import Blueprint, HTTPResponse, Request, json 2 | from sanic.views import HTTPMethodView 3 | 4 | bp = Blueprint("Hello", url_prefix="/hello") 5 | 6 | 7 | class HelloView(HTTPMethodView, attach=bp, uri="/"): 8 | async def get(self, request: Request, name: str) -> HTTPResponse: 9 | return json({"hello": name}) 10 | -------------------------------------------------------------------------------- /Chapter10/httpredirect/wadsworth/blueprints/info/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter10/httpredirect/wadsworth/blueprints/info/__init__.py -------------------------------------------------------------------------------- /Chapter10/httpredirect/wadsworth/blueprints/info/view.py: -------------------------------------------------------------------------------- 1 | from sanic import Blueprint, HTTPResponse, Request, json 2 | from sanic.views import HTTPMethodView 3 | 4 | bp = Blueprint("Info", url_prefix="/info") 5 | 6 | 7 | class InfoView(HTTPMethodView, attach=bp): 8 | async def get(self, request: Request) -> HTTPResponse: 9 | return json({"server": request.app.name}) 10 | -------------------------------------------------------------------------------- /Chapter10/httpredirect/wadsworth/blueprints/redirect/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter10/httpredirect/wadsworth/blueprints/redirect/__init__.py -------------------------------------------------------------------------------- /Chapter10/httpredirect/wadsworth/blueprints/redirect/view.py: -------------------------------------------------------------------------------- 1 | from sanic import Blueprint, HTTPResponse, Request, response 2 | from sanic.constants import HTTP_METHODS 3 | 4 | bp = Blueprint("Redirect") 5 | 6 | 7 | @bp.route("/", methods=HTTP_METHODS, name="redirection_proxy") 8 | async def proxy(request: Request, path: str) -> HTTPResponse: 9 | return response.redirect( 10 | request.app.url_for( 11 | "Redirect.redirection_proxy", 12 | path=path, 13 | _server=request.app.ctx.main_app.config.SERVER_NAME, 14 | _external=True, 15 | _scheme="https", 16 | ), 17 | status=301, 18 | ) 19 | -------------------------------------------------------------------------------- /Chapter10/httpredirect/wadsworth/blueprints/view.py: -------------------------------------------------------------------------------- 1 | from sanic import Blueprint 2 | 3 | from .hello.view import bp as hello_bp 4 | 5 | bp = Blueprint.group(hello_bp, version=1) 6 | -------------------------------------------------------------------------------- /Chapter10/httpredirect/wadsworth/certs/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | SOMECERTHERE 3 | -----END CERTIFICATE----- 4 | -------------------------------------------------------------------------------- /Chapter10/httpredirect/wadsworth/certs/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN ENCRYPTED PRIVATE KEY----- 2 | SOMEKEYHERE 3 | -----END ENCRYPTED PRIVATE KEY----- 4 | -------------------------------------------------------------------------------- /Chapter10/pwa/my-svelte-project/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /public/build/ 3 | 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /Chapter10/pwa/my-svelte-project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-app", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "rollup -c", 7 | "dev": "rollup -c -w", 8 | "start": "sirv public --no-clear" 9 | }, 10 | "devDependencies": { 11 | "@rollup/plugin-commonjs": "^17.0.0", 12 | "@rollup/plugin-node-resolve": "^11.0.0", 13 | "rollup": "^2.3.4", 14 | "rollup-plugin-css-only": "^3.1.0", 15 | "rollup-plugin-livereload": "^2.0.0", 16 | "rollup-plugin-svelte": "^7.0.0", 17 | "rollup-plugin-terser": "^7.0.0", 18 | "svelte": "^3.0.0" 19 | }, 20 | "dependencies": { 21 | "sirv-cli": "^1.0.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Chapter10/pwa/my-svelte-project/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter10/pwa/my-svelte-project/public/favicon.png -------------------------------------------------------------------------------- /Chapter10/pwa/my-svelte-project/public/global.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | position: relative; 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | body { 8 | color: #333; 9 | margin: 0; 10 | padding: 8px; 11 | box-sizing: border-box; 12 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; 13 | } 14 | 15 | a { 16 | color: rgb(0,100,200); 17 | text-decoration: none; 18 | } 19 | 20 | a:hover { 21 | text-decoration: underline; 22 | } 23 | 24 | a:visited { 25 | color: rgb(0,80,160); 26 | } 27 | 28 | label { 29 | display: block; 30 | } 31 | 32 | input, button, select, textarea { 33 | font-family: inherit; 34 | font-size: inherit; 35 | -webkit-padding: 0.4em 0; 36 | padding: 0.4em; 37 | margin: 0 0 0.5em 0; 38 | box-sizing: border-box; 39 | border: 1px solid #ccc; 40 | border-radius: 2px; 41 | } 42 | 43 | input:disabled { 44 | color: #ccc; 45 | } 46 | 47 | button { 48 | color: #333; 49 | background-color: #f4f4f4; 50 | outline: none; 51 | } 52 | 53 | button:disabled { 54 | color: #999; 55 | } 56 | 57 | button:not(:disabled):active { 58 | background-color: #ddd; 59 | } 60 | 61 | button:focus { 62 | border-color: #666; 63 | } 64 | -------------------------------------------------------------------------------- /Chapter10/pwa/my-svelte-project/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Svelte app 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Chapter10/pwa/my-svelte-project/rollup.config.js: -------------------------------------------------------------------------------- 1 | import svelte from 'rollup-plugin-svelte'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import resolve from '@rollup/plugin-node-resolve'; 4 | import livereload from 'rollup-plugin-livereload'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | import css from 'rollup-plugin-css-only'; 7 | 8 | const production = !process.env.ROLLUP_WATCH; 9 | 10 | function serve() { 11 | let server; 12 | 13 | function toExit() { 14 | if (server) server.kill(0); 15 | } 16 | 17 | return { 18 | writeBundle() { 19 | if (server) return; 20 | server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], { 21 | stdio: ['ignore', 'inherit', 'inherit'], 22 | shell: true 23 | }); 24 | 25 | process.on('SIGTERM', toExit); 26 | process.on('exit', toExit); 27 | } 28 | }; 29 | } 30 | 31 | export default { 32 | input: 'src/main.js', 33 | output: { 34 | sourcemap: true, 35 | format: 'iife', 36 | name: 'app', 37 | file: 'public/build/bundle.js' 38 | }, 39 | plugins: [ 40 | svelte({ 41 | compilerOptions: { 42 | // enable run-time checks when not in production 43 | dev: !production 44 | } 45 | }), 46 | // we'll extract any component CSS out into 47 | // a separate file - better for performance 48 | css({ output: 'bundle.css' }), 49 | 50 | // If you have external dependencies installed from 51 | // npm, you'll most likely need these plugins. In 52 | // some cases you'll need additional configuration - 53 | // consult the documentation for details: 54 | // https://github.com/rollup/plugins/tree/master/packages/commonjs 55 | resolve({ 56 | browser: true, 57 | dedupe: ['svelte'] 58 | }), 59 | commonjs(), 60 | 61 | // In dev mode, call `npm run start` once 62 | // the bundle has been generated 63 | !production && serve(), 64 | 65 | // Watch the `public` directory and refresh the 66 | // browser on changes when not in production 67 | !production && livereload('public'), 68 | 69 | // If we're building for production (npm run build 70 | // instead of npm run dev), minify 71 | production && terser() 72 | ], 73 | watch: { 74 | clearScreen: false 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /Chapter10/pwa/my-svelte-project/src/App.svelte: -------------------------------------------------------------------------------- 1 | 18 | 19 |
20 |

Hello {name}!

21 |

22 | Visit the Svelte tutorial to learn 23 | how to build Svelte apps. 24 |

25 |
26 | 27 |
28 | {#await loadTime} 29 | loading ... 30 | {:then time} 31 | Server time was:
32 | {time} 33 | {/await} 34 |
35 |
36 |
37 | 38 | 59 | -------------------------------------------------------------------------------- /Chapter10/pwa/my-svelte-project/src/dummy.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter10/pwa/my-svelte-project/src/dummy.txt -------------------------------------------------------------------------------- /Chapter10/pwa/my-svelte-project/src/foo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter10/pwa/my-svelte-project/src/foo -------------------------------------------------------------------------------- /Chapter10/pwa/my-svelte-project/src/main.js: -------------------------------------------------------------------------------- 1 | import App from './App.svelte'; 2 | 3 | const app = new App({ 4 | target: document.body, 5 | props: { 6 | name: 'world' 7 | } 8 | }); 9 | 10 | export default app; 11 | -------------------------------------------------------------------------------- /Chapter10/wdsbot/from_bot/bot.py: -------------------------------------------------------------------------------- 1 | from typing import cast 2 | 3 | from nextcord.client import Client 4 | from nextcord.message import Message 5 | from sanic.server.async_server import AsyncioServer 6 | 7 | from server import app 8 | 9 | client = Client() 10 | 11 | 12 | async def runner(app_server: AsyncioServer) -> None: 13 | app.is_running = True 14 | try: 15 | await app_server.startup() 16 | await app_server.before_start() 17 | await app_server.after_start() 18 | await app_server.serve_forever() 19 | finally: 20 | app.is_running = False 21 | app.is_stopping = True 22 | await app_server.before_stop() 23 | await app_server.close() 24 | for connection in app_server.connections: 25 | connection.close_if_idle() 26 | await app_server.after_stop() 27 | app.is_stopping = False 28 | 29 | 30 | @client.event 31 | async def on_ready() -> None: 32 | app.config.GENERAL_CHANNEL_ID = 9999999999 33 | app.ctx.wadsworth = client 34 | app.ctx.general = client.get_channel(app.config.GENERAL_CHANNEL_ID) 35 | 36 | if not app.is_running: 37 | app_server = await app.create_server( 38 | port=9999, return_asyncio_server=True 39 | ) 40 | app.ctx.app_server = app_server 41 | client.loop.create_task(runner(app_server)) 42 | 43 | 44 | @client.event 45 | async def on_message(message: Message) -> None: 46 | if message.author == client.user: 47 | return 48 | 49 | if message.content.startswith("$hello"): 50 | await message.channel.send("Hello!") 51 | 52 | 53 | client.run("ABCDEFG") 54 | -------------------------------------------------------------------------------- /Chapter10/wdsbot/from_bot/server.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from sanic import HTTPResponse, Request, Sanic, json 4 | 5 | app = Sanic("WDSApp") 6 | 7 | 8 | @app.get("/") 9 | async def handler(request: Request) -> HTTPResponse: 10 | await request.app.ctx.general.send("Someone sent a message") 11 | return json({"foo": "bar"}) 12 | 13 | 14 | @app.before_server_start 15 | async def before_server_start(app: Sanic, _: Any) -> None: 16 | await app.ctx.general.send("Wadsworth, reporting for duty") 17 | -------------------------------------------------------------------------------- /Chapter10/wdsbot/from_sanic/bot.py: -------------------------------------------------------------------------------- 1 | from nextcord.client import Client 2 | from nextcord.message import Message 3 | 4 | client = Client() 5 | 6 | 7 | @client.event 8 | async def on_message(message: Message) -> None: 9 | if message.author == client.user: 10 | return 11 | 12 | if message.content.startswith("$hello"): 13 | await message.channel.send("Hello!") 14 | -------------------------------------------------------------------------------- /Chapter10/wdsbot/from_sanic/server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Any 3 | 4 | from nextcord.threads import Thread 5 | from sanic import HTTPResponse, Request, Sanic, json 6 | 7 | from bot import client 8 | 9 | app = Sanic("WDSApp") 10 | app.config.GENERAL_CHANNEL_ID = 9999999999 11 | app.config.DISCORD_TOKEN = "ABCDEFG" 12 | 13 | 14 | @app.get("/") 15 | async def handler(request: Request) -> HTTPResponse: 16 | 17 | await request.app.ctx.general.send("Someone sent a message") 18 | return json({"foo": "bar"}) 19 | 20 | 21 | @app.before_server_start 22 | async def startup_wadsworth(app: Sanic, _: Any) -> None: 23 | app.ctx.wadsworth = client 24 | app.add_task(client.start(app.config.DISCORD_TOKEN)) 25 | 26 | while True: 27 | if client.is_ready(): 28 | app.ctx.general = client.get_channel(app.config.GENERAL_CHANNEL_ID) 29 | if isinstance(app.ctx.general, Thread): 30 | await app.ctx.general.send("Wadsworth, reporting for duty") 31 | break 32 | await asyncio.sleep(0.1) 33 | 34 | 35 | @app.before_server_stop 36 | async def shutdown(app: Sanic, _: Any) -> None: 37 | await client.close() 38 | -------------------------------------------------------------------------------- /Chapter10/wsfeed/README.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | ``` 4 | docker-compose build 5 | ``` 6 | 7 | ``` 8 | docker-compose up 9 | ``` 10 | -------------------------------------------------------------------------------- /Chapter10/wsfeed/application/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9 2 | 3 | WORKDIR /app 4 | 5 | COPY ./requirements.txt /app/requirements.txt 6 | RUN pip install -r requirements.txt 7 | 8 | COPY ./feeder /app/feeder 9 | -------------------------------------------------------------------------------- /Chapter10/wsfeed/application/feeder/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter10/wsfeed/application/feeder/__init__.py -------------------------------------------------------------------------------- /Chapter10/wsfeed/application/feeder/blueprints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter10/wsfeed/application/feeder/blueprints/__init__.py -------------------------------------------------------------------------------- /Chapter10/wsfeed/application/feeder/blueprints/feed/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter10/wsfeed/application/feeder/blueprints/feed/__init__.py -------------------------------------------------------------------------------- /Chapter10/wsfeed/application/feeder/blueprints/feed/client.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from uuid import UUID, uuid4 3 | 4 | from aioredis import Redis 5 | from sanic.server.websockets.impl import WebsocketImplProtocol 6 | 7 | 8 | @dataclass 9 | class Client: 10 | protocol: WebsocketImplProtocol 11 | redis: Redis 12 | channel_name: str 13 | uid: UUID = field(default_factory=uuid4) 14 | 15 | def __hash__(self) -> int: 16 | return self.uid.int 17 | 18 | async def receiver(self) -> None: 19 | while True: 20 | message = await self.protocol.recv() 21 | if not message: 22 | break 23 | await self.redis.publish(self.channel_name, message) 24 | 25 | async def shutdown(self) -> None: 26 | await self.protocol.close() 27 | -------------------------------------------------------------------------------- /Chapter10/wsfeed/application/feeder/blueprints/feed/view.py: -------------------------------------------------------------------------------- 1 | from sanic import Blueprint, Request 2 | from sanic.log import logger 3 | from sanic.server.websockets.impl import WebsocketImplProtocol 4 | 5 | from .channel import Channel 6 | 7 | bp = Blueprint("Feed", url_prefix="/feed") 8 | 9 | 10 | @bp.websocket("/") 11 | async def feed(request: Request, ws: WebsocketImplProtocol, channel_name: str) -> None: 12 | logger.info("Incoming WS request") 13 | channel, is_existing = await Channel.get( 14 | request.app.ctx.pubsub, request.app.ctx.redis, channel_name 15 | ) 16 | 17 | if not is_existing: 18 | request.app.add_task(channel.receiver()) 19 | client = await channel.register(ws) 20 | 21 | try: 22 | await client.receiver() 23 | finally: 24 | await channel.unregister(client) 25 | -------------------------------------------------------------------------------- /Chapter10/wsfeed/application/feeder/blueprints/view.py: -------------------------------------------------------------------------------- 1 | from sanic import Blueprint 2 | 3 | from .feed.view import bp as feed_bp 4 | 5 | bp = Blueprint.group(feed_bp, version=1) 6 | -------------------------------------------------------------------------------- /Chapter10/wsfeed/application/feeder/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter10/wsfeed/application/feeder/common/__init__.py -------------------------------------------------------------------------------- /Chapter10/wsfeed/application/feeder/middleware/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter10/wsfeed/application/feeder/middleware/__init__.py -------------------------------------------------------------------------------- /Chapter10/wsfeed/application/feeder/middleware/request_context.py: -------------------------------------------------------------------------------- 1 | from contextvars import ContextVar 2 | from typing import Any 3 | 4 | from sanic import Request, Sanic 5 | 6 | app = Sanic.get_app() 7 | 8 | 9 | @app.after_server_start 10 | async def setup_request_context(app: Sanic, _: Any) -> None: 11 | app.ctx.request = ContextVar("request") 12 | 13 | 14 | @app.on_request 15 | async def attach_request(request: Request) -> None: 16 | request.app.ctx.request.set(request) 17 | -------------------------------------------------------------------------------- /Chapter10/wsfeed/application/feeder/server.py: -------------------------------------------------------------------------------- 1 | from sanic import Sanic 2 | 3 | from feeder.common.log import setup_logging 4 | 5 | 6 | def create_app() -> Sanic: 7 | app = Sanic("feeder") 8 | setup_logging(app) 9 | 10 | from feeder.middleware import request_context # noqa 11 | from feeder.blueprints.view import bp # noqa 12 | from feeder.worker import redis # noqa 13 | 14 | app.blueprint(bp) 15 | 16 | return app 17 | -------------------------------------------------------------------------------- /Chapter10/wsfeed/application/feeder/worker/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter10/wsfeed/application/feeder/worker/__init__.py -------------------------------------------------------------------------------- /Chapter10/wsfeed/application/feeder/worker/redis.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | import aioredis 4 | from sanic import Sanic 5 | 6 | app = Sanic.get_app() 7 | 8 | 9 | @app.before_server_start 10 | async def setup_redis(app: Sanic, _: Any) -> None: 11 | app.ctx.redis_pool = aioredis.BlockingConnectionPool.from_url( 12 | app.config.REDIS_DSN, max_connections=app.config.REDIS_MAX 13 | ) 14 | app.ctx.redis = aioredis.Redis(connection_pool=app.ctx.redis_pool) 15 | app.ctx.pubsub = app.ctx.redis.pubsub() 16 | 17 | 18 | @app.after_server_stop 19 | async def shutdown_redis(app: Sanic, _: Any) -> None: 20 | await app.ctx.redis_pool.disconnect() 21 | -------------------------------------------------------------------------------- /Chapter10/wsfeed/application/requirements.txt: -------------------------------------------------------------------------------- 1 | aioredis 2 | sanic 3 | -------------------------------------------------------------------------------- /Chapter10/wsfeed/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | build: ./application 4 | ports: 5 | - 7777:7777 6 | depends_on: 7 | - db-redis 8 | command: 9 | sanic 10 | --port=7777 11 | --host=0.0.0.0 12 | --workers=2 13 | --factory 14 | --debug 15 | feeder.server:create_app 16 | volumes: 17 | - ./application/feeder:/app/feeder 18 | environment: 19 | SANIC_REDIS_DSN: "redis://db-redis:6379" 20 | SANIC_REDIS_MAX: 12 21 | db-redis: 22 | image: redis:alpine 23 | ports: 24 | - 6379:6379 25 | -------------------------------------------------------------------------------- /Chapter11/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter11/.gitkeep -------------------------------------------------------------------------------- /Chapter11/booktracker/README.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | ``` 4 | $ docker-compose build 5 | ``` 6 | 7 | ``` 8 | $ docker-compose up 9 | ``` 10 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10 2 | 3 | RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - 4 | RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list 5 | RUN apt-get update && apt-get install -y yarn wait-for-it 6 | 7 | WORKDIR /app 8 | 9 | COPY ./package.json /app/package.json 10 | COPY ./rollup.config.js /app/rollup.config.js 11 | RUN yarn install 12 | 13 | COPY ./requirements.txt /app/requirements.txt 14 | RUN pip install -r requirements.txt 15 | 16 | COPY ./booktracker /app/booktracker 17 | COPY ./ui /app/ui 18 | 19 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/Makefile: -------------------------------------------------------------------------------- 1 | black: 2 | black booktracker -l 79 3 | 4 | isort: 5 | isort booktracker -l 79 --profile=black 6 | 7 | pretty: black isort 8 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter11/booktracker/application/booktracker/__init__.py -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter11/booktracker/application/booktracker/blueprints/__init__.py -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/author/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter11/booktracker/application/booktracker/blueprints/author/__init__.py -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/author/executor.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from booktracker.blueprints.author.model import Author 4 | from booktracker.common.dao.executor import BaseExecutor 5 | 6 | 7 | class AuthorExecutor(BaseExecutor): 8 | async def get_all_authors( 9 | self, 10 | *, 11 | exclude: Optional[List[str]] = None, 12 | limit: int = 15, 13 | offset: int = 0, 14 | ) -> List[Author]: 15 | ... 16 | 17 | async def get_authors_by_name( 18 | self, 19 | *, 20 | name: str, 21 | exclude: Optional[List[str]] = None, 22 | limit: int = 15, 23 | offset: int = 0, 24 | ) -> List[Author]: 25 | ... 26 | 27 | async def create_author( 28 | self, 29 | *, 30 | name: Optional[str] = None, 31 | eid: str = "", 32 | ) -> Author: 33 | ... 34 | 35 | async def get_author_by_eid( 36 | self, 37 | *, 38 | eid: str, 39 | ) -> Author: 40 | ... 41 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/author/model.py: -------------------------------------------------------------------------------- 1 | from dataclasses import field 2 | from typing import Optional 3 | 4 | from booktracker.common.base_model import BaseModel 5 | 6 | 7 | class Author(BaseModel): 8 | author_id: int 9 | eid: str 10 | name: str 11 | num_books: Optional[int] = field(default=None) 12 | 13 | class Meta: 14 | pk_field = "author_id" 15 | 16 | 17 | class CreateAuthorBody(BaseModel): 18 | name: str 19 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/author/queries/create_author.sql: -------------------------------------------------------------------------------- 1 | WITH ref as ( 2 | INSERT INTO eids (eid, type) 3 | VALUES (:eid, 'authors') 4 | RETURNING ref_id 5 | ) 6 | INSERT INTO authors (ref_id, name) 7 | SELECT ref_id, 8 | :name 9 | FROM ref 10 | RETURNING author_id, 11 | name, 12 | :eid as eid; 13 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/author/queries/get_all_authors.sql: -------------------------------------------------------------------------------- 1 | SELECT e.eid, 2 | a.author_id, 3 | a.name, 4 | ( 5 | SELECT count(1) 6 | FROM books b 7 | WHERE b.author_id = a.author_id 8 | ) as num_books 9 | FROM authors a 10 | JOIN eids e ON a.ref_id = e.ref_id 11 | LIMIT :limit OFFSET :offset; 12 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/author/queries/get_author_by_eid.sql: -------------------------------------------------------------------------------- 1 | SELECT e.eid, 2 | a.author_id, 3 | a.name, 4 | ( 5 | SELECT count(1) 6 | FROM books b 7 | WHERE b.author_id = a.author_id 8 | ) as num_books 9 | FROM authors a 10 | JOIN eids e ON a.ref_id = e.ref_id 11 | WHERE e.eid = :eid; 12 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/author/queries/get_authors_by_name.sql: -------------------------------------------------------------------------------- 1 | SELECT e.eid, 2 | a.author_id, 3 | a.name, 4 | ( 5 | SELECT count(1) 6 | FROM books b 7 | WHERE b.author_id = a.author_id 8 | ) as num_books 9 | FROM authors a 10 | JOIN eids e ON a.ref_id = e.ref_id 11 | WHERE a.name % :name 12 | LIMIT :limit OFFSET :offset; 13 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/author/view.py: -------------------------------------------------------------------------------- 1 | from logging import getLogger 2 | from typing import Awaitable, Callable, List 3 | 4 | from sanic import Blueprint, HTTPResponse, Request, json 5 | from sanic.exceptions import NotFound 6 | from sanic.views import HTTPMethodView 7 | from sanic_ext import validate 8 | 9 | from booktracker.common.csrf import csrf_protected 10 | from booktracker.common.pagination import Pagination 11 | 12 | from .executor import AuthorExecutor 13 | from .model import Author, CreateAuthorBody 14 | 15 | bp = Blueprint("Authors", url_prefix="/authors") 16 | logger = getLogger("booktracker") 17 | 18 | 19 | class AuthorListView(HTTPMethodView, attach=bp): 20 | @staticmethod 21 | async def get(request: Request, pagination: Pagination) -> HTTPResponse: 22 | executor = AuthorExecutor(request.app.ctx.postgres) 23 | kwargs = {**pagination.to_dict()} 24 | getter: Callable[ 25 | ..., Awaitable[List[Author]] 26 | ] = executor.get_all_authors 27 | 28 | if name := request.args.get("name"): 29 | kwargs["name"] = name 30 | getter = executor.get_authors_by_name 31 | 32 | try: 33 | authors = await getter(**kwargs) 34 | except NotFound: 35 | authors = [] 36 | 37 | return json({"meta": pagination, "authors": authors}) 38 | 39 | @staticmethod 40 | @validate(json=CreateAuthorBody) 41 | @csrf_protected 42 | async def post(request: Request, body: CreateAuthorBody) -> HTTPResponse: 43 | executor = AuthorExecutor(request.app.ctx.postgres) 44 | author = await executor.create_author(**body.to_dict()) 45 | return json({"author": author}, status=201) 46 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/book/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter11/booktracker/application/booktracker/blueprints/book/__init__.py -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/book/hydrator.py: -------------------------------------------------------------------------------- 1 | from json import loads 2 | from typing import Any, List, Mapping, Optional, Type, TypeVar 3 | 4 | from booktracker.blueprints.author.model import Author 5 | from booktracker.blueprints.book.model import Series 6 | from booktracker.common.base_model import BaseModel 7 | from booktracker.common.dao.hydrator import Hydrator 8 | 9 | T = TypeVar("T", bound=BaseModel) 10 | 11 | 12 | class BookHydrator(Hydrator): 13 | def do_hydration( 14 | self, 15 | record: Mapping[str, Any], 16 | model: Type[T], 17 | exclude: Optional[List[str]] = None, 18 | ) -> T: 19 | series = ( 20 | Series(**loads(record["series"])) if record["series"] else None 21 | ) 22 | kwargs = { 23 | **record, 24 | "author": Author(**loads(record["author"])), 25 | "series": series, 26 | } 27 | obj = model(**kwargs) 28 | if exclude: 29 | obj.set_state("exclude", exclude) 30 | 31 | return obj 32 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/book/model.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import field 4 | from enum import auto 5 | from typing import Optional 6 | 7 | from booktracker.blueprints.author.model import Author 8 | from booktracker.blueprints.user.model import User 9 | from booktracker.common.base_model import BaseEnum, BaseModel 10 | 11 | 12 | class BookState(BaseEnum): 13 | READ = auto() 14 | READING = auto() 15 | UNREAD = auto() 16 | 17 | 18 | class Book(BaseModel): 19 | book_id: int 20 | eid: str 21 | title: str 22 | author: Optional[Author] = field(default=None) 23 | series: Optional[Series] = field(default=None) 24 | user: Optional[User] = field(default=None) 25 | is_loved: Optional[bool] = field(default=None) 26 | state: Optional[BookState] = field(default=None) 27 | 28 | class Meta: 29 | pk_field = "book_id" 30 | 31 | 32 | class Series(BaseModel): 33 | series_id: int 34 | eid: str 35 | name: str 36 | 37 | class Meta: 38 | pk_field = "series_id" 39 | 40 | 41 | class CreateBookBody(BaseModel): 42 | title: str 43 | author: str 44 | series: Optional[str] = field(default=None) 45 | title_is_eid: bool = field(default=False) 46 | author_is_eid: bool = field(default=False) 47 | series_is_eid: bool = field(default=False) 48 | 49 | 50 | class CreateSeriesBody(BaseModel): 51 | name: str 52 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/book/queries/create_book.sql: -------------------------------------------------------------------------------- 1 | WITH ref as ( 2 | INSERT INTO eids (eid, type) 3 | VALUES (:eid, 'books') 4 | RETURNING ref_id 5 | ) 6 | INSERT INTO books (ref_id, title, author_id, series_id) 7 | SELECT ref_id, 8 | :title, 9 | :author_id, 10 | :series_id 11 | FROM ref 12 | RETURNING book_id; 13 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/book/queries/create_book_series.sql: -------------------------------------------------------------------------------- 1 | WITH ref as ( 2 | INSERT INTO eids (eid, type) 3 | VALUES (:eid, 'series') 4 | RETURNING ref_id 5 | ) 6 | INSERT INTO series (ref_id, name) 7 | SELECT ref_id, 8 | :name 9 | FROM ref 10 | RETURNING series_id, 11 | name, 12 | :eid as eid; 13 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/book/queries/create_book_to_user.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO book_to_user (book_id, user_id) 2 | VALUES (:book_id, :user_id); 3 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/book/queries/get_all_books.sql: -------------------------------------------------------------------------------- 1 | SELECT e.eid, 2 | b.book_id, 3 | b.title, 4 | COALESCE( 5 | ( 6 | SELECT json_build_object('name', a.name, 'eid', ae.eid) 7 | FROM authors a 8 | JOIN eids ae ON a.ref_id = ae.ref_id 9 | WHERE a.author_id = b.author_id 10 | ), 11 | '{}'::json 12 | ) author, 13 | COALESCE( 14 | ( 15 | SELECT json_build_object('name', s.name, 'eid', se.eid) 16 | FROM series s 17 | JOIN eids se ON s.ref_id = se.ref_id 18 | WHERE s.series_id = b.series_id 19 | ), 20 | null 21 | ) series 22 | FROM books b 23 | JOIN eids e ON b.ref_id = e.ref_id 24 | LIMIT :limit OFFSET :offset; 25 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/book/queries/get_all_books_for_user.sql: -------------------------------------------------------------------------------- 1 | SELECT e.eid, 2 | b.book_id, 3 | b.title, 4 | row_to_json( 5 | ( 6 | SELECT q 7 | FROM ( 8 | SELECT a.name, 9 | ae.eid 10 | FROM authors a 11 | JOIN eids ae ON a.ref_id = ae.ref_id 12 | WHERE a.author_id = b.author_id 13 | ) q 14 | ) 15 | ) author, 16 | row_to_json( 17 | ( 18 | SELECT q 19 | FROM ( 20 | SELECT s.name, 21 | se.eid 22 | FROM series s 23 | JOIN eids se ON s.ref_id = se.ref_id 24 | WHERE s.series_id = b.series_id 25 | ) q 26 | ) 27 | ) series, 28 | bu.is_loved, 29 | bu.state 30 | FROM books b 31 | JOIN eids e ON b.ref_id = e.ref_id 32 | JOIN book_to_user bu ON b.book_id = bu.book_id 33 | WHERE bu.user_id = :user_id 34 | LIMIT :limit OFFSET :offset; 35 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/book/queries/get_all_series.sql: -------------------------------------------------------------------------------- 1 | SELECT e.eid, 2 | s.series_id, 3 | s.name 4 | FROM series s 5 | JOIN eids e ON s.ref_id = e.ref_id 6 | LIMIT :limit OFFSET :offset; 7 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/book/queries/get_book_by_eid.sql: -------------------------------------------------------------------------------- 1 | SELECT e.eid, 2 | b.book_id, 3 | b.title, 4 | COALESCE( 5 | ( 6 | SELECT json_build_object('name', a.name, 'eid', ae.eid) 7 | FROM authors a 8 | JOIN eids ae ON a.ref_id = ae.ref_id 9 | WHERE a.author_id = b.author_id 10 | ), 11 | '{}'::json 12 | ) author, 13 | COALESCE( 14 | ( 15 | SELECT json_build_object('name', s.name, 'eid', se.eid) 16 | FROM series s 17 | JOIN eids se ON s.ref_id = se.ref_id 18 | WHERE s.series_id = b.series_id 19 | ), 20 | null 21 | ) series 22 | FROM books b 23 | JOIN eids e ON b.ref_id = e.ref_id 24 | WHERE e.eid = :eid; 25 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/book/queries/get_book_by_eid_for_user.sql: -------------------------------------------------------------------------------- 1 | SELECT e.eid, 2 | b.book_id, 3 | b.title, 4 | COALESCE( 5 | ( 6 | SELECT json_build_object('name', a.name, 'eid', ae.eid) 7 | FROM authors a 8 | JOIN eids ae ON a.ref_id = ae.ref_id 9 | WHERE a.author_id = b.author_id 10 | ), 11 | '{}'::json 12 | ) author, 13 | COALESCE( 14 | ( 15 | SELECT json_build_object('name', s.name, 'eid', se.eid) 16 | FROM series s 17 | JOIN eids se ON s.ref_id = se.ref_id 18 | WHERE s.series_id = b.series_id 19 | ), 20 | null 21 | ) series, 22 | bu.is_loved, 23 | bu.state 24 | FROM books b 25 | JOIN eids e ON b.ref_id = e.ref_id 26 | JOIN book_to_user bu ON b.book_id = bu.book_id 27 | WHERE e.eid = :eid 28 | AND bu.user_id = :user_id; 29 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/book/queries/get_book_series_by_eid.sql: -------------------------------------------------------------------------------- 1 | SELECT s.series_id, 2 | s.name, 3 | e.eid 4 | FROM series s 5 | JOIN eids e ON s.ref_id = e.ref_id 6 | WHERE e.eid = :eid; 7 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/book/queries/get_books_by_title.sql: -------------------------------------------------------------------------------- 1 | SELECT e.eid, 2 | b.book_id, 3 | b.title, 4 | COALESCE( 5 | ( 6 | SELECT json_build_object('name', a.name, 'eid', ae.eid) 7 | FROM authors a 8 | JOIN eids ae ON a.ref_id = ae.ref_id 9 | WHERE a.author_id = b.author_id 10 | ), 11 | '{}'::json 12 | ) author, 13 | COALESCE( 14 | ( 15 | SELECT json_build_object('name', s.name, 'eid', se.eid) 16 | FROM series s 17 | JOIN eids se ON s.ref_id = se.ref_id 18 | WHERE s.series_id = b.series_id 19 | ), 20 | null 21 | ) series 22 | FROM books b 23 | JOIN eids e ON b.ref_id = e.ref_id 24 | WHERE b.title % :title 25 | LIMIT :limit OFFSET :offset; 26 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/book/queries/get_series_by_name.sql: -------------------------------------------------------------------------------- 1 | SELECT e.eid, 2 | s.series_id, 3 | s.name 4 | FROM series s 5 | JOIN eids e ON s.ref_id = e.ref_id 6 | WHERE s.name % :name 7 | LIMIT :limit OFFSET :offset; 8 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/book/queries/update_book_state.sql: -------------------------------------------------------------------------------- 1 | UPDATE book_to_user bu 2 | SET state = :state 3 | FROM ( 4 | SELECT book_id 5 | FROM books b 6 | JOIN eids e ON b.ref_id = e.ref_id 7 | WHERE e.eid = :eid 8 | ) q 9 | WHERE bu.book_id = q.book_id 10 | AND bu.user_id = :user_id; 11 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/book/queries/update_toggle_book_is_loved.sql: -------------------------------------------------------------------------------- 1 | UPDATE book_to_user bu 2 | SET is_loved = NOT bu.is_loved 3 | FROM ( 4 | SELECT book_id 5 | FROM books b 6 | JOIN eids e ON b.ref_id = e.ref_id 7 | WHERE e.eid = :eid 8 | ) q 9 | WHERE bu.book_id = q.book_id 10 | AND bu.user_id = :user_id; 11 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/frontend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter11/booktracker/application/booktracker/blueprints/frontend/__init__.py -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/frontend/view.py: -------------------------------------------------------------------------------- 1 | from logging import getLogger 2 | from pathlib import Path 3 | 4 | from sanic import Blueprint, HTTPResponse, Request 5 | from sanic.response import file 6 | 7 | from .reload import setup_livereload 8 | 9 | logger = getLogger("booktracker") 10 | bp = Blueprint("Frontend") 11 | setup_livereload(bp) 12 | 13 | 14 | @bp.get("/") 15 | async def index(request: Request, path: str) -> HTTPResponse: 16 | base: Path = request.app.config.UI_DIR / "public" 17 | requested_path = base / path 18 | logger.debug(f"Checking for {requested_path}") 19 | html = ( 20 | requested_path 21 | if path and requested_path.exists() and not requested_path.is_dir() 22 | else base / "index.html" 23 | ) 24 | return await file(html) 25 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/user/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter11/booktracker/application/booktracker/blueprints/user/__init__.py -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/user/executor.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from booktracker.blueprints.user.model import User 4 | from booktracker.common.dao.executor import BaseExecutor 5 | 6 | 7 | class UserExecutor(BaseExecutor): 8 | async def get_by_eid( 9 | self, 10 | *, 11 | eid: str, 12 | exclude: Optional[List[str]] = None, 13 | ) -> User: 14 | ... 15 | 16 | async def get_by_login( 17 | self, 18 | *, 19 | login: str, 20 | exclude: Optional[List[str]] = None, 21 | ) -> User: 22 | ... 23 | 24 | async def create_user( 25 | self, 26 | *, 27 | login: str, 28 | name: Optional[str] = None, 29 | avatar: Optional[str] = None, 30 | profile: Optional[str] = None, 31 | eid: str = "", 32 | ) -> User: 33 | ... 34 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/user/model.py: -------------------------------------------------------------------------------- 1 | from booktracker.common.base_model import BaseModel 2 | 3 | 4 | class User(BaseModel): 5 | user_id: int 6 | eid: str 7 | login: str 8 | name: str 9 | avatar: str 10 | profile: str 11 | 12 | class Meta: 13 | pk_field = "user_id" 14 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/user/queries/create_user.sql: -------------------------------------------------------------------------------- 1 | WITH ref as ( 2 | INSERT INTO eids (eid, type) 3 | VALUES (:eid, 'users') 4 | RETURNING ref_id 5 | ) 6 | INSERT INTO users (login, ref_id, name, avatar, profile) 7 | SELECT :login, 8 | ref_id, 9 | :name, 10 | :avatar, 11 | :profile 12 | FROM ref 13 | RETURNING user_id; 14 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/user/queries/get_by_eid.sql: -------------------------------------------------------------------------------- 1 | SELECT e.eid, 2 | u.user_id, 3 | u.login, 4 | u.name, 5 | u.avatar, 6 | u.profile 7 | FROM users u 8 | JOIN eids e ON u.ref_id = e.ref_id 9 | WHERE e.eid = :eid; 10 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/user/queries/get_by_login.sql: -------------------------------------------------------------------------------- 1 | SELECT e.eid, 2 | u.user_id, 3 | u.login, 4 | u.name, 5 | u.avatar, 6 | u.profile 7 | FROM users u 8 | JOIN eids e ON u.ref_id = e.ref_id 9 | WHERE u.login = :login; 10 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/blueprints/view.py: -------------------------------------------------------------------------------- 1 | from sanic import Blueprint 2 | 3 | from .author.view import bp as author_bp 4 | from .book.view import bp as book_bp 5 | from .frontend.view import bp as frontend_bp 6 | 7 | api = Blueprint.group(author_bp, book_bp, version=1, version_prefix="/api/v") 8 | bp = Blueprint.group(frontend_bp, api) 9 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter11/booktracker/application/booktracker/common/__init__.py -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/common/auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter11/booktracker/application/booktracker/common/auth/__init__.py -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/common/auth/endpoint.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from sanic import HTTPResponse, Request 4 | from sanic.response import redirect 5 | from sanic_jwt import BaseEndpoint 6 | 7 | from booktracker.common.cookie import set_cookie 8 | from booktracker.common.csrf import generate_csrf 9 | 10 | 11 | class GitHubOAuthLogin(BaseEndpoint): 12 | @staticmethod 13 | async def get(request: Request) -> Optional[HTTPResponse]: 14 | url = ( 15 | "https://github.com/login/oauth/authorize?scope=read:user" 16 | f"&client_id={request.app.config.GITHUB_OAUTH_CLIENT_ID}" 17 | ) 18 | 19 | response = redirect(url) 20 | 21 | if ( 22 | "csrf_token" not in request.cookies 23 | or "ref_token" not in request.cookies 24 | ): 25 | ref, token = generate_csrf( 26 | request.app.config.CSRF_SECRET, 27 | request.app.config.CSRF_REF_LENGTH, 28 | request.app.config.CSRF_REF_PADDING, 29 | ) 30 | 31 | set_cookie( 32 | response=response, 33 | domain="localhost", 34 | key="ref_token", 35 | value=ref, 36 | httponly=True, 37 | samesite="strict", 38 | secure=(not request.app.config.LOCAL), 39 | ) 40 | set_cookie( 41 | response=response, 42 | domain="localhost", 43 | key="csrf_token", 44 | value=token, 45 | samesite="strict", 46 | secure=(not request.app.config.LOCAL), 47 | ) 48 | 49 | return response 50 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/common/auth/model.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass 5 | class GitHubAuthCode: 6 | code: str 7 | 8 | 9 | @dataclass 10 | class RefreshTokenKey: 11 | eid: str 12 | 13 | def __str__(self) -> str: 14 | return f"refresh_token:{self.eid}" 15 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/common/auth/startup.py: -------------------------------------------------------------------------------- 1 | from sanic import Sanic 2 | from sanic_jwt import Initialize 3 | 4 | from .endpoint import GitHubOAuthLogin 5 | from .handler import ( 6 | authenticate, 7 | payload_extender, 8 | retrieve_refresh_token, 9 | retrieve_user, 10 | store_refresh_token, 11 | ) 12 | 13 | 14 | def setup_auth(app: Sanic) -> None: 15 | Initialize( 16 | app, 17 | url_prefix="/api/v1/auth", 18 | authenticate=authenticate, 19 | retrieve_user=retrieve_user, 20 | extend_payload=payload_extender, 21 | store_refresh_token=store_refresh_token, 22 | retrieve_refresh_token=retrieve_refresh_token, 23 | class_views=[("/github", GitHubOAuthLogin)], 24 | user_id="eid", 25 | cookie_set=True, 26 | cookie_split=True, 27 | cookie_strict=False, 28 | refresh_token_enabled=True, 29 | secret="qqqq", 30 | cookie_secure=(not app.config.LOCAL), 31 | expiration_delta=60 * 15, 32 | leeway=0, 33 | ) 34 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/common/cookie.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Optional 3 | 4 | from sanic import HTTPResponse 5 | 6 | 7 | def set_cookie( 8 | response: HTTPResponse, 9 | key: str, 10 | value: str, 11 | httponly: bool = False, 12 | samesite: str = "lax", 13 | domain: Optional[str] = None, 14 | exp: Optional[datetime] = None, 15 | secure: bool = True, 16 | ) -> None: 17 | response.cookies[key] = value 18 | response.cookies[key]["httponly"] = httponly 19 | response.cookies[key]["path"] = "/" 20 | response.cookies[key]["secure"] = secure 21 | response.cookies[key]["samesite"] = samesite 22 | 23 | if domain: 24 | response.cookies[key]["domain"] = domain 25 | 26 | if exp: 27 | response.cookies[key]["expires"] = exp 28 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/common/dao/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter11/booktracker/application/booktracker/common/dao/__init__.py -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/common/dao/hydrator.py: -------------------------------------------------------------------------------- 1 | from typing import ( 2 | Any, 3 | List, 4 | Literal, 5 | Mapping, 6 | Optional, 7 | Type, 8 | TypeVar, 9 | Union, 10 | overload, 11 | ) 12 | 13 | from booktracker.common.base_model import BaseModel 14 | 15 | ModelT = TypeVar("ModelT", bound=BaseModel) 16 | RecordT = Mapping[str, Any] 17 | ExcludeT = Optional[List[str]] 18 | 19 | 20 | class Hydrator: 21 | """ 22 | Responsible from converting a dict-like object into a model. This will 23 | mainly be used for converting from DB results to Python objects. 24 | """ 25 | 26 | @overload 27 | def hydrate( 28 | self, 29 | record: RecordT, 30 | model: Type[ModelT], 31 | as_list: Literal[False], 32 | exclude: ExcludeT = None, 33 | ) -> ModelT: 34 | ... 35 | 36 | @overload 37 | def hydrate( 38 | self, 39 | record: List[RecordT], 40 | model: Type[ModelT], 41 | as_list: Literal[True], 42 | exclude: ExcludeT = None, 43 | ) -> List[ModelT]: 44 | ... 45 | 46 | def hydrate( 47 | self, 48 | record: Union[RecordT, List[RecordT]], 49 | model: Type[ModelT], 50 | as_list: bool, 51 | exclude: ExcludeT = None, 52 | ) -> Union[ModelT, List[ModelT]]: 53 | if as_list: 54 | record = [record] if not isinstance(record, list) else record 55 | return [self.do_hydration(r, model, exclude) for r in record] 56 | if isinstance(record, list): 57 | raise TypeError( 58 | "Unexpectedly found multiple records while hydrating" 59 | ) 60 | return self.do_hydration(record, model, exclude) 61 | 62 | def do_hydration( 63 | self, 64 | record: RecordT, 65 | model: Type[ModelT], 66 | exclude: ExcludeT = None, 67 | ) -> ModelT: 68 | obj = model(**record) 69 | if exclude: 70 | obj.__state__.exclude = exclude 71 | return obj 72 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/common/eid.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from string import ascii_letters, digits 3 | 4 | REQUEST_ID_ALPHABET = ascii_letters + digits 5 | REQUEST_ID_ALPHABET_LENGTH = len(REQUEST_ID_ALPHABET) 6 | 7 | 8 | def generate(width: int = 0, fillchar: str = "x") -> str: 9 | """ 10 | Generate a UUID and make is smaller 11 | """ 12 | output = "" 13 | uid = uuid.uuid4() 14 | num = uid.int 15 | while num: 16 | num, pos = divmod(num, REQUEST_ID_ALPHABET_LENGTH) 17 | output += REQUEST_ID_ALPHABET[pos] 18 | eid = output[::-1] 19 | if width: 20 | eid = eid.rjust(width, fillchar) 21 | return eid 22 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/common/pagination.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass, field 4 | 5 | from sanic import Request, Sanic 6 | 7 | from booktracker.common.base_model import BaseModel 8 | 9 | 10 | @dataclass 11 | class Pagination(BaseModel): 12 | limit: int = field(default=15) 13 | offset: int = field(default=0) 14 | 15 | @staticmethod 16 | async def from_request(request: Request) -> Pagination: 17 | args = { 18 | key: int(value) 19 | for key in ("limit", "offset") 20 | if (value := request.args.get(key)) 21 | } 22 | return Pagination(**args) 23 | 24 | 25 | def setup_pagination(app: Sanic) -> None: 26 | @app.before_server_start 27 | async def setup_pagination(app: Sanic, _): 28 | app.ext.injection(Pagination, Pagination.from_request) 29 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/middleware/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter11/booktracker/application/booktracker/middleware/__init__.py -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/middleware/request_context.py: -------------------------------------------------------------------------------- 1 | from contextvars import ContextVar 2 | from typing import Any 3 | 4 | from sanic import Request, Sanic 5 | 6 | app = Sanic.get_app("BooktrackerApp") 7 | 8 | 9 | @app.after_server_start 10 | async def setup_request_context(app: Sanic, _) -> None: 11 | app.ctx.request = ContextVar("request") 12 | 13 | 14 | @app.on_request 15 | async def attach_request(request: Request) -> None: 16 | request.app.ctx.request.set(request) 17 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/server.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Optional, Sequence, Tuple 3 | 4 | from sanic import Sanic 5 | 6 | # Modules imported here should NOT have a Sanic.get_app() call in the global 7 | # scope. Doing so will cause a circular import. Therefore, we programmatically 8 | # import those modules inside the create_app() factory. 9 | from booktracker.common.auth.startup import setup_auth 10 | from booktracker.common.csrf import setup_csrf 11 | from booktracker.common.log import setup_logging 12 | from booktracker.common.pagination import setup_pagination 13 | from booktracker.worker.module import setup_modules 14 | from booktracker.worker.request import BooktrackerRequest 15 | 16 | DEFAULT: Tuple[str, ...] = ( 17 | "booktracker.blueprints.view", 18 | "booktracker.middleware.request_context", 19 | "booktracker.worker.postgres", 20 | "booktracker.worker.redis", 21 | ) 22 | 23 | 24 | def create_app(module_names: Optional[Sequence[str]] = None) -> Sanic: 25 | """ 26 | Application factory: responsible for gluing all of the pieces of the 27 | application together. In most use cases, running the application will be 28 | done will a None value for module_names. Therefore, we provide a default 29 | list. This provides flexibility when unit testing the application. The main 30 | purpose for this pattern is to avoid import issues. This should be the 31 | first thing that is called. 32 | """ 33 | if module_names is None: 34 | module_names = DEFAULT 35 | 36 | app = Sanic("BooktrackerApp", request_class=BooktrackerRequest) 37 | app.config.UI_DIR = Path(__file__).parent.parent / "ui" 38 | app.config.CSRF_REF_PADDING = 12 39 | app.config.CSRF_REF_LENGTH = 18 40 | 41 | if not app.config.get("CORS_ORIGINS"): 42 | app.config.CORS_ORIGINS = "http://localhost:7777" 43 | 44 | setup_logging(app) 45 | setup_pagination(app) 46 | setup_auth(app) 47 | setup_modules(app, *module_names) 48 | setup_csrf(app) 49 | 50 | return app 51 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/worker/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter11/booktracker/application/booktracker/worker/__init__.py -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/worker/module.py: -------------------------------------------------------------------------------- 1 | from importlib import import_module 2 | 3 | from sanic import Sanic 4 | 5 | 6 | def setup_modules(app: Sanic, *module_names: str) -> None: 7 | """ 8 | Load some modules 9 | """ 10 | for module_name in module_names: 11 | module = import_module(module_name) 12 | if bp := getattr(module, "bp", None): 13 | app.blueprint(bp) 14 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/worker/postgres.py: -------------------------------------------------------------------------------- 1 | from databases import Database 2 | from sanic import Sanic 3 | 4 | from booktracker.common.dao.executor import BaseExecutor 5 | from booktracker.common.dao.hydrator import Hydrator 6 | 7 | app = Sanic.get_app("BooktrackerApp") 8 | 9 | 10 | @app.before_server_start 11 | async def setup_postgres(app: Sanic, _) -> None: 12 | app.ctx.postgres = Database( 13 | app.config.POSTGRES_DSN, 14 | min_size=app.config.POSTGRES_MIN, 15 | max_size=app.config.POSTGRES_MAX, 16 | ) 17 | 18 | 19 | @app.after_server_start 20 | async def connect_postgres(app: Sanic, _) -> None: 21 | await app.ctx.postgres.connect() 22 | 23 | 24 | @app.after_server_start 25 | async def load_sql(app: Sanic, _) -> None: 26 | BaseExecutor.load(Hydrator()) 27 | 28 | 29 | @app.after_server_stop 30 | async def shutdown_postgres(app: Sanic, _) -> None: 31 | await app.ctx.postgres.disconnect() 32 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/worker/redis.py: -------------------------------------------------------------------------------- 1 | import aioredis 2 | from sanic import Sanic 3 | 4 | app = Sanic.get_app("BooktrackerApp") 5 | 6 | 7 | @app.before_server_start 8 | async def setup_redis(app: Sanic, _) -> None: 9 | app.ctx.redis_pool = aioredis.BlockingConnectionPool.from_url( 10 | app.config.REDIS_DSN, max_connections=app.config.REDIS_MAX 11 | ) 12 | app.ctx.redis = aioredis.Redis(connection_pool=app.ctx.redis_pool) 13 | 14 | 15 | @app.after_server_stop 16 | async def shutdown_redis(app: Sanic, _) -> None: 17 | await app.ctx.redis_pool.disconnect() 18 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/booktracker/worker/request.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from sanic import Request 4 | 5 | from booktracker.common.eid import generate 6 | 7 | 8 | class BooktrackerRequest(Request): 9 | @classmethod 10 | def generate_id(*_: Any) -> str: 11 | return generate() 12 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "booktracker", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "rollup -c", 7 | "dev": "rollup -c -w", 8 | "start": "sirv public --no-clear" 9 | }, 10 | "devDependencies": { 11 | "@rollup/plugin-commonjs": "^17.0.0", 12 | "@rollup/plugin-node-resolve": "^11.0.0", 13 | "prettier": "2.5.1", 14 | "rollup": "^2.3.4", 15 | "rollup-plugin-css-only": "^3.1.0", 16 | "rollup-plugin-livereload": "^2.0.0", 17 | "rollup-plugin-svelte": "^7.0.0", 18 | "rollup-plugin-terser": "^7.0.0", 19 | "svelte": "^3.0.0" 20 | }, 21 | "dependencies": { 22 | "jwt-decode": "^3.1.2", 23 | "rollup-plugin-commonjs": "^10.1.0", 24 | "rollup-plugin-node-resolve": "^5.2.0", 25 | "simple-svelte-autocomplete": "^2.2.4", 26 | "sirv-cli": "^1.0.0", 27 | "svelte-keydown": "^0.4.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/requirements.txt: -------------------------------------------------------------------------------- 1 | aioredis 2 | cryptography 3 | databases[postgresql] 4 | httpx 5 | sanic 6 | sanic-ext 7 | sanic-jwt 8 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/ui/.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | coverage 4 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/ui/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4 3 | } 4 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/ui/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Web-Development-with-Sanic/9a71bb3f4f1b170d415b0a59d528c0234573c10e/Chapter11/booktracker/application/ui/public/favicon.png -------------------------------------------------------------------------------- /Chapter11/booktracker/application/ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Python Web Development with Sanic 8 | 9 | 10 | 11 | 12 | 16 | 17 | 21 | 22 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Chapter11/booktracker/application/ui/src/App.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
15 |