├── .flake8 ├── CodeSamples ├── Chapter10 │ ├── alpine-python-container │ │ └── Dockerfile │ ├── container_basic │ │ └── Dockerfile │ ├── container_with_cleanup │ │ └── Dockerfile │ ├── etcd-quart │ │ └── etcd_basic.py │ ├── kubernetes │ │ └── nginx.yml │ ├── quart_container_basic │ │ ├── Dockerfile │ │ ├── requirements.in │ │ ├── requirements.txt │ │ └── setup.py │ └── swarm-terraform │ │ ├── .terraform.lock.hcl │ │ ├── ec2.tf │ │ └── var.tf ├── Chapter2 │ ├── authorization.py │ ├── blueprints.py │ ├── catch_all_errors.py │ ├── converter_custom.py │ ├── email_render.py │ ├── email_template.j2 │ ├── error_handler.py │ ├── globals.py │ ├── load_settings.py │ ├── middleware.py │ ├── prod_settings.json │ ├── prod_settings.py │ ├── prod_settings.yml │ ├── quart_basic.py │ ├── quart_details.py │ ├── requirements.txt │ ├── signals.py │ ├── url_for.py │ ├── variables_and_converters.py │ └── yamlify.py ├── Chapter3 │ ├── molotov_example1.py │ ├── quart_basic.py │ ├── quart_error.py │ ├── quart_profiled.py │ ├── requests_example1.py │ ├── requests_example2.py │ ├── test_quart_basic.py │ ├── test_quart_error.py │ ├── test_requests_example2.py │ └── test_requests_example2_full.py ├── Chapter4 │ ├── forms_example │ │ ├── app.py │ │ ├── database.py │ │ ├── forms.py │ │ └── templates │ │ │ ├── create_user.html │ │ │ └── users.html │ ├── logging-in │ │ ├── app.py │ │ └── templates │ │ │ ├── login.html │ │ │ └── welcome.html │ ├── oauth2 │ │ ├── app.py │ │ └── templates │ │ │ ├── logged_in.html │ │ │ └── welcome.html │ ├── password_hash.py │ ├── requirements.txt │ ├── sqlalchemy │ │ ├── models.py │ │ └── sqlalchemy-async.py │ ├── weather-celery │ │ ├── database.py │ │ ├── example.py │ │ ├── scheduler.py │ │ └── weather_worker.py │ └── wtforms_example.py ├── Chapter5 │ ├── clean_interfaces.py │ ├── feature_flags.py │ ├── logging_example.py │ ├── prod_settings.json │ ├── prometheus.yml │ ├── quart_hosted.py │ ├── quart_logging.py │ ├── quart_metrics.py │ ├── quart_migration.py │ ├── quart_slow.py │ ├── quart_structlog.py │ ├── runthings.sh │ └── structlog1.py ├── Chapter6 │ ├── clientsession.py │ ├── clientsession_list.py │ ├── globalsession.py │ ├── gzip_example.py │ ├── gzip_example_post.py │ ├── gzip_server.py │ ├── openapi.yml │ ├── pika_sender.py │ ├── playstore_receiver.py │ ├── publish_receiver.py │ ├── quart_etag.py │ ├── requirements.txt │ ├── semaphores.py │ ├── test_aiohttp.py │ └── test_aiohttp_fixture.py ├── Chapter7 │ ├── auth_caller.py │ ├── fetch_token.py │ ├── flask_debug.py │ ├── jwt_decode.py │ ├── jwt_tokens.py │ ├── openresty │ │ ├── resty.conf │ │ └── resty_limiting.conf │ ├── quart_after_response.py │ ├── quart_cors_example.py │ ├── quart_debug.py │ ├── quart_uber.py │ ├── tokendealer.py │ └── vulnerable.py ├── Chapter8 │ ├── quart_cors_example.py │ ├── react_intro.html │ ├── static-example │ │ ├── person_example.html │ │ ├── quart_serve_data.py │ │ └── static │ │ │ └── people.jsx │ ├── static │ │ └── people.jsx │ ├── templates │ │ └── person_example.html │ └── transpiled-example │ │ ├── babel.config.json │ │ ├── js-src │ │ └── people.jsx │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── person_example.html │ │ ├── quart_serve_data.py │ │ └── static │ │ └── people.js ├── Chapter9 │ ├── README.rst │ ├── circus.ini │ ├── hypercorn_server.py │ ├── requirements.in │ └── setup-example │ │ ├── README.rst │ │ ├── requirements.txt │ │ └── setup.py └── README.md ├── ERRATA.md ├── LICENSE ├── README.md ├── authservice ├── MANIFEST.in ├── Makefile ├── README.rst ├── authservice │ ├── __init__.py │ ├── app.py │ ├── settings.ini │ ├── settings.py │ ├── static │ │ └── user.jsx │ ├── templates │ │ └── index.html │ ├── tests │ │ ├── __init__.py │ │ └── test_home.py │ └── views │ │ ├── __init__.py │ │ └── home.py ├── requirements.txt ├── setup.py └── tox.ini ├── dataservice ├── Dockerfile ├── MANIFEST.in ├── Makefile ├── README.rst ├── dataservice │ ├── __init__.py │ ├── app.py │ ├── database.py │ ├── js-src │ │ ├── like_button.js │ │ ├── people.jsx │ │ └── user.jsx │ ├── settings.ini │ ├── settings.py │ ├── static │ │ ├── like_button.js │ │ ├── people.js │ │ ├── people.jsx │ │ ├── user.js │ │ └── user.jsx │ ├── templates │ │ ├── all_people.html │ │ ├── index.html │ │ └── user_snippet.html │ ├── tests │ │ ├── __init__.py │ │ └── test_home.py │ └── views │ │ ├── __init__.py │ │ └── home.py ├── package.json ├── requirements.txt ├── setup.py └── tox.ini ├── docker-compose.yml ├── micro-frontend ├── MANIFEST.in ├── Makefile ├── README.rst ├── datafrontend │ ├── __init__.py │ ├── app.py │ ├── js-src │ │ ├── like_button.js │ │ └── user.jsx │ ├── settings.ini │ ├── settings.py │ ├── static │ │ ├── like_button.js │ │ ├── user.js │ │ └── user.jsx │ ├── templates │ │ ├── index.html │ │ └── user_snippet.html │ ├── tests │ │ ├── __init__.py │ │ └── test_home.py │ └── views │ │ ├── __init__.py │ │ └── home.py ├── requirements.txt ├── setup.py └── tox.ini ├── microservice ├── .coverage ├── .vscode │ └── settings.json ├── Dockerfile ├── MANIFEST.in ├── Makefile ├── README.rst ├── docs │ ├── Makefile │ └── source │ │ ├── _static │ │ └── .keep │ │ ├── _templates │ │ └── .keep │ │ ├── api.rst │ │ ├── conf.py │ │ └── index.rst ├── myservice │ ├── __init__.py │ ├── app.py │ ├── foo.py │ ├── settings.ini │ ├── tests │ │ ├── __init__.py │ │ └── test_home.py │ └── views │ │ ├── __init__.py │ │ └── home.py ├── requirements.in ├── requirements.txt ├── setup.py └── tox.ini ├── monolith ├── .coverage ├── .vscode │ └── settings.json ├── MANIFEST.in ├── Makefile ├── README.rst ├── docs │ ├── Makefile │ └── source │ │ ├── _static │ │ └── .keep │ │ ├── _templates │ │ └── .keep │ │ ├── api.rst │ │ ├── conf.py │ │ └── index.rst ├── jeeves │ ├── __init__.py │ ├── actions │ │ ├── help.py │ │ ├── misc.py │ │ ├── user.py │ │ └── weather.py │ ├── app.py │ ├── auth.py │ ├── background.py │ ├── controller │ │ └── message_router.py │ ├── database.py │ ├── forms.py │ ├── outgoing │ │ ├── default.py │ │ └── slack.py │ ├── settings.py │ ├── templates │ │ ├── create_user.html │ │ ├── index.html │ │ ├── login.html │ │ └── users.html │ ├── tests │ │ ├── __init__.py │ │ └── test_home.py │ └── views │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── auth.py │ │ ├── home.py │ │ └── slack_api.py ├── requirements.txt ├── setup.py └── tox.ini └── tokendealer ├── .vscode └── settings.json ├── Dockerfile ├── MANIFEST.in ├── Makefile ├── README.rst ├── pyvenv.cfg ├── requirements.txt ├── setup.py ├── tokendealer ├── __init__.py ├── app.py ├── settings.ini ├── settings.yml ├── tests │ ├── __init__.py │ └── test_home.py └── views │ ├── __init__.py │ └── home.py └── tox.ini /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E501,F401,F821,F841 3 | -------------------------------------------------------------------------------- /CodeSamples/Chapter10/alpine-python-container/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-alpine 2 | CMD ["python3.9"] 3 | 4 | -------------------------------------------------------------------------------- /CodeSamples/Chapter10/container_basic/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | RUN apt-get update && apt-get install -y python3 3 | CMD ["bash"] 4 | 5 | -------------------------------------------------------------------------------- /CodeSamples/Chapter10/container_with_cleanup/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | RUN apt-get update && \ 3 | apt-get install -y python3 && \ 4 | apt-get clean && \ 5 | rm -fr /var/lib/apt/lists 6 | CMD ["bash"] 7 | 8 | -------------------------------------------------------------------------------- /CodeSamples/Chapter10/etcd-quart/etcd_basic.py: -------------------------------------------------------------------------------- 1 | # etcd_basic.py 2 | from quart import Quart, current_app 3 | import etcd3 4 | 5 | 6 | # Can read this map from a traditional config file 7 | settings_map = { 8 | "dataservice_url": "/services/dataservice/url", 9 | } 10 | settings_reverse_map = {v: k for k, v in settings_map.items()} 11 | 12 | etcd_client = etcd3.client() 13 | 14 | 15 | def load_settings(): 16 | config = dict() 17 | for setting, etcd_key in settings_map.items(): 18 | config[setting] = etcd_client.get(etcd_key)[0].decode("utf-8") 19 | return config 20 | 21 | 22 | def create_app(name=__name__): 23 | app = Quart(name) 24 | app.config.update(load_settings()) 25 | return app 26 | 27 | 28 | app = create_app() 29 | 30 | 31 | def watch_callback(event): 32 | global app 33 | for update in event.events: 34 | # Determine which setting to update, and convert from bytes to str 35 | config_option = settings_reverse_map[update.key.decode("utf-8")] 36 | app.config[config_option] = update.value.decode("utf-8") 37 | 38 | 39 | # Start to watch for dataservice url changes 40 | # You can also watch entire areas with add_watch_prefix_callback 41 | watch_id = etcd_client.add_watch_callback("/services/dataservice/url", watch_callback) 42 | 43 | 44 | @app.route("/api") 45 | def what_is_url(): 46 | return {"url": app.config["dataservice_url"]} 47 | 48 | 49 | app.run() 50 | -------------------------------------------------------------------------------- /CodeSamples/Chapter10/kubernetes/nginx.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: nginx-deployment 5 | labels: 6 | app: nginx 7 | spec: 8 | replicas: 8 9 | selector: 10 | matchLabels: 11 | app: nginx 12 | template: 13 | metadata: 14 | labels: 15 | app: nginx 16 | spec: 17 | containers: 18 | - name: nginx 19 | image: nginx:1.21.0 20 | ports: 21 | - containerPort: 80 22 | -------------------------------------------------------------------------------- /CodeSamples/Chapter10/quart_container_basic/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9 2 | COPY . /app/ 3 | RUN pip install -r /app/requirements.txt 4 | RUN pip install /app/ 5 | CMD ["python3.9"] 6 | 7 | -------------------------------------------------------------------------------- /CodeSamples/Chapter10/quart_container_basic/requirements.in: -------------------------------------------------------------------------------- 1 | quart 2 | -------------------------------------------------------------------------------- /CodeSamples/Chapter10/quart_container_basic/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile requirements.in 6 | # 7 | aiofiles==0.6.0 8 | # via quart 9 | blinker==1.4 10 | # via quart 11 | click==8.0.0 12 | # via quart 13 | h11==0.12.0 14 | # via 15 | # hypercorn 16 | # wsproto 17 | h2==4.0.0 18 | # via hypercorn 19 | hpack==4.0.0 20 | # via h2 21 | hypercorn==0.11.2 22 | # via quart 23 | hyperframe==6.0.1 24 | # via h2 25 | itsdangerous==2.0.0 26 | # via quart 27 | jinja2==3.0.0 28 | # via quart 29 | markupsafe==2.0.0 30 | # via jinja2 31 | priority==1.3.0 32 | # via hypercorn 33 | quart==0.15.0 34 | # via -r requirements.in 35 | toml==0.10.2 36 | # via 37 | # hypercorn 38 | # quart 39 | werkzeug==2.0.0 40 | # via quart 41 | wsproto==1.0.0 42 | # via hypercorn 43 | -------------------------------------------------------------------------------- /CodeSamples/Chapter10/quart_container_basic/setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from setuptools import setup, find_packages 3 | 4 | 5 | with open("requirements.txt") as f: 6 | deps = [ 7 | dep 8 | for dep in f.read().split("\n") 9 | if dep.strip() != "" and not dep.startswith("-e") 10 | ] 11 | install_requires = deps 12 | 13 | 14 | setup( 15 | name="tokendealer", 16 | version="0.1", 17 | packages=find_packages(), 18 | zip_safe=False, 19 | include_package_data=True, 20 | install_requires=install_requires, 21 | ) 22 | -------------------------------------------------------------------------------- /CodeSamples/Chapter10/swarm-terraform/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/aws" { 5 | version = "3.42.0" 6 | hashes = [ 7 | "h1:C6/yDp6BhuDFx0qdkBuJj/OWUJpAoraHTJaU6ac38Rw=", 8 | "zh:126c856a6eedddd8571f161a826a407ba5655a37a6241393560a96b8c4beca1a", 9 | "zh:1a4868e6ac734b5fc2e79a4a889d176286b66664aad709435aa6acee5871d5b0", 10 | "zh:40fed7637ab8ddeb93bef06aded35d970f0628025b97459ae805463e8aa0a58a", 11 | "zh:68def3c0a5a1aac1db6372c51daef858b707f03052626d3427ac24cba6f2014d", 12 | "zh:6db7ec9c8d1803a0b6f40a664aa892e0f8894562de83061fa7ac1bc51ff5e7e5", 13 | "zh:7058abaad595930b3f97dc04e45c112b2dbf37d098372a849081f7081da2fb52", 14 | "zh:8c25adb15a19da301c478aa1f4a4d8647cabdf8e5dae8331d4490f80ea718c26", 15 | "zh:8e129b847401e39fcbc54817726dab877f36b7f00ff5ed76f7b43470abe99ff9", 16 | "zh:d268bb267a2d6b39df7ddee8efa7c1ef7a15cf335dfa5f2e64c9dae9b623a1b8", 17 | "zh:d6eeb3614a0ab50f8e9ab5666ae5754ea668ce327310e5b21b7f04a18d7611a8", 18 | "zh:f5d3c58055dff6e38562b75d3edc908cb2f1e45c6914f6b00f4773359ce49324", 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /CodeSamples/Chapter10/swarm-terraform/ec2.tf: -------------------------------------------------------------------------------- 1 | data "aws_ami" "ubuntu_focal" { 2 | most_recent = true 3 | filter { 4 | name = "name" 5 | values = ["ubuntu/images/nvm-ssd/ubuntu-bionic-20.04-amd64-server"] 6 | } 7 | 8 | filter { 9 | name = "virtualization-type" 10 | values = ["hvm"] 11 | } 12 | 13 | owners = ["099720109477"] # Canonical 14 | } 15 | 16 | 17 | resource "aws_vpc" "swarm_cluster_vpc" { 18 | cidr_block = "172.20.1.0/24" 19 | enable_dns_hostnames = true 20 | 21 | } 22 | 23 | resource "aws_internet_gateway" "main_gateway" { 24 | vpc_id = aws_vpc.swarm_cluster_vpc.id 25 | } 26 | 27 | resource "aws_route_table" "public_internet_access" { 28 | vpc_id = aws_vpc.swarm_cluster_vpc.id 29 | route { 30 | cidr_block = "0.0.0.0/0" 31 | gateway_id = aws_internet_gateway.main_gateway.id 32 | } 33 | } 34 | 35 | resource "aws_security_group" "swarm_security_group" { 36 | name = "swarm security group" 37 | description = "Swarm Security Group" 38 | vpc_id = aws_vpc.swarm_cluster_vpc.id 39 | 40 | ingress = [{ 41 | cidr_blocks = ["0.0.0.0/0"] 42 | description = "Allow HTTPS inbound" 43 | from_port = 0 44 | ipv6_cidr_blocks = ["::/0"] 45 | prefix_list_ids = ["value"] 46 | protocol = "tcp" 47 | security_groups = ["value"] 48 | self = false 49 | to_port = 443 50 | }] 51 | 52 | 53 | egress = [{ 54 | cidr_blocks = ["0.0.0.0/0"] 55 | description = "allow all" 56 | protocol = -1 57 | from_port = 0 58 | to_port = 0 59 | }] 60 | 61 | 62 | } 63 | 64 | resource "aws_security_group" "foobar" { 65 | } 66 | 67 | resource "aws_instance" "swarm_cluster" { 68 | count = var.swarm_node_count 69 | ami = data.aws_ami.ubuntu_focal.id 70 | instance_type = var.ec2_instance_type 71 | 72 | vpc_security_group_ids = [aws_security_group.swarm_security_group.id] 73 | key_name = var.ec2_key_name 74 | 75 | root_block_device { 76 | volume_type = "gp2" 77 | volume_size = "20" # GiB 78 | encrypted = true 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /CodeSamples/Chapter10/swarm-terraform/var.tf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Microservices-Development-2nd-Edition/ff7f9eabefaf69267879daad3e114d0601ad4671/CodeSamples/Chapter10/swarm-terraform/var.tf -------------------------------------------------------------------------------- /CodeSamples/Chapter2/authorization.py: -------------------------------------------------------------------------------- 1 | # authorization.py 2 | from quart import Quart, request 3 | 4 | app = Quart(__name__) 5 | 6 | 7 | @app.route("/") 8 | def auth(): 9 | print("Flask's Authorization information") 10 | print(request.authorization) 11 | return "" 12 | 13 | 14 | if __name__ == "__main__": 15 | app.run() 16 | -------------------------------------------------------------------------------- /CodeSamples/Chapter2/blueprints.py: -------------------------------------------------------------------------------- 1 | # blueprints.py 2 | from quart import Blueprint 3 | 4 | teams = Blueprint("teams", __name__) 5 | 6 | _DEVS = ["Alice", "Bob"] 7 | _OPS = ["Charles"] 8 | _TEAMS = {1: _DEVS, 2: _OPS} 9 | 10 | 11 | @teams.route("/teams") 12 | def get_all(): 13 | return _TEAMS 14 | 15 | 16 | @teams.route("/teams/") 17 | def get_team(team_id): 18 | return _TEAMS[team_id] 19 | -------------------------------------------------------------------------------- /CodeSamples/Chapter2/catch_all_errors.py: -------------------------------------------------------------------------------- 1 | # catch_all_errors.py 2 | from quart import Quart, jsonify, abort 3 | from quart.exceptions import HTTPException, default_exceptions 4 | 5 | 6 | def JsonApp(app): 7 | def error_handling(error): 8 | if isinstance(error, HTTPException): 9 | result = { 10 | "code": error.code, 11 | "description": error.description, 12 | "message": str(error), 13 | } 14 | else: 15 | description = abort.mapping[500].description 16 | result = {"code": 500, "description": description, "message": str(error)} 17 | 18 | resp = jsonify(result) 19 | resp.status_code = result["code"] 20 | return resp 21 | 22 | for code in default_exceptions.keys(): 23 | app.register_error_handler(code, error_handling) 24 | 25 | return app 26 | 27 | 28 | app = JsonApp(Quart(__name__)) 29 | 30 | 31 | @app.route("/api") 32 | def my_microservice(): 33 | raise TypeError("Some Exception") 34 | 35 | 36 | if __name__ == "__main__": 37 | app.run() 38 | -------------------------------------------------------------------------------- /CodeSamples/Chapter2/converter_custom.py: -------------------------------------------------------------------------------- 1 | # converter_custom.py 2 | from quart import Quart 3 | from werkzeug.routing import BaseConverter, ValidationError 4 | 5 | _USERS = {"1": "Alice", "2": "Bob"} 6 | _IDS = {val: id for id, val in _USERS.items()} 7 | 8 | 9 | class RegisteredUser(BaseConverter): 10 | def to_python(self, value): 11 | if value in _USERS: 12 | return _USERS[value] 13 | raise ValidationError() 14 | 15 | def to_url(self, value): 16 | return _IDS[value] 17 | 18 | 19 | app = Quart(__name__) 20 | app.url_map.converters["registered"] = RegisteredUser 21 | 22 | 23 | @app.route("/api/person/") 24 | def person(name): 25 | response = {"Hello": name} 26 | return response 27 | 28 | 29 | if __name__ == "__main__": 30 | app.run() 31 | -------------------------------------------------------------------------------- /CodeSamples/Chapter2/email_render.py: -------------------------------------------------------------------------------- 1 | # email_render.py 2 | from datetime import datetime 3 | from jinja2 import Template 4 | from email.utils import format_datetime 5 | 6 | 7 | def render_email(**data): 8 | with open("email_template.j2") as f: 9 | template = Template(f.read()) 10 | return template.render(**data) 11 | 12 | 13 | data = { 14 | "date": format_datetime(datetime.now()), 15 | "to": "bob@example.com", 16 | "from": "shopping@example-shop.com", 17 | "subject": "Your Burger order", 18 | "name": "Bob", 19 | "items": [ 20 | {"name": "Cheeseburger", "price": 4.5}, 21 | {"name": "Fries", "price": 2.0}, 22 | {"name": "Root Beer", "price": 3.0}, 23 | ], 24 | } 25 | 26 | print(render_email(**data)) 27 | -------------------------------------------------------------------------------- /CodeSamples/Chapter2/email_template.j2: -------------------------------------------------------------------------------- 1 | 2 | Date: {{date}} 3 | From: {{from}} 4 | Subject: {{subject}} 5 | To: {{to}} 6 | Content-Type: text/plain 7 | 8 | 9 | Hello {{name}}, 10 | 11 | We have received your payment! 12 | 13 | Below is the list of items we will deliver for lunch: 14 | 15 | {% for item in items %}- {{item['name']}} ({{item['price']}} Euros) 16 | {% endfor %} 17 | 18 | Thank you for your business! 19 | 20 | -- 21 | My Fictional Burger Place 22 | 23 | -------------------------------------------------------------------------------- /CodeSamples/Chapter2/error_handler.py: -------------------------------------------------------------------------------- 1 | # error_handler.py 2 | from quart import Quart 3 | 4 | app = Quart(__name__) 5 | 6 | 7 | @app.errorhandler(500) 8 | def error_handling(error): 9 | return {"Error": str(error)}, 500 10 | 11 | 12 | @app.route("/api") 13 | def my_microservice(): 14 | raise TypeError("Some Exception") 15 | 16 | 17 | if __name__ == "__main__": 18 | app.run() 19 | -------------------------------------------------------------------------------- /CodeSamples/Chapter2/globals.py: -------------------------------------------------------------------------------- 1 | # globals.py 2 | from quart import Quart, g, request 3 | 4 | app = Quart(__name__) 5 | 6 | 7 | @app.before_request 8 | def authenticate(): 9 | if request.authorization: 10 | g.user = request.authorization["username"] 11 | else: 12 | g.user = "Anonymous" 13 | 14 | 15 | @app.route("/api") 16 | def my_microservice(): 17 | return {"Hello": g.user} 18 | 19 | 20 | if __name__ == "__main__": 21 | app.run() 22 | -------------------------------------------------------------------------------- /CodeSamples/Chapter2/load_settings.py: -------------------------------------------------------------------------------- 1 | # Load settings in an interactive Python environment. 2 | # To start, run the python executable, and type the following as commands 3 | # at the >>> prompt 4 | 5 | from quart import Quart 6 | import pprint 7 | import yaml 8 | 9 | pp = pprint.PrettyPrinter(indent=4) 10 | app = Quart(__name__) 11 | app.config.from_object("prod_settings.Config") 12 | pp.pprint(app.config) 13 | 14 | 15 | app.config.from_json("prod_settings.json") 16 | 17 | 18 | app.config.from_file("settings.yml", yaml.safe_load) 19 | -------------------------------------------------------------------------------- /CodeSamples/Chapter2/middleware.py: -------------------------------------------------------------------------------- 1 | # middleware.py 2 | from quart import Quart, request 3 | from werkzeug.datastructures import Headers 4 | 5 | 6 | class XFFMiddleware(object): 7 | def __init__(self, app, real_ip="10.1.1.1"): 8 | self.app = app 9 | self.real_ip = real_ip 10 | 11 | async def __call__(self, scope, receive, send): 12 | if "headers" in scope and "HTTP_X_FORWARDED_FOR" not in scope["headers"]: 13 | new_headers = scope["headers"].raw_items() + [ 14 | ( 15 | b"X-Forwarded-For", 16 | f"{self.real_ip}, 10.3.4.5, 127.0.0.1".encode(), 17 | ) 18 | ] 19 | scope["headers"] = Headers(new_headers) 20 | return await self.app(scope, receive, send) 21 | 22 | 23 | app = Quart(__name__) 24 | app.asgi_app = XFFMiddleware(app.asgi_app) 25 | 26 | 27 | @app.route("/api") 28 | def my_microservice(): 29 | if "X-Forwarded-For" in request.headers: 30 | ips = [ip.strip() for ip in request.headers["X-Forwarded-For"].split(",")] 31 | ip = ips[0] 32 | else: 33 | ip = request.remote_addr 34 | 35 | return {"Hello": ip} 36 | 37 | 38 | if __name__ == "__main__": 39 | app.run() 40 | -------------------------------------------------------------------------------- /CodeSamples/Chapter2/prod_settings.json: -------------------------------------------------------------------------------- 1 | {"DEBUG": false, "SQLURI": "postgres://username:xxx@localhost/db"} 2 | -------------------------------------------------------------------------------- /CodeSamples/Chapter2/prod_settings.py: -------------------------------------------------------------------------------- 1 | # prod_settings.py 2 | class Config: 3 | DEBUG = False 4 | SQLURI = "postgres://username:xxx@localhost/db" 5 | -------------------------------------------------------------------------------- /CodeSamples/Chapter2/prod_settings.yml: -------------------------------------------------------------------------------- 1 | --- 2 | DEBUG: False 3 | SQLURI: "postgres://username:xxx@localhost/db" 4 | -------------------------------------------------------------------------------- /CodeSamples/Chapter2/quart_basic.py: -------------------------------------------------------------------------------- 1 | # quart_basic.py 2 | from quart import Quart 3 | 4 | app = Quart(__name__) 5 | 6 | 7 | @app.route("/api") 8 | def my_microservice(): 9 | return {"Hello": "World!"} 10 | 11 | 12 | if __name__ == "__main__": 13 | app.run() 14 | -------------------------------------------------------------------------------- /CodeSamples/Chapter2/quart_details.py: -------------------------------------------------------------------------------- 1 | # quart_details.py 2 | from quart import Quart, request, jsonify 3 | 4 | app = Quart(__name__) 5 | 6 | 7 | @app.route("/api", provide_automatic_options=False) 8 | async def my_microservice(): 9 | print(dir(request)) 10 | response = jsonify({"Hello": "World!"}) 11 | print(response) 12 | print(await response.get_data()) 13 | return response 14 | 15 | 16 | if __name__ == "__main__": 17 | print(app.url_map) 18 | app.run() 19 | -------------------------------------------------------------------------------- /CodeSamples/Chapter2/requirements.txt: -------------------------------------------------------------------------------- 1 | aiofiles==0.6.0 2 | blinker==1.4 3 | click==7.1.2 4 | h11==0.11.0 5 | h2==4.0.0 6 | hpack==4.0.0 7 | Hypercorn==0.11.1 8 | hyperframe==6.0.0 9 | itsdangerous==1.1.0 10 | Jinja2==2.11.2 11 | MarkupSafe==1.1.1 12 | priority==1.3.0 13 | PyYAML==5.3.1 14 | Quart==0.14.0 15 | toml==0.10.2 16 | Werkzeug==1.0.1 17 | wsproto==1.0.0 18 | -------------------------------------------------------------------------------- /CodeSamples/Chapter2/signals.py: -------------------------------------------------------------------------------- 1 | # signals.py 2 | from quart import Quart, request_finished 3 | from quart.signals import signals_available 4 | 5 | if not signals_available: 6 | raise RuntimeError("pip install blinker") 7 | 8 | app = Quart(__name__) 9 | 10 | 11 | def finished(sender, response, **extra): 12 | print("About to send a Response") 13 | print(response) 14 | 15 | 16 | request_finished.connect(finished) 17 | 18 | 19 | @app.route("/api") 20 | def my_microservice(): 21 | return {"Hello": "World"} 22 | 23 | 24 | if __name__ == "__main__": 25 | app.run() 26 | -------------------------------------------------------------------------------- /CodeSamples/Chapter2/url_for.py: -------------------------------------------------------------------------------- 1 | # Run url_for in an interactive Python environment. 2 | # To start, run the python executable, and type the following as commands 3 | # at the >>> prompt 4 | 5 | from quart_converter import app 6 | from quart import url_for 7 | import asyncio 8 | 9 | 10 | async def run_url_for(): 11 | async with app.test_request_context("/", method="GET"): 12 | print(url_for("person", name="Tarek")) 13 | 14 | 15 | loop = asyncio.get_event_loop() 16 | loop.run_until_complete(run_url_for()) 17 | -------------------------------------------------------------------------------- /CodeSamples/Chapter2/variables_and_converters.py: -------------------------------------------------------------------------------- 1 | from quart import Quart 2 | 3 | app = Quart(__name__) 4 | 5 | 6 | @app.route("/person/") 7 | def person_friendly(person_id): 8 | return f"Hello {person_id}" 9 | 10 | 11 | @app.route("/person/") 12 | def person(person_id): 13 | return {"Hello": person_id} 14 | 15 | 16 | @app.route("/decimals/") 17 | def decimal_example(my_number): 18 | return f"Hello {my_number}" 19 | 20 | 21 | @app.route("/many///") 22 | def path_friendly(some_text, other_text, my_number): 23 | return ( 24 | f"You sent {some_text} and also {other_text}, as well as a number: {my_number}" 25 | ) 26 | 27 | 28 | @app.route("/path/") 29 | def path_friendly(some_path): 30 | return f"Hello {some_path}" 31 | 32 | 33 | @app.route("/") 34 | def page_friendly(page_name): 35 | return f"Hello {page_name}" 36 | 37 | 38 | app.run() 39 | -------------------------------------------------------------------------------- /CodeSamples/Chapter2/yamlify.py: -------------------------------------------------------------------------------- 1 | # yamlify.py 2 | from quart import Quart 3 | import yaml # requires PyYAML 4 | 5 | app = Quart(__name__) 6 | 7 | 8 | def yamlify(data, status=200, headers=None): 9 | _headers = {"Content-Type": "application/x-yaml"} 10 | if headers is not None: 11 | _headers.update(headers) 12 | return yaml.safe_dump(data), status, _headers 13 | 14 | 15 | @app.route("/api") 16 | def my_microservice(): 17 | return yamlify(["Hello", "YAML", "World!"]) 18 | 19 | 20 | if __name__ == "__main__": 21 | app.run() 22 | -------------------------------------------------------------------------------- /CodeSamples/Chapter3/molotov_example1.py: -------------------------------------------------------------------------------- 1 | # molotov_example.py 2 | # Run: 3 | # molotov molotov_example.py -p 10 -w 200 -d 60 4 | from molotov import scenario 5 | 6 | 7 | @scenario(weight=40) 8 | async def scenario_one(session): 9 | async with session.get("http://localhost:5000/api") as resp: 10 | res = await resp.json() 11 | assert res["Hello"] == "World!" 12 | assert resp.status == 200 13 | 14 | 15 | @scenario(weight=60) 16 | async def scenario_two(session): 17 | async with session.get("http://localhost:5000/api") as resp: 18 | assert resp.status == 200 19 | -------------------------------------------------------------------------------- /CodeSamples/Chapter3/quart_basic.py: -------------------------------------------------------------------------------- 1 | # quart_basic.py 2 | from quart import Quart 3 | 4 | app = Quart(__name__) 5 | 6 | 7 | @app.route("/api") 8 | def my_microservice(): 9 | return {"Hello": "World!"} 10 | 11 | 12 | if __name__ == "__main__": 13 | app.run() 14 | -------------------------------------------------------------------------------- /CodeSamples/Chapter3/quart_error.py: -------------------------------------------------------------------------------- 1 | # quart_basic.py 2 | from quart import Quart 3 | 4 | app = Quart(__name__) 5 | 6 | text_404 = ( 7 | "The requested URL was not found on the server. " 8 | "If you entered the URL manually please check your " 9 | "spelling and try again." 10 | ) 11 | 12 | 13 | @app.errorhandler(500) 14 | def error_handling_500(error): 15 | return {"Error": str(error)}, 500 16 | 17 | 18 | @app.errorhandler(404) 19 | def error_handling_404(error): 20 | return {"Error": str(error), "description": text_404}, 404 21 | 22 | 23 | @app.route("/api") 24 | def my_microservice(): 25 | raise TypeError("This is a testing exception.") 26 | 27 | 28 | if __name__ == "__main__": 29 | app.run() 30 | -------------------------------------------------------------------------------- /CodeSamples/Chapter3/quart_profiled.py: -------------------------------------------------------------------------------- 1 | # quart_profiled.py 2 | import flask_profiler 3 | import quart.flask_patch # noqa 4 | from quart import Quart 5 | 6 | app = Quart(__name__) 7 | 8 | app.config["DEBUG"] = True 9 | 10 | # You need to declare necessary configuration to initialize 11 | # flask-profiler as follows: 12 | app.config["flask_profiler"] = { 13 | "enabled": app.config["DEBUG"], 14 | "storage": {"engine": "sqlite"}, 15 | "basicAuth": {"enabled": True, "username": "admin", "password": "admin"}, 16 | "ignore": ["^/static/.*"], 17 | } 18 | 19 | 20 | @app.route("/api") 21 | def my_microservice(): 22 | return {"Hello": "World!"} 23 | 24 | 25 | flask_profiler.init_app(app) 26 | 27 | 28 | if __name__ == "__main__": 29 | app.run() 30 | -------------------------------------------------------------------------------- /CodeSamples/Chapter3/requests_example1.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | def query_url(url): 5 | response = requests.get(url) 6 | response.raise_for_status() 7 | return response.json() 8 | 9 | 10 | def get_hero_names(hero_filter=None): 11 | url = "https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json" # noqa 12 | json_body = query_url(url) 13 | for member in json_body.get("members", []): 14 | if hero_filter(member): 15 | yield member["name"] 16 | 17 | 18 | def format_heroes_over(age=0): 19 | hero_names = get_hero_names(hero_filter=lambda hero: hero.get("age", 0) > age) 20 | formatted_text = "" 21 | for hero in hero_names: 22 | formatted_text += f"{hero} is over {age}\n" 23 | return formatted_text 24 | 25 | 26 | if __name__ == "__main__": 27 | print(format_heroes_over(age=30)) 28 | -------------------------------------------------------------------------------- /CodeSamples/Chapter3/requests_example2.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | def query_url(url): 5 | response = requests.get(url) 6 | response.raise_for_status() 7 | return response.json() 8 | 9 | 10 | def get_hero_names(hero_data, hero_filter=None): 11 | for member in hero_data.get("members", []): 12 | if hero_filter is None or hero_filter(member): 13 | yield member 14 | 15 | 16 | def render_hero_message(heroes, age): 17 | formatted_text = "" 18 | for hero in heroes: 19 | formatted_text += f"{hero['name']} is over {age}\n" 20 | return formatted_text 21 | 22 | 23 | def render_heroes_over(age=0): 24 | url = "https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json" 25 | json_body = query_url(url) 26 | relevant_heroes = get_hero_names( 27 | json_body, hero_filter=lambda hero: hero.get("age", 0) > age 28 | ) 29 | return render_hero_message(relevant_heroes, age) 30 | 31 | 32 | if __name__ == "__main__": 33 | print(render_heroes_over(age=30)) 34 | -------------------------------------------------------------------------------- /CodeSamples/Chapter3/test_quart_basic.py: -------------------------------------------------------------------------------- 1 | import json 2 | import unittest 3 | 4 | from quart_basic import app as app_under_test 5 | 6 | 7 | class TestApp(unittest.IsolatedAsyncioTestCase): 8 | async def test_help(self): 9 | # creating a QuartClient instance to interact with the app 10 | app = app_under_test.test_client() 11 | 12 | # calling /api/ endpoint 13 | hello = await app.get("/api") 14 | 15 | # asserting the body 16 | body = json.loads(str(await hello.get_data(), "utf8")) 17 | self.assertEqual(body["Hello"], "World!") 18 | 19 | 20 | if __name__ == "__main__": 21 | unittest.main() 22 | -------------------------------------------------------------------------------- /CodeSamples/Chapter3/test_quart_error.py: -------------------------------------------------------------------------------- 1 | # test_quart_error.py 2 | import json 3 | import unittest 4 | 5 | from quart_error import app as app_under_test 6 | from quart_error import text_404 7 | 8 | 9 | class TestApp(unittest.IsolatedAsyncioTestCase): 10 | async def asyncSetUp(self): 11 | # Create a client to interact with the app 12 | self.app = app_under_test.test_client() 13 | 14 | async def test_raise(self): 15 | # This won't raise a Python exception but return a 500 16 | hello = await self.app.get("/api") 17 | self.assertEqual(hello.status_code, 500) 18 | 19 | async def test_proper_404(self): 20 | # Call a non-existing endpoint 21 | hello = await self.app.get("/dwdwqqwdwqd") 22 | 23 | # It's not there 24 | self.assertEqual(hello.status_code, 404) 25 | 26 | # but we still get a nice JSON body 27 | body = json.loads(str(await hello.get_data(), "utf8")) 28 | self.assertEqual(hello.status_code, 404) 29 | self.assertEqual( 30 | body["Error"], 31 | "404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.", 32 | ) 33 | self.assertEqual(body["description"], text_404) 34 | 35 | 36 | if __name__ == "__main__": 37 | unittest.main() 38 | -------------------------------------------------------------------------------- /CodeSamples/Chapter3/test_requests_example2.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import requests_mock 4 | 5 | import requests_example2 6 | 7 | 8 | class TestHeroCode(unittest.TestCase): 9 | def setUp(self): 10 | self.fake_heroes = { 11 | "members": [ 12 | {"name": "Age 20 Hero", "age": 20}, 13 | {"name": "Age 30 Hero", "age": 30}, 14 | {"name": "Age 40 Hero", "age": 40}, 15 | ] 16 | } 17 | 18 | def test_get_hero_names_age_filter(self): 19 | result = list( 20 | requests_example2.get_hero_names( 21 | self.fake_heroes, hero_filter=lambda x: x.get("age", 0) > 30 22 | ) 23 | ) 24 | self.assertEqual(result, [{"name": "Age 40 Hero", "age": 40}]) 25 | 26 | @requests_mock.mock() 27 | def test_display_heroes_over(self, mocker): 28 | mocker.get(requests_mock.ANY, json=self.fake_heroes) 29 | rendered_text = requests_example2.render_heroes_over(age=30) 30 | self.assertEqual(rendered_text, "Age 40 Hero is over 30\n") 31 | 32 | 33 | if __name__ == "__main__": 34 | unittest.main() 35 | -------------------------------------------------------------------------------- /CodeSamples/Chapter3/test_requests_example2_full.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import requests_mock 4 | 5 | import requests_example2 6 | 7 | 8 | class TestHeroCode(unittest.TestCase): 9 | def setUp(self): 10 | self.fake_heroes = { 11 | "members": [ 12 | {"name": "Age 20 Hero", "age": 20}, 13 | {"name": "Age 30 Hero", "age": 30}, 14 | {"name": "Age 40 Hero", "age": 40}, 15 | ] 16 | } 17 | 18 | def test_get_hero_names(self): 19 | result = list(requests_example2.get_hero_names(self.fake_heroes)) 20 | self.assertEqual(result, self.fake_heroes["members"]) 21 | 22 | def test_get_hero_names_age_filter(self): 23 | result = list( 24 | requests_example2.get_hero_names( 25 | self.fake_heroes, hero_filter=lambda x: x.get("age", 0) > 30 26 | ) 27 | ) 28 | self.assertEqual(result, [{"name": "Age 40 Hero", "age": 40}]) 29 | 30 | def test_get_hero_names_age_filter_no_results(self): 31 | result = list( 32 | requests_example2.get_hero_names( 33 | self.fake_heroes, hero_filter=lambda x: x.get("age", 0) > 50 34 | ) 35 | ) 36 | self.assertEqual(result, []) 37 | 38 | @requests_mock.mock() 39 | def test_display_heroes_over(self, mocker): 40 | mocker.get(requests_mock.ANY, json=self.fake_heroes) 41 | rendered_text = requests_example2.render_heroes_over(age=30) 42 | self.assertEqual(rendered_text, "Age 40 Hero is over 30\n") 43 | 44 | 45 | if __name__ == "__main__": 46 | unittest.main() 47 | -------------------------------------------------------------------------------- /CodeSamples/Chapter4/forms_example/app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from quart import Quart, redirect, render_template, request 4 | 5 | from database import user_dal, initialize_database 6 | from forms import UserForm 7 | 8 | 9 | app = Quart(__name__) 10 | app.config["WTF_CSRF_SECRET_KEY"] = "A SECRET KEY" 11 | app.config["SECRET_KEY"] = "ANOTHER ONE" 12 | 13 | 14 | @app.before_serving 15 | async def startup(): 16 | await initialize_database() 17 | 18 | 19 | @app.get("/users") 20 | async def get_all_users(): 21 | async with user_dal() as ud: 22 | users = await ud.get_all_users() 23 | return await render_template("users.html", users=users) 24 | 25 | 26 | @app.route("/create_user", methods=["GET", "POST"]) 27 | async def create_user(): 28 | form = UserForm() 29 | if request.method == "POST" and form.validate(): 30 | async with user_dal() as ud: 31 | await ud.create_user(form.name.data, form.email.data, form.slack_id.data) 32 | return redirect("/users") 33 | 34 | return await render_template("create_user.html", form=form) 35 | 36 | 37 | if __name__ == "__main__": 38 | 39 | app.run() 40 | -------------------------------------------------------------------------------- /CodeSamples/Chapter4/forms_example/database.py: -------------------------------------------------------------------------------- 1 | # sqlachemy-async.py 2 | from contextlib import asynccontextmanager 3 | 4 | from quart import Quart 5 | from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession 6 | from sqlalchemy.orm import declarative_base, sessionmaker 7 | from sqlalchemy import Column, Integer, String, Boolean, JSON 8 | from sqlalchemy.orm import Session 9 | from sqlalchemy.future import select 10 | from sqlalchemy import update 11 | from werkzeug.security import generate_password_hash, check_password_hash 12 | 13 | # Initialize SQLAlchemy with a test database 14 | DATABASE_URL = "sqlite+aiosqlite:///./test.db" 15 | engine = create_async_engine(DATABASE_URL, future=True, echo=True) 16 | async_session = sessionmaker(engine, expire_on_commit=False, class_=AsyncSession) 17 | Base = declarative_base() 18 | 19 | 20 | # Data Model 21 | class User(Base): 22 | __tablename__ = "user" 23 | id = Column(Integer, primary_key=True, autoincrement=True) 24 | name = Column(String) 25 | email = Column(String) 26 | slack_id = Column(String) 27 | password = Column(String) 28 | config = Column(JSON) 29 | is_active = Column(Boolean, default=True) 30 | is_admin = Column(Boolean, default=False) 31 | 32 | def __init__(self, *args, **kwargs): 33 | super().__init__(*args, **kwargs) 34 | 35 | def set_password(self, password): 36 | self.password = generate_password_hash(password) 37 | 38 | def check_password(self, password): 39 | return check_password_hash(self.password, password) 40 | 41 | def json(self): 42 | return { 43 | "id": self.id, 44 | "email": self.email, 45 | "slack_id": self.slack_id, 46 | "config": self.config, 47 | "is_active": self.is_active, 48 | "is_admin": self.is_admin, 49 | } 50 | 51 | 52 | # Data Access Layer 53 | class UserDAL: 54 | def __init__(self, db_session): 55 | self.db_session = db_session 56 | 57 | async def create_user( 58 | self, 59 | name, 60 | email, 61 | slack_id, 62 | password=None, 63 | config=None, 64 | is_active=True, 65 | is_admin=False, 66 | ): 67 | new_user = User( 68 | name=name, 69 | email=email, 70 | slack_id=slack_id, 71 | password=password, 72 | config=config, 73 | is_active=is_active, 74 | is_admin=is_admin, 75 | ) 76 | self.db_session.add(new_user) 77 | await self.db_session.flush() 78 | return new_user.json() 79 | 80 | async def get_all_users(self): 81 | query_result = await self.db_session.execute(select(User).order_by(User.id)) 82 | return [user.json() for user in query_result.scalars().all()] 83 | 84 | async def get_user(self, user_id): 85 | query = select(User).where(User.id == user_id) 86 | query_result = await self.db_session.execute(query) 87 | user = query_result.one() 88 | return user[0].json() 89 | 90 | async def set_password(self, user_id, password): 91 | query = select(User).where(User.id == user_id) 92 | query_result = await self.db_session.execute(query) 93 | user = query_result.one()[0] 94 | user.set_password(password) 95 | await self.db_session.flush() 96 | 97 | async def authenticate(self, user_id, password): 98 | query = select(User).where(User.id == user_id) 99 | query_result = await self.db_session.execute(query) 100 | user = query_result.one()[0] 101 | return user.check_password(password) 102 | 103 | 104 | async def initialize_database(): 105 | # create db tables 106 | async with engine.begin() as conn: 107 | await conn.run_sync(Base.metadata.drop_all) 108 | await conn.run_sync(Base.metadata.create_all) 109 | async with user_dal() as bd: 110 | await bd.create_user("name", "email", "slack_id") 111 | 112 | 113 | @asynccontextmanager 114 | async def user_dal(): 115 | async with async_session() as session: 116 | async with session.begin(): 117 | yield UserDAL(session) 118 | -------------------------------------------------------------------------------- /CodeSamples/Chapter4/forms_example/forms.py: -------------------------------------------------------------------------------- 1 | import quart.flask_patch 2 | import wtforms 3 | from flask_wtf import FlaskForm 4 | from wtforms.validators import DataRequired 5 | 6 | 7 | class UserForm(FlaskForm): 8 | email = wtforms.StringField("email", validators=[DataRequired()]) 9 | slack_id = wtforms.StringField("Slack ID") 10 | name = wtforms.StringField("Name") 11 | 12 | display = ["email", "slack_id", "name"] 13 | -------------------------------------------------------------------------------- /CodeSamples/Chapter4/forms_example/templates/create_user.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | {{ form.hidden_tag() }} 6 |
7 | {% for field in form.display %} 8 |
{{ form[field].label }}
9 |
{{ form[field]() }} 10 | {% if form[field].errors %} 11 | {% for e in form[field].errors %}

{{ e }}

{% endfor %} 12 | {% endif %} 13 |
14 | {% endfor %} 15 |
16 |

17 | 18 |

19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /CodeSamples/Chapter4/forms_example/templates/users.html: -------------------------------------------------------------------------------- 1 | {{ users }} 2 | 3 | 4 |

User List

5 |
    6 | {% for user in users: %} 7 |
  • 8 | {{ user.email }} {{ user.slack_id }} 9 |
  • 10 | {% endfor %} 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /CodeSamples/Chapter4/logging-in/app.py: -------------------------------------------------------------------------------- 1 | # logging-in.py 2 | import os 3 | from quart import Quart, request, render_template, redirect, url_for 4 | from quart_auth import ( 5 | AuthManager, 6 | login_required, 7 | logout_user, 8 | login_user, 9 | AuthUser, 10 | Unauthorized, 11 | ) 12 | import aiohttp 13 | import secrets 14 | 15 | app = Quart(__name__) 16 | AuthManager(app) 17 | app.secret_key = secrets.token_urlsafe(16) 18 | 19 | 20 | @app.route("/") 21 | @login_required 22 | async def welcome_page(): 23 | return await render_template("welcome.html") 24 | 25 | 26 | @app.route("/slack_login") 27 | async def slack_login(): 28 | client_id = os.environ["SLACK_CLIENT_ID"] 29 | return await render_template("login.html", client_id=client_id) 30 | 31 | 32 | @app.route("/logout") 33 | async def logout(): 34 | logout_user() 35 | 36 | 37 | @app.errorhandler(Unauthorized) 38 | async def redirect_to_login(_): 39 | return redirect(url_for("slack_login")) 40 | 41 | 42 | @app.route("/slack/callback") 43 | async def oauth2_slack_callback(): 44 | code = request.args["code"] 45 | client_id = os.environ["SLACK_CLIENT_ID"] 46 | client_secret = os.environ["SLACK_CLIENT_SECRET"] 47 | access_url = f"https://slack.com/api/oauth.v2.access?client_id={client_id}&client_secret={client_secret}&code={code}" 48 | async with aiohttp.ClientSession() as session: 49 | async with session.get(access_url) as resp: 50 | access_data = await resp.json() 51 | if access_data["ok"] is True: 52 | authed_user = access_data["authed_user"]["id"] 53 | login_user(AuthUser(authed_user)) 54 | return redirect(url_for("welcome_page")) 55 | return redirect(url_for("slack_login")) 56 | 57 | 58 | if __name__ == "__main__": 59 | app.run() 60 | -------------------------------------------------------------------------------- /CodeSamples/Chapter4/logging-in/templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Welcome to Jeeves!

4 | 5 | 6 | -------------------------------------------------------------------------------- /CodeSamples/Chapter4/logging-in/templates/welcome.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Welcome to Jeeves!

4 | You have logged in! 5 | 6 | 7 | -------------------------------------------------------------------------------- /CodeSamples/Chapter4/oauth2/app.py: -------------------------------------------------------------------------------- 1 | # slack_oauth2.py 2 | import os 3 | from quart import Quart, request, render_template 4 | import aiohttp 5 | 6 | app = Quart(__name__) 7 | 8 | 9 | @app.route("/") 10 | async def welcome_page(): 11 | client_id = os.environ["SLACK_CLIENT_ID"] 12 | return await render_template("welcome.html", client_id=client_id) 13 | 14 | 15 | @app.route("/slack/callback") 16 | async def oauth2_slack_callback(): 17 | code = request.args["code"] 18 | client_id = os.environ["SLACK_CLIENT_ID"] 19 | client_secret = os.environ["SLACK_CLIENT_SECRET"] 20 | access_url = f"https://slack.com/api/oauth.v2.access?client_id={client_id}&client_secret={client_secret}&code={code}" 21 | async with aiohttp.ClientSession() as session: 22 | async with session.get(access_url) as resp: 23 | access_data = await resp.json() 24 | print(access_data) 25 | return await render_template("logged_in.html") 26 | 27 | 28 | if __name__ == "__main__": 29 | app.run() 30 | -------------------------------------------------------------------------------- /CodeSamples/Chapter4/oauth2/templates/logged_in.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Logged in

4 | 5 | 6 | -------------------------------------------------------------------------------- /CodeSamples/Chapter4/oauth2/templates/welcome.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Welcome to Jeeves!

4 | 5 | 6 | -------------------------------------------------------------------------------- /CodeSamples/Chapter4/password_hash.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer 2 | from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession 3 | from sqlalchemy.orm import declarative_base, sessionmaker 4 | 5 | from werkzeug.security import generate_password_hash, check_password_hash 6 | 7 | 8 | DATABASE_URL = "sqlite+aiosqlite:///./test.db" 9 | engine = create_async_engine(DATABASE_URL, future=True, echo=True) 10 | async_session = sessionmaker(engine, expire_on_commit=False, class_=AsyncSession) 11 | Base = declarative_base() 12 | 13 | 14 | class User(Base): 15 | __tablename__ = "user" 16 | id = Column(Integer, primary_key=True, autoincrement=True) 17 | # ... all the Columns ... 18 | 19 | def __init__(self, *args, **kw): 20 | super(User, self).__init__(*args, **kw) 21 | self._authenticated = False 22 | 23 | def set_password(self, password): 24 | self.password = generate_password_hash(password) 25 | 26 | @property 27 | def is_authenticated(self): 28 | return self._authenticated 29 | 30 | def authenticate(self, password): 31 | checked = check_password_hash(self.password, password) 32 | self._authenticated = checked 33 | return self._authenticated 34 | -------------------------------------------------------------------------------- /CodeSamples/Chapter4/requirements.txt: -------------------------------------------------------------------------------- 1 | # Dependencies for Chapter 4 code samples 2 | Quart==0.15.1 3 | SQLAlchemy==1.4.23 4 | Werkzeug==2.0.1 5 | WTForms==2.3.3 6 | Flask-WTF==0.15.1 7 | aiosqlite==0.17.0 8 | aiohttp==3.7.4.post0 9 | celery==5.1.2 10 | asgiref==3.4.1 11 | quart-auth==0.5.0 12 | -------------------------------------------------------------------------------- /CodeSamples/Chapter4/sqlalchemy/sqlalchemy-async.py: -------------------------------------------------------------------------------- 1 | # sqlachemy-async.py 2 | from contextlib import asynccontextmanager 3 | 4 | from quart import Quart 5 | from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession 6 | from sqlalchemy.orm import declarative_base, sessionmaker 7 | from sqlalchemy import Column, Integer, String, Boolean, JSON 8 | from sqlalchemy.orm import Session 9 | from sqlalchemy.future import select 10 | from sqlalchemy import update 11 | 12 | # Initialize SQLAlchemy with a test database 13 | DATABASE_URL = "sqlite+aiosqlite:///./test.db" 14 | engine = create_async_engine(DATABASE_URL, future=True, echo=True) 15 | async_session = sessionmaker(engine, expire_on_commit=False, class_=AsyncSession) 16 | Base = declarative_base() 17 | 18 | 19 | # Data Model 20 | class User(Base): 21 | __tablename__ = "user" 22 | id = Column(Integer, primary_key=True, autoincrement=True) 23 | name = Column(String) 24 | email = Column(String) 25 | slack_id = Column(String) 26 | password = Column(String) 27 | config = Column(JSON) 28 | is_active = Column(Boolean, default=True) 29 | is_admin = Column(Boolean, default=False) 30 | 31 | def json(self): 32 | return { 33 | "id": self.id, 34 | "email": self.email, 35 | "slack_id": self.slack_id, 36 | "config": self.config, 37 | "is_active": self.is_active, 38 | "is_admin": self.is_admin, 39 | } 40 | 41 | 42 | # Data Access Layer 43 | class UserDAL: 44 | def __init__(self, db_session): 45 | self.db_session = db_session 46 | 47 | async def create_user( 48 | self, 49 | name, 50 | email, 51 | slack_id, 52 | password=None, 53 | config=None, 54 | is_active=True, 55 | is_admin=False, 56 | ): 57 | new_user = User( 58 | name=name, 59 | email=email, 60 | slack_id=slack_id, 61 | password=password, 62 | config=config, 63 | is_active=is_active, 64 | is_admin=is_admin, 65 | ) 66 | self.db_session.add(new_user) 67 | await self.db_session.flush() 68 | return new_user.json() 69 | 70 | async def get_all_users(self): 71 | query_result = await self.db_session.execute(select(User).order_by(User.id)) 72 | return {"users": [user.json() for user in query_result.scalars().all()]} 73 | 74 | async def get_user(self, user_id): 75 | query = select(User).where(User.id == user_id) 76 | query_result = await self.db_session.execute(query) 77 | user = query_result.one() 78 | return user[0].json() 79 | 80 | 81 | # 82 | # Quart App 83 | # 84 | app = Quart(__name__) 85 | 86 | 87 | @app.before_serving 88 | async def startup(): 89 | # create db tables 90 | async with engine.begin() as conn: 91 | await conn.run_sync(Base.metadata.drop_all) 92 | await conn.run_sync(Base.metadata.create_all) 93 | async with user_dal() as bd: 94 | await bd.create_user("name", "email", "slack_id") 95 | 96 | 97 | @asynccontextmanager 98 | async def user_dal(): 99 | async with async_session() as session: 100 | async with session.begin(): 101 | yield UserDAL(session) 102 | 103 | 104 | @app.post("/users") 105 | async def create_user(name, email, slack_id): 106 | async with user_dal() as ud: 107 | await ud.create_user(name, email, slack_id) 108 | 109 | 110 | @app.get("/users/") 111 | async def get_user(user_id): 112 | async with user_dal() as ud: 113 | return await ud.get_user(user_id) 114 | 115 | 116 | @app.get("/users") 117 | async def get_all_users(): 118 | async with user_dal() as ud: 119 | return await ud.get_all_users() 120 | 121 | 122 | @app.get("/users/page") 123 | async def get_all_users_templated(): 124 | async with user_dal() as ud: 125 | users = await ud.get_all_users() 126 | return await render_template("users.html", users=users) 127 | 128 | 129 | if __name__ == "__main__": 130 | app.run() 131 | -------------------------------------------------------------------------------- /CodeSamples/Chapter4/weather-celery/database.py: -------------------------------------------------------------------------------- 1 | # sqlachemy-async.py 2 | from contextlib import asynccontextmanager 3 | 4 | from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession 5 | from sqlalchemy.orm import declarative_base, sessionmaker 6 | from sqlalchemy import Column, Integer, String, Boolean, JSON 7 | from sqlalchemy.orm import Session 8 | from sqlalchemy.future import select 9 | from sqlalchemy import update 10 | 11 | # Initialize SQLAlchemy with a test database 12 | DATABASE_URL = "sqlite+aiosqlite:///./test.db" 13 | engine = create_async_engine(DATABASE_URL, future=True, echo=True) 14 | async_session = sessionmaker(engine, expire_on_commit=False, class_=AsyncSession) 15 | Base = declarative_base() 16 | 17 | 18 | # Data Model 19 | class User(Base): 20 | __tablename__ = "user" 21 | id = Column(Integer, primary_key=True, autoincrement=True) 22 | name = Column(String) 23 | email = Column(String) 24 | slack_id = Column(String) 25 | location = Column(String) 26 | 27 | def json(self): 28 | return { 29 | "id": self.id, 30 | "email": self.email, 31 | "slack_id": self.slack_id, 32 | "location": self.location, 33 | } 34 | 35 | 36 | # Data Access Layer 37 | class UserDAL: 38 | def __init__(self, db_session): 39 | self.db_session = db_session 40 | 41 | async def create_user( 42 | self, 43 | name, 44 | email, 45 | slack_id, 46 | location, 47 | ): 48 | new_user = User( 49 | name=name, 50 | email=email, 51 | slack_id=slack_id, 52 | location=location, 53 | ) 54 | self.db_session.add(new_user) 55 | await self.db_session.flush() 56 | return new_user.json() 57 | 58 | async def get_all_users(self): 59 | query_result = await self.db_session.execute(select(User).order_by(User.id)) 60 | return {"users": [user.json() for user in query_result.scalars().all()]} 61 | 62 | async def get_user(self, user_id): 63 | query = select(User).where(User.id == user_id) 64 | query_result = await self.db_session.execute(query) 65 | user = query_result.one() 66 | return user[0].json() 67 | 68 | async def get_users_with_locations(self): 69 | query = select(User).where(User.location is not None) 70 | return await self.db_session.execute(query) 71 | 72 | 73 | @asynccontextmanager 74 | async def user_dal(): 75 | async with async_session() as session: 76 | async with session.begin(): 77 | yield UserDAL(session) 78 | 79 | 80 | async def database_startup(): 81 | # create db tables 82 | async with engine.begin() as conn: 83 | await conn.run_sync(Base.metadata.drop_all) 84 | await conn.run_sync(Base.metadata.create_all) 85 | async with user_dal() as bd: 86 | await bd.create_user("alice", "email", "slack_id", "London, UK") 87 | await bd.create_user("bob", "email", "slack_id", "Paris, France") 88 | -------------------------------------------------------------------------------- /CodeSamples/Chapter4/weather-celery/example.py: -------------------------------------------------------------------------------- 1 | from celery import Celery 2 | 3 | celery_app = Celery("tasks", broker="pyamqp://guest@localhost//") 4 | 5 | 6 | @celery_app.task 7 | def add(): 8 | return 7 9 | 10 | 11 | @celery_app.on_after_configure.connect 12 | def setup_periodic_tasks(sender, **kwargs): 13 | sender.add_periodic_task(10.0, add, name="add every 10", expires=30) 14 | -------------------------------------------------------------------------------- /CodeSamples/Chapter4/weather-celery/scheduler.py: -------------------------------------------------------------------------------- 1 | # weather_worker.py 2 | import asyncio 3 | 4 | from asgiref.sync import async_to_sync 5 | from celery import Celery 6 | from weather_worker import do_weather_alerts 7 | 8 | celery_app = Celery("tasks", broker="amqp://localhost") 9 | 10 | 11 | @celery_app.on_after_configure.connect 12 | def setup_periodic_tasks(sender, **kwargs): 13 | sender.add_periodic_task(10.0, do_weather_alerts, name="add every 10", expires=30) 14 | -------------------------------------------------------------------------------- /CodeSamples/Chapter4/weather-celery/weather_worker.py: -------------------------------------------------------------------------------- 1 | # weather_worker.py 2 | import asyncio 3 | 4 | from asgiref.sync import async_to_sync 5 | from celery import Celery 6 | from database import user_dal 7 | 8 | celery_app = Celery("tasks", broker="amqp://localhost") 9 | 10 | 11 | async def fetch_weather(location): 12 | return "This is where we would call the weather service" 13 | 14 | 15 | async def post_to_slack(message, options): 16 | print(f"This is where we would post {message}") 17 | 18 | 19 | async def weather_alerts_async(): 20 | async with user_dal() as ud: 21 | query_results = await ud.get_users_with_locations() 22 | for user in query_results: 23 | user = user[0] # the database returns a tuple 24 | weather_message = await fetch_weather(user.location) 25 | username = user.slack_id 26 | if not username.startswith("@"): 27 | username = "@" + username 28 | await post_to_slack(weather_message, {"channel": username}) 29 | 30 | 31 | @celery_app.task 32 | def do_weather_alerts(): 33 | async_to_sync(weather_alerts_async)() 34 | 35 | 36 | @celery_app.on_after_configure.connect 37 | def setup_periodic_tasks(sender, **kwargs): 38 | sender.add_periodic_task( 39 | 10.0, do_weather_alerts, name="fetch the weather", expires=30 40 | ) 41 | -------------------------------------------------------------------------------- /CodeSamples/Chapter4/wtforms_example.py: -------------------------------------------------------------------------------- 1 | import quart.flask_patch 2 | 3 | from flask_wtf import FlaskForm 4 | import wtforms as f 5 | from wtforms.validators import DataRequired 6 | 7 | 8 | class UserForm(FlaskForm): 9 | email = f.StringField("email", validators=[DataRequired()]) 10 | slack_id = f.StringField("Slack ID") 11 | password = f.PasswordField("password") 12 | 13 | display = ["email", slack_id, "password"] 14 | -------------------------------------------------------------------------------- /CodeSamples/Chapter5/clean_interfaces.py: -------------------------------------------------------------------------------- 1 | ACTION_MAP = { 2 | "help": show_help_text, 3 | "weather": weather_action, 4 | "config": user_config, 5 | "get location": show_location, 6 | # TODO: Give the user a link to their profile on the web, for oauth. 7 | # "login": direct_user_to_web, 8 | # "signin": direct_user_to_web, 9 | } 10 | 11 | OUTGOING_MAP = {"slack": post_to_slack} 12 | 13 | 14 | async def show_help_text(message, metadata): 15 | return "This is some help text" 16 | 17 | 18 | async def process_message(message, metadata): 19 | """Decide on an action for a chat message. 20 | 21 | Arguments: 22 | message (str): The body of the chat message 23 | metadata (dict): Data about who sent the message, 24 | the time and channel. 25 | """ 26 | reply = None 27 | 28 | for test, action in ACTION_MAP.items(): 29 | if message.startswith(test): 30 | reply = await action(message.lstrip(test), metadata) 31 | break 32 | 33 | if reply: 34 | post_to_slack(reply, metadata) 35 | 36 | 37 | async def extract_location(text): 38 | return text.replace("weather", "").replace("in", "").strip() 39 | 40 | 41 | async def fetch_user_location(slack_id): 42 | user = db.session.query(User).filter(User.slack_id == slack_id).first() 43 | return user.location 44 | 45 | 46 | async def weather_action(text, metadata): 47 | potential_location = extract_location(text) 48 | if not potential_location: 49 | potential_location = fetch_user_location(metadata["sender"]) 50 | if potential_location: 51 | await process_weather_action(potential_location, metadata) 52 | else: 53 | await send_response("I don't know where you are", metadata) 54 | 55 | 56 | async def process_weather_action(location, metadata): 57 | reply = await fetch_weather(location) 58 | await send_response(reply, metadata) 59 | -------------------------------------------------------------------------------- /CodeSamples/Chapter5/feature_flags.py: -------------------------------------------------------------------------------- 1 | from quart import Quart, current_app 2 | 3 | app = Quart(__name__) 4 | app.config.from_json("prod_settings.json") 5 | 6 | 7 | def original_worker(): 8 | """Do some work, as an example.""" 9 | pass 10 | 11 | 12 | def new_worker(): 13 | """Do some work, as an example.""" 14 | pass 15 | 16 | 17 | @app.route("/migrating_endpoint") 18 | async def migration_example(): 19 | if current_app.config.get("USE_NEW_WORKER") is True: 20 | new_worker() 21 | else: 22 | original_worker() 23 | 24 | 25 | app.run() 26 | -------------------------------------------------------------------------------- /CodeSamples/Chapter5/logging_example.py: -------------------------------------------------------------------------------- 1 | # logging_example.py 2 | 3 | import logging 4 | 5 | # logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG) 6 | logging.basicConfig( 7 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.DEBUG 8 | ) 9 | 10 | 11 | logger = logging.getLogger(__file__) 12 | logger.setLevel(logging.DEBUG) 13 | 14 | logger.info("This is an informational message") 15 | 16 | data = {"a": 1} 17 | logger.debug("The data I am processing is %s" % data) 18 | -------------------------------------------------------------------------------- /CodeSamples/Chapter5/prod_settings.json: -------------------------------------------------------------------------------- 1 | {"DEBUG": false, "SQLURI": "postgres://username:xxx@localhost/db"} 2 | -------------------------------------------------------------------------------- /CodeSamples/Chapter5/prometheus.yml: -------------------------------------------------------------------------------- 1 | # prometheus.yml 2 | --- 3 | global: 4 | scrape_interval: 15s 5 | external_labels: 6 | monitor: 'quart-monitor' 7 | 8 | scrape_configs: 9 | - job_name: 'prometheus' 10 | scrape_interval: 5s 11 | static_configs: 12 | - targets: ['192.168.1.100:5000'] # Replace with your app's IP address 13 | labels: 14 | group: 'quart' 15 | -------------------------------------------------------------------------------- /CodeSamples/Chapter5/quart_hosted.py: -------------------------------------------------------------------------------- 1 | # quart_hosted.py 2 | from quart import Quart 3 | 4 | app = Quart(__name__) 5 | 6 | 7 | @app.route("/api") 8 | async def my_get_handler(): 9 | return {"Hello": "World!"} 10 | 11 | 12 | @app.route("/api_post", methods=["POST"]) 13 | async def my_post_handler(): 14 | return "ok", 200 15 | 16 | 17 | if __name__ == "__main__": 18 | app.run(host="REPLACE WITH YOUR IP ADDRESS") 19 | -------------------------------------------------------------------------------- /CodeSamples/Chapter5/quart_logging.py: -------------------------------------------------------------------------------- 1 | # quart_logging.py 2 | import logging 3 | from quart import Quart, request 4 | 5 | app = Quart(__name__) 6 | app.logger.setLevel(logging.DEBUG) 7 | 8 | 9 | @app.route("/hello") 10 | def hello_handler(): 11 | app.logger.info("hello_handler called") 12 | app.logger.debug(f"The request was {request}") 13 | return {"Hello": "World!"} 14 | 15 | 16 | if __name__ == "__main__": 17 | app.run() 18 | -------------------------------------------------------------------------------- /CodeSamples/Chapter5/quart_metrics.py: -------------------------------------------------------------------------------- 1 | # quart_metrics.py 2 | import asyncio 3 | from random import randint 4 | 5 | from aioprometheus import Gauge, Registry, Summary, inprogress, render, timer 6 | from quart import Quart, request 7 | 8 | app = Quart(__name__) 9 | app.registry = Registry() 10 | app.api_requests_gauge = Gauge( 11 | "quart_active_requests", "Number of active requests per endpoint" 12 | ) 13 | app.request_timer = Summary( 14 | "request_processing_seconds", "Time spent processing request" 15 | ) 16 | app.registry.register(app.api_requests_gauge) 17 | app.registry.register(app.request_timer) 18 | 19 | 20 | @app.route("/") 21 | @timer(app.request_timer, labels={"path": "/"}) 22 | @inprogress(app.api_requests_gauge, labels={"path": "/"}) 23 | async def index_handler(): 24 | await asyncio.sleep(1.0) 25 | return "index" 26 | 27 | 28 | @app.route("/endpoint1") 29 | @timer(app.request_timer, labels={"path": "/endpoint1"}) 30 | @inprogress(app.api_requests_gauge, labels={"path": "/endpoint1"}) 31 | async def endpoint1_handler(): 32 | await asyncio.sleep(randint(1000, 1500) / 1000.0) 33 | return "endpoint1" 34 | 35 | 36 | @app.route("/endpoint2") 37 | @timer(app.request_timer, labels={"path": "/endpoint2"}) 38 | @inprogress(app.api_requests_gauge, labels={"path": "/endpoint2"}) 39 | async def endpoint2_handler(): 40 | await asyncio.sleep(randint(2000, 2500) / 1000.0) 41 | return "endpoint2" 42 | 43 | 44 | @app.route("/metrics") 45 | async def handle_metrics(): 46 | return render(app.registry, request.headers.getlist("accept")) 47 | 48 | 49 | if __name__ == "__main__": 50 | app.run(host="0.0.0.0") 51 | -------------------------------------------------------------------------------- /CodeSamples/Chapter5/quart_migration.py: -------------------------------------------------------------------------------- 1 | # quart_metrics.py 2 | import asyncio 3 | from random import randint 4 | 5 | from aioprometheus import Gauge, Registry, Summary, inprogress, render, timer, Counter 6 | from quart import Quart, request 7 | import random 8 | 9 | app = Quart(__name__) 10 | app.config["NEW_WORKER_PERCENTAGE"] = 90 11 | app.registry = Registry() 12 | app.api_requests_gauge = Gauge( 13 | "quart_active_requests", "Number of active requests per endpoint" 14 | ) 15 | app.worker_usage = Counter("workers_used", "Number of active workers") 16 | app.request_timer = Summary( 17 | "request_processing_seconds", "Time spent processing request" 18 | ) 19 | app.registry.register(app.api_requests_gauge) 20 | app.registry.register(app.worker_usage) 21 | app.registry.register(app.request_timer) 22 | 23 | 24 | # @inprogress(app.worker_usage, labels={"func": "original_worker"}) 25 | async def original_worker(): 26 | app.worker_usage.inc({"func": "original_worker"}) 27 | asyncio.sleep(2.0) 28 | return "data" 29 | 30 | 31 | # @inprogress(app.worker_usage, labels={"func": "new_worker"}) 32 | async def new_worker(): 33 | app.worker_usage.inc({"func": "new_worker"}) 34 | asyncio.sleep(1.0) 35 | return "data" 36 | 37 | 38 | @app.route("/migrating_gradually") 39 | @timer(app.request_timer, labels={"path": "/migrating_gradually"}) 40 | @inprogress(app.api_requests_gauge, labels={"path": "/migrating_gradually"}) 41 | async def migrating_gradually_example(): 42 | percentage_split = app.config.get("NEW_WORKER_PERCENTAGE") 43 | if percentage_split and random.randint(1, 100) <= percentage_split: 44 | return await new_worker() 45 | else: 46 | return await original_worker() 47 | 48 | 49 | @app.route("/metrics") 50 | async def handle_metrics(): 51 | return render(app.registry, request.headers.getlist("accept")) 52 | 53 | 54 | app.run(host="REPLACE WITH YOUR IP ADDRESS") 55 | -------------------------------------------------------------------------------- /CodeSamples/Chapter5/quart_slow.py: -------------------------------------------------------------------------------- 1 | # quart_metrics.py 2 | import asyncio 3 | from random import randint 4 | 5 | from aioprometheus import Gauge, Registry, Summary, inprogress, render, timer 6 | from quart import Quart, request 7 | 8 | app = Quart(__name__) 9 | app.registry = Registry() 10 | app.api_requests_gauge = Gauge( 11 | "quart_active_requests", "Number of active requests per endpoint" 12 | ) 13 | app.request_timer = Summary( 14 | "request_processing_seconds", "Time spent processing request" 15 | ) 16 | app.registry.register(app.api_requests_gauge) 17 | app.registry.register(app.request_timer) 18 | 19 | 20 | @app.route("/") 21 | @timer(app.request_timer, labels={"path": "/"}) 22 | @inprogress(app.api_requests_gauge, labels={"path": "/"}) 23 | async def index_handler(): 24 | await asyncio.sleep(1.0) 25 | return "index" 26 | 27 | 28 | @app.route("/endpoint1") 29 | @timer(app.request_timer, labels={"path": "/endpoint1"}) 30 | @inprogress(app.api_requests_gauge, labels={"path": "/endpoint1"}) 31 | async def endpoint1_handler(): 32 | await asyncio.sleep(randint(1000, 1500) / 1000.0) 33 | return "endpoint1" 34 | 35 | 36 | @app.route("/endpoint2") 37 | @timer(app.request_timer, labels={"path": "/endpoint2"}) 38 | @inprogress(app.api_requests_gauge, labels={"path": "/endpoint2"}) 39 | async def endpoint2_handler(): 40 | await asyncio.sleep(randint(2000, 2500) / 1000.0) 41 | return "endpoint2" 42 | 43 | 44 | @app.route("/metrics") 45 | async def handle_metrics(): 46 | return render(app.registry, request.headers.getlist("accept")) 47 | 48 | 49 | app.run(host="REPLACE WITH YOUR IP ADDRESS") 50 | -------------------------------------------------------------------------------- /CodeSamples/Chapter5/quart_structlog.py: -------------------------------------------------------------------------------- 1 | # quart_structlog.py 2 | import logging 3 | from quart import Quart, request 4 | import structlog 5 | from structlog import wrap_logger 6 | from structlog.processors import JSONRenderer 7 | 8 | app = Quart(__name__) 9 | 10 | logger = wrap_logger( 11 | app.logger, 12 | processors=[ 13 | structlog.processors.add_log_level, 14 | structlog.processors.TimeStamper(), 15 | JSONRenderer(indent=4, sort_keys=True), 16 | ], 17 | ) 18 | app.logger.setLevel(logging.DEBUG) 19 | 20 | 21 | @app.route("/hello") 22 | def hello_handler(): 23 | logger.info("hello_handler called") 24 | logger.debug(f"The request was {request}") 25 | return {"Hello": "World!"} 26 | 27 | 28 | if __name__ == "__main__": 29 | app.run() 30 | -------------------------------------------------------------------------------- /CodeSamples/Chapter5/runthings.sh: -------------------------------------------------------------------------------- 1 | docker run -p 9090:9090 -v $(pwd)/prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus 2 | 3 | 4 | -------------------------------------------------------------------------------- /CodeSamples/Chapter5/structlog1.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import structlog 3 | from structlog.processors import JSONRenderer 4 | 5 | structlog.configure( 6 | processors=[ 7 | structlog.processors.add_log_level, 8 | structlog.processors.StackInfoRenderer(), 9 | structlog.dev.set_exc_info, 10 | structlog.processors.format_exc_info, 11 | structlog.processors.TimeStamper(), 12 | JSONRenderer(indent=4, sort_keys=True), 13 | ], 14 | wrapper_class=structlog.make_filtering_bound_logger(logging.NOTSET), 15 | context_class=dict, 16 | logger_factory=structlog.PrintLoggerFactory(), 17 | cache_logger_on_first_use=False, 18 | ) 19 | log = structlog.get_logger() 20 | 21 | log.msg("greeted", whom="world", more_than_a_string=[1, 2, 3]) 22 | -------------------------------------------------------------------------------- /CodeSamples/Chapter6/clientsession.py: -------------------------------------------------------------------------------- 1 | # clientsession.py 2 | import asyncio 3 | import aiohttp 4 | 5 | 6 | async def make_request(url): 7 | headers = { 8 | "Content-Type": "application/json", 9 | } 10 | async with aiohttp.ClientSession(headers=headers) as session: 11 | async with session.get(url) as response: 12 | print(await response.text()) 13 | 14 | 15 | url = "http://localhost:5000/api" 16 | loop = asyncio.get_event_loop() 17 | loop.run_until_complete(make_request(url)) 18 | -------------------------------------------------------------------------------- /CodeSamples/Chapter6/clientsession_list.py: -------------------------------------------------------------------------------- 1 | # clientsession_list.py 2 | import asyncio 3 | 4 | import aiohttp 5 | 6 | 7 | async def make_request(): 8 | urls = [ 9 | "http://localhost:5000/api", 10 | "http://localhost:5000/api2", 11 | ] 12 | headers = { 13 | "Content-Type": "application/json", 14 | } 15 | async with aiohttp.ClientSession(headers=headers) as session: 16 | for url in urls: 17 | async with session.get(url) as response: 18 | print(await response.text()) 19 | 20 | 21 | loop = asyncio.get_event_loop() 22 | loop.run_until_complete(make_request()) 23 | -------------------------------------------------------------------------------- /CodeSamples/Chapter6/globalsession.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import aiohttp 4 | from quart import Quart, g as quart_globals 5 | 6 | 7 | async def get_connector(): 8 | if "clientsession" not in quart_globals: 9 | headers = {"Content-Type": "application/json"} 10 | quart_globals.clientsession = aiohttp.ClientSession(headers=headers) 11 | return quart_globals.clientsession 12 | 13 | 14 | app = Quart(__name__) 15 | 16 | 17 | @app.route("/api") 18 | async def my_microservice(): 19 | conn = await get_connector() 20 | # conn = aiohttp.ClientSession() 21 | async with conn.get("http://localhost:5000/api") as response: 22 | sub_result = await response.json() 23 | return {"result": sub_result, "Hello": "World!"} 24 | 25 | 26 | if __name__ == "__main__": 27 | app.run(port=5001) 28 | -------------------------------------------------------------------------------- /CodeSamples/Chapter6/gzip_example.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import aiohttp 4 | 5 | 6 | async def make_request(): 7 | url = "http://127.0.0.1:8080/api" 8 | headers = { 9 | "Content-Type": "application/json", 10 | "Accept-Encoding": "gzip", 11 | } 12 | async with aiohttp.ClientSession(headers=headers) as session: 13 | async with session.get(url) as response: 14 | print(await response.text()) 15 | 16 | 17 | loop = asyncio.get_event_loop() 18 | loop.run_until_complete(make_request()) 19 | -------------------------------------------------------------------------------- /CodeSamples/Chapter6/gzip_example_post.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import gzip 3 | import json 4 | 5 | import aiohttp 6 | 7 | 8 | async def make_request(): 9 | url = "http://127.0.0.1:5000/api_post" 10 | headers = { 11 | "Content-Encoding": "gzip", 12 | } 13 | data = {"Hello": "World!", "result": "OK"} 14 | data = bytes(json.dumps(data), "utf8") 15 | data = gzip.compress(data) 16 | async with aiohttp.ClientSession(headers=headers) as session: 17 | async with session.post(url, data=data) as response: 18 | print(await response.text()) 19 | 20 | 21 | loop = asyncio.get_event_loop() 22 | loop.run_until_complete(make_request()) 23 | -------------------------------------------------------------------------------- /CodeSamples/Chapter6/gzip_server.py: -------------------------------------------------------------------------------- 1 | # gzip_server.py 2 | # Receive data from gzip_example_post.py 3 | 4 | from quart import Quart, request 5 | import gzip 6 | 7 | app = Quart(__name__) 8 | 9 | 10 | @app.route("/api_post", methods=["POST"]) 11 | async def receive_gzip_data(): 12 | if request.headers["Content-Encoding"] == "gzip": 13 | return gzip.decompress(await request.data) 14 | else: 15 | return await request.data 16 | 17 | 18 | app.run() 19 | -------------------------------------------------------------------------------- /CodeSamples/Chapter6/openapi.yml: -------------------------------------------------------------------------------- 1 | --- 2 | openapi: "3.0.0" 3 | info: 4 | title: Data Service 5 | description: returns info about users 6 | license: 7 | name: APLv2 8 | url: https://www.apache.org/licenses/LICENSE-2.0.html 9 | version: 0.1.0 10 | basePath: /api 11 | paths: 12 | /user_ids: 13 | get: 14 | operationId: getUserIds 15 | description: Returns a list of ids 16 | produces: 17 | - application/json 18 | responses: 19 | '200': 20 | description: List of Ids 21 | schema: 22 | type: array 23 | items: 24 | type: integer 25 | -------------------------------------------------------------------------------- /CodeSamples/Chapter6/pika_sender.py: -------------------------------------------------------------------------------- 1 | from pika import BlockingConnection, BasicProperties 2 | 3 | 4 | # assuming there's a working local RabbitMQ server with a working guest/guest account 5 | def message(topic, message): 6 | connection = BlockingConnection() 7 | try: 8 | channel = connection.channel() 9 | props = BasicProperties(content_type="text/plain", delivery_mode=1) 10 | channel.basic_publish("incoming", topic, message, props) 11 | finally: 12 | connection.close() 13 | 14 | 15 | message("publish.playstore", "We are publishing an Android App!") 16 | 17 | message("publish.newsletter", "We are publishing a newsletter!") 18 | -------------------------------------------------------------------------------- /CodeSamples/Chapter6/playstore_receiver.py: -------------------------------------------------------------------------------- 1 | import pika 2 | 3 | 4 | def on_message(channel, method_frame, header_frame, body): 5 | print(f"Now publishing to the play store: {body}!") 6 | channel.basic_ack(delivery_tag=method_frame.delivery_tag) 7 | 8 | 9 | connection = pika.BlockingConnection() 10 | channel = connection.channel() 11 | channel.basic_consume("playstore", on_message) 12 | try: 13 | channel.start_consuming() 14 | except KeyboardInterrupt: 15 | channel.stop_consuming() 16 | 17 | connection.close() 18 | -------------------------------------------------------------------------------- /CodeSamples/Chapter6/publish_receiver.py: -------------------------------------------------------------------------------- 1 | import pika 2 | 3 | 4 | def on_message(channel, method_frame, header_frame, body): 5 | print(f"We have some news! {body}!") 6 | channel.basic_ack(delivery_tag=method_frame.delivery_tag) 7 | 8 | 9 | connection = pika.BlockingConnection() 10 | channel = connection.channel() 11 | channel.basic_consume("notifications", on_message) 12 | try: 13 | channel.start_consuming() 14 | except KeyboardInterrupt: 15 | channel.stop_consuming() 16 | 17 | connection.close() 18 | -------------------------------------------------------------------------------- /CodeSamples/Chapter6/quart_etag.py: -------------------------------------------------------------------------------- 1 | # quart_etag.py 2 | from datetime import datetime 3 | 4 | from quart import Quart, Response, abort, jsonify, request 5 | 6 | app = Quart(__name__) 7 | 8 | 9 | def _time2etag(): 10 | return datetime.now().isoformat() 11 | 12 | 13 | _USERS = {"1": {"name": "Simon", "modified": _time2etag()}} 14 | 15 | 16 | @app.route("/api/user/") 17 | async def get_user(user_id): 18 | if user_id not in _USERS: 19 | return abort(404) 20 | user = _USERS[user_id] 21 | 22 | # returning 304 if If-None-Match matches 23 | if user["modified"] in request.if_none_match: 24 | return Response("Not modified", status=304) 25 | 26 | resp = jsonify(user) 27 | 28 | # setting the ETag 29 | resp.set_etag(user["modified"]) 30 | return resp 31 | 32 | 33 | if __name__ == "__main__": 34 | app.run() 35 | -------------------------------------------------------------------------------- /CodeSamples/Chapter6/requirements.txt: -------------------------------------------------------------------------------- 1 | # Dependencies for Chapter 6 code samples 2 | aiohttp==3.7.4.post0 3 | Quart==0.15.1 4 | pika==1.2.0 5 | pytest==6.2.4 6 | aioresponses==0.7.2 7 | pytest-asyncio==0.15.1 8 | -------------------------------------------------------------------------------- /CodeSamples/Chapter6/semaphores.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import aiohttp 4 | 5 | 6 | async def make_request(url, session, semaphore): 7 | async with semaphore, session.get(url) as response: 8 | print(f"Fetching {url}") 9 | await asyncio.sleep(1) 10 | return await response.text() 11 | 12 | 13 | async def organise_requests(url_list): 14 | semaphore = asyncio.Semaphore(3) 15 | tasks = list() 16 | 17 | async with aiohttp.ClientSession() as session: 18 | for url in url_list: 19 | tasks.append(make_request(url, session, semaphore)) 20 | 21 | await asyncio.gather(*tasks) 22 | 23 | 24 | urls = [ 25 | "https://www.google.com", 26 | "https://developer.mozilla.org/en-US/", 27 | "https://www.packtpub.com/", 28 | "https://aws.amazon.com/", 29 | ] 30 | loop = asyncio.get_event_loop() 31 | loop.run_until_complete(organise_requests(urls)) 32 | -------------------------------------------------------------------------------- /CodeSamples/Chapter6/test_aiohttp.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import aiohttp 3 | import pytest 4 | from aioresponses import aioresponses 5 | 6 | 7 | @pytest.mark.asyncio 8 | async def test_ctx(): 9 | with aioresponses() as mocked: 10 | async with aiohttp.ClientSession() as session: 11 | mocked.get("http://test.example.com", payload={"foo": "bar"}) 12 | resp = await session.get("http://test.example.com") 13 | data = await resp.json() 14 | 15 | assert {"foo": "bar"} == data 16 | -------------------------------------------------------------------------------- /CodeSamples/Chapter6/test_aiohttp_fixture.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import aiohttp 3 | import pytest 4 | from aioresponses import aioresponses 5 | 6 | 7 | @pytest.fixture 8 | def mock_aioresponse(): 9 | with aioresponses() as m: 10 | yield m 11 | 12 | 13 | @pytest.mark.asyncio 14 | async def test_ctx(mock_aioresponse): 15 | async with aiohttp.ClientSession() as session: 16 | mock_aioresponse.get("http://test.example.com", payload={"foo": "bar"}) 17 | resp = await session.get("http://test.example.com") 18 | data = await resp.json() 19 | 20 | assert {"foo": "bar"} == data 21 | -------------------------------------------------------------------------------- /CodeSamples/Chapter7/auth_caller.py: -------------------------------------------------------------------------------- 1 | # auth_caller.py 2 | _TOKEN = None 3 | 4 | 5 | def get_auth_header(new=False): 6 | global _TOKEN 7 | if _TOKEN is None or new: 8 | _TOKEN = get_token() 9 | return "Bearer " + _TOKEN 10 | 11 | 12 | _dataservice = "http://localhost:5001" 13 | 14 | 15 | def _call_service(endpoint, token): 16 | # not using session and other tools, to simplify the code 17 | url = _dataservice + "/" + endpoint 18 | headers = {"Authorization": token} 19 | return requests.get(url, headers=headers) 20 | 21 | 22 | def call_data_service(endpoint): 23 | token = get_auth_header() 24 | response = _call_service(endpoint, token) 25 | if response.status_code == 401: 26 | # the token might be revoked, let's try with a fresh one 27 | token = get_auth_header(new=True) 28 | response = _call_service(endpoint, token) 29 | return response 30 | -------------------------------------------------------------------------------- /CodeSamples/Chapter7/fetch_token.py: -------------------------------------------------------------------------------- 1 | # fetch_token.py 2 | import requests 3 | 4 | TOKENDEALER_SERVER = "http://localhost:5000" 5 | SECRET = "f0fdeb1f1584fd5431c4250b2e859457" 6 | 7 | 8 | def get_token(): 9 | data = { 10 | "client_id": "worker1", 11 | "client_secret": SECRET, 12 | "audience": "jeeves.domain", 13 | "grant_type": "client_credentials", 14 | } 15 | headers = {"Content-Type": "application/x-www-form-urlencoded"} 16 | url = tokendealer_server + "/oauth/token" 17 | response = requests.post(url, data=data, headers=headers) 18 | return response.json()["access_token"] 19 | 20 | 21 | def verify_token(token): 22 | url = tokendealer_server + "/verify_token" 23 | response = requests.post( 24 | url, data={"access_token": token, "audience": "jeeves.domain"} 25 | ) 26 | return response.json() 27 | 28 | 29 | if __name__ == "__main__": 30 | token = get_token() 31 | print(token) 32 | print(verify_token(token)) 33 | -------------------------------------------------------------------------------- /CodeSamples/Chapter7/flask_debug.py: -------------------------------------------------------------------------------- 1 | # flask_debug.py for vulnerability checking. 2 | # Uses unsafe setting for production 3 | from flask import Flask 4 | 5 | app = Flask(__name__) 6 | 7 | 8 | @app.route("/api") 9 | def my_microservice(): 10 | return {"Hello": "World!"} 11 | 12 | 13 | if __name__ == "__main__": 14 | app.run(debug=True) 15 | -------------------------------------------------------------------------------- /CodeSamples/Chapter7/jwt_decode.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | 4 | def decode(data): 5 | # adding extra = for padding if needed 6 | pad = len(data) % 4 7 | if pad > 0: 8 | data += "=" * (4 - pad) 9 | return base64.urlsafe_b64decode(data) 10 | -------------------------------------------------------------------------------- /CodeSamples/Chapter7/jwt_tokens.py: -------------------------------------------------------------------------------- 1 | import jwt 2 | 3 | 4 | def create_token(alg="HS256", secret="secret", data=None): 5 | return jwt.encode(data, secret, algorithm=alg) 6 | 7 | 8 | def read_token(token, secret="secret", algs=["HS256"]): 9 | return jwt.decode(token, secret, algorithms=algs) 10 | 11 | 12 | token = create_token(data={"some": "data", "inthe": "token"}) 13 | 14 | print(token) 15 | # eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoiZGF0YSIsImludGhlIjoidG9rZW4ifQ.vMHiSS_vk-Z3gMMxcM22Ssjk3vW3aSmJXQ8YCSCwFu4 16 | print(read_token(token)) 17 | -------------------------------------------------------------------------------- /CodeSamples/Chapter7/openresty/resty.conf: -------------------------------------------------------------------------------- 1 | # resty.conf 2 | daemon off; 3 | worker_processes 1; 4 | error_log /dev/stdout info; 5 | events { 6 | worker_connections 1024; 7 | } 8 | http { 9 | access_log /dev/stdout; 10 | server { 11 | listen 8888; 12 | server_name localhost; 13 | location / { 14 | proxy_pass http://localhost:5000; 15 | proxy_set_header Host $host; 16 | proxy_set_header X-Real-IP $remote_addr; 17 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 18 | } 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /CodeSamples/Chapter7/openresty/resty_limiting.conf: -------------------------------------------------------------------------------- 1 | # resty_limiting.conf 2 | daemon off; 3 | worker_processes 1; 4 | error_log /dev/stdout info; 5 | events { 6 | worker_connections 1024; 7 | } 8 | http { 9 | lua_shared_dict my_limit_req_store 100m; 10 | 11 | server { 12 | listen 8888; 13 | server_name localhost; 14 | access_log /dev/stdout; 15 | location / { 16 | access_by_lua_block { 17 | local limit_req = require "resty.limit.req" 18 | local lim, err = limit_req.new("my_limit_req_store", 200, 100) 19 | local key = ngx.var.binary_remote_addr 20 | local delay, err = lim:incoming(key, true) 21 | if not delay then 22 | if err == "rejected" then 23 | return ngx.exit(503) 24 | end 25 | ngx.log(ngx.ERR, "failed to limit req: ", err) 26 | return ngx.exit(500) 27 | end 28 | 29 | if delay >= 0.001 then 30 | local excess = err 31 | ngx.sleep(delay) 32 | end 33 | } 34 | proxy_pass http://localhost:5000; 35 | proxy_set_header Host $host; 36 | proxy_set_header X-Real-IP $remote_addr; 37 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 38 | } 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /CodeSamples/Chapter7/quart_after_response.py: -------------------------------------------------------------------------------- 1 | # quart_after_response.py 2 | from quart import Quart, redirect 3 | from quart.helpers import make_response 4 | from urllib.parse import urlparse 5 | 6 | app = Quart(__name__) 7 | 8 | 9 | @app.route("/api") 10 | async def my_microservice(): 11 | return redirect("https://github.com:443/") 12 | 13 | 14 | # domain:port 15 | SAFE_DOMAINS = ["github.com:443", "google.com:443"] 16 | 17 | 18 | @app.after_request 19 | async def check_redirect(response): 20 | if response.status_code != 302: 21 | return response 22 | url = urlparse(response.location) 23 | netloc = url.netloc 24 | if netloc not in SAFE_DOMAINS: 25 | # not using abort() here or it'll break the hook 26 | return await make_response("Forbidden", 403) 27 | return response 28 | 29 | 30 | if __name__ == "__main__": 31 | app.run(debug=True) 32 | -------------------------------------------------------------------------------- /CodeSamples/Chapter7/quart_cors_example.py: -------------------------------------------------------------------------------- 1 | # quart_cors.py 2 | from quart import Quart 3 | from quart_cors import cors, route_cors 4 | 5 | app = Quart(__name__) 6 | app = cors(app) 7 | 8 | # @route_cors(allow_origin="*") 9 | 10 | 11 | @app.route("/api") 12 | async def my_microservice(): 13 | return {"Hello": "World!"} 14 | 15 | 16 | if __name__ == "__main__": 17 | app.config["QUART_CORS_ALLOW_ORIGIN"] = ["http://quart.com"] 18 | # app = cors(app, allow_origin=["http://befuddle.flummox.org:5200"]) 19 | print(app.config) 20 | app.run(port=5200) 21 | -------------------------------------------------------------------------------- /CodeSamples/Chapter7/quart_debug.py: -------------------------------------------------------------------------------- 1 | # quart_debug.py 2 | from quart import Quart 3 | 4 | app = Quart(__name__) 5 | 6 | 7 | @app.route("/api") 8 | def my_microservice(): 9 | return {"Hello": "World!"} 10 | 11 | 12 | if __name__ == "__main__": 13 | app.run(debug=True) 14 | -------------------------------------------------------------------------------- /CodeSamples/Chapter7/quart_uber.py: -------------------------------------------------------------------------------- 1 | from quart import Quart, request, render_template_string 2 | 3 | app = Quart(__name__) 4 | 5 | SECRET = "oh no!" 6 | 7 | _TEMPLATE = """ 8 | Hello %s 9 | 10 | Welcome to my API! 11 | """ 12 | 13 | 14 | class Extra: 15 | def __init__(self, data): 16 | self.data = data 17 | 18 | 19 | # @app.route('/') 20 | @app.route("/") 21 | async def my_microservice(): 22 | user_id = request.args.get("user_id", "Anonymous") 23 | tmpl = _TEMPLATE % user_id 24 | return await render_template_string(tmpl, extra=Extra("something")) 25 | 26 | 27 | app.run() 28 | -------------------------------------------------------------------------------- /CodeSamples/Chapter7/tokendealer.py: -------------------------------------------------------------------------------- 1 | import time 2 | from hmac import compare_digest 3 | 4 | import jwt 5 | from quart import Quart, abort, current_app, request, jsonify 6 | from werkzeug.exceptions import HTTPException 7 | 8 | app = Quart(__name__) 9 | 10 | app.config["TOKENDEALER_URL"] = "https://tokendealer.example.com" 11 | 12 | _SECRETS = {"worker1": "f0fdeb1f1584fd5431c4250b2e859457"} 13 | 14 | 15 | def bad_request(desc): 16 | exc = HTTPException() 17 | exc.code = 400 18 | exc.description = desc 19 | return error_handling(exc) 20 | 21 | 22 | def error_handling(error): 23 | error_result = { 24 | "code": error.code, 25 | "description": error.description, 26 | "message": str(error), 27 | } 28 | resp = jsonify(error_result) 29 | resp.status_code = error_result["code"] 30 | return resp 31 | 32 | 33 | @app.route("/.well-known/jwks.json") 34 | async def _jwks(): 35 | """Returns the public key in the Json Web Key Set (JWKS) format""" 36 | with open(current_app.config["PUBLIC_KEY_PATH"]) as f: 37 | key = f.read().strip() 38 | data = { 39 | "alg": "RS512", 40 | "e": "AQAB", 41 | "n": key, 42 | "kty": "RSA", 43 | "use": "sig", 44 | } 45 | 46 | return jsonify({"keys": [data]}) 47 | 48 | 49 | def is_authorized_app(client_id, client_secret): 50 | return compare_digest(_SECRETS.get(client_id), client_secret) 51 | 52 | 53 | @app.route("/oauth/token", methods=["POST"]) 54 | async def create_token(): 55 | with open(current_app.config["PRIVATE_KEY_PATH"]) as f: 56 | key = f.read().strip() 57 | try: 58 | data = await request.form 59 | if data.get("grant_type") != "client_credentials": 60 | return bad_request(f"Wrong grant_type {data.get('grant_type')}") 61 | 62 | client_id = data.get("client_id") 63 | client_secret = data.get("client_secret") 64 | aud = data.get("audience", "") 65 | 66 | if not is_authorized_app(client_id, client_secret): 67 | return abort(401) 68 | 69 | now = int(time.time()) 70 | 71 | token = { 72 | "iss": current_app.config["TOKENDEALER_URL"], 73 | "aud": aud, 74 | "iat": now, 75 | "exp": now + 3600 * 24, 76 | } 77 | token = jwt.encode(token, key, algorithm="RS512") 78 | return {"access_token": token} 79 | except Exception as e: 80 | return bad_request("Unable to create a token") 81 | 82 | 83 | @app.route("/verify_token", methods=["POST"]) 84 | async def verify_token(): 85 | with open(current_app.config["PUBLIC_KEY_PATH"]) as f: 86 | key = f.read() 87 | try: 88 | input_data = await request.form 89 | token = input_data["access_token"] 90 | audience = input_data.get("audience", "") 91 | return jwt.decode(token, key, algorithms=["RS512"], audience=audience) 92 | except Exception as e: 93 | return bad_request("Unable to verify the token") 94 | -------------------------------------------------------------------------------- /CodeSamples/Chapter7/vulnerable.py: -------------------------------------------------------------------------------- 1 | # vulnerable.py 2 | import subprocess 3 | from sqlalchemy import create_engine 4 | from sqlalchemy.orm import sessionmaker 5 | import yaml 6 | 7 | 8 | def read_file(filename): 9 | with open(filename) as f: 10 | data = yaml.load(f.read()) 11 | 12 | 13 | def run_command(cmd): 14 | return subprocess.check_call(cmd, shell=True) 15 | 16 | 17 | db = create_engine("sqlite:///somedatabase") 18 | Session = sessionmaker(bind=db) 19 | 20 | 21 | def get_user(uid): 22 | session = Session() 23 | query = "select * from user where id='%s'" % uid 24 | return session.execute(query) 25 | -------------------------------------------------------------------------------- /CodeSamples/Chapter8/quart_cors_example.py: -------------------------------------------------------------------------------- 1 | # quart_cors.py 2 | from quart import Quart 3 | from quart_cors import cors, route_cors 4 | 5 | app = Quart(__name__) 6 | app = cors(app, allow_origin="https://quart.com") 7 | # app = cors(app, allow_origin="*") 8 | 9 | 10 | @app.route("/api") 11 | # @route_cors(allow_origin=["https://quart.com"]) 12 | async def my_microservice(): 13 | return {"Hello": "World!"} 14 | 15 | 16 | if __name__ == "__main__": 17 | # app.config["QUART_CORS_ALLOW_ORIGIN"] = ["http://quart.com"] 18 | # app = cors(app, allow_origin=["http://befuddle.flummox.org:5200"]) 19 | print(app.config) 20 | app.run(port=5200) 21 | -------------------------------------------------------------------------------- /CodeSamples/Chapter8/react_intro.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /CodeSamples/Chapter8/static-example/person_example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |

Jeeves Dashboard

11 |
12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /CodeSamples/Chapter8/static-example/quart_serve_data.py: -------------------------------------------------------------------------------- 1 | # quart_serve_data.py 2 | from quart import Quart, render_template, jsonify 3 | 4 | app = Quart(__name__) 5 | 6 | 7 | @app.route("/users") 8 | async def show_users_page(): 9 | return await render_template("person_example.html") 10 | 11 | 12 | @app.route("/api/users") 13 | async def serve_pretend_userdata(): 14 | return jsonify([{"name": "Alice", "email": "alice@example.com"}]) 15 | 16 | 17 | if __name__ == "__main__": 18 | app.run() 19 | -------------------------------------------------------------------------------- /CodeSamples/Chapter8/static-example/static/people.jsx: -------------------------------------------------------------------------------- 1 | 2 | class Person extends React.Component { 3 | render() { 4 | return ( 5 |
{this.props.name} ({this.props.email})
6 | ); 7 | } 8 | } 9 | 10 | class People extends React.Component { 11 | render() { 12 | var peopleNodes = this.props.data.map(function (person) { 13 | return ( 14 | 19 | ); 20 | }); 21 | return ( 22 |
23 | {peopleNodes} 24 |
25 | ); 26 | } 27 | } 28 | 29 | class PeopleBox extends React.Component { 30 | constructor(props) { 31 | super(props); 32 | this.state = { data: [] }; 33 | } 34 | 35 | loadPeopleFromServer() { 36 | fetch('http://localhost:5000/api/users') 37 | .then(response => response.json()) 38 | .then(data => { 39 | console.log(data); 40 | this.setState({ 41 | data: data, 42 | }); 43 | console.log(this.state); 44 | }) 45 | .catch(function (error) { 46 | console.log(error); 47 | }); 48 | } 49 | componentDidMount() { 50 | this.loadPeopleFromServer(); 51 | } 52 | render() { 53 | return ( 54 |
55 |

People

56 | 57 |
58 | ); 59 | } 60 | 61 | } 62 | 63 | const domContainer = document.querySelector('#people_list'); 64 | ReactDOM.render(React.createElement(PeopleBox), domContainer); 65 | -------------------------------------------------------------------------------- /CodeSamples/Chapter8/static/people.jsx: -------------------------------------------------------------------------------- 1 | 2 | class Person extends React.Component { 3 | render() { 4 | return ( 5 |
{this.props.name} ({this.props.email})
6 | ); 7 | } 8 | } 9 | 10 | class People extends React.Component { 11 | render() { 12 | var peopleNodes = this.props.data.map(function (person) { 13 | return ( 14 | 19 | ); 20 | }); 21 | return ( 22 |
23 | {peopleNodes} 24 |
25 | ); 26 | } 27 | } 28 | 29 | class PeopleBox extends React.Component { 30 | constructor(props) { 31 | super(props); 32 | this.state = { data: [] }; 33 | } 34 | 35 | loadPeopleFromServer() { 36 | fetch('http://localhost:5000/api/users') 37 | .then(response => response.json()) 38 | .then(data => { 39 | console.log(data); 40 | this.setState({ 41 | data: data, 42 | }); 43 | console.log(this.state); 44 | }) 45 | .catch(function (error) { 46 | console.log(error); 47 | }); 48 | } 49 | componentDidMount() { 50 | this.loadPeopleFromServer(); 51 | } 52 | render() { 53 | return ( 54 |
55 |

People

56 | 57 |
58 | ); 59 | } 60 | 61 | } 62 | 63 | const domContainer = document.querySelector('#people_list'); 64 | ReactDOM.render(React.createElement(PeopleBox), domContainer); 65 | -------------------------------------------------------------------------------- /CodeSamples/Chapter8/templates/person_example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |

Jeeves Dashboard

11 |
12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /CodeSamples/Chapter8/transpiled-example/babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-react"] 3 | } 4 | 5 | 6 | -------------------------------------------------------------------------------- /CodeSamples/Chapter8/transpiled-example/js-src/people.jsx: -------------------------------------------------------------------------------- 1 | 2 | class Person extends React.Component { 3 | render() { 4 | return ( 5 |
6 | {this.props.name} ({this.props.email}) 7 |
8 | ); 9 | } 10 | } 11 | 12 | class People extends React.Component { 13 | render() { 14 | var peopleNodes = this.props.data.map(function (person) { 15 | return ( 16 | 21 | ); 22 | }); 23 | return ( 24 |
25 | {peopleNodes} 26 |
27 | ); 28 | } 29 | } 30 | 31 | class PeopleBox extends React.Component { 32 | constructor(props) { 33 | super(props); 34 | this.state = { data: [] }; 35 | } 36 | 37 | loadPeopleFromServer() { 38 | fetch('http://localhost:5000/api/users') 39 | .then(response => response.json()) 40 | .then(data => { 41 | console.log(data); 42 | this.setState({ 43 | data: data, 44 | }); 45 | console.log(this.state); 46 | }) 47 | .catch(function (error) { 48 | console.log(error); 49 | }); 50 | } 51 | componentDidMount() { 52 | this.loadPeopleFromServer(); 53 | } 54 | render() { 55 | return ( 56 |
57 |

People

58 | 59 |
60 | ); 61 | } 62 | 63 | } 64 | 65 | const domContainer = document.querySelector('#people_list'); 66 | ReactDOM.render(React.createElement(PeopleBox), domContainer); 67 | -------------------------------------------------------------------------------- /CodeSamples/Chapter8/transpiled-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@babel/cli": "^7.14.8", 4 | "@babel/core": "^7.14.8", 5 | "@babel/preset-env": "^7.14.8", 6 | "@babel/preset-react": "^7.14.5" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /CodeSamples/Chapter8/transpiled-example/person_example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |

Jeeves Dashboard

11 |
12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /CodeSamples/Chapter8/transpiled-example/quart_serve_data.py: -------------------------------------------------------------------------------- 1 | # quart_serve_data.py 2 | from quart import Quart, render_template, jsonify 3 | 4 | app = Quart(__name__) 5 | 6 | 7 | @app.route("/users") 8 | async def show_users_page(): 9 | return await render_template("person_example.html") 10 | 11 | 12 | @app.route("/api/users") 13 | async def serve_pretend_userdata(): 14 | return jsonify([{"name": "Alice", "email": "alice@example.com"}]) 15 | 16 | 17 | if __name__ == "__main__": 18 | app.run() 19 | -------------------------------------------------------------------------------- /CodeSamples/Chapter8/transpiled-example/static/people.js: -------------------------------------------------------------------------------- 1 | class Person extends React.Component { 2 | render() { 3 | return /*#__PURE__*/React.createElement("div", null, this.props.name, " (", this.props.email, ")"); 4 | } 5 | 6 | } 7 | 8 | class People extends React.Component { 9 | render() { 10 | var peopleNodes = this.props.data.map(function (person) { 11 | return /*#__PURE__*/React.createElement(Person, { 12 | key: person.email, 13 | name: person.name, 14 | email: person.email 15 | }); 16 | }); 17 | return /*#__PURE__*/React.createElement("div", null, peopleNodes); 18 | } 19 | 20 | } 21 | 22 | class PeopleBox extends React.Component { 23 | constructor(props) { 24 | super(props); 25 | this.state = { 26 | data: [] 27 | }; 28 | } 29 | 30 | loadPeopleFromServer() { 31 | fetch('http://localhost:5000/api/users').then(response => response.json()).then(data => { 32 | console.log(data); 33 | this.setState({ 34 | data: data 35 | }); 36 | console.log(this.state); 37 | }).catch(function (error) { 38 | console.log(error); 39 | }); 40 | } 41 | 42 | componentDidMount() { 43 | this.loadPeopleFromServer(); 44 | } 45 | 46 | render() { 47 | return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("h2", null, "People"), /*#__PURE__*/React.createElement(People, { 48 | data: this.state.data 49 | })); 50 | } 51 | 52 | } 53 | 54 | const domContainer = document.querySelector('#people_list'); 55 | ReactDOM.render(React.createElement(PeopleBox), domContainer); -------------------------------------------------------------------------------- /CodeSamples/Chapter9/README.rst: -------------------------------------------------------------------------------- 1 | long description! 2 | -------------------------------------------------------------------------------- /CodeSamples/Chapter9/circus.ini: -------------------------------------------------------------------------------- 1 | [watcher:web] 2 | cmd = hypercorn --bind fd://$(circus.sockets.web) server.app 3 | use_sockets = True 4 | numprocesses = 5 5 | 6 | [socket:web] 7 | host = 0.0.0.0 8 | port = 8000 9 | 10 | -------------------------------------------------------------------------------- /CodeSamples/Chapter9/hypercorn_server.py: -------------------------------------------------------------------------------- 1 | # hypercorn_server.py 2 | from quart import Quart 3 | 4 | app = Quart(__name__) 5 | 6 | 7 | @app.route("/api") 8 | def my_microservice(): 9 | return {"Hello": "World!"} 10 | -------------------------------------------------------------------------------- /CodeSamples/Chapter9/requirements.in: -------------------------------------------------------------------------------- 1 | aiohttp 2 | arrow 3 | python-dateutil 4 | pytz 5 | quart 6 | requests 7 | six 8 | units 9 | -------------------------------------------------------------------------------- /CodeSamples/Chapter9/setup-example/README.rst: -------------------------------------------------------------------------------- 1 | long description! 2 | -------------------------------------------------------------------------------- /CodeSamples/Chapter9/setup-example/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open("README.rst") as f: 4 | LONG_DESC = f.read() 5 | 6 | setup( 7 | name="MyProject", 8 | version="1.0.0", 9 | url="http://example.com", 10 | description="This is a cool microservice based on Quart.", 11 | long_description=LONG_DESC, 12 | author="Simon", 13 | author_email="simon@flmx.org", 14 | license="MIT", 15 | classifiers=[ 16 | "Development Status :: 3 - Alpha", 17 | "License :: OSI Approved :: MIT License", 18 | "Programming Language :: Python :: 3", 19 | ], 20 | keywords=["quart", "microservice"], 21 | packages=find_packages(), 22 | include_package_data=True, 23 | zip_safe=False, 24 | install_requires=["quart"], 25 | ) 26 | -------------------------------------------------------------------------------- /CodeSamples/README.md: -------------------------------------------------------------------------------- 1 | 2 | The files under CodeSamples are copies of the code that is found throughout the book 3 | Python Microservices Development, 2nd Edition. 4 | 5 | We recommend creating a virtual environment, and using pip to install the required packaages: 6 | 7 | ``` 8 | pip install -r requirements.txt 9 | ``` 10 | 11 | Most of the examples can be run using python: 12 | 13 | ``` 14 | python example_name.py 15 | ``` 16 | -------------------------------------------------------------------------------- /ERRATA.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Errata and changes for Python Microservices Development, 2nd Edition 4 | 5 | # Chapter 9 - Packaging and Running Python 6 | 7 | ## Page 226 8 | 9 | The list of files to include in a project mentions `pyproject.toml`, but 10 | describes `setup.py`. Given the text in the chapter, the list should include 11 | `setup.py`, although more recent Python practices include using 12 | `pyproject.toml` instead. Please consult the [pip 13 | documentation](https://pip.pypa.io/en/stable/reference/build-system/pyproject-toml/) 14 | for further information. 15 | 16 | The list of files to include in a project should also include `MANIFEST.in`, 17 | which while not required, can be a good idea, and is described in the chapter. 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Packt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Python-Microservices-Development-2nd-Edition 5 | Python Microservices Development – 2nd edition, published by Packt 6 | ### Download a free PDF 7 | 8 | If you have already purchased a print or Kindle version of this book, you can get a DRM-free PDF version at no cost.
Simply click on the link to claim your free PDF.
9 |

https://packt.link/free-ebook/9781801076302

-------------------------------------------------------------------------------- /authservice/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | include README.rst 3 | recursive-include authservice *.ini 4 | recursive-include docs *.rst *.png *.svg *.css *.html conf.py 5 | prune docs/build/* 6 | -------------------------------------------------------------------------------- /authservice/Makefile: -------------------------------------------------------------------------------- 1 | HERE = $(shell pwd) 2 | VENV = . 3 | VIRTUALENV = virtualenv 4 | BIN = $(VENV)/bin 5 | PYTHON = $(BIN)/python 6 | 7 | INSTALL = $(BIN)/pip install --no-deps 8 | 9 | .PHONY: all test docs build_extras 10 | 11 | all: build 12 | 13 | $(PYTHON): 14 | $(VIRTUALENV) $(VTENV_OPTS) $(VENV) 15 | 16 | build: $(PYTHON) 17 | $(PYTHON) setup.py develop 18 | 19 | clean: 20 | rm -rf $(VENV) 21 | 22 | test_dependencies: 23 | $(BIN)/pip install flake8 tox 24 | 25 | test: build test_dependencies 26 | $(BIN)/flake8 authservice 27 | $(BIN)/tox 28 | 29 | run: 30 | QUART_APP=authservice bin/quart run 31 | -------------------------------------------------------------------------------- /authservice/README.rst: -------------------------------------------------------------------------------- 1 | microservice-skeleton 2 | ===================== 3 | 4 | **DISCLAIMER** This repository is part of an application made for 5 | the Python Microservices Development. It was made for educational 6 | purpose and not suitable for production. It's still being updated. 7 | If you find any issue or want to talk with the author, feel free to 8 | open an issue in the issue tracker. 9 | 10 | 11 | This project is a template for building microservices with Quart. 12 | 13 | .. image:: https://coveralls.io/repos/github/PythonMicroservices/microservice-skeleton/badge.svg?branch=main 14 | :target: https://coveralls.io/github/PythonMicroservices/microservice-skeleton?branch=main 15 | 16 | .. image:: https://github.com/PythonMicroservices/microservice-skeleton/workflows/Python%20Testing/badge.svg 17 | :target: https://github.com/PythonMicroservices/microservice-skeleton/actions 18 | 19 | .. image:: https://readthedocs.org/projects/microservice/badge/?version=latest 20 | :target: https://microservice.readthedocs.io 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /authservice/authservice/__init__.py: -------------------------------------------------------------------------------- 1 | from authservice.app import app # NOQA 2 | -------------------------------------------------------------------------------- /authservice/authservice/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | from authservice.views import blueprints 3 | from quart import Quart 4 | import asyncio 5 | 6 | _HERE = os.path.dirname(__file__) 7 | SETTINGS = os.path.join(_HERE, "settings.py") 8 | 9 | 10 | async def create_app(name=__name__, blueprints=None, settings=None): 11 | app = Quart(name) 12 | 13 | # load configuration 14 | settings = os.environ.get("QUART_SETTINGS", settings) 15 | if settings is not None: 16 | app.config.from_pyfile(settings) 17 | 18 | # register blueprints 19 | if blueprints is not None: 20 | for bp in blueprints: 21 | app.register_blueprint(bp) 22 | 23 | return app 24 | 25 | 26 | app = None 27 | 28 | loop = asyncio.get_event_loop() 29 | app = loop.run_until_complete(create_app(blueprints=blueprints, settings=SETTINGS)) 30 | -------------------------------------------------------------------------------- /authservice/authservice/settings.ini: -------------------------------------------------------------------------------- 1 | [quart] 2 | DEBUG = true 3 | -------------------------------------------------------------------------------- /authservice/authservice/settings.py: -------------------------------------------------------------------------------- 1 | DEBUG = True 2 | SQLALCHEMY_TRACK_MODIFICATIONS = False 3 | -------------------------------------------------------------------------------- /authservice/authservice/static/user.jsx: -------------------------------------------------------------------------------- 1 | class NameForm extends React.Component { 2 | constructor(props) { 3 | super(props); 4 | this.state = { name: '' }; 5 | } 6 | 7 | handleChange = (event) => { 8 | this.setState({[event.target.name]: event.target.value}); 9 | } 10 | 11 | handleSubmit = (event) => { 12 | alert('A form was submitted: ' + this.state); 13 | 14 | fetch('http://localhost/users/1', { 15 | method: 'POST', 16 | // We convert the React state to JSON and send it as the POST body 17 | body: JSON.stringify(this.state) 18 | }).then(function(response) { 19 | console.log(response) 20 | return response.json(); 21 | }); 22 | 23 | event.preventDefault(); 24 | } 25 | 26 | render() { 27 | return ( 28 |
29 | 33 | 34 |
35 | ); 36 | } 37 | } -------------------------------------------------------------------------------- /authservice/authservice/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Jeeves Settings 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |

Dashboard

13 | 14 |
15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /authservice/authservice/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Microservices-Development-2nd-Edition/ff7f9eabefaf69267879daad3e114d0601ad4671/authservice/authservice/tests/__init__.py -------------------------------------------------------------------------------- /authservice/authservice/tests/test_home.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | class TestSomething(unittest.TestCase): 5 | def test_my_view(self): 6 | pass 7 | -------------------------------------------------------------------------------- /authservice/authservice/views/__init__.py: -------------------------------------------------------------------------------- 1 | from authservice.views.home import home 2 | 3 | 4 | blueprints = [home] 5 | -------------------------------------------------------------------------------- /authservice/authservice/views/home.py: -------------------------------------------------------------------------------- 1 | from quart import Blueprint, jsonify, render_template 2 | 3 | 4 | home = Blueprint("home", __name__) 5 | 6 | 7 | @home.route("/") 8 | async def index(): 9 | """Home view. 10 | 11 | This view will return an empty JSON mapping. 12 | """ 13 | return await render_template("index.html") 14 | -------------------------------------------------------------------------------- /authservice/requirements.txt: -------------------------------------------------------------------------------- 1 | quart 2 | sqlalchemy 3 | flask_sqlalchemy 4 | -------------------------------------------------------------------------------- /authservice/setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from setuptools import setup, find_packages 3 | 4 | 5 | with open("requirements.txt") as f: 6 | deps = [ 7 | dep 8 | for dep in f.read().split("\n") 9 | if dep.strip() != "" and not dep.startswith("-e") 10 | ] 11 | install_requires = deps 12 | 13 | 14 | setup( 15 | name="authservice", 16 | version="0.1", 17 | packages=find_packages(), 18 | zip_safe=False, 19 | include_package_data=True, 20 | install_requires=install_requires, 21 | ) 22 | -------------------------------------------------------------------------------- /authservice/tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py37,py38,py39,formatting,docs 3 | 4 | 5 | [gh-actions] 6 | python = 7 | 3.7: py37 8 | 3.8: py38 9 | 3.9: py39,formatting,docs 10 | 11 | [testenv] 12 | deps = pytest 13 | pytest-cov 14 | coveralls 15 | -rrequirements.txt 16 | 17 | commands = 18 | pytest --cov-config .coveragerc --cov authservice authservice/tests 19 | - coveralls 20 | 21 | 22 | [testenv:formatting] 23 | commands = 24 | black --check --diff authservice setup.py 25 | isort --check --diff authservice 26 | deps = 27 | black 28 | isort 29 | 30 | [testenv:docs] 31 | basepython=python 32 | deps = 33 | -rrequirements.txt 34 | sphinx 35 | commands= 36 | sphinx-build -W -b html docs/source docs/build 37 | -------------------------------------------------------------------------------- /dataservice/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9 2 | COPY . /app/ 3 | RUN pip install -r /app/requirements.txt 4 | RUN pip install /app/ 5 | CMD ["hypercorn", "--bind", "0.0.0.0:8080", "dataservice:app"] 6 | 7 | -------------------------------------------------------------------------------- /dataservice/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | include README.rst 3 | recursive-include dataservice *.ini 4 | recursive-include docs *.rst *.png *.svg *.css *.html conf.py 5 | prune docs/build/* 6 | -------------------------------------------------------------------------------- /dataservice/Makefile: -------------------------------------------------------------------------------- 1 | HERE = $(shell pwd) 2 | VENV = . 3 | VIRTUALENV = virtualenv 4 | BIN = $(VENV)/bin 5 | PYTHON = $(BIN)/python 6 | 7 | INSTALL = $(BIN)/pip install --no-deps 8 | 9 | .PHONY: all test docs build_extras 10 | 11 | all: build 12 | 13 | $(PYTHON): 14 | $(VIRTUALENV) $(VTENV_OPTS) $(VENV) 15 | 16 | build: $(PYTHON) 17 | $(PYTHON) setup.py develop 18 | 19 | clean: 20 | rm -rf $(VENV) 21 | 22 | test_dependencies: 23 | $(BIN)/pip install flake8 tox 24 | 25 | test: build test_dependencies 26 | $(BIN)/flake8 dataservice 27 | $(BIN)/tox 28 | 29 | run: 30 | QUART_APP=dataservice bin/quart run 31 | -------------------------------------------------------------------------------- /dataservice/README.rst: -------------------------------------------------------------------------------- 1 | microservice-skeleton 2 | ===================== 3 | 4 | **DISCLAIMER** This repository is part of an application made for 5 | the Python Microservices Development. It was made for educational 6 | purpose and not suitable for production. It's still being updated. 7 | If you find any issue or want to talk with the author, feel free to 8 | open an issue in the issue tracker. 9 | 10 | 11 | This project is a template for building microservices with Quart. 12 | 13 | .. image:: https://coveralls.io/repos/github/PythonMicroservices/microservice-skeleton/badge.svg?branch=main 14 | :target: https://coveralls.io/github/PythonMicroservices/microservice-skeleton?branch=main 15 | 16 | .. image:: https://github.com/PythonMicroservices/microservice-skeleton/workflows/Python%20Testing/badge.svg 17 | :target: https://github.com/PythonMicroservices/microservice-skeleton/actions 18 | 19 | .. image:: https://readthedocs.org/projects/microservice/badge/?version=latest 20 | :target: https://microservice.readthedocs.io 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /dataservice/dataservice/__init__.py: -------------------------------------------------------------------------------- 1 | from dataservice.app import app # NOQA 2 | -------------------------------------------------------------------------------- /dataservice/dataservice/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dataservice.views import blueprints 3 | from dataservice.database import User, db 4 | from quart import Quart 5 | import asyncio 6 | 7 | _HERE = os.path.dirname(__file__) 8 | SETTINGS = os.path.join(_HERE, "settings.py") 9 | 10 | 11 | async def create_app(name=__name__, blueprints=None, settings=None): 12 | app = Quart(name) 13 | 14 | # load configuration 15 | settings = os.environ.get("QUART_SETTINGS", settings) 16 | if settings is not None: 17 | app.config.from_pyfile(settings) 18 | 19 | # register blueprints 20 | if blueprints is not None: 21 | for bp in blueprints: 22 | app.register_blueprint(bp) 23 | 24 | db.init_app(app) 25 | # login_manager.init_app(app) 26 | db.create_all(app=app) 27 | # create a user 28 | async with app.app_context(): 29 | q = db.session.query(User).filter(User.email == "simon@flmx.org") 30 | user = q.first() 31 | if user is None: 32 | entry = User() 33 | entry.name = "Simon" 34 | entry.email = "simon@flmx.org" 35 | entry.slack_id = "U136F44A0" 36 | entry.is_admin = True 37 | entry.set_password("ok") 38 | entry.location = "London, UK" 39 | db.session.add(entry) 40 | db.session.commit() 41 | entry = User() 42 | entry.name = "Tarek" 43 | entry.email = "tarek@example.com" 44 | entry.is_admin = True 45 | entry.set_password("ok") 46 | entry.location = "France" 47 | db.session.add(entry) 48 | db.session.commit() 49 | entry = User() 50 | entry.name = "Alice" 51 | entry.email = "alice@example.com" 52 | entry.is_admin = False 53 | entry.set_password("ok") 54 | entry.location = "London, UK" 55 | db.session.add(entry) 56 | db.session.commit() 57 | entry = User() 58 | entry.name = "Bob" 59 | entry.email = "bob@aol.com" 60 | entry.is_admin = False 61 | entry.set_password("ok") 62 | entry.location = "London, UK" 63 | db.session.add(entry) 64 | db.session.commit() 65 | 66 | return app 67 | 68 | 69 | app = None 70 | 71 | loop = asyncio.get_event_loop() 72 | app = loop.run_until_complete(create_app(blueprints=blueprints, settings=SETTINGS)) 73 | -------------------------------------------------------------------------------- /dataservice/dataservice/database.py: -------------------------------------------------------------------------------- 1 | # encoding: utf8 2 | from dataclasses import dataclass 3 | import quart.flask_patch 4 | from flask_sqlalchemy import SQLAlchemy 5 | from sqlalchemy.orm import relationship 6 | from werkzeug.security import check_password_hash, generate_password_hash 7 | 8 | db = SQLAlchemy() 9 | 10 | from sqlalchemy.inspection import inspect 11 | 12 | 13 | class Serializer(object): 14 | def serialize(self): 15 | return {c: getattr(self, c) for c in inspect(self).attrs.keys()} 16 | 17 | @staticmethod 18 | def serialize_list(l): 19 | return [m.serialize() for m in l] 20 | 21 | 22 | @dataclass 23 | class User(db.Model, Serializer): 24 | __tablename__ = "user" 25 | id: int = db.Column(db.Integer, primary_key=True, autoincrement=True) 26 | name: str = db.Column(db.Unicode(128)) 27 | email: str = db.Column(db.Unicode(128)) 28 | slack_id: str = db.Column(db.Unicode(128)) 29 | email_address: str = db.Column(db.String(128)) 30 | password: str = db.Column(db.Unicode(128)) 31 | strava_tokens: str = db.Column(db.String(128)) 32 | location: str = db.Column(db.String(128)) 33 | config: dict = db.Column(db.JSON) 34 | location: str = db.Column(db.String(128)) 35 | is_active: bool = db.Column(db.Boolean, default=True) 36 | is_admin: bool = db.Column(db.Boolean, default=False) 37 | 38 | def __init__(self, *args, **kw): 39 | super(User, self).__init__(*args, **kw) 40 | self._authenticated = False 41 | 42 | def set_password(self, password): 43 | self.password = generate_password_hash(password) 44 | 45 | @property 46 | def is_authenticated(self): 47 | return self._authenticated 48 | 49 | def authenticate(self, password): 50 | checked = check_password_hash(self.password, password) 51 | self._authenticated = checked 52 | return self._authenticated 53 | 54 | def get_id(self): 55 | return self.id 56 | -------------------------------------------------------------------------------- /dataservice/dataservice/js-src/like_button.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const e = React.createElement; 4 | 5 | class LikeButton extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { liked: false }; 9 | } 10 | 11 | render() { 12 | if (this.state.liked) { 13 | return 'You liked this.'; 14 | } 15 | 16 | return e( 17 | 'button', 18 | { onClick: () => this.setState({ liked: true }) }, 19 | 'Like' 20 | ); 21 | } 22 | } 23 | 24 | const domContainer = document.querySelector('#like_button_container'); 25 | ReactDOM.render(e(LikeButton), domContainer); -------------------------------------------------------------------------------- /dataservice/dataservice/js-src/people.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | class Person extends React.Component { 4 | render() { 5 | return ( 6 |
{this.props.name} ({this.props.email})
7 | ); 8 | } 9 | } 10 | 11 | class People extends React.Component { 12 | render() { 13 | var peopleNodes = this.props.data.map(function (person) { 14 | return ( 15 | 20 | ); 21 | }); 22 | return ( 23 |
24 | {peopleNodes} 25 |
26 | ); 27 | } 28 | } 29 | 30 | class PeopleBox extends React.Component { 31 | constructor(props) { 32 | super(props); 33 | this.state = { data: [] }; 34 | } 35 | 36 | loadPeopleFromServer() { 37 | fetch('http://localhost:5000/api/users') 38 | .then(response => response.json()) 39 | .then(data => { 40 | console.log(data); 41 | this.setState({ 42 | data: data, 43 | }); 44 | console.log(this.state); 45 | }) 46 | .catch(function (error) { 47 | console.log(error); 48 | }); 49 | } 50 | componentDidMount() { 51 | this.loadPeopleFromServer(); 52 | } 53 | render() { 54 | return ( 55 |
56 |

People

57 | 58 |
59 | ); 60 | } 61 | } 62 | 63 | const domContainer = document.querySelector('#people_list'); 64 | ReactDOM.render(React.createElement(PeopleBox), domContainer); -------------------------------------------------------------------------------- /dataservice/dataservice/js-src/user.jsx: -------------------------------------------------------------------------------- 1 | 2 | class NameForm extends React.Component { 3 | constructor(props) { 4 | super(props); 5 | this.state = { name: 'sausage', email: '' }; 6 | } 7 | 8 | handleChange = (event) => { 9 | this.setState({ [event.target.name]: event.target.value }); 10 | } 11 | 12 | handleSubmit = (event) => { 13 | fetch('http://localhost:5000/api/users/1', { 14 | method: 'POST', 15 | // We convert the React state to JSON and send it as the POST body 16 | body: JSON.stringify(this.state) 17 | }).then(function (response) { 18 | console.log(response) 19 | return response.json(); 20 | }); 21 | 22 | event.preventDefault(); 23 | } 24 | 25 | fetchData() { 26 | fetch('http://localhost:5000/api/users/1') 27 | .then(response => response.json()) 28 | .then(data => { 29 | console.log(data); 30 | this.setState({ 31 | name: 'asdasd', 32 | email: data.email 33 | }); 34 | console.log(this.state); 35 | }) 36 | .catch(function (error) { 37 | console.log(error); 38 | }); 39 | } 40 | componentDidMount() { 41 | this.fetchData(); 42 | } 43 | render() { 44 | return ( 45 |
46 | 50 | 54 | 55 |
56 | ); 57 | } 58 | } 59 | 60 | 61 | const domContainer = document.querySelector('#user_edit_form'); 62 | ReactDOM.render(React.createElement(NameForm), domContainer); -------------------------------------------------------------------------------- /dataservice/dataservice/settings.ini: -------------------------------------------------------------------------------- 1 | [quart] 2 | DEBUG = true 3 | -------------------------------------------------------------------------------- /dataservice/dataservice/settings.py: -------------------------------------------------------------------------------- 1 | DEBUG = True 2 | SQLALCHEMY_TRACK_MODIFICATIONS = False 3 | -------------------------------------------------------------------------------- /dataservice/dataservice/static/like_button.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 4 | 5 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 6 | 7 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 8 | 9 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 10 | 11 | var e = React.createElement; 12 | 13 | var LikeButton = function (_React$Component) { 14 | _inherits(LikeButton, _React$Component); 15 | 16 | function LikeButton(props) { 17 | _classCallCheck(this, LikeButton); 18 | 19 | var _this = _possibleConstructorReturn(this, (LikeButton.__proto__ || Object.getPrototypeOf(LikeButton)).call(this, props)); 20 | 21 | _this.state = { liked: false }; 22 | return _this; 23 | } 24 | 25 | _createClass(LikeButton, [{ 26 | key: 'render', 27 | value: function render() { 28 | var _this2 = this; 29 | 30 | if (this.state.liked) { 31 | return 'You liked this.'; 32 | } 33 | 34 | return e('button', { onClick: function onClick() { 35 | return _this2.setState({ liked: true }); 36 | } }, 'Like'); 37 | } 38 | }]); 39 | 40 | return LikeButton; 41 | }(React.Component); 42 | 43 | var domContainer = document.querySelector('#like_button_container'); 44 | ReactDOM.render(e(LikeButton), domContainer); -------------------------------------------------------------------------------- /dataservice/dataservice/static/people.jsx: -------------------------------------------------------------------------------- 1 | 2 | 3 | class Person extends React.Component { 4 | render() { 5 | return ( 6 |
{this.props.name} ({this.props.email})
7 | ); 8 | } 9 | } 10 | 11 | class People extends React.Component { 12 | render() { 13 | var peopleNodes = this.props.data.map(function (person) { 14 | return ( 15 | 20 | ); 21 | }); 22 | return ( 23 |
24 | {peopleNodes} 25 |
26 | ); 27 | } 28 | } 29 | 30 | class PeopleBox extends React.Component { 31 | constructor(props) { 32 | super(props); 33 | this.state = { data: [] }; 34 | } 35 | 36 | loadPeopleFromServer() { 37 | fetch('http://localhost:5000/api/users') 38 | .then(response => response.json()) 39 | .then(data => { 40 | console.log(data); 41 | this.setState({ 42 | data: data, 43 | }); 44 | console.log(this.state); 45 | }) 46 | .catch(function (error) { 47 | console.log(error); 48 | }); 49 | } 50 | componentDidMount() { 51 | this.loadPeopleFromServer(); 52 | } 53 | render() { 54 | return ( 55 |
56 |

People

57 | 58 |
59 | ); 60 | } 61 | 62 | } 63 | 64 | const domContainer = document.querySelector('#people_list'); 65 | ReactDOM.render(React.createElement(PeopleBox), domContainer); -------------------------------------------------------------------------------- /dataservice/dataservice/static/user.jsx: -------------------------------------------------------------------------------- 1 | class NameForm extends React.Component { 2 | constructor(props) { 3 | super(props); 4 | this.state = { name: '' }; 5 | } 6 | 7 | handleChange = (event) => { 8 | this.setState({[event.target.name]: event.target.value}); 9 | } 10 | 11 | handleSubmit = (event) => { 12 | alert('A form was submitted: ' + this.state); 13 | 14 | fetch('http://localhost:5000/api/users/1', { 15 | method: 'POST', 16 | // We convert the React state to JSON and send it as the POST body 17 | body: JSON.stringify(this.state) 18 | }).then(function(response) { 19 | console.log(response) 20 | return response.json(); 21 | }); 22 | 23 | event.preventDefault(); 24 | } 25 | 26 | render() { 27 | return ( 28 |
29 | 33 | 34 |
35 | ); 36 | } 37 | } 38 | 39 | 40 | const domContainer = document.querySelector('#user_edit_form'); 41 | ReactDOM.render(React.createElement(NameForm), domContainer); -------------------------------------------------------------------------------- /dataservice/dataservice/templates/all_people.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Settings

8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /dataservice/dataservice/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Jeeves Settings 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

Settings

16 | 17 | 18 |
19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /dataservice/dataservice/templates/user_snippet.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Settings

8 | 9 | 10 |
11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /dataservice/dataservice/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Microservices-Development-2nd-Edition/ff7f9eabefaf69267879daad3e114d0601ad4671/dataservice/dataservice/tests/__init__.py -------------------------------------------------------------------------------- /dataservice/dataservice/tests/test_home.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | class TestSomething(unittest.TestCase): 5 | def test_my_view(self): 6 | pass 7 | -------------------------------------------------------------------------------- /dataservice/dataservice/views/__init__.py: -------------------------------------------------------------------------------- 1 | from dataservice.views.home import home 2 | 3 | 4 | blueprints = [home] 5 | -------------------------------------------------------------------------------- /dataservice/dataservice/views/home.py: -------------------------------------------------------------------------------- 1 | from quart import Blueprint, jsonify, render_template 2 | 3 | from dataservice.database import User, db 4 | 5 | home = Blueprint("home", __name__) 6 | 7 | 8 | @home.route("/") 9 | async def index(): 10 | """Home view. 11 | 12 | This view will return an empty JSON mapping. 13 | """ 14 | return {} 15 | 16 | 17 | @home.route("/api/users") 18 | async def list_all_users(): 19 | users = User.query.all() 20 | users_list = [u.serialize() for u in users] 21 | return jsonify(users_list) 22 | 23 | 24 | @home.route("/api/users/", methods=["GET", "POST"]) 25 | async def fetch_user(user_id): 26 | user = db.session.query(User).filter(User.id == user_id).first() 27 | return user.serialize() 28 | 29 | 30 | @home.route("/views/user") 31 | async def show_user_form(): 32 | return await render_template("index.html") 33 | 34 | 35 | @home.route("/views/all_people") 36 | async def show_all_people(): 37 | return await render_template("all_people.html") 38 | 39 | 40 | @home.route("/views/user_snippet") 41 | async def show_user_snippet(): 42 | return await render_template("user_snippet.html") 43 | 44 | 45 | # Create user 46 | 47 | # Fetch / update user 48 | 49 | # Fetch myself 50 | 51 | 52 | # Authentication service 53 | # - get passed a service url, return a micro-UI element for logging in if not already authenticated? 54 | # - Return who's logged in with that token 55 | # 56 | # Refer to: 57 | # Database service 58 | # - Get user details 59 | # - Present edit form. Should this be a microfrontend too? Why not. 60 | -------------------------------------------------------------------------------- /dataservice/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dataservice", 3 | "version": "1.0.0", 4 | "description": "microservice-skeleton =====================", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "axios": "^0.21.1", 14 | "babel-cli": "^6.26.0", 15 | "babel-preset-react-app": "^3.1.2" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /dataservice/requirements.txt: -------------------------------------------------------------------------------- 1 | quart 2 | sqlalchemy 3 | flask_sqlalchemy 4 | -------------------------------------------------------------------------------- /dataservice/setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from setuptools import setup, find_packages 3 | 4 | 5 | with open("requirements.txt") as f: 6 | deps = [ 7 | dep 8 | for dep in f.read().split("\n") 9 | if dep.strip() != "" and not dep.startswith("-e") 10 | ] 11 | install_requires = deps 12 | 13 | 14 | with open("README.rst") as f: 15 | LONG_DESC = f.read() 16 | 17 | setup( 18 | name="dataservice", 19 | version="0.1", 20 | author="Simon Fraser", 21 | author_email="simon@flmx.org", 22 | url="https://github.com/PythonMicroservices/dataservice", 23 | license="MIT", 24 | description="This is a cool microservice based on strava.", 25 | long_description=LONG_DESC, 26 | packages=find_packages(), 27 | zip_safe=False, 28 | include_package_data=True, 29 | install_requires=install_requires, 30 | ) 31 | -------------------------------------------------------------------------------- /dataservice/tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py37,py38,py39,formatting,docs 3 | 4 | 5 | [gh-actions] 6 | python = 7 | 3.7: py37 8 | 3.8: py38 9 | 3.9: py39,formatting,docs 10 | 11 | [testenv] 12 | deps = pytest 13 | pytest-cov 14 | coveralls 15 | -rrequirements.txt 16 | 17 | commands = 18 | pytest --cov-config .coveragerc --cov dataservice dataservice/tests 19 | - coveralls 20 | 21 | 22 | [testenv:formatting] 23 | commands = 24 | black --check --diff dataservice setup.py 25 | isort --check --diff dataservice 26 | deps = 27 | black 28 | isort 29 | 30 | [testenv:docs] 31 | basepython=python 32 | deps = 33 | -rrequirements.txt 34 | sphinx 35 | commands= 36 | sphinx-build -W -b html docs/source docs/build 37 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | networks: 3 | jeeves: 4 | services: 5 | dataservice: 6 | networks: 7 | - jeeves 8 | build: 9 | context: dataservice/ 10 | ports: 11 | - "8080:8080" 12 | tokendealer: 13 | networks: 14 | - jeeves 15 | build: 16 | context: tokendealer/ 17 | ports: 18 | - "8090:8090" 19 | rabbitmq: 20 | image: "rabbitmq:latest" 21 | networks: 22 | - jeeves 23 | 24 | -------------------------------------------------------------------------------- /micro-frontend/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | include README.rst 3 | recursive-include datafrontend *.ini 4 | recursive-include docs *.rst *.png *.svg *.css *.html conf.py 5 | prune docs/build/* 6 | -------------------------------------------------------------------------------- /micro-frontend/Makefile: -------------------------------------------------------------------------------- 1 | HERE = $(shell pwd) 2 | VENV = . 3 | VIRTUALENV = virtualenv 4 | BIN = $(VENV)/bin 5 | PYTHON = $(BIN)/python 6 | 7 | INSTALL = $(BIN)/pip install --no-deps 8 | 9 | .PHONY: all test docs build_extras 10 | 11 | all: build 12 | 13 | $(PYTHON): 14 | $(VIRTUALENV) $(VTENV_OPTS) $(VENV) 15 | 16 | build: $(PYTHON) 17 | $(PYTHON) setup.py develop 18 | 19 | clean: 20 | rm -rf $(VENV) 21 | 22 | test_dependencies: 23 | $(BIN)/pip install flake8 tox 24 | 25 | test: build test_dependencies 26 | $(BIN)/flake8 datafrontend 27 | $(BIN)/tox 28 | 29 | run: 30 | QUART_APP=datafrontend bin/quart run 31 | -------------------------------------------------------------------------------- /micro-frontend/README.rst: -------------------------------------------------------------------------------- 1 | microservice-skeleton 2 | ===================== 3 | 4 | **DISCLAIMER** This repository is part of an application made for 5 | the Python Microservices Development. It was made for educational 6 | purpose and not suitable for production. It's still being updated. 7 | If you find any issue or want to talk with the author, feel free to 8 | open an issue in the issue tracker. 9 | 10 | 11 | This project is a template for building microservices with Quart. 12 | 13 | .. image:: https://coveralls.io/repos/github/PythonMicroservices/microservice-skeleton/badge.svg?branch=main 14 | :target: https://coveralls.io/github/PythonMicroservices/microservice-skeleton?branch=main 15 | 16 | .. image:: https://github.com/PythonMicroservices/microservice-skeleton/workflows/Python%20Testing/badge.svg 17 | :target: https://github.com/PythonMicroservices/microservice-skeleton/actions 18 | 19 | .. image:: https://readthedocs.org/projects/microservice/badge/?version=latest 20 | :target: https://microservice.readthedocs.io 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /micro-frontend/datafrontend/__init__.py: -------------------------------------------------------------------------------- 1 | from datafrontend.app import app # NOQA 2 | -------------------------------------------------------------------------------- /micro-frontend/datafrontend/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datafrontend.views import blueprints 3 | from quart import Quart 4 | import asyncio 5 | 6 | _HERE = os.path.dirname(__file__) 7 | SETTINGS = os.path.join(_HERE, "settings.py") 8 | 9 | 10 | async def create_app(name=__name__, blueprints=None, settings=None): 11 | app = Quart(name) 12 | 13 | # load configuration 14 | settings = os.environ.get("QUART_SETTINGS", settings) 15 | if settings is not None: 16 | app.config.from_pyfile(settings) 17 | 18 | # register blueprints 19 | if blueprints is not None: 20 | for bp in blueprints: 21 | app.register_blueprint(bp) 22 | 23 | return app 24 | 25 | 26 | app = None 27 | 28 | loop = asyncio.get_event_loop() 29 | app = loop.run_until_complete(create_app(blueprints=blueprints, settings=SETTINGS)) 30 | -------------------------------------------------------------------------------- /micro-frontend/datafrontend/js-src/like_button.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const e = React.createElement; 4 | 5 | class LikeButton extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { liked: false }; 9 | } 10 | 11 | render() { 12 | if (this.state.liked) { 13 | return 'You liked this.'; 14 | } 15 | 16 | return e( 17 | 'button', 18 | { onClick: () => this.setState({ liked: true }) }, 19 | 'Like' 20 | ); 21 | } 22 | } 23 | 24 | const domContainer = document.querySelector('#like_button_container'); 25 | ReactDOM.render(e(LikeButton), domContainer); -------------------------------------------------------------------------------- /micro-frontend/datafrontend/js-src/user.jsx: -------------------------------------------------------------------------------- 1 | 2 | class NameForm extends React.Component { 3 | constructor(props) { 4 | super(props); 5 | this.state = { name: 'sausage', email: '' }; 6 | } 7 | 8 | handleChange = (event) => { 9 | this.setState({ [event.target.name]: event.target.value }); 10 | } 11 | 12 | handleSubmit = (event) => { 13 | fetch('http://localhost:5000/api/users/1', { 14 | method: 'POST', 15 | // We convert the React state to JSON and send it as the POST body 16 | body: JSON.stringify(this.state) 17 | }).then(function (response) { 18 | console.log(response) 19 | return response.json(); 20 | }); 21 | 22 | event.preventDefault(); 23 | } 24 | 25 | fetchData() { 26 | fetch('http://localhost:5000/api/users/1') 27 | .then(response => response.json()) 28 | .then(data => { 29 | console.log(data); 30 | this.setState({ 31 | name: 'asdasd', 32 | email: data.email 33 | }); 34 | console.log(this.state); 35 | }) 36 | .catch(function (error) { 37 | console.log(error); 38 | }); 39 | } 40 | componentDidMount() { 41 | this.fetchData(); 42 | } 43 | render() { 44 | return ( 45 |
46 | 50 | 54 | 55 |
56 | ); 57 | } 58 | } 59 | 60 | 61 | const domContainer = document.querySelector('#user_edit_form'); 62 | ReactDOM.render(React.createElement(NameForm), domContainer); -------------------------------------------------------------------------------- /micro-frontend/datafrontend/settings.ini: -------------------------------------------------------------------------------- 1 | [quart] 2 | DEBUG = true 3 | -------------------------------------------------------------------------------- /micro-frontend/datafrontend/settings.py: -------------------------------------------------------------------------------- 1 | DEBUG = True 2 | SQLALCHEMY_TRACK_MODIFICATIONS = False 3 | -------------------------------------------------------------------------------- /micro-frontend/datafrontend/static/like_button.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 4 | 5 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 6 | 7 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 8 | 9 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 10 | 11 | var e = React.createElement; 12 | 13 | var LikeButton = function (_React$Component) { 14 | _inherits(LikeButton, _React$Component); 15 | 16 | function LikeButton(props) { 17 | _classCallCheck(this, LikeButton); 18 | 19 | var _this = _possibleConstructorReturn(this, (LikeButton.__proto__ || Object.getPrototypeOf(LikeButton)).call(this, props)); 20 | 21 | _this.state = { liked: false }; 22 | return _this; 23 | } 24 | 25 | _createClass(LikeButton, [{ 26 | key: 'render', 27 | value: function render() { 28 | var _this2 = this; 29 | 30 | if (this.state.liked) { 31 | return 'You liked this.'; 32 | } 33 | 34 | return e('button', { onClick: function onClick() { 35 | return _this2.setState({ liked: true }); 36 | } }, 'Like'); 37 | } 38 | }]); 39 | 40 | return LikeButton; 41 | }(React.Component); 42 | 43 | var domContainer = document.querySelector('#like_button_container'); 44 | ReactDOM.render(e(LikeButton), domContainer); -------------------------------------------------------------------------------- /micro-frontend/datafrontend/static/user.js: -------------------------------------------------------------------------------- 1 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 2 | 3 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 4 | 5 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 6 | 7 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 8 | 9 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 10 | 11 | var NameForm = function (_React$Component) { 12 | _inherits(NameForm, _React$Component); 13 | 14 | function NameForm(props) { 15 | _classCallCheck(this, NameForm); 16 | 17 | var _this = _possibleConstructorReturn(this, (NameForm.__proto__ || Object.getPrototypeOf(NameForm)).call(this, props)); 18 | 19 | _this.handleChange = function (event) { 20 | _this.setState(_defineProperty({}, event.target.name, event.target.value)); 21 | }; 22 | 23 | _this.handleSubmit = function (event) { 24 | fetch('http://localhost:5000/api/users/1', { 25 | method: 'POST', 26 | // We convert the React state to JSON and send it as the POST body 27 | body: JSON.stringify(_this.state) 28 | }).then(function (response) { 29 | console.log(response); 30 | return response.json(); 31 | }); 32 | 33 | event.preventDefault(); 34 | }; 35 | 36 | _this.state = { name: 'sausage', email: '' }; 37 | return _this; 38 | } 39 | 40 | _createClass(NameForm, [{ 41 | key: 'fetchData', 42 | value: function fetchData() { 43 | var _this2 = this; 44 | 45 | fetch('http://localhost:5000/api/users/1').then(function (response) { 46 | return response.json(); 47 | }).then(function (data) { 48 | console.log(data); 49 | _this2.setState({ 50 | name: 'asdasd', 51 | email: data.email 52 | }); 53 | console.log(_this2.state); 54 | }).catch(function (error) { 55 | console.log(error); 56 | }); 57 | } 58 | }, { 59 | key: 'componentDidMount', 60 | value: function componentDidMount() { 61 | this.fetchData(); 62 | } 63 | }, { 64 | key: 'render', 65 | value: function render() { 66 | return React.createElement( 67 | 'form', 68 | { onSubmit: this.handleSubmit }, 69 | React.createElement( 70 | 'label', 71 | null, 72 | 'Name:', 73 | React.createElement('input', { type: 'text', value: this.state.name, name: 'name', onChange: this.handleChange }) 74 | ), 75 | React.createElement( 76 | 'label', 77 | null, 78 | 'Email:', 79 | React.createElement('input', { type: 'text', value: this.state.email, name: 'email', onChange: this.handleChange }) 80 | ), 81 | React.createElement('input', { type: 'submit', value: 'Submit' }) 82 | ); 83 | } 84 | }]); 85 | 86 | return NameForm; 87 | }(React.Component); 88 | 89 | var domContainer = document.querySelector('#user_edit_form'); 90 | ReactDOM.render(React.createElement(NameForm), domContainer); -------------------------------------------------------------------------------- /micro-frontend/datafrontend/static/user.jsx: -------------------------------------------------------------------------------- 1 | class NameForm extends React.Component { 2 | constructor(props) { 3 | super(props); 4 | this.state = { name: '' }; 5 | } 6 | 7 | handleChange = (event) => { 8 | this.setState({[event.target.name]: event.target.value}); 9 | } 10 | 11 | handleSubmit = (event) => { 12 | alert('A form was submitted: ' + this.state); 13 | 14 | fetch('http://localhost:5000/api/users/1', { 15 | method: 'POST', 16 | // We convert the React state to JSON and send it as the POST body 17 | body: JSON.stringify(this.state) 18 | }).then(function(response) { 19 | console.log(response) 20 | return response.json(); 21 | }); 22 | 23 | event.preventDefault(); 24 | } 25 | 26 | render() { 27 | return ( 28 |
29 | 33 | 34 |
35 | ); 36 | } 37 | } 38 | 39 | 40 | const domContainer = document.querySelector('#user_edit_form'); 41 | ReactDOM.render(React.createElement(NameForm), domContainer); -------------------------------------------------------------------------------- /micro-frontend/datafrontend/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Jeeves Settings 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

Settings

16 | 17 | 18 |
19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /micro-frontend/datafrontend/templates/user_snippet.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Settings

8 | 9 | 10 |
11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /micro-frontend/datafrontend/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Microservices-Development-2nd-Edition/ff7f9eabefaf69267879daad3e114d0601ad4671/micro-frontend/datafrontend/tests/__init__.py -------------------------------------------------------------------------------- /micro-frontend/datafrontend/tests/test_home.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | class TestSomething(unittest.TestCase): 5 | def test_my_view(self): 6 | pass 7 | -------------------------------------------------------------------------------- /micro-frontend/datafrontend/views/__init__.py: -------------------------------------------------------------------------------- 1 | from datafrontend.views.home import home 2 | 3 | 4 | blueprints = [home] 5 | -------------------------------------------------------------------------------- /micro-frontend/datafrontend/views/home.py: -------------------------------------------------------------------------------- 1 | from quart import Blueprint, jsonify, render_template 2 | 3 | 4 | home = Blueprint("home", __name__) 5 | 6 | 7 | @home.route("/") 8 | async def index(): 9 | """Home view. 10 | 11 | This view will return an empty JSON mapping. 12 | """ 13 | return await render_template("index.html") 14 | -------------------------------------------------------------------------------- /micro-frontend/requirements.txt: -------------------------------------------------------------------------------- 1 | quart 2 | sqlalchemy 3 | flask_sqlalchemy 4 | -------------------------------------------------------------------------------- /micro-frontend/setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from setuptools import setup, find_packages 3 | 4 | 5 | with open("requirements.txt") as f: 6 | deps = [ 7 | dep 8 | for dep in f.read().split("\n") 9 | if dep.strip() != "" and not dep.startswith("-e") 10 | ] 11 | install_requires = deps 12 | 13 | 14 | setup( 15 | name="datafrontend", 16 | version="0.1", 17 | packages=find_packages(), 18 | zip_safe=False, 19 | include_package_data=True, 20 | install_requires=install_requires, 21 | ) 22 | -------------------------------------------------------------------------------- /micro-frontend/tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py37,py38,py39,formatting,docs 3 | 4 | 5 | [gh-actions] 6 | python = 7 | 3.7: py37 8 | 3.8: py38 9 | 3.9: py39,formatting,docs 10 | 11 | [testenv] 12 | deps = pytest 13 | pytest-cov 14 | coveralls 15 | -rrequirements.txt 16 | 17 | commands = 18 | pytest --cov-config .coveragerc --cov datafrontend datafrontend/tests 19 | - coveralls 20 | 21 | 22 | [testenv:formatting] 23 | commands = 24 | black --check --diff datafrontend setup.py 25 | isort --check --diff datafrontend 26 | deps = 27 | black 28 | isort 29 | 30 | [testenv:docs] 31 | basepython=python 32 | deps = 33 | -rrequirements.txt 34 | sphinx 35 | commands= 36 | sphinx-build -W -b html docs/source docs/build 37 | -------------------------------------------------------------------------------- /microservice/.coverage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Microservices-Development-2nd-Edition/ff7f9eabefaf69267879daad3e114d0601ad4671/microservice/.coverage -------------------------------------------------------------------------------- /microservice/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.formatting.provider": "black" 3 | } -------------------------------------------------------------------------------- /microservice/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9 2 | COPY . /app/ 3 | RUN pip install -r /app/requirements.txt 4 | RUN pip install /app/ 5 | CMD ["hypercorn", "--bind", "0.0.0.0:5000", "myservice:app"] 6 | 7 | -------------------------------------------------------------------------------- /microservice/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | include README.rst 3 | recursive-include myservice *.ini 4 | recursive-include docs *.rst *.png *.svg *.css *.html conf.py 5 | prune docs/build/* 6 | -------------------------------------------------------------------------------- /microservice/Makefile: -------------------------------------------------------------------------------- 1 | HERE = $(shell pwd) 2 | VENV = . 3 | VIRTUALENV = virtualenv 4 | BIN = $(VENV)/bin 5 | PYTHON = $(BIN)/python 6 | 7 | INSTALL = $(BIN)/pip install --no-deps 8 | 9 | .PHONY: all test docs build_extras 10 | 11 | all: build 12 | 13 | $(PYTHON): 14 | $(VIRTUALENV) $(VTENV_OPTS) $(VENV) 15 | 16 | build: $(PYTHON) 17 | $(PYTHON) setup.py develop 18 | 19 | clean: 20 | rm -rf $(VENV) 21 | 22 | test_dependencies: 23 | $(BIN)/pip install flake8 tox 24 | 25 | test: build test_dependencies 26 | $(BIN)/flake8 myservice 27 | $(BIN)/tox 28 | 29 | run: 30 | QUART_APP=myservice bin/quart run 31 | -------------------------------------------------------------------------------- /microservice/README.rst: -------------------------------------------------------------------------------- 1 | microservice-skeleton 2 | ===================== 3 | 4 | **DISCLAIMER** This repository is part of an application made for 5 | the Python Microservices Development. It was made for educational 6 | purpose and not suitable for production. It's still being updated. 7 | If you find any issue or want to talk with the author, feel free to 8 | open an issue in the issue tracker. 9 | 10 | 11 | This project is a template for building microservices with Quart. 12 | 13 | .. image:: https://coveralls.io/repos/github/PythonMicroservices/microservice-skeleton/badge.svg?branch=main 14 | :target: https://coveralls.io/github/PythonMicroservices/microservice-skeleton?branch=main 15 | 16 | .. image:: https://github.com/PythonMicroservices/microservice-skeleton/workflows/Python%20Testing/badge.svg 17 | :target: https://github.com/PythonMicroservices/microservice-skeleton/actions 18 | 19 | .. image:: https://readthedocs.org/projects/microservice/badge/?version=latest 20 | :target: https://microservice.readthedocs.io 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /microservice/docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = myservice 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /microservice/docs/source/_static/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Microservices-Development-2nd-Edition/ff7f9eabefaf69267879daad3e114d0601ad4671/microservice/docs/source/_static/.keep -------------------------------------------------------------------------------- /microservice/docs/source/_templates/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Microservices-Development-2nd-Edition/ff7f9eabefaf69267879daad3e114d0601ad4671/microservice/docs/source/_templates/.keep -------------------------------------------------------------------------------- /microservice/docs/source/api.rst: -------------------------------------------------------------------------------- 1 | APIS 2 | ==== 3 | 4 | 5 | **myservice** includes one view that's linked to the root path: 6 | 7 | .. autofunction:: myservice.views.home.index 8 | -------------------------------------------------------------------------------- /microservice/docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = "microservice-skeleton" 21 | copyright = "2021, Simon Fraser" 22 | author = "Simon Fraser" 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = "1.0" 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = ["sphinx.ext.autodoc"] 34 | 35 | # Add any paths that contain templates here, relative to this directory. 36 | templates_path = ["_templates"] 37 | 38 | # List of patterns, relative to source directory, that match files and 39 | # directories to ignore when looking for source files. 40 | # This pattern also affects html_static_path and html_extra_path. 41 | exclude_patterns = [] 42 | 43 | 44 | # -- Options for HTML output ------------------------------------------------- 45 | 46 | 47 | # The name of the Pygments (syntax highlighting) style to use. 48 | pygments_style = "sphinx" 49 | 50 | # The theme to use for HTML and HTML Help pages. See the documentation for 51 | # a list of builtin themes. 52 | # 53 | html_theme = "alabaster" 54 | 55 | # Add any paths that contain custom static files (such as style sheets) here, 56 | # relative to this directory. They are copied after the builtin static files, 57 | # so a file named "default.css" will overwrite the builtin "default.css". 58 | html_static_path = ["_static"] 59 | -------------------------------------------------------------------------------- /microservice/docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Myservice 2 | ========= 3 | 4 | 5 | **myservice** is a simple JSON Quart application. 6 | 7 | The application is created with :func:`create_app`: 8 | 9 | .. literalinclude:: ../../myservice/app.py 10 | 11 | 12 | The :file:`settings.ini` file which is passed to :func:`create_app` 13 | contains options for running the Quart app, like the DEBUG flag: 14 | 15 | .. literalinclude:: ../../myservice/settings.ini 16 | :language: ini 17 | 18 | 19 | Blueprint are imported from :mod:`myservice.views` and one 20 | Blueprint and view example was provided in :file:`myservice/views/home.py`: 21 | 22 | .. literalinclude:: ../../myservice/views/home.py 23 | :name: home.py 24 | :emphasize-lines: 13 25 | 26 | 27 | Views can return simple mappings (as highlighted in the example above), 28 | in that case they will be converted into a JSON response. 29 | 30 | .. toctree:: 31 | :maxdepth: 2 32 | 33 | api 34 | -------------------------------------------------------------------------------- /microservice/myservice/__init__.py: -------------------------------------------------------------------------------- 1 | from myservice.app import app # NOQA 2 | -------------------------------------------------------------------------------- /microservice/myservice/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | from myservice.views import blueprints 3 | from quart import Quart 4 | 5 | _HERE = os.path.dirname(__file__) 6 | _SETTINGS = os.path.join(_HERE, "settings.ini") 7 | 8 | 9 | def create_app(name=__name__, blueprints=None, settings=None): 10 | app = Quart(name) 11 | 12 | # load configuration 13 | settings = os.environ.get("QUART_SETTINGS", settings) 14 | if settings is not None: 15 | app.config.from_pyfile(settings) 16 | 17 | # register blueprints 18 | if blueprints is not None: 19 | for bp in blueprints: 20 | app.register_blueprint(bp) 21 | 22 | return app 23 | 24 | 25 | app = create_app(blueprints=blueprints, settings=_SETTINGS) 26 | -------------------------------------------------------------------------------- /microservice/myservice/foo.py: -------------------------------------------------------------------------------- 1 | import os 2 | from myservice.views import blueprints 3 | from quart import Quart 4 | 5 | _HERE = os.path.dirname(__file__) 6 | _SETTINGS = os.path.join(_HERE, "settings.ini") 7 | 8 | 9 | def create_app(name=__name__, blueprints=None, settings=None): 10 | app = Quart(name) 11 | 12 | app.config["REMOTE_URL"] = os.environ.get("OTHER_SERVICE_URL", DEFAULT_URL) 13 | # load configuration 14 | settings = os.environ.get("QUART_SETTINGS", settings) 15 | if settings is not None: 16 | app.config.from_pyfile(settings) 17 | 18 | # register blueprints 19 | if blueprints is not None: 20 | for bp in blueprints: 21 | app.register_blueprint(bp) 22 | 23 | return app 24 | 25 | 26 | app = create_app(blueprints=blueprints, settings=_SETTINGS) 27 | -------------------------------------------------------------------------------- /microservice/myservice/settings.ini: -------------------------------------------------------------------------------- 1 | [quart] 2 | DEBUG = true 3 | -------------------------------------------------------------------------------- /microservice/myservice/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Microservices-Development-2nd-Edition/ff7f9eabefaf69267879daad3e114d0601ad4671/microservice/myservice/tests/__init__.py -------------------------------------------------------------------------------- /microservice/myservice/tests/test_home.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | class TestSomething(unittest.TestCase): 5 | def test_my_view(self): 6 | pass 7 | -------------------------------------------------------------------------------- /microservice/myservice/views/__init__.py: -------------------------------------------------------------------------------- 1 | from myservice.views.home import home 2 | 3 | 4 | blueprints = [home] 5 | -------------------------------------------------------------------------------- /microservice/myservice/views/home.py: -------------------------------------------------------------------------------- 1 | from quart import Blueprint 2 | 3 | 4 | home = Blueprint("home", __name__) 5 | 6 | 7 | @home.route("/") 8 | def index(): 9 | """Home view. 10 | 11 | This view will return an empty JSON mapping. 12 | """ 13 | return {} 14 | -------------------------------------------------------------------------------- /microservice/requirements.in: -------------------------------------------------------------------------------- 1 | quart 2 | -------------------------------------------------------------------------------- /microservice/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile requirements.in 6 | # 7 | aiofiles==0.6.0 8 | # via quart 9 | blinker==1.4 10 | # via quart 11 | click==8.0.0 12 | # via quart 13 | h11==0.12.0 14 | # via 15 | # hypercorn 16 | # wsproto 17 | h2==4.0.0 18 | # via hypercorn 19 | hpack==4.0.0 20 | # via h2 21 | hypercorn==0.11.2 22 | # via quart 23 | hyperframe==6.0.1 24 | # via h2 25 | itsdangerous==2.0.0 26 | # via quart 27 | jinja2==3.0.0 28 | # via quart 29 | markupsafe==2.0.0 30 | # via jinja2 31 | priority==1.3.0 32 | # via hypercorn 33 | quart==0.15.0 34 | # via -r requirements.in 35 | toml==0.10.2 36 | # via 37 | # hypercorn 38 | # quart 39 | werkzeug==2.0.0 40 | # via quart 41 | wsproto==1.0.0 42 | # via hypercorn 43 | -------------------------------------------------------------------------------- /microservice/setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from setuptools import setup, find_packages 3 | 4 | 5 | with open("requirements.txt") as f: 6 | deps = [ 7 | dep 8 | for dep in f.read().split("\n") 9 | if dep.strip() != "" and not dep.startswith("-e") 10 | ] 11 | install_requires = deps 12 | 13 | 14 | setup( 15 | name="myservice", 16 | version="0.1", 17 | packages=find_packages(), 18 | zip_safe=False, 19 | include_package_data=True, 20 | install_requires=install_requires, 21 | ) 22 | -------------------------------------------------------------------------------- /microservice/tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py37,py38,py39,formatting,docs 3 | 4 | 5 | [gh-actions] 6 | python = 7 | 3.7: py37 8 | 3.8: py38 9 | 3.9: py39,formatting,docs 10 | 11 | [testenv] 12 | deps = pytest 13 | pytest-cov 14 | coveralls 15 | -rrequirements.txt 16 | 17 | commands = 18 | pytest --cov-config .coveragerc --cov myservice myservice/tests 19 | - coveralls 20 | 21 | 22 | [testenv:formatting] 23 | commands = 24 | black --check --diff myservice setup.py 25 | isort --check --diff myservice 26 | deps = 27 | black 28 | isort 29 | 30 | [testenv:docs] 31 | basepython=python 32 | deps = 33 | -rrequirements.txt 34 | sphinx 35 | commands= 36 | sphinx-build -W -b html docs/source docs/build 37 | -------------------------------------------------------------------------------- /monolith/.coverage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Microservices-Development-2nd-Edition/ff7f9eabefaf69267879daad3e114d0601ad4671/monolith/.coverage -------------------------------------------------------------------------------- /monolith/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.formatting.provider": "black", 3 | "python.pythonPath": "/home/linuxbrew/.linuxbrew/bin/python3" 4 | } -------------------------------------------------------------------------------- /monolith/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | include README.rst 3 | recursive-include jeeves *.ini 4 | recursive-include docs *.rst *.png *.svg *.css *.html conf.py 5 | prune docs/build/* 6 | -------------------------------------------------------------------------------- /monolith/Makefile: -------------------------------------------------------------------------------- 1 | HERE = $(shell pwd) 2 | VENV = . 3 | VIRTUALENV = virtualenv 4 | BIN = $(VENV)/bin 5 | PYTHON = $(BIN)/python 6 | 7 | INSTALL = $(BIN)/pip install --no-deps 8 | 9 | .PHONY: all test docs build_extras 10 | 11 | all: build 12 | 13 | $(PYTHON): 14 | $(VIRTUALENV) $(VTENV_OPTS) $(VENV) 15 | 16 | build: $(PYTHON) 17 | $(PYTHON) setup.py develop 18 | 19 | clean: 20 | rm -rf $(VENV) 21 | 22 | test_dependencies: 23 | $(BIN)/pip install flake8 tox 24 | 25 | test: build test_dependencies 26 | $(BIN)/flake8 jeeves 27 | $(BIN)/tox 28 | 29 | run: 30 | QUART_APP=jeeves bin/quart run 31 | -------------------------------------------------------------------------------- /monolith/README.rst: -------------------------------------------------------------------------------- 1 | microservice-skeleton 2 | ===================== 3 | 4 | **DISCLAIMER** This repository is part of an application made for 5 | the Python Microservices Development. It was made for educational 6 | purpose and not suitable for production. It's still being updated. 7 | If you find any issue or want to talk with the author, feel free to 8 | open an issue in the issue tracker. 9 | 10 | 11 | This project is a template for building microservices with Quart. 12 | 13 | .. image:: https://coveralls.io/repos/github/PythonMicroservices/microservice-skeleton/badge.svg?branch=main 14 | :target: https://coveralls.io/github/PythonMicroservices/microservice-skeleton?branch=main 15 | 16 | .. image:: https://github.com/PythonMicroservices/microservice-skeleton/workflows/Python%20Testing/badge.svg 17 | :target: https://github.com/PythonMicroservices/microservice-skeleton/actions 18 | 19 | .. image:: https://readthedocs.org/projects/microservice/badge/?version=latest 20 | :target: https://microservice.readthedocs.io 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /monolith/docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = myservice 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /monolith/docs/source/_static/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Microservices-Development-2nd-Edition/ff7f9eabefaf69267879daad3e114d0601ad4671/monolith/docs/source/_static/.keep -------------------------------------------------------------------------------- /monolith/docs/source/_templates/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Microservices-Development-2nd-Edition/ff7f9eabefaf69267879daad3e114d0601ad4671/monolith/docs/source/_templates/.keep -------------------------------------------------------------------------------- /monolith/docs/source/api.rst: -------------------------------------------------------------------------------- 1 | APIS 2 | ==== 3 | 4 | 5 | **myservice** includes one view that's linked to the root path: 6 | 7 | .. autofunction:: myservice.views.home.index 8 | -------------------------------------------------------------------------------- /monolith/docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = "microservice-skeleton" 21 | copyright = "2021, Simon Fraser" 22 | author = "Simon Fraser" 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = "1.0" 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = ["sphinx.ext.autodoc"] 34 | 35 | # Add any paths that contain templates here, relative to this directory. 36 | templates_path = ["_templates"] 37 | 38 | # List of patterns, relative to source directory, that match files and 39 | # directories to ignore when looking for source files. 40 | # This pattern also affects html_static_path and html_extra_path. 41 | exclude_patterns = [] 42 | 43 | 44 | # -- Options for HTML output ------------------------------------------------- 45 | 46 | 47 | # The name of the Pygments (syntax highlighting) style to use. 48 | pygments_style = "sphinx" 49 | 50 | # The theme to use for HTML and HTML Help pages. See the documentation for 51 | # a list of builtin themes. 52 | # 53 | html_theme = "alabaster" 54 | 55 | # Add any paths that contain custom static files (such as style sheets) here, 56 | # relative to this directory. They are copied after the builtin static files, 57 | # so a file named "default.css" will overwrite the builtin "default.css". 58 | html_static_path = ["_static"] 59 | -------------------------------------------------------------------------------- /monolith/docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Myservice 2 | ========= 3 | 4 | 5 | **myservice** is a simple JSON Quart application. 6 | 7 | The application is created with :func:`create_app`: 8 | 9 | .. literalinclude:: ../../myservice/app.py 10 | 11 | 12 | The :file:`settings.ini` file which is passed to :func:`create_app` 13 | contains options for running the Quart app, like the DEBUG flag: 14 | 15 | .. literalinclude:: ../../myservice/settings.ini 16 | :language: ini 17 | 18 | 19 | Blueprint are imported from :mod:`myservice.views` and one 20 | Blueprint and view example was provided in :file:`myservice/views/home.py`: 21 | 22 | .. literalinclude:: ../../myservice/views/home.py 23 | :name: home.py 24 | :emphasize-lines: 13 25 | 26 | 27 | Views can return simple mappings (as highlighted in the example above), 28 | in that case they will be converted into a JSON response. 29 | 30 | .. toctree:: 31 | :maxdepth: 2 32 | 33 | api 34 | -------------------------------------------------------------------------------- /monolith/jeeves/__init__.py: -------------------------------------------------------------------------------- 1 | from jeeves.app import app # NOQA 2 | -------------------------------------------------------------------------------- /monolith/jeeves/actions/help.py: -------------------------------------------------------------------------------- 1 | async def show_help_text(): 2 | return "Some help text" 3 | -------------------------------------------------------------------------------- /monolith/jeeves/actions/misc.py: -------------------------------------------------------------------------------- 1 | async def do_the_thing(message, metadata): 2 | return f"You said {message}" 3 | -------------------------------------------------------------------------------- /monolith/jeeves/actions/user.py: -------------------------------------------------------------------------------- 1 | from jeeves.database import User, db 2 | 3 | CONFIG_CMDS = [ 4 | "get", 5 | "set", 6 | "show", 7 | ] 8 | 9 | 10 | def set_config(label, value): 11 | pass 12 | 13 | 14 | async def fetch_user(metadata): 15 | user = db.session.query(User).filter(User.slack_id == metadata["sender"]).first() 16 | if user is None: 17 | user = User() 18 | user.is_admin = False 19 | user.slack_id = metadata["sender"] 20 | db.session.add(user) 21 | db.session.commit() 22 | user = ( 23 | db.session.query(User).filter(User.slack_id == metadata["sender"]).first() 24 | ) 25 | 26 | return user 27 | 28 | 29 | async def show_location(message, metadata): 30 | user = await fetch_user(metadata) 31 | if user.location: 32 | return f"You are in {user.location}" 33 | else: 34 | return "I don't know where you are." 35 | 36 | 37 | async def user_config(message, metadata): 38 | print(message) 39 | print(metadata) 40 | operation = message.split()[0].lstrip() 41 | print(f"operation {operation}") 42 | if operation not in CONFIG_CMDS: 43 | return f"I don't know what to do. Possible keywords: {CONFIG_CMDS}" 44 | 45 | user = await fetch_user(metadata) 46 | 47 | if operation == "show": 48 | reply = "" 49 | if not user.config: 50 | return "No config values found!" 51 | print(user.config) 52 | for k, v in user.config.items(): 53 | reply += f"{k}: {v}\n" 54 | print(f"Returning {reply}") 55 | return reply 56 | if operation == "set": 57 | label, value = message.split(maxsplit=2)[1:] 58 | if user.config: 59 | new_config = dict(user.config) 60 | else: 61 | new_config = {} 62 | new_config.update({label: value}) 63 | user.config = new_config 64 | db.session.commit() 65 | reply = f"Set {label} to {value}" 66 | print(f"Returning {reply}") 67 | return reply 68 | -------------------------------------------------------------------------------- /monolith/jeeves/actions/weather.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from quart import current_app 3 | from urllib.parse import quote as urlquote 4 | 5 | # TODO aiohttp 6 | # TODO wrapper for metrics recording. 7 | 8 | 9 | def kelvin_to_celsius(kelvin): 10 | return int(kelvin - 273.15) 11 | 12 | 13 | def process_weather_response(weather_data): 14 | # todo jinja template. render_template likely not suitable 15 | temperature = kelvin_to_celsius(weather_data["main"]["temp"]) 16 | feels_like = kelvin_to_celsius(weather_data["main"]["feels_like"]) 17 | return f"The temperature is {temperature}℃ and it feels like {feels_like}℃" 18 | 19 | dummy_response = { 20 | "coord": {"lon": 0.1167, "lat": 52.2}, 21 | "weather": [ 22 | { 23 | "id": 804, 24 | "main": "Clouds", 25 | "description": "overcast clouds", 26 | "icon": "04n", 27 | } 28 | ], 29 | "base": "stations", 30 | "main": { 31 | "temp": 272.55, 32 | "feels_like": 270.05, 33 | "temp_min": 272.04, 34 | "temp_max": 273.15, 35 | "pressure": 1016, 36 | "humidity": 94, 37 | }, 38 | "visibility": 10000, 39 | "wind": {"speed": 0.45, "deg": 273, "gust": 4.02}, 40 | "clouds": {"all": 94}, 41 | "dt": 1610056912, 42 | "sys": { 43 | "type": 3, 44 | "id": 39620, 45 | "country": "GB", 46 | "sunrise": 1610006808, 47 | "sunset": 1610035470, 48 | }, 49 | "timezone": 0, 50 | "id": 2653941, 51 | "name": "Cambridge", 52 | "cod": 200, 53 | } 54 | 55 | 56 | async def fetch_weather(location): 57 | text = urlquote(location) 58 | 59 | url = current_app.config["WEATHER_URL"].format( 60 | text=text, token=current_app.config["WEATHER_TOKEN"] 61 | ) 62 | 63 | response = requests.get(url) 64 | response.raise_for_status() 65 | return process_weather_response(response.json()) 66 | 67 | 68 | async def weather_action(text, metadata): 69 | # Turn the text into something that's probably a location 70 | # Allow: 71 | # weather in location 72 | # weather location 73 | if not text: 74 | # TODO look up user. 75 | location = "London, UK" 76 | else: 77 | location = text.replace("weather", "").replace("in", "").strip() 78 | 79 | return await fetch_weather(location) 80 | -------------------------------------------------------------------------------- /monolith/jeeves/app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | 4 | from quart import Quart 5 | 6 | from jeeves.auth import login_manager 7 | from jeeves.database import User, db 8 | from jeeves.views import blueprints 9 | 10 | _HERE = os.path.dirname(__file__) 11 | 12 | SETTINGS = os.path.join(_HERE, "settings.py") 13 | 14 | 15 | async def create_app(name=__name__, blueprints=None, settings=None): 16 | app = Quart(name) 17 | # TODO instance_relative_config=True 18 | 19 | # load configuration 20 | settings = os.environ.get("QUARTSETTINGS", settings) 21 | if settings is not None: 22 | app.config.from_pyfile(settings) 23 | 24 | app.config["WTF_CSRF_SECRET_KEY"] = "A SECRET KEY" 25 | app.config["SECRET_KEY"] = "ANOTHER ONE" 26 | app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:////tmp/jeeves" 27 | 28 | # register blueprints 29 | if blueprints is not None: 30 | for bp in blueprints: 31 | app.register_blueprint(bp) 32 | 33 | db.init_app(app) 34 | login_manager.init_app(app) 35 | db.create_all(app=app) 36 | 37 | # create a user 38 | async with app.app_context(): 39 | q = db.session.query(User).filter(User.email == "simon@flmx.org") 40 | user = q.first() 41 | if user is None: 42 | simon = User() 43 | simon.email = "simon@flmx.org" 44 | simon.slack_id = "U136F44A0" 45 | simon.is_admin = True 46 | simon.set_password("ok") 47 | simon.location = "London, UK" 48 | db.session.add(simon) 49 | db.session.commit() 50 | return app 51 | 52 | 53 | app = None 54 | 55 | loop = asyncio.get_event_loop() 56 | app = loop.run_until_complete(create_app(blueprints=blueprints, settings=SETTINGS)) 57 | -------------------------------------------------------------------------------- /monolith/jeeves/auth.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | import quart.flask_patch 4 | from flask_login import LoginManager, current_user 5 | 6 | from jeeves.database import User 7 | 8 | login_manager = LoginManager() 9 | 10 | 11 | def admin_required(func): 12 | @functools.wraps(func) 13 | def _admin_required(*args, **kw): 14 | admin = current_user.is_authenticated and current_user.is_admin 15 | if not admin: 16 | return login_manager.unauthorized() 17 | return func(*args, **kw) 18 | 19 | return _admin_required 20 | 21 | 22 | @login_manager.user_loader 23 | def load_user(user_id): 24 | user = User.query.get(user_id) 25 | if user is not None: 26 | user._authenticated = True 27 | return user 28 | -------------------------------------------------------------------------------- /monolith/jeeves/background.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from asgiref.sync import async_to_sync 4 | from celery import Celery 5 | 6 | from jeeves.actions.weather import fetch_weather 7 | from jeeves.app import SETTINGS, blueprints, create_app 8 | from jeeves.database import User, db 9 | from jeeves.outgoing.slack import post_to_slack 10 | 11 | celery_app = Celery("tasks", backend="rpc://", broker="amqp://localhost") 12 | 13 | loop = asyncio.get_event_loop() 14 | quart_app = loop.run_until_complete( 15 | create_app(blueprints=blueprints, settings=SETTINGS) 16 | ) 17 | quart_app.app_context().push() 18 | 19 | 20 | @celery_app.on_after_configure.connect 21 | def setup_periodic_tasks(sender, **kwargs): 22 | sender.add_periodic_task(10.0, do_weather_alerts, name="add every 10", expires=30) 23 | 24 | 25 | async def weather_alerts_async(): 26 | async with quart_app.app_context(): 27 | q = ( 28 | db.session.query(User) 29 | .filter(User.location.isnot(None)) 30 | .filter(User.slack_id.isnot(None)) 31 | ) 32 | for user in q: 33 | weather_message = await fetch_weather(user.location) 34 | username = user.slack_id 35 | if not username.startswith("@"): 36 | username = "@" + username 37 | post_to_slack(weather_message, {"channel": username}) 38 | 39 | 40 | @celery_app.task 41 | def do_weather_alerts(): 42 | async_to_sync(weather_alerts_async)() 43 | -------------------------------------------------------------------------------- /monolith/jeeves/controller/message_router.py: -------------------------------------------------------------------------------- 1 | from jeeves.actions.misc import do_the_thing 2 | from jeeves.actions.user import user_config, show_location 3 | from jeeves.actions.weather import weather_action 4 | from jeeves.outgoing.default import default_outgoing 5 | from jeeves.actions.help import show_help_text 6 | from jeeves.outgoing.slack import post_to_slack 7 | 8 | ACTION_MAP = { 9 | "help": show_help_text, 10 | "weather": weather_action, 11 | "config": user_config, 12 | "get location": show_location, 13 | # TODO: Give the user a link to their profile on the web, for oauth. 14 | # "login": direct_user_to_web, 15 | # "signin": direct_user_to_web, 16 | } 17 | 18 | OUTGOING_MAP = {"slack": post_to_slack} 19 | 20 | 21 | async def process_message(message, metadata): 22 | """Decide on an action for a chat message. 23 | 24 | Arguments: 25 | message (str): The body of the chat message 26 | metadata (dict): Data about who sent the message, 27 | the time and channel. 28 | """ 29 | reply = None 30 | 31 | print(f"In process message with '{message}'") 32 | for test, action in ACTION_MAP.items(): 33 | if message.startswith(test): 34 | print(f"Working on {test}") 35 | reply = await action(message.lstrip(test), metadata) 36 | break 37 | 38 | if reply: 39 | post_to_slack(reply, metadata) 40 | 41 | """ If we have different response routes, we can use this method 42 | OUTGOING_MAP.get(metadata["type"], default_outgoing)( 43 | reply, 44 | metadata, 45 | ) 46 | """ 47 | -------------------------------------------------------------------------------- /monolith/jeeves/database.py: -------------------------------------------------------------------------------- 1 | # encoding: utf8 2 | 3 | import quart.flask_patch 4 | from flask_sqlalchemy import SQLAlchemy 5 | from sqlalchemy.orm import relationship 6 | from werkzeug.security import check_password_hash, generate_password_hash 7 | 8 | db = SQLAlchemy() 9 | 10 | 11 | class User(db.Model): 12 | __tablename__ = "user" 13 | id = db.Column(db.Integer, primary_key=True, autoincrement=True) 14 | email = db.Column(db.Unicode(128)) 15 | slack_id = db.Column(db.Unicode(128)) 16 | password = db.Column(db.Unicode(128)) 17 | strava_tokens = db.Column(db.String(128)) 18 | location = db.Column(db.String(128)) 19 | config = db.Column(db.JSON) 20 | is_active = db.Column(db.Boolean, default=True) 21 | is_admin = db.Column(db.Boolean, default=False) 22 | 23 | def __init__(self, *args, **kw): 24 | super(User, self).__init__(*args, **kw) 25 | self._authenticated = False 26 | 27 | def set_password(self, password): 28 | self.password = generate_password_hash(password) 29 | 30 | @property 31 | def is_authenticated(self): 32 | return self._authenticated 33 | 34 | def authenticate(self, password): 35 | checked = check_password_hash(self.password, password) 36 | self._authenticated = checked 37 | return self._authenticated 38 | 39 | def get_id(self): 40 | return self.id 41 | -------------------------------------------------------------------------------- /monolith/jeeves/forms.py: -------------------------------------------------------------------------------- 1 | import quart.flask_patch 2 | import wtforms as f 3 | from flask_wtf import FlaskForm 4 | from wtforms.validators import DataRequired 5 | 6 | 7 | class LoginForm(FlaskForm): 8 | email = f.StringField("email", validators=[DataRequired()]) 9 | password = f.PasswordField("password", validators=[DataRequired()]) 10 | display = ["email", "password"] 11 | 12 | 13 | class UserForm(FlaskForm): 14 | email = f.StringField("email", validators=[DataRequired()]) 15 | password = f.PasswordField("password") 16 | slack_id = f.StringField("Slack Username") 17 | admin = f.BooleanField("Admin?") 18 | 19 | display = ["email", "password", "slack_id", "admin"] 20 | -------------------------------------------------------------------------------- /monolith/jeeves/outgoing/default.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger(__name__) 4 | 5 | 6 | def default_outgoing(message, metadata): 7 | logger.warning("No outgoing message router for %s: %s", str(metadata), message) 8 | -------------------------------------------------------------------------------- /monolith/jeeves/outgoing/slack.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from quart import current_app 3 | 4 | 5 | def post_to_slack(message, metadata): 6 | print(f"post_to_slack {message}") 7 | headers = { 8 | "Content-type": "application/json", 9 | "Authorization": f"Bearer {current_app.config['SLACK_TOKEN']}", 10 | } 11 | print(f"headers {headers}") 12 | print(metadata) 13 | response = requests.post( 14 | current_app.config["SLACK_POST_URL"], 15 | json={ 16 | "token": current_app.config["SLACK_TOKEN"], 17 | "text": message, 18 | "channel": metadata["channel"], 19 | }, 20 | headers=headers, 21 | ) 22 | response.raise_for_status() 23 | -------------------------------------------------------------------------------- /monolith/jeeves/settings.py: -------------------------------------------------------------------------------- 1 | DEBUG = True 2 | SLACK_POST_URL = "https://slack.com/api/chat.postMessage" 3 | SLACK_TOKEN = "" 4 | WEATHER_TOKEN = "" 5 | WEATHER_URL = "https://api.openweathermap.org/data/2.5/weather?q={text}&appid={token}" 6 | -------------------------------------------------------------------------------- /monolith/jeeves/templates/create_user.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | {{ form.hidden_tag() }} 5 |
6 | {% for field in form.display %} 7 |
{{ form[field].label }}
8 |
{{ form[field]() }}
9 | {% if form[field].errors %} 10 | {% for e in form[field].errors %} 11 |

{{ e }}

12 | {% endfor %} 13 | {% endif %} 14 | {% endfor %} 15 |
16 |

17 | 18 |

19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /monolith/jeeves/templates/index.html: -------------------------------------------------------------------------------- 1 |

Slackbot Admin

2 | 3 | {% if current_user.is_authenticated %} 4 | Hi {{ current_user.email }}! Log out 5 | 6 | {% else %} 7 | Hi Anonymous, Log in 8 | {% endif %} 9 | -------------------------------------------------------------------------------- /monolith/jeeves/templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | {{ form.hidden_tag() }} 5 |
6 | {% for field in form.display %} 7 |
{{ form[field].label }}
8 |
{{ form[field]() }}
9 | {% if form[field].errors %} 10 | {% for e in form[field].errors %} 11 |

{{ e }}

12 | {% endfor %} 13 | {% endif %} 14 | {% endfor %} 15 |
16 |

17 | 18 |

19 | 20 | 21 | -------------------------------------------------------------------------------- /monolith/jeeves/templates/users.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

User List

4 |
    5 | {% for user in users: %} 6 |
  • 7 | {{user.email}} {{user.slack_id}} 8 |
  • 9 | {% endfor %} 10 |
11 |

12 | Create a new user 13 |

14 | 15 | 16 | -------------------------------------------------------------------------------- /monolith/jeeves/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Microservices-Development-2nd-Edition/ff7f9eabefaf69267879daad3e114d0601ad4671/monolith/jeeves/tests/__init__.py -------------------------------------------------------------------------------- /monolith/jeeves/tests/test_home.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | class TestSomething(unittest.TestCase): 5 | def test_my_view(self): 6 | pass 7 | -------------------------------------------------------------------------------- /monolith/jeeves/views/__init__.py: -------------------------------------------------------------------------------- 1 | from jeeves.views.admin import admin 2 | from jeeves.views.auth import auth 3 | from jeeves.views.home import home 4 | from jeeves.views.slack_api import slack_api 5 | 6 | blueprints = [home, auth, admin, slack_api] 7 | -------------------------------------------------------------------------------- /monolith/jeeves/views/admin.py: -------------------------------------------------------------------------------- 1 | import quart.flask_patch 2 | from quart import Blueprint, redirect, render_template, request 3 | 4 | from jeeves.auth import admin_required 5 | from jeeves.database import User, db 6 | from jeeves.forms import UserForm 7 | 8 | admin = Blueprint("admin", __name__) 9 | 10 | 11 | @admin.route("/users") 12 | def _users(): 13 | users = db.session.query(User) 14 | return render_template("users.html", users=users) 15 | 16 | 17 | @admin.route("/create_user", methods=["GET", "POST"]) 18 | @admin_required 19 | def create_user(): 20 | form = UserForm() 21 | if request.method == "POST": 22 | 23 | if form.validate_on_submit(): 24 | new_user = User() 25 | form.populate_obj(new_user) 26 | db.session.add(new_user) 27 | db.session.commit() 28 | return redirect("/users") 29 | 30 | return render_template("create_user.html", form=form) 31 | -------------------------------------------------------------------------------- /monolith/jeeves/views/auth.py: -------------------------------------------------------------------------------- 1 | import quart.flask_patch 2 | from flask_login import current_user, login_required, login_user, logout_user 3 | from quart import Blueprint, redirect, render_template, request 4 | 5 | from jeeves.database import User, db 6 | from jeeves.forms import LoginForm 7 | 8 | auth = Blueprint("auth", __name__) 9 | 10 | 11 | @auth.route("/login", methods=["GET", "POST"]) 12 | def login(): 13 | form = LoginForm() 14 | if form.validate_on_submit(): 15 | email, password = form.data["email"], form.data["password"] 16 | q = db.session.query(User).filter(User.email == email) 17 | user = q.first() 18 | if user is not None and user.authenticate(password): 19 | login_user(user) 20 | return redirect("/") 21 | return render_template("login.html", form=form) 22 | 23 | 24 | @auth.route("/logout") 25 | def logout(): 26 | logout_user() 27 | return redirect("/") 28 | -------------------------------------------------------------------------------- /monolith/jeeves/views/home.py: -------------------------------------------------------------------------------- 1 | from quart import Blueprint, render_template 2 | 3 | from jeeves.auth import current_user 4 | 5 | home = Blueprint("home", __name__) 6 | 7 | 8 | @home.route("/") 9 | async def index(): 10 | """Home view. 11 | 12 | This view will return an empty JSON mapping. 13 | """ 14 | return await render_template("index.html", current_user=current_user) 15 | -------------------------------------------------------------------------------- /monolith/jeeves/views/slack_api.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from quart import Blueprint, request 4 | 5 | from jeeves.controller.message_router import process_message 6 | 7 | # Respond to the SlackBot setup challenge 8 | 9 | slack_api = Blueprint("api", __name__) 10 | 11 | logger = logging.getLogger(__name__) 12 | logger.setLevel(logging.DEBUG) 13 | 14 | 15 | def respond_to_slack_challenge(incoming_challenge): 16 | return incoming_challenge.get("challenge", ""), 200 17 | 18 | 19 | def extract_slack_text(request_body): 20 | # Deep JSON structure 21 | elements = request_body["event"]["blocks"][0]["elements"][0]["elements"] 22 | for part in elements: 23 | if part["type"] == "text": 24 | return part["text"].lstrip() 25 | # fallback to full text + replace so that 26 | # some text 27 | # becomes: 28 | # some text 29 | return request_body["event"]["text"].partition(">")[2].lstrip() 30 | 31 | 32 | def outgoing_metadata(request_body): 33 | return { 34 | "type": "slack", 35 | "message_type": request_body["event"]["type"], 36 | "team": request_body["event"]["team"], 37 | "sender": request_body["event"]["user"], 38 | "channel": request_body["event"]["channel"], 39 | "ts": request_body["event"]["ts"], # used for replies 40 | } 41 | 42 | 43 | @slack_api.route("/slack", methods=["POST"]) 44 | async def incoming_slack_endpoint(): 45 | """Receive an event from Slack.""" 46 | 47 | request_body = await request.get_json() 48 | 49 | # When setting up a Slack app, we are sent a verification 50 | # challenge, and we must respond with the token provided. 51 | if request_body.get("type", "") == "url_verification": 52 | logger.info("Responding to url verification challenge") 53 | return respond_to_slack_challenge(request_body) 54 | 55 | logger.debug("Received message: %s", extract_slack_text(request_body)) 56 | await process_message( 57 | extract_slack_text(request_body), outgoing_metadata(request_body) 58 | ) 59 | 60 | # Slack ignores the data here, but a value may help our debugging. 61 | return {"status": "OK"}, 200 62 | -------------------------------------------------------------------------------- /monolith/requirements.txt: -------------------------------------------------------------------------------- 1 | aiofiles==0.6.0 2 | amqp==5.0.3 3 | appdirs==1.4.4 4 | asgiref==3.3.1 5 | attrs==20.3.0 6 | billiard==3.6.3.0 7 | black==20.8b1 8 | blinker==1.4 9 | celery==5.0.5 10 | certifi==2020.12.5 11 | chardet==4.0.0 12 | click==7.1.2 13 | click-didyoumean==0.0.3 14 | click-plugins==1.1.1 15 | click-repl==0.1.6 16 | Flask==1.1.2 17 | Flask-Login==0.5.0 18 | Flask-SQLAlchemy==2.4.4 19 | Flask-WTF==0.14.3 20 | h11==0.12.0 21 | h2==4.0.0 22 | hpack==4.0.0 23 | Hypercorn==0.11.1 24 | hyperframe==6.0.0 25 | idna==2.10 26 | itsdangerous==1.1.0 27 | Jinja2==2.11.2 28 | jsonschema==3.2.0 29 | kombu==5.0.2 30 | MarkupSafe==1.1.1 31 | mypy-extensions==0.4.3 32 | pathspec==0.8.1 33 | priority==1.3.0 34 | prompt-toolkit==3.0.11 35 | pyrsistent==0.17.3 36 | pytz==2020.5 37 | PyYAML==5.2b1 38 | Quart==0.14.1 39 | regex==2020.11.13 40 | requests==2.25.1 41 | six==1.15.0 42 | SQLAlchemy==1.3.22 43 | swagger-parser==1.0.2 44 | swagger-spec-validator==2.7.3 45 | toml==0.10.2 46 | typed-ast==1.4.2 47 | typing-extensions==3.7.4.3 48 | urllib3==1.26.2 49 | vine==5.0.0 50 | wcwidth==0.2.5 51 | Werkzeug==1.0.1 52 | wsproto==1.0.0 53 | WTForms==2.3.3 54 | -------------------------------------------------------------------------------- /monolith/setup.py: -------------------------------------------------------------------------------- 1 | 2 | from setuptools import find_packages, setup 3 | 4 | with open("requirements.txt") as f: 5 | deps = [ 6 | dep 7 | for dep in f.read().split("\n") 8 | if dep.strip() != "" and not dep.startswith("-e") 9 | ] 10 | install_requires = deps 11 | 12 | 13 | setup( 14 | name="jeeves", 15 | version="0.1", 16 | packages=find_packages(), 17 | zip_safe=False, 18 | include_package_data=True, 19 | install_requires=install_requires, 20 | ) 21 | -------------------------------------------------------------------------------- /monolith/tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py37,py38,py39,formatting,docs 3 | 4 | 5 | [gh-actions] 6 | python = 7 | 3.7: py37 8 | 3.8: py38 9 | 3.9: py39,formatting,docs 10 | 11 | [testenv] 12 | deps = pytest 13 | pytest-cov 14 | coveralls 15 | -rrequirements.txt 16 | 17 | commands = 18 | pytest --cov-config .coveragerc --cov jeeves jeeves/tests 19 | - coveralls 20 | 21 | 22 | [testenv:formatting] 23 | commands = 24 | black --check --diff jeeves setup.py 25 | isort --check --diff jeeves 26 | deps = 27 | black 28 | isort 29 | 30 | [testenv:docs] 31 | basepython=python 32 | deps = 33 | -rrequirements.txt 34 | sphinx 35 | commands= 36 | sphinx-build -W -b html docs/source docs/build 37 | -------------------------------------------------------------------------------- /tokendealer/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.formatting.provider": "black" 3 | } -------------------------------------------------------------------------------- /tokendealer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9 2 | COPY . /app/ 3 | RUN pip install -r /app/requirements.txt 4 | RUN pip install /app/ 5 | CMD ["hypercorn", "--bind", "0.0.0.0:8090", "tokendealer:app"] 6 | 7 | -------------------------------------------------------------------------------- /tokendealer/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | include README.rst 3 | include tokendealer/settings.yml 4 | recursive-include tokendealer *.ini 5 | recursive-include docs *.rst *.png *.svg *.css *.html conf.py 6 | prune docs/build/* 7 | 8 | -------------------------------------------------------------------------------- /tokendealer/Makefile: -------------------------------------------------------------------------------- 1 | HERE = $(shell pwd) 2 | VENV = . 3 | VIRTUALENV = virtualenv 4 | BIN = $(VENV)/bin 5 | PYTHON = $(BIN)/python 6 | 7 | INSTALL = $(BIN)/pip install --no-deps 8 | 9 | .PHONY: all test docs build_extras 10 | 11 | all: build 12 | 13 | $(PYTHON): 14 | $(VIRTUALENV) $(VTENV_OPTS) $(VENV) 15 | 16 | build: $(PYTHON) 17 | $(PYTHON) setup.py develop 18 | 19 | clean: 20 | rm -rf $(VENV) 21 | 22 | test_dependencies: 23 | $(BIN)/pip install flake8 tox 24 | 25 | test: build test_dependencies 26 | $(BIN)/flake8 tokendealer 27 | $(BIN)/tox 28 | 29 | run: 30 | QUART_APP=tokendealer bin/quart run 31 | -------------------------------------------------------------------------------- /tokendealer/README.rst: -------------------------------------------------------------------------------- 1 | microservice-skeleton 2 | ===================== 3 | 4 | **DISCLAIMER** This repository is part of an application made for 5 | the Python Microservices Development. It was made for educational 6 | purpose and not suitable for production. It's still being updated. 7 | If you find any issue or want to talk with the author, feel free to 8 | open an issue in the issue tracker. 9 | 10 | 11 | This project is a template for building microservices with Quart. 12 | 13 | .. image:: https://coveralls.io/repos/github/PythonMicroservices/microservice-skeleton/badge.svg?branch=main 14 | :target: https://coveralls.io/github/PythonMicroservices/microservice-skeleton?branch=main 15 | 16 | .. image:: https://github.com/PythonMicroservices/microservice-skeleton/workflows/Python%20Testing/badge.svg 17 | :target: https://github.com/PythonMicroservices/microservice-skeleton/actions 18 | 19 | .. image:: https://readthedocs.org/projects/microservice/badge/?version=latest 20 | :target: https://microservice.readthedocs.io 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tokendealer/pyvenv.cfg: -------------------------------------------------------------------------------- 1 | home = /home/linuxbrew/.linuxbrew/opt/python@3.9 2 | implementation = CPython 3 | version_info = 3.9.2.final.0 4 | virtualenv = 20.2.2 5 | include-system-site-packages = false 6 | base-prefix = /home/linuxbrew/.linuxbrew/opt/python@3.9 7 | base-exec-prefix = /home/linuxbrew/.linuxbrew/opt/python@3.9 8 | base-executable = /home/linuxbrew/.linuxbrew/opt/python@3.9/bin/python3.9 9 | -------------------------------------------------------------------------------- /tokendealer/requirements.txt: -------------------------------------------------------------------------------- 1 | quart 2 | pyjwt 3 | cryptography 4 | pyyaml 5 | -------------------------------------------------------------------------------- /tokendealer/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | 4 | with open("requirements.txt") as f: 5 | deps = [ 6 | dep 7 | for dep in f.read().split("\n") 8 | if dep.strip() != "" and not dep.startswith("-e") 9 | ] 10 | install_requires = deps 11 | 12 | 13 | setup( 14 | name="tokendealer", 15 | version="0.1", 16 | packages=find_packages(), 17 | zip_safe=False, 18 | include_package_data=True, 19 | install_requires=install_requires, 20 | ) 21 | -------------------------------------------------------------------------------- /tokendealer/tokendealer/__init__.py: -------------------------------------------------------------------------------- 1 | from tokendealer.app import app # NOQA 2 | -------------------------------------------------------------------------------- /tokendealer/tokendealer/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from quart import Quart 4 | 5 | from pathlib import Path 6 | import yaml 7 | 8 | 9 | from tokendealer.views import blueprints 10 | 11 | _HERE = Path(__file__).parent 12 | _SETTINGS = _HERE / "settings.yml" 13 | 14 | 15 | def create_app(name=__name__, blueprints=None, settings=None): 16 | app = Quart(name) 17 | 18 | # load configuration 19 | settings = os.environ.get("QUART_SETTINGS", settings) 20 | if settings is not None: 21 | app.config.from_file(settings, yaml.safe_load) 22 | # app.config.from_pyfile(settings) 23 | 24 | # register blueprints 25 | if blueprints is not None: 26 | for bp in blueprints: 27 | app.register_blueprint(bp) 28 | 29 | return app 30 | 31 | 32 | app = create_app(blueprints=blueprints, settings=_SETTINGS) 33 | -------------------------------------------------------------------------------- /tokendealer/tokendealer/settings.ini: -------------------------------------------------------------------------------- 1 | [quart] 2 | priv_key = /home/simon/jwt-test/privkey.pem 3 | pub_key = /home/simon/jwt-test/pubkey.pem 4 | -------------------------------------------------------------------------------- /tokendealer/tokendealer/settings.yml: -------------------------------------------------------------------------------- 1 | --- 2 | PRIVATE_KEY: /home/simon/jwt-test/privkey.pem 3 | PUBLIC_KEY: /home/simon/jwt-test/pubkey.pem 4 | -------------------------------------------------------------------------------- /tokendealer/tokendealer/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Microservices-Development-2nd-Edition/ff7f9eabefaf69267879daad3e114d0601ad4671/tokendealer/tokendealer/tests/__init__.py -------------------------------------------------------------------------------- /tokendealer/tokendealer/tests/test_home.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | 4 | os.environ["TESTDIR"] = os.path.dirname(__file__) 5 | 6 | from tokendealer.app import app as quart_app # NOQA 7 | 8 | _SECRET = "f0fdeb1f1584fd5431c4250b2e859457" 9 | 10 | 11 | @pytest.fixture 12 | def app(): 13 | return quart_app 14 | 15 | 16 | @pytest.mark.asyncio 17 | async def test_get_pub_key(app): 18 | client = app.test_client() 19 | response = await client.get("/.well-known/jwks.json") 20 | assert response.status_code == 200 21 | json_data = await response.json 22 | assert "n" in json_data[0] 23 | 24 | 25 | @pytest.mark.asyncio 26 | async def test_roundtrip(app): 27 | client = app.test_client() 28 | data = { 29 | "client_id": "worker1", 30 | "client_secret": _SECRET, 31 | "audience": "audience", 32 | "grant_type": "client_credentials", 33 | } 34 | 35 | headers = {"Content-Type": "application/x-www-form-urlencoded"} 36 | response = await client.post("/oauth/token", form=data, headers=headers) 37 | data = await response.json 38 | data["audience"] = "audience" 39 | 40 | response = await client.post("/verify_token", form=data) 41 | json_response = await response.json 42 | assert json_response["iss"] == "https://tokendealer.example.com" 43 | 44 | 45 | @pytest.mark.asyncio 46 | @pytest.mark.parametrize( 47 | "token,expected", (({"access_token": "d.A.D"}, 400), ({}, 400)) 48 | ) 49 | async def test_bad_tokens(app, token, expected): 50 | client = app.test_client() 51 | response = await client.post("/verify_token", json=token) 52 | assert response.status_code == expected 53 | -------------------------------------------------------------------------------- /tokendealer/tokendealer/views/__init__.py: -------------------------------------------------------------------------------- 1 | from tokendealer.views.home import home 2 | 3 | blueprints = [home] 4 | -------------------------------------------------------------------------------- /tokendealer/tokendealer/views/home.py: -------------------------------------------------------------------------------- 1 | import time 2 | from hmac import compare_digest 3 | 4 | import jwt 5 | from quart import Blueprint, abort, current_app, request, jsonify 6 | from werkzeug.exceptions import HTTPException 7 | 8 | home = Blueprint("home", __name__) 9 | 10 | 11 | _SECRETS = {"worker1": "f0fdeb1f1584fd5431c4250b2e859457"} 12 | 13 | 14 | def _400(desc): 15 | exc = HTTPException() 16 | exc.code = 400 17 | exc.description = desc 18 | return error_handling(exc) 19 | 20 | 21 | def error_handling(error): 22 | if isinstance(error, HTTPException): 23 | result = { 24 | "code": error.code, 25 | "description": error.description, 26 | "message": str(error), 27 | } 28 | else: 29 | description = abort.mapping[500].description 30 | result = {"code": 500, "description": description, "message": str(error)} 31 | 32 | resp = jsonify(result) 33 | resp.status_code = result["code"] 34 | return resp 35 | 36 | 37 | @home.route("/.well-known/jwks.json") 38 | async def _jwks(): 39 | """Returns the public key in the Json Web Key (JWK) format""" 40 | with open(current_app.config["PUBLIC_KEY"]) as f: 41 | key = f.read() 42 | key = { 43 | "alg": "RS512", 44 | "e": "AQAB", 45 | "n": key, 46 | "kty": "RSA", 47 | "use": "sig", 48 | } 49 | 50 | return jsonify([key]) 51 | 52 | 53 | def is_authorized_app(client_id, client_secret): 54 | return compare_digest(_SECRETS.get(client_id), client_secret) 55 | 56 | 57 | @home.route("/oauth/token", methods=["POST"]) 58 | async def create_token(): 59 | with open(current_app.config["PRIVATE_KEY"]) as f: 60 | key = f.read() 61 | try: 62 | data = await request.form 63 | if data.get("grant_type") != "client_credentials": 64 | return _400(f"Wrong grant_type {data.get('grant_type')}") 65 | 66 | client_id = data.get("client_id") 67 | client_secret = data.get("client_secret") 68 | aud = data.get("audience", "") 69 | 70 | if not is_authorized_app(client_id, client_secret): 71 | return abort(401) 72 | 73 | now = int(time.time()) 74 | 75 | token = { 76 | "iss": "https://tokendealer.example.com", 77 | "aud": aud, 78 | "iat": now, 79 | "exp": now + 3600 * 24, 80 | } 81 | token = jwt.encode(token, key, algorithm="RS512") 82 | return {"access_token": token} 83 | except Exception as e: 84 | return _400(str(e)) 85 | 86 | 87 | @home.route("/verify_token", methods=["POST"]) 88 | async def verify_token(): 89 | with open(current_app.config["PUBLIC_KEY"]) as f: 90 | key = f.read() 91 | try: 92 | json_body = await request.form 93 | token = json_body["access_token"] 94 | audience = json_body.get("audience", "") 95 | print(token, audience) 96 | return jwt.decode(token, key, algorithms=["RS512"], audience=audience) 97 | except Exception as e: 98 | return _400(str(e)) 99 | -------------------------------------------------------------------------------- /tokendealer/tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py38,py39,formatting,docs 3 | 4 | 5 | [gh-actions] 6 | python = 7 | 3.8: py38 8 | 3.9: py39,formatting,docs 9 | 10 | [testenv] 11 | deps = pytest 12 | pytest-asyncio 13 | pytest-cov 14 | coveralls 15 | black 16 | isort 17 | -rrequirements.txt 18 | 19 | commands = 20 | pytest --cov-config .coveragerc --cov tokendealer tokendealer/tests 21 | - coveralls 22 | 23 | 24 | [testenv:formatting] 25 | commands = 26 | black --check --diff tokendealer setup.py 27 | isort --check --diff tokendealer 28 | deps = 29 | black 30 | isort 31 | 32 | [testenv:docs] 33 | basepython=python 34 | deps = 35 | -rrequirements.txt 36 | sphinx 37 | commands= 38 | sphinx-build -W -b html docs/source docs/build 39 | --------------------------------------------------------------------------------