diff --git a/docs/docs/04_docker_intro/02_run_docker_container/end/Dockerfile b/docs/docs/04_docker_intro/02_run_docker_container/end/Dockerfile new file mode 100644 index 00000000..4d33d373 --- /dev/null +++ b/docs/docs/04_docker_intro/02_run_docker_container/end/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.10 +EXPOSE 5000 +WORKDIR /app +RUN pip install flask +COPY . . +CMD ["flask", "run", "--host", "0.0.0.0"] \ No newline at end of file diff --git a/docs/docs/04_docker_intro/02_run_docker_container/end/app.py b/docs/docs/04_docker_intro/02_run_docker_container/end/app.py new file mode 100644 index 00000000..48c08c35 --- /dev/null +++ b/docs/docs/04_docker_intro/02_run_docker_container/end/app.py @@ -0,0 +1,45 @@ +from flask import Flask, request + +app = Flask(__name__) + +stores = [{"name": "My Store", "items": [{"name": "Chair", "price": 15.99}]}] + + +@app.get("/store") +def get_stores(): + return {"stores": stores} + + +@app.post("/store") +def create_store(): + request_data = request.get_json() + new_store = {"name": request_data["name"], "items": []} + stores.append(new_store) + return new_store, 201 + + +@app.post("/store//item") +def create_item(name): + request_data = request.get_json() + for store in stores: + if store["name"] == name: + new_item = {"name": request_data["name"], "price": request_data["price"]} + store["items"].append(new_item) + return new_item, 201 + return {"message": "Store not found"}, 404 + + +@app.get("/store/") +def get_store(name): + for store in stores: + if store["name"] == name: + return store + return {"message": "Store not found"}, 404 + + +@app.get("/store//item") +def get_item_in_store(name): + for store in stores: + if store["name"] == name: + return {"items": store["items"]} + return {"message": "Store not found"}, 404 diff --git a/docs/docs/04_docker_intro/02_run_docker_container/start/app.py b/docs/docs/04_docker_intro/02_run_docker_container/start/app.py new file mode 100644 index 00000000..48c08c35 --- /dev/null +++ b/docs/docs/04_docker_intro/02_run_docker_container/start/app.py @@ -0,0 +1,45 @@ +from flask import Flask, request + +app = Flask(__name__) + +stores = [{"name": "My Store", "items": [{"name": "Chair", "price": 15.99}]}] + + +@app.get("/store") +def get_stores(): + return {"stores": stores} + + +@app.post("/store") +def create_store(): + request_data = request.get_json() + new_store = {"name": request_data["name"], "items": []} + stores.append(new_store) + return new_store, 201 + + +@app.post("/store//item") +def create_item(name): + request_data = request.get_json() + for store in stores: + if store["name"] == name: + new_item = {"name": request_data["name"], "price": request_data["price"]} + store["items"].append(new_item) + return new_item, 201 + return {"message": "Store not found"}, 404 + + +@app.get("/store/") +def get_store(name): + for store in stores: + if store["name"] == name: + return store + return {"message": "Store not found"}, 404 + + +@app.get("/store//item") +def get_item_in_store(name): + for store in stores: + if store["name"] == name: + return {"items": store["items"]} + return {"message": "Store not found"}, 404 diff --git a/docs/docs/04_docker_intro/04_run_with_docker_compose/README.md b/docs/docs/04_docker_intro/04_run_with_docker_compose/README.md new file mode 100644 index 00000000..82a09ed4 --- /dev/null +++ b/docs/docs/04_docker_intro/04_run_with_docker_compose/README.md @@ -0,0 +1,57 @@ +# Run the REST API using Docker Compose + +Now that we've got a Docker container for our REST API, we can set up Docker Compose to run the container. + +Docker Compose is most useful to start multiple Docker containers at the same time, specifying configuration values for them and dependencies between them. + +Later on, I'll show you how to use Docker Compose to start both a PostgreSQL database and the REST API. For now, we'll use it only for the REST API, to simplify starting its container up. + +If you have Docker Desktop installed, you already have Docker Compose. If you want to install Docker Compose in a system without Docker Desktop, please refer to the [official installation instructions](https://docs.docker.com/compose/install/). + +## How to write a `docker-compose.yml` file + +Create a file called `docker-compose.yml` in the root of your project (alongside your `Dockerfile`). Inside it, add the following contents: + +```yaml +version: '3' +services: + web: + build: . + ports: + - "5000:5000" + volumes: + - .:/app +``` + +This small file is all you need to tell Docker Compose that you have a service, called `web`, which is built using the current directory (by default, that looks for a file called `Dockerfile`). + +Other settings provided are: + +- `ports`, used to map a port in your local computer to one in the container. Since our container runs the Flask app on port 5000, we're targeting that port so that any traffic we access in port 5000 of our computer is sent to the container's port 5000. +- `volumes`, to map a local directory into a directory within the container. This makes it so you don't have to rebuild the image each time you make a code change. + +## How to run the Docker Compose services + +Simply type: + +``` +docker compose up +``` + +And that will start all your services. For now, there's just one service, but later on when we add a database, this command will start everything. + +When the services are running, you'll start seeing logs appear. These are the same logs as for running the `Dockerfile` on its own, but preceded by the service name. + +In our case, we'll see `web-1 | ...` and the logs saying the service is running on `http://127.0.0.1:5000`. When you access that URL, you'll see the request logs printed in the console. + +Congratulations, you've ran your first Docker Compose service! + +## Rebuilding the Docker image + +If you need to rebuild the Docker image of your REST API service for whatever reason (e.g. configuration changes), you can run: + +``` +docker compose up --build --force-recreate --no-deps web +``` + +More information [here](https://stackoverflow.com/a/50802581). diff --git a/docs/docs/04_docker_intro/04_run_with_docker_compose/end/Dockerfile b/docs/docs/04_docker_intro/04_run_with_docker_compose/end/Dockerfile new file mode 100644 index 00000000..4d33d373 --- /dev/null +++ b/docs/docs/04_docker_intro/04_run_with_docker_compose/end/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.10 +EXPOSE 5000 +WORKDIR /app +RUN pip install flask +COPY . . +CMD ["flask", "run", "--host", "0.0.0.0"] \ No newline at end of file diff --git a/docs/docs/04_docker_intro/04_run_with_docker_compose/end/app.py b/docs/docs/04_docker_intro/04_run_with_docker_compose/end/app.py new file mode 100644 index 00000000..48c08c35 --- /dev/null +++ b/docs/docs/04_docker_intro/04_run_with_docker_compose/end/app.py @@ -0,0 +1,45 @@ +from flask import Flask, request + +app = Flask(__name__) + +stores = [{"name": "My Store", "items": [{"name": "Chair", "price": 15.99}]}] + + +@app.get("/store") +def get_stores(): + return {"stores": stores} + + +@app.post("/store") +def create_store(): + request_data = request.get_json() + new_store = {"name": request_data["name"], "items": []} + stores.append(new_store) + return new_store, 201 + + +@app.post("/store//item") +def create_item(name): + request_data = request.get_json() + for store in stores: + if store["name"] == name: + new_item = {"name": request_data["name"], "price": request_data["price"]} + store["items"].append(new_item) + return new_item, 201 + return {"message": "Store not found"}, 404 + + +@app.get("/store/") +def get_store(name): + for store in stores: + if store["name"] == name: + return store + return {"message": "Store not found"}, 404 + + +@app.get("/store//item") +def get_item_in_store(name): + for store in stores: + if store["name"] == name: + return {"items": store["items"]} + return {"message": "Store not found"}, 404 diff --git a/docs/docs/04_docker_intro/04_run_with_docker_compose/end/docker-compose.yml b/docs/docs/04_docker_intro/04_run_with_docker_compose/end/docker-compose.yml new file mode 100644 index 00000000..6f77f8ad --- /dev/null +++ b/docs/docs/04_docker_intro/04_run_with_docker_compose/end/docker-compose.yml @@ -0,0 +1,8 @@ +version: '3' +services: + web: + build: . + ports: + - "5000:5000" + volumes: + - .:/app \ No newline at end of file diff --git a/docs/docs/04_docker_intro/04_run_with_docker_compose/start/Dockerfile b/docs/docs/04_docker_intro/04_run_with_docker_compose/start/Dockerfile new file mode 100644 index 00000000..4d33d373 --- /dev/null +++ b/docs/docs/04_docker_intro/04_run_with_docker_compose/start/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.10 +EXPOSE 5000 +WORKDIR /app +RUN pip install flask +COPY . . +CMD ["flask", "run", "--host", "0.0.0.0"] \ No newline at end of file diff --git a/docs/docs/04_docker_intro/04_run_with_docker_compose/start/app.py b/docs/docs/04_docker_intro/04_run_with_docker_compose/start/app.py new file mode 100644 index 00000000..48c08c35 --- /dev/null +++ b/docs/docs/04_docker_intro/04_run_with_docker_compose/start/app.py @@ -0,0 +1,45 @@ +from flask import Flask, request + +app = Flask(__name__) + +stores = [{"name": "My Store", "items": [{"name": "Chair", "price": 15.99}]}] + + +@app.get("/store") +def get_stores(): + return {"stores": stores} + + +@app.post("/store") +def create_store(): + request_data = request.get_json() + new_store = {"name": request_data["name"], "items": []} + stores.append(new_store) + return new_store, 201 + + +@app.post("/store//item") +def create_item(name): + request_data = request.get_json() + for store in stores: + if store["name"] == name: + new_item = {"name": request_data["name"], "price": request_data["price"]} + store["items"].append(new_item) + return new_item, 201 + return {"message": "Store not found"}, 404 + + +@app.get("/store/") +def get_store(name): + for store in stores: + if store["name"] == name: + return store + return {"message": "Store not found"}, 404 + + +@app.get("/store//item") +def get_item_in_store(name): + for store in stores: + if store["name"] == name: + return {"items": store["items"]} + return {"message": "Store not found"}, 404 diff --git a/docs/docs/11_deploy_to_render/05_environment_variables_and_migrations/README.md b/docs/docs/11_deploy_to_render/05_environment_variables_and_migrations/README.md index f0387189..204cf84c 100644 --- a/docs/docs/11_deploy_to_render/05_environment_variables_and_migrations/README.md +++ b/docs/docs/11_deploy_to_render/05_environment_variables_and_migrations/README.md @@ -193,114 +193,7 @@ class UserModel(db.Model): ### Running our migration with string length changes -Now we want to create a new migration so that our changes to the `UserModel` will be applied. But because we're changing the length of a string column, we need to first make a modification to the Alembic configuration. - -The changes we want to make are to add `compare_type=True`[^alembic_docs] in both `context.configure()` calls: - -```python title="migrations/env.py" -from __future__ import with_statement - -import logging -from logging.config import fileConfig - -from flask import current_app - -from alembic import context - -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. -config = context.config - -# Interpret the config file for Python logging. -# This line sets up loggers basically. -fileConfig(config.config_file_name) -logger = logging.getLogger('alembic.env') - -# add your model's MetaData object here -# for 'autogenerate' support -# from myapp import mymodel -# target_metadata = mymodel.Base.metadata -config.set_main_option( - 'sqlalchemy.url', - str(current_app.extensions['migrate'].db.get_engine().url).replace( - '%', '%%')) -target_metadata = current_app.extensions['migrate'].db.metadata - -# other values from the config, defined by the needs of env.py, -# can be acquired: -# my_important_option = config.get_main_option("my_important_option") -# ... etc. - - -def run_migrations_offline(): - """Run migrations in 'offline' mode. - - This configures the context with just a URL - and not an Engine, though an Engine is acceptable - here as well. By skipping the Engine creation - we don't even need a DBAPI to be available. - - Calls to context.execute() here emit the given string to the - script output. - - """ - url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, - target_metadata=target_metadata, - # highlight-start - compare_type=True, - # highlight-end - literal_binds=True - ) - - with context.begin_transaction(): - context.run_migrations() - - -def run_migrations_online(): - """Run migrations in 'online' mode. - - In this scenario we need to create an Engine - and associate a connection with the context. - - """ - - # this callback is used to prevent an auto-migration from being generated - # when there are no changes to the schema - # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html - def process_revision_directives(context, revision, directives): - if getattr(config.cmd_opts, 'autogenerate', False): - script = directives[0] - if script.upgrade_ops.is_empty(): - directives[:] = [] - logger.info('No changes in schema detected.') - - connectable = current_app.extensions['migrate'].db.get_engine() - - with connectable.connect() as connection: - context.configure( - connection=connection, - target_metadata=target_metadata, - process_revision_directives=process_revision_directives, - # highlight-start - compare_type=True, - # highlight-end - **current_app.extensions['migrate'].configure_args, - ) - - with context.begin_transaction(): - context.run_migrations() - - -if context.is_offline_mode(): - run_migrations_offline() -else: - run_migrations_online() - -``` - -Next, let's create the new migration: +Now we want to create a new migration so that our changes to the `UserModel` will be applied: ```bash flask db migrate diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/README.md b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/README.md new file mode 100644 index 00000000..5a3290c5 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/README.md @@ -0,0 +1,116 @@ +# How to run the app and database with Docker Compose + +Up until now we've been running `docker compose up` to start the REST API container. + +Now let's modify our `docker-compose.yml` file to include spinning up a new PostgreSQL database. + +```yaml +version: '3' +services: + web: + build: . + ports: + - "5000:80" + depends_on: + - db + env_file: + - ./.env + volumes: + - .:/app + db: + image: postgres + environment: + - POSTGRES_PASSWORD=password + - POSTGRES_DB=myapp + volumes: + - postgres_data:/var/lib/postgresql/data +volumes: + postgres_data: +``` + +The `postgres` image accepts various environment variables, among them: + +- `POSTGRES_PASSWORD`, defaulting to `postgres` +- `POSTGERS_DB`, defaulting to `postgres` +- `POSTGRES_USER`, defaulting to `postgres` +- `POSTGRES_HOST`, defaulting to `localhost` +- `POSTGRES_PORT`, defaulting to `5432` + +We should at least set a secure password. Above we're changing the password and database to `password` and `myapp` respectively. + +:::caution +Remember to also change your `DATABASE_URL` in your `.env` file that the REST API container is using. It should look like this: + +``` +DATABASE_URL=postgresql://postgres:password@db/myapp +``` + +When Docker Compose runs, it creates a virtual network[^1] which allows you to connect to `db`, which connects to the running `db` service container. +::: + +## Named volumes in Docker Compose + +You'll notice that our `docker-compose.yml` file has these lines: + +``` + volumes: + - postgres_data:/var/lib/postgresql/data +volumes: + postgres_data: +``` + +The bottom two lines define a named volume. This is data that will be stored by Docker and can be reused across container runs. We're calling it `postgres_data`, but it isn't assigned to anything there. + +In the top two lines, which are part of the `db` service definition, we say that the `postgres_data` named volume is mapped to `/var/lib/postgresql/data` in the container. + +`/var/lib/postgresql/data` is where the `postgres` image saves PostgreSQL data (such as databases, tables, etc). Therefore, as you create databases, tables, and store data, the named volume `postgres_data` will contain them. + +When you restart the container (or even rebuilt it), you can use the same named volume to keep access to old data. + +If you want to delete the entire database content, you can do so by deleting the volume through Docker Desktop, or with this command: + +``` +docker compose down -v +``` + +## Starting the whole system + +Now you're ready to start the Docker Compose system! If you need to rebuild the REST API container first, run: + +``` +docker compose up --build --force-recreate --no-deps web +``` + +You'll get an error due to no database being available. That's OK, as long as the container is rebuilt! + +Then press `CTRL+C` to stop it, and start the whole system with: + +``` +docker compose up +``` + +Now you can make a request to your API on port 5000, and it should work, storing the data in the database! + +## Running the system in background mode + +When we run the system with `docker compose up`, it takes up the terminal until we stop it with `CTRL+C`. + +If you want to run it in "Daemon" mode, or in the background, so you can use the terminal for other things, you can use: + +``` +docker compose up -d +``` + +Then to stop the system, use: + +``` +docker compose down +``` + +Note you must be in the folder that contains your `docker-compose.yml` file in order to bring the system up or down. + +:::warning +Running `docker compose down` will **not** delete your named volumes. You need to use the `-v` flag for that. Deleting the named volumes deletes the data in them irreversibly. +::: + +[^1]: [Networking in Compose (official docs)](https://docs.docker.com/compose/networking/) \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/.env.example b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/.env.example new file mode 100644 index 00000000..4cc714a2 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/.env.example @@ -0,0 +1 @@ +DATABASE_URL= \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/.flaskenv b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/.flaskenv new file mode 100644 index 00000000..ccb3106e --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/.flaskenv @@ -0,0 +1,2 @@ +FLASK_APP=app +FLASK_DEBUG=True \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/.gitignore b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/.gitignore new file mode 100644 index 00000000..6104f428 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/.gitignore @@ -0,0 +1,7 @@ +.env +.venv +.vscode +__pycache__ +data.db +*.pyc +.DS_Store \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/Dockerfile b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/Dockerfile new file mode 100644 index 00000000..121cf5b6 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.10 +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir --upgrade -r requirements.txt +COPY . . +CMD ["/bin/bash", "docker-entrypoint.sh"] \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/README.md b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/README.md new file mode 100644 index 00000000..ae704d28 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/README.md @@ -0,0 +1,3 @@ +# REST APIs Recording Project + +Nothing here yet! diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/app.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/app.py new file mode 100644 index 00000000..0adde16e --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/app.py @@ -0,0 +1,104 @@ +import os + +from flask import Flask, jsonify +from flask_smorest import Api +from flask_jwt_extended import JWTManager +from flask_migrate import Migrate +from dotenv import load_dotenv + + +from db import db +from blocklist import BLOCKLIST + +from resources.item import blp as ItemBlueprint +from resources.store import blp as StoreBlueprint +from resources.tag import blp as TagBlueprint +from resources.user import blp as UserBlueprint + + +def create_app(db_url=None): + app = Flask(__name__) + load_dotenv() + + app.config["PROPAGATE_EXCEPTIONS"] = True + app.config["API_TITLE"] = "Stores REST API" + app.config["API_VERSION"] = "v1" + app.config["OPENAPI_VERSION"] = "3.0.3" + app.config["OPENAPI_URL_PREFIX"] = "/" + app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui" + app.config["OPENAPI_SWAGGER_UI_URL"] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/" + app.config["SQLALCHEMY_DATABASE_URI"] = db_url or os.getenv("DATABASE_URL", "sqlite:///data.db") + app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False + db.init_app(app) + migrate = Migrate(app, db) + api = Api(app) + + app.config["JWT_SECRET_KEY"] = "jose" + jwt = JWTManager(app) + + @jwt.token_in_blocklist_loader + def check_if_token_in_blocklist(jwt_header, jwt_payload): + return jwt_payload["jti"] in BLOCKLIST + + @jwt.revoked_token_loader + def revoked_token_callback(jwt_header, jwt_payload): + return ( + jsonify( + {"description": "The token has been revoked.", "error": "token_revoked"} + ), + 401, + ) + + @jwt.needs_fresh_token_loader + def token_not_fresh_callback(jwt_header, jwt_payload): + return ( + jsonify( + { + "description": "The token is not fresh.", + "error": "fresh_token_required", + } + ), + 401, + ) + + @jwt.additional_claims_loader + def add_claims_to_jwt(identity): + # Look in the database and see whether the user is an admin + if identity == 1: + return {"is_admin": True} + return {"is_admin": False} + + @jwt.expired_token_loader + def expired_token_callback(jwt_header, jwt_payload): + return ( + jsonify({"message": "The token has expired.", "error": "token_expired"}), + 401, + ) + + @jwt.invalid_token_loader + def invalid_token_callback(error): + return ( + jsonify( + {"message": "Signature verification failed.", "error": "invalid_token"} + ), + 401, + ) + + @jwt.unauthorized_loader + def missing_token_callback(error): + return ( + jsonify( + { + "description": "Request does not contain an access token.", + "error": "authorization_required", + } + ), + 401, + ) + + api.register_blueprint(ItemBlueprint) + api.register_blueprint(StoreBlueprint) + api.register_blueprint(TagBlueprint) + api.register_blueprint(UserBlueprint) + + return app \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/blocklist.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/blocklist.py new file mode 100644 index 00000000..77751bef --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/blocklist.py @@ -0,0 +1,9 @@ +""" +blocklist.py + +This file just contains the blocklist of the JWT tokens. It will be imported by +app and the logout resource so that tokens can be added to the blocklist when the +user logs out. +""" + +BLOCKLIST = set() \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/db.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/db.py new file mode 100644 index 00000000..f0b13d6f --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/db.py @@ -0,0 +1,3 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/docker-compose.yml b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/docker-compose.yml new file mode 100644 index 00000000..e748d8b6 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/docker-compose.yml @@ -0,0 +1,21 @@ +version: '3' +services: + web: + build: . + ports: + - "5000:80" + depends_on: + - db + env_file: + - ./.env + volumes: + - .:/app + db: + image: postgres + environment: + - POSTGRES_PASSWORD=password + - POSTGRES_DB=myapp + volumes: + - postgres_data:/var/lib/postgresql/data +volumes: + postgres_data: \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/docker-entrypoint.sh b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/docker-entrypoint.sh new file mode 100644 index 00000000..134c2988 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/docker-entrypoint.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +flask db upgrade + +exec gunicorn --bind 0.0.0.0:80 "app:create_app()" \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/migrations/README b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/migrations/README new file mode 100644 index 00000000..0e048441 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/migrations/alembic.ini b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/migrations/alembic.ini new file mode 100644 index 00000000..ec9d45c2 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/migrations/env.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/migrations/env.py new file mode 100644 index 00000000..753d8f06 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/migrations/env.py @@ -0,0 +1,93 @@ +from __future__ import with_statement + +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option( + 'sqlalchemy.url', + str(current_app.extensions['migrate'].db.get_engine().url).replace( + '%', '%%')) +target_metadata = current_app.extensions['migrate'].db.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + connectable = current_app.extensions['migrate'].db.get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=target_metadata, + process_revision_directives=process_revision_directives, + **current_app.extensions['migrate'].configure_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/migrations/script.py.mako b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/migrations/script.py.mako new file mode 100644 index 00000000..2c015630 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/migrations/versions/07006e31e788_.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/migrations/versions/07006e31e788_.py new file mode 100644 index 00000000..e58a46db --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/migrations/versions/07006e31e788_.py @@ -0,0 +1,68 @@ +"""empty message + +Revision ID: 07006e31e788 +Revises: +Create Date: 2022-08-15 12:44:59.705694 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '07006e31e788' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('stores', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=80), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_table('users', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('username', sa.String(length=80), nullable=False), + sa.Column('password', sa.String(length=80), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('username') + ) + op.create_table('items', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=80), nullable=False), + sa.Column('price', sa.Float(precision=2), nullable=False), + sa.Column('store_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('tags', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=80), nullable=False), + sa.Column('store_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_table('items_tags', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('item_id', sa.Integer(), nullable=True), + sa.Column('tag_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['item_id'], ['items.id'], ), + sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('items_tags') + op.drop_table('tags') + op.drop_table('items') + op.drop_table('users') + op.drop_table('stores') + # ### end Alembic commands ### diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/migrations/versions/8ca023a4a4b0_.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/migrations/versions/8ca023a4a4b0_.py new file mode 100644 index 00000000..3c369e48 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/migrations/versions/8ca023a4a4b0_.py @@ -0,0 +1,28 @@ +"""empty message + +Revision ID: 8ca023a4a4b0 +Revises: 07006e31e788 +Create Date: 2022-08-15 12:52:41.303543 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '8ca023a4a4b0' +down_revision = '07006e31e788' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('items', sa.Column('description', sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('items', 'description') + # ### end Alembic commands ### diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/migrations/versions/bb5da1e68550_.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/migrations/versions/bb5da1e68550_.py new file mode 100644 index 00000000..e6e23e40 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/migrations/versions/bb5da1e68550_.py @@ -0,0 +1,42 @@ +"""empty message + +Revision ID: bb5da1e68550 +Revises: 8ca023a4a4b0 +Create Date: 2022-08-29 13:06:57.697368 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'bb5da1e68550' +down_revision = '8ca023a4a4b0' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('items', 'price', + existing_type=sa.REAL(), + type_=sa.Float(precision=2), + existing_nullable=False) + op.alter_column('users', 'password', + existing_type=sa.VARCHAR(length=80), + type_=sa.String(length=256), + existing_nullable=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('users', 'password', + existing_type=sa.String(length=256), + type_=sa.VARCHAR(length=80), + existing_nullable=False) + op.alter_column('items', 'price', + existing_type=sa.Float(precision=2), + type_=sa.REAL(), + existing_nullable=False) + # ### end Alembic commands ### diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/models/__init__.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/models/__init__.py new file mode 100644 index 00000000..04f2e012 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/models/__init__.py @@ -0,0 +1,5 @@ +from models.store import StoreModel +from models.item import ItemModel +from models.tag import TagModel +from models.item_tags import ItemTags +from models.user import UserModel \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/models/item.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/models/item.py new file mode 100644 index 00000000..45006d57 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/models/item.py @@ -0,0 +1,13 @@ +from sqlalchemy import ForeignKey +from db import db + +class ItemModel(db.Model): + __tablename__ = "items" + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(80), unique=False, nullable=False) + description = db.Column(db.String) + price = db.Column(db.Float(precision=2), unique=False, nullable=False) + store_id = db.Column(db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False) + store = db.relationship("StoreModel", back_populates="items") + tags = db.relationship("TagModel", back_populates="items", secondary="items_tags") \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/models/item_tags.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/models/item_tags.py new file mode 100644 index 00000000..5dfd5cf5 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/models/item_tags.py @@ -0,0 +1,9 @@ +from db import db + + +class ItemTags(db.Model): + __tablename__ = "items_tags" + + id = db.Column(db.Integer, primary_key=True) + item_id = db.Column(db.Integer, db.ForeignKey("items.id")) + tag_id = db.Column(db.Integer, db.ForeignKey("tags.id")) \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/models/store.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/models/store.py new file mode 100644 index 00000000..90ad43d5 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/models/store.py @@ -0,0 +1,10 @@ +from db import db + + +class StoreModel(db.Model): + __tablename__ = "stores" + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(80), unique=True, nullable=False) + items = db.relationship("ItemModel", back_populates="store", lazy="dynamic") + tags = db.relationship("TagModel", back_populates="store", lazy="dynamic") \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/models/tag.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/models/tag.py new file mode 100644 index 00000000..37b8ed85 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/models/tag.py @@ -0,0 +1,12 @@ +from db import db + + +class TagModel(db.Model): + __tablename__ = "tags" + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(80), unique=False, nullable=False) + store_id = db.Column(db.Integer, db.ForeignKey("stores.id"), nullable=False) + + store = db.relationship("StoreModel", back_populates="tags") + items = db.relationship("ItemModel", back_populates="tags", secondary="items_tags") \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/models/user.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/models/user.py new file mode 100644 index 00000000..fefdf936 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/models/user.py @@ -0,0 +1,9 @@ +from db import db + + +class UserModel(db.Model): + __tablename__ = "users" + + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(80), unique=True, nullable=False) + password = db.Column(db.String(256), nullable=False) \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/requirements.txt b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/requirements.txt new file mode 100644 index 00000000..3ec0fe24 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/requirements.txt @@ -0,0 +1,10 @@ +flask==2.3.2 +flask-smorest +python-dotenv +sqlalchemy +flask-sqlalchemy +flask-jwt-extended +passlib +flask-migrate +gunicorn +psycopg2 \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/resources/item.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/resources/item.py new file mode 100644 index 00000000..545f73b5 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/resources/item.py @@ -0,0 +1,67 @@ +from flask.views import MethodView +from flask_smorest import Blueprint, abort +from flask_jwt_extended import jwt_required, get_jwt +from sqlalchemy.exc import SQLAlchemyError + +from db import db +from models import ItemModel +from schemas import ItemSchema, ItemUpdateSchema + +blp = Blueprint("Items", __name__, description="Operations on items") + + +@blp.route("/item/") +class Item(MethodView): + @jwt_required() + @blp.response(200, ItemSchema) + def get(self, item_id): + item = ItemModel.query.get_or_404(item_id) + return item + + @jwt_required() + def delete(self, item_id): + jwt = get_jwt() + if not jwt.get("is_admin"): + abort(401, message="Admin privilege required.") + + item = ItemModel.query.get_or_404(item_id) + db.session.delete(item) + db.session.commit() + return {"message": "Item deleted."} + + @blp.arguments(ItemUpdateSchema) + @blp.response(200, ItemSchema) + def put(self, item_data, item_id): + item = ItemModel.query.get(item_id) + if item: + item.price = item_data["price"] + item.name = item_data["name"] + else: + item = ItemModel(id=item_id, **item_data) + + db.session.add(item) + db.session.commit() + + return item + + +@blp.route("/item") +class ItemList(MethodView): + @jwt_required() + @blp.response(200, ItemSchema(many=True)) + def get(self): + return ItemModel.query.all() + + @jwt_required(fresh=True) + @blp.arguments(ItemSchema) + @blp.response(201, ItemSchema) + def post(self, item_data): + item = ItemModel(**item_data) + + try: + db.session.add(item) + db.session.commit() + except SQLAlchemyError: + abort(500, message="An error occurred whilte inserting the item.") + + return item \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/resources/store.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/resources/store.py new file mode 100644 index 00000000..488c1f67 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/resources/store.py @@ -0,0 +1,51 @@ +import uuid +from flask import request +from flask.views import MethodView +from flask_smorest import Blueprint, abort + +from sqlalchemy.exc import SQLAlchemyError, IntegrityError + +from db import db +from models import StoreModel +from schemas import StoreSchema + + +blp = Blueprint("stores", __name__, description="Operations on stores") + + +@blp.route("/store/") +class Store(MethodView): + @blp.response(200, StoreSchema) + def get(self, store_id): + store = StoreModel.query.get_or_404(store_id) + return store + + def delete(self, store_id): + store = StoreModel.query.get_or_404(store_id) + db.session.delete(store) + db.session.commit() + return {"message": "Store deleted"} + + +@blp.route("/store") +class StoreList(MethodView): + @blp.response(200, StoreSchema(many=True)) + def get(self): + return StoreModel.query.all() + + @blp.arguments(StoreSchema) + @blp.response(200, StoreSchema) + def post(self, store_data): + store = StoreModel(**store_data) + try: + db.session.add(store) + db.session.commit() + except IntegrityError: + abort( + 400, + message="A store with that name already exists.", + ) + except SQLAlchemyError: + abort(500, message="An error occurred creating the store.") + + return store \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/resources/tag.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/resources/tag.py new file mode 100644 index 00000000..f15c41b9 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/resources/tag.py @@ -0,0 +1,97 @@ +from flask.views import MethodView +from flask_smorest import Blueprint, abort +from sqlalchemy.exc import SQLAlchemyError + +from db import db +from models import TagModel, StoreModel, ItemModel +from schemas import TagSchema, TagAndItemSchema + +blp = Blueprint("Tags", "tags", description="Operations on tags") + + +@blp.route("/store//tag") +class TagsInStore(MethodView): + @blp.response(200, TagSchema(many=True)) + def get(self, store_id): + store = StoreModel.query.get_or_404(store_id) + + return store.tags.all() + + @blp.arguments(TagSchema) + @blp.response(201, TagSchema) + def post(self, tag_data, store_id): + tag = TagModel(**tag_data, store_id=store_id) + + try: + db.session.add(tag) + db.session.commit() + except SQLAlchemyError as e: + abort( + 500, + message=str(e) + ) + + return tag + + +@blp.route("/item//tag/") +class LinkTagsToItem(MethodView): + @blp.response(201, TagSchema) + def post(self, item_id, tag_id): + item = ItemModel.query.get_or_404(item_id) + tag = TagModel.query.get_or_404(tag_id) + + item.tags.append(tag) + + try: + db.session.add(item) + db.session.commit() + except SQLAlchemyError: + abort(500, message="An error occurred while inserting the tag.") + + return tag + + @blp.response(200, TagAndItemSchema) + def delete(self, item_id, tag_id): + item = ItemModel.query.get_or_404(item_id) + tag = TagModel.query.get_or_404(tag_id) + + item.tags.remove(tag) + + try: + db.session.add(item) + db.session.commit() + except SQLAlchemyError: + abort(500, message="An error occurred while inserting the tag.") + + return {"message": "Item removed from tag", "item": item, "tag": tag} + + +@blp.route("/tag/") +class Tag(MethodView): + @blp.response(200, TagSchema) + def get(self, tag_id): + tag = TagModel.query.get_or_404(tag_id) + return tag + + @blp.response( + 202, + description="Deletes a tag if no item is tagged with it.", + example={"message": "Tag deleted."} + ) + @blp.alt_response(404, description="Tag not found.") + @blp.alt_response( + 400, + description="Returned if the tag is assigned to one or more items. In this case, the tag is not deleted." + ) + def delete(self, tag_id): + tag = TagModel.query.get_or_404(tag_id) + + if not tag.items: + db.session.delete(tag) + db.session.commit() + return {"message": "Tag deleted."} + abort( + 400, + message="Could not delete tag. Make sure tag is not associated with any items, then try again.", + ) \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/resources/user.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/resources/user.py new file mode 100644 index 00000000..a3edc00d --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/resources/user.py @@ -0,0 +1,77 @@ +from flask.views import MethodView +from flask_smorest import Blueprint, abort +from passlib.hash import pbkdf2_sha256 +from flask_jwt_extended import create_access_token, create_refresh_token, get_jwt_identity, jwt_required, get_jwt + +from db import db +from blocklist import BLOCKLIST +from models import UserModel +from schemas import UserSchema + + +blp = Blueprint("Users", "users", description="Operations on users") + + +@blp.route("/register") +class UserRegister(MethodView): + @blp.arguments(UserSchema) + def post(self, user_data): + if UserModel.query.filter(UserModel.username == user_data["username"]).first(): + abort(409, message="A user with that username already exists.") + + user = UserModel( + username=user_data["username"], + password=pbkdf2_sha256.hash(user_data["password"]) + ) + db.session.add(user) + db.session.commit() + + return {"message": "User created successfully."}, 201 + + +@blp.route("/login") +class UserLogin(MethodView): + @blp.arguments(UserSchema) + def post(self, user_data): + user = UserModel.query.filter( + UserModel.username == user_data["username"] + ).first() + + if user and pbkdf2_sha256.verify(user_data["password"], user.password): + access_token = create_access_token(identity=user.id, fresh=True) + refresh_token = create_refresh_token(identity=user.id) + return {"access_token": access_token, "refresh_token": refresh_token} + + abort(401, message="Invalid credentials.") + + +@blp.route("/refresh") +class TokenRefresh(MethodView): + @jwt_required(refresh=True) + def post(self): + current_user = get_jwt_identity() + new_token = create_access_token(identity=current_user, fresh=False) + return {"access_token": new_token} + + +@blp.route("/logout") +class UserLogout(MethodView): + @jwt_required() + def post(self): + jti = get_jwt()["jti"] + BLOCKLIST.add(jti) + return {"message": "Successfully logged out."} + + +@blp.route("/user/") +class User(MethodView): + @blp.response(200, UserSchema) + def get(self, user_id): + user = UserModel.query.get_or_404(user_id) + return user + + def delete(self, user_id): + user = UserModel.query.get_or_404(user_id) + db.session.delete(user) + db.session.commit() + return {"message": "User deleted."}, 200 \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/schemas.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/schemas.py new file mode 100644 index 00000000..cb3f7a07 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/end/schemas.py @@ -0,0 +1,52 @@ +from marshmallow import Schema, fields + + +class PlainItemSchema(Schema): + id = fields.Int(dump_only=True) + name = fields.Str(required=True) + price = fields.Float(required=True) + + +class PlainStoreSchema(Schema): + id = fields.Int(dump_only=True) + name = fields.Str(required=True) + + +class PlainTagSchema(Schema): + id = fields.Int(dump_only=True) + name = fields.Str() + + +class ItemUpdateSchema(Schema): + name = fields.Str() + price = fields.Float() + store_id = fields.Int() + + +class ItemSchema(PlainItemSchema): + store_id = fields.Int(required=True, load_only=True) + store = fields.Nested(PlainStoreSchema(), dump_only=True) + tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True) + + +class StoreSchema(PlainStoreSchema): + items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True) + tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True) + + +class TagSchema(PlainTagSchema): + store_id = fields.Int(load_only=True) + store = fields.Nested(PlainStoreSchema(), dump_only=True) + items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True) + + +class TagAndItemSchema(Schema): + message = fields.Str() + item = fields.Nested(ItemSchema) + tag = fields.Nested(TagSchema) + + +class UserSchema(Schema): + id = fields.Int(dump_only=True) + username = fields.Str(required=True) + password = fields.Str(required=True) \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/.env.example b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/.env.example new file mode 100644 index 00000000..4cc714a2 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/.env.example @@ -0,0 +1 @@ +DATABASE_URL= \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/.flaskenv b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/.flaskenv new file mode 100644 index 00000000..ccb3106e --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/.flaskenv @@ -0,0 +1,2 @@ +FLASK_APP=app +FLASK_DEBUG=True \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/.gitignore b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/.gitignore new file mode 100644 index 00000000..6104f428 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/.gitignore @@ -0,0 +1,7 @@ +.env +.venv +.vscode +__pycache__ +data.db +*.pyc +.DS_Store \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/Dockerfile b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/Dockerfile new file mode 100644 index 00000000..121cf5b6 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.10 +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir --upgrade -r requirements.txt +COPY . . +CMD ["/bin/bash", "docker-entrypoint.sh"] \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/README.md b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/README.md new file mode 100644 index 00000000..ae704d28 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/README.md @@ -0,0 +1,3 @@ +# REST APIs Recording Project + +Nothing here yet! diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/app.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/app.py new file mode 100644 index 00000000..0adde16e --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/app.py @@ -0,0 +1,104 @@ +import os + +from flask import Flask, jsonify +from flask_smorest import Api +from flask_jwt_extended import JWTManager +from flask_migrate import Migrate +from dotenv import load_dotenv + + +from db import db +from blocklist import BLOCKLIST + +from resources.item import blp as ItemBlueprint +from resources.store import blp as StoreBlueprint +from resources.tag import blp as TagBlueprint +from resources.user import blp as UserBlueprint + + +def create_app(db_url=None): + app = Flask(__name__) + load_dotenv() + + app.config["PROPAGATE_EXCEPTIONS"] = True + app.config["API_TITLE"] = "Stores REST API" + app.config["API_VERSION"] = "v1" + app.config["OPENAPI_VERSION"] = "3.0.3" + app.config["OPENAPI_URL_PREFIX"] = "/" + app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui" + app.config["OPENAPI_SWAGGER_UI_URL"] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/" + app.config["SQLALCHEMY_DATABASE_URI"] = db_url or os.getenv("DATABASE_URL", "sqlite:///data.db") + app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False + db.init_app(app) + migrate = Migrate(app, db) + api = Api(app) + + app.config["JWT_SECRET_KEY"] = "jose" + jwt = JWTManager(app) + + @jwt.token_in_blocklist_loader + def check_if_token_in_blocklist(jwt_header, jwt_payload): + return jwt_payload["jti"] in BLOCKLIST + + @jwt.revoked_token_loader + def revoked_token_callback(jwt_header, jwt_payload): + return ( + jsonify( + {"description": "The token has been revoked.", "error": "token_revoked"} + ), + 401, + ) + + @jwt.needs_fresh_token_loader + def token_not_fresh_callback(jwt_header, jwt_payload): + return ( + jsonify( + { + "description": "The token is not fresh.", + "error": "fresh_token_required", + } + ), + 401, + ) + + @jwt.additional_claims_loader + def add_claims_to_jwt(identity): + # Look in the database and see whether the user is an admin + if identity == 1: + return {"is_admin": True} + return {"is_admin": False} + + @jwt.expired_token_loader + def expired_token_callback(jwt_header, jwt_payload): + return ( + jsonify({"message": "The token has expired.", "error": "token_expired"}), + 401, + ) + + @jwt.invalid_token_loader + def invalid_token_callback(error): + return ( + jsonify( + {"message": "Signature verification failed.", "error": "invalid_token"} + ), + 401, + ) + + @jwt.unauthorized_loader + def missing_token_callback(error): + return ( + jsonify( + { + "description": "Request does not contain an access token.", + "error": "authorization_required", + } + ), + 401, + ) + + api.register_blueprint(ItemBlueprint) + api.register_blueprint(StoreBlueprint) + api.register_blueprint(TagBlueprint) + api.register_blueprint(UserBlueprint) + + return app \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/blocklist.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/blocklist.py new file mode 100644 index 00000000..77751bef --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/blocklist.py @@ -0,0 +1,9 @@ +""" +blocklist.py + +This file just contains the blocklist of the JWT tokens. It will be imported by +app and the logout resource so that tokens can be added to the blocklist when the +user logs out. +""" + +BLOCKLIST = set() \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/db.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/db.py new file mode 100644 index 00000000..f0b13d6f --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/db.py @@ -0,0 +1,3 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/docker-entrypoint.sh b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/docker-entrypoint.sh new file mode 100644 index 00000000..134c2988 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/docker-entrypoint.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +flask db upgrade + +exec gunicorn --bind 0.0.0.0:80 "app:create_app()" \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/migrations/README b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/migrations/README new file mode 100644 index 00000000..0e048441 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/migrations/alembic.ini b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/migrations/alembic.ini new file mode 100644 index 00000000..ec9d45c2 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/migrations/env.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/migrations/env.py new file mode 100644 index 00000000..753d8f06 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/migrations/env.py @@ -0,0 +1,93 @@ +from __future__ import with_statement + +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option( + 'sqlalchemy.url', + str(current_app.extensions['migrate'].db.get_engine().url).replace( + '%', '%%')) +target_metadata = current_app.extensions['migrate'].db.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + connectable = current_app.extensions['migrate'].db.get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=target_metadata, + process_revision_directives=process_revision_directives, + **current_app.extensions['migrate'].configure_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/migrations/script.py.mako b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/migrations/script.py.mako new file mode 100644 index 00000000..2c015630 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/migrations/versions/07006e31e788_.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/migrations/versions/07006e31e788_.py new file mode 100644 index 00000000..e58a46db --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/migrations/versions/07006e31e788_.py @@ -0,0 +1,68 @@ +"""empty message + +Revision ID: 07006e31e788 +Revises: +Create Date: 2022-08-15 12:44:59.705694 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '07006e31e788' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('stores', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=80), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_table('users', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('username', sa.String(length=80), nullable=False), + sa.Column('password', sa.String(length=80), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('username') + ) + op.create_table('items', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=80), nullable=False), + sa.Column('price', sa.Float(precision=2), nullable=False), + sa.Column('store_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('tags', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=80), nullable=False), + sa.Column('store_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['store_id'], ['stores.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_table('items_tags', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('item_id', sa.Integer(), nullable=True), + sa.Column('tag_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['item_id'], ['items.id'], ), + sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('items_tags') + op.drop_table('tags') + op.drop_table('items') + op.drop_table('users') + op.drop_table('stores') + # ### end Alembic commands ### diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/migrations/versions/8ca023a4a4b0_.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/migrations/versions/8ca023a4a4b0_.py new file mode 100644 index 00000000..3c369e48 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/migrations/versions/8ca023a4a4b0_.py @@ -0,0 +1,28 @@ +"""empty message + +Revision ID: 8ca023a4a4b0 +Revises: 07006e31e788 +Create Date: 2022-08-15 12:52:41.303543 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '8ca023a4a4b0' +down_revision = '07006e31e788' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('items', sa.Column('description', sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('items', 'description') + # ### end Alembic commands ### diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/migrations/versions/bb5da1e68550_.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/migrations/versions/bb5da1e68550_.py new file mode 100644 index 00000000..e6e23e40 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/migrations/versions/bb5da1e68550_.py @@ -0,0 +1,42 @@ +"""empty message + +Revision ID: bb5da1e68550 +Revises: 8ca023a4a4b0 +Create Date: 2022-08-29 13:06:57.697368 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'bb5da1e68550' +down_revision = '8ca023a4a4b0' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('items', 'price', + existing_type=sa.REAL(), + type_=sa.Float(precision=2), + existing_nullable=False) + op.alter_column('users', 'password', + existing_type=sa.VARCHAR(length=80), + type_=sa.String(length=256), + existing_nullable=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('users', 'password', + existing_type=sa.String(length=256), + type_=sa.VARCHAR(length=80), + existing_nullable=False) + op.alter_column('items', 'price', + existing_type=sa.Float(precision=2), + type_=sa.REAL(), + existing_nullable=False) + # ### end Alembic commands ### diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/models/__init__.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/models/__init__.py new file mode 100644 index 00000000..04f2e012 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/models/__init__.py @@ -0,0 +1,5 @@ +from models.store import StoreModel +from models.item import ItemModel +from models.tag import TagModel +from models.item_tags import ItemTags +from models.user import UserModel \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/models/item.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/models/item.py new file mode 100644 index 00000000..45006d57 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/models/item.py @@ -0,0 +1,13 @@ +from sqlalchemy import ForeignKey +from db import db + +class ItemModel(db.Model): + __tablename__ = "items" + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(80), unique=False, nullable=False) + description = db.Column(db.String) + price = db.Column(db.Float(precision=2), unique=False, nullable=False) + store_id = db.Column(db.Integer, db.ForeignKey("stores.id"), unique=False, nullable=False) + store = db.relationship("StoreModel", back_populates="items") + tags = db.relationship("TagModel", back_populates="items", secondary="items_tags") \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/models/item_tags.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/models/item_tags.py new file mode 100644 index 00000000..5dfd5cf5 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/models/item_tags.py @@ -0,0 +1,9 @@ +from db import db + + +class ItemTags(db.Model): + __tablename__ = "items_tags" + + id = db.Column(db.Integer, primary_key=True) + item_id = db.Column(db.Integer, db.ForeignKey("items.id")) + tag_id = db.Column(db.Integer, db.ForeignKey("tags.id")) \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/models/store.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/models/store.py new file mode 100644 index 00000000..90ad43d5 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/models/store.py @@ -0,0 +1,10 @@ +from db import db + + +class StoreModel(db.Model): + __tablename__ = "stores" + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(80), unique=True, nullable=False) + items = db.relationship("ItemModel", back_populates="store", lazy="dynamic") + tags = db.relationship("TagModel", back_populates="store", lazy="dynamic") \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/models/tag.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/models/tag.py new file mode 100644 index 00000000..37b8ed85 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/models/tag.py @@ -0,0 +1,12 @@ +from db import db + + +class TagModel(db.Model): + __tablename__ = "tags" + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(80), unique=False, nullable=False) + store_id = db.Column(db.Integer, db.ForeignKey("stores.id"), nullable=False) + + store = db.relationship("StoreModel", back_populates="tags") + items = db.relationship("ItemModel", back_populates="tags", secondary="items_tags") \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/models/user.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/models/user.py new file mode 100644 index 00000000..fefdf936 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/models/user.py @@ -0,0 +1,9 @@ +from db import db + + +class UserModel(db.Model): + __tablename__ = "users" + + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(80), unique=True, nullable=False) + password = db.Column(db.String(256), nullable=False) \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/requirements.txt b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/requirements.txt new file mode 100644 index 00000000..3ec0fe24 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/requirements.txt @@ -0,0 +1,10 @@ +flask==2.3.2 +flask-smorest +python-dotenv +sqlalchemy +flask-sqlalchemy +flask-jwt-extended +passlib +flask-migrate +gunicorn +psycopg2 \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/resources/item.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/resources/item.py new file mode 100644 index 00000000..545f73b5 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/resources/item.py @@ -0,0 +1,67 @@ +from flask.views import MethodView +from flask_smorest import Blueprint, abort +from flask_jwt_extended import jwt_required, get_jwt +from sqlalchemy.exc import SQLAlchemyError + +from db import db +from models import ItemModel +from schemas import ItemSchema, ItemUpdateSchema + +blp = Blueprint("Items", __name__, description="Operations on items") + + +@blp.route("/item/") +class Item(MethodView): + @jwt_required() + @blp.response(200, ItemSchema) + def get(self, item_id): + item = ItemModel.query.get_or_404(item_id) + return item + + @jwt_required() + def delete(self, item_id): + jwt = get_jwt() + if not jwt.get("is_admin"): + abort(401, message="Admin privilege required.") + + item = ItemModel.query.get_or_404(item_id) + db.session.delete(item) + db.session.commit() + return {"message": "Item deleted."} + + @blp.arguments(ItemUpdateSchema) + @blp.response(200, ItemSchema) + def put(self, item_data, item_id): + item = ItemModel.query.get(item_id) + if item: + item.price = item_data["price"] + item.name = item_data["name"] + else: + item = ItemModel(id=item_id, **item_data) + + db.session.add(item) + db.session.commit() + + return item + + +@blp.route("/item") +class ItemList(MethodView): + @jwt_required() + @blp.response(200, ItemSchema(many=True)) + def get(self): + return ItemModel.query.all() + + @jwt_required(fresh=True) + @blp.arguments(ItemSchema) + @blp.response(201, ItemSchema) + def post(self, item_data): + item = ItemModel(**item_data) + + try: + db.session.add(item) + db.session.commit() + except SQLAlchemyError: + abort(500, message="An error occurred whilte inserting the item.") + + return item \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/resources/store.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/resources/store.py new file mode 100644 index 00000000..488c1f67 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/resources/store.py @@ -0,0 +1,51 @@ +import uuid +from flask import request +from flask.views import MethodView +from flask_smorest import Blueprint, abort + +from sqlalchemy.exc import SQLAlchemyError, IntegrityError + +from db import db +from models import StoreModel +from schemas import StoreSchema + + +blp = Blueprint("stores", __name__, description="Operations on stores") + + +@blp.route("/store/") +class Store(MethodView): + @blp.response(200, StoreSchema) + def get(self, store_id): + store = StoreModel.query.get_or_404(store_id) + return store + + def delete(self, store_id): + store = StoreModel.query.get_or_404(store_id) + db.session.delete(store) + db.session.commit() + return {"message": "Store deleted"} + + +@blp.route("/store") +class StoreList(MethodView): + @blp.response(200, StoreSchema(many=True)) + def get(self): + return StoreModel.query.all() + + @blp.arguments(StoreSchema) + @blp.response(200, StoreSchema) + def post(self, store_data): + store = StoreModel(**store_data) + try: + db.session.add(store) + db.session.commit() + except IntegrityError: + abort( + 400, + message="A store with that name already exists.", + ) + except SQLAlchemyError: + abort(500, message="An error occurred creating the store.") + + return store \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/resources/tag.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/resources/tag.py new file mode 100644 index 00000000..f15c41b9 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/resources/tag.py @@ -0,0 +1,97 @@ +from flask.views import MethodView +from flask_smorest import Blueprint, abort +from sqlalchemy.exc import SQLAlchemyError + +from db import db +from models import TagModel, StoreModel, ItemModel +from schemas import TagSchema, TagAndItemSchema + +blp = Blueprint("Tags", "tags", description="Operations on tags") + + +@blp.route("/store//tag") +class TagsInStore(MethodView): + @blp.response(200, TagSchema(many=True)) + def get(self, store_id): + store = StoreModel.query.get_or_404(store_id) + + return store.tags.all() + + @blp.arguments(TagSchema) + @blp.response(201, TagSchema) + def post(self, tag_data, store_id): + tag = TagModel(**tag_data, store_id=store_id) + + try: + db.session.add(tag) + db.session.commit() + except SQLAlchemyError as e: + abort( + 500, + message=str(e) + ) + + return tag + + +@blp.route("/item//tag/") +class LinkTagsToItem(MethodView): + @blp.response(201, TagSchema) + def post(self, item_id, tag_id): + item = ItemModel.query.get_or_404(item_id) + tag = TagModel.query.get_or_404(tag_id) + + item.tags.append(tag) + + try: + db.session.add(item) + db.session.commit() + except SQLAlchemyError: + abort(500, message="An error occurred while inserting the tag.") + + return tag + + @blp.response(200, TagAndItemSchema) + def delete(self, item_id, tag_id): + item = ItemModel.query.get_or_404(item_id) + tag = TagModel.query.get_or_404(tag_id) + + item.tags.remove(tag) + + try: + db.session.add(item) + db.session.commit() + except SQLAlchemyError: + abort(500, message="An error occurred while inserting the tag.") + + return {"message": "Item removed from tag", "item": item, "tag": tag} + + +@blp.route("/tag/") +class Tag(MethodView): + @blp.response(200, TagSchema) + def get(self, tag_id): + tag = TagModel.query.get_or_404(tag_id) + return tag + + @blp.response( + 202, + description="Deletes a tag if no item is tagged with it.", + example={"message": "Tag deleted."} + ) + @blp.alt_response(404, description="Tag not found.") + @blp.alt_response( + 400, + description="Returned if the tag is assigned to one or more items. In this case, the tag is not deleted." + ) + def delete(self, tag_id): + tag = TagModel.query.get_or_404(tag_id) + + if not tag.items: + db.session.delete(tag) + db.session.commit() + return {"message": "Tag deleted."} + abort( + 400, + message="Could not delete tag. Make sure tag is not associated with any items, then try again.", + ) \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/resources/user.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/resources/user.py new file mode 100644 index 00000000..a3edc00d --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/resources/user.py @@ -0,0 +1,77 @@ +from flask.views import MethodView +from flask_smorest import Blueprint, abort +from passlib.hash import pbkdf2_sha256 +from flask_jwt_extended import create_access_token, create_refresh_token, get_jwt_identity, jwt_required, get_jwt + +from db import db +from blocklist import BLOCKLIST +from models import UserModel +from schemas import UserSchema + + +blp = Blueprint("Users", "users", description="Operations on users") + + +@blp.route("/register") +class UserRegister(MethodView): + @blp.arguments(UserSchema) + def post(self, user_data): + if UserModel.query.filter(UserModel.username == user_data["username"]).first(): + abort(409, message="A user with that username already exists.") + + user = UserModel( + username=user_data["username"], + password=pbkdf2_sha256.hash(user_data["password"]) + ) + db.session.add(user) + db.session.commit() + + return {"message": "User created successfully."}, 201 + + +@blp.route("/login") +class UserLogin(MethodView): + @blp.arguments(UserSchema) + def post(self, user_data): + user = UserModel.query.filter( + UserModel.username == user_data["username"] + ).first() + + if user and pbkdf2_sha256.verify(user_data["password"], user.password): + access_token = create_access_token(identity=user.id, fresh=True) + refresh_token = create_refresh_token(identity=user.id) + return {"access_token": access_token, "refresh_token": refresh_token} + + abort(401, message="Invalid credentials.") + + +@blp.route("/refresh") +class TokenRefresh(MethodView): + @jwt_required(refresh=True) + def post(self): + current_user = get_jwt_identity() + new_token = create_access_token(identity=current_user, fresh=False) + return {"access_token": new_token} + + +@blp.route("/logout") +class UserLogout(MethodView): + @jwt_required() + def post(self): + jti = get_jwt()["jti"] + BLOCKLIST.add(jti) + return {"message": "Successfully logged out."} + + +@blp.route("/user/") +class User(MethodView): + @blp.response(200, UserSchema) + def get(self, user_id): + user = UserModel.query.get_or_404(user_id) + return user + + def delete(self, user_id): + user = UserModel.query.get_or_404(user_id) + db.session.delete(user) + db.session.commit() + return {"message": "User deleted."}, 200 \ No newline at end of file diff --git a/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/schemas.py b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/schemas.py new file mode 100644 index 00000000..cb3f7a07 --- /dev/null +++ b/docs/docs/11_deploy_to_render/06_run_everything_docker_compose/start/schemas.py @@ -0,0 +1,52 @@ +from marshmallow import Schema, fields + + +class PlainItemSchema(Schema): + id = fields.Int(dump_only=True) + name = fields.Str(required=True) + price = fields.Float(required=True) + + +class PlainStoreSchema(Schema): + id = fields.Int(dump_only=True) + name = fields.Str(required=True) + + +class PlainTagSchema(Schema): + id = fields.Int(dump_only=True) + name = fields.Str() + + +class ItemUpdateSchema(Schema): + name = fields.Str() + price = fields.Float() + store_id = fields.Int() + + +class ItemSchema(PlainItemSchema): + store_id = fields.Int(required=True, load_only=True) + store = fields.Nested(PlainStoreSchema(), dump_only=True) + tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True) + + +class StoreSchema(PlainStoreSchema): + items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True) + tags = fields.List(fields.Nested(PlainTagSchema()), dump_only=True) + + +class TagSchema(PlainTagSchema): + store_id = fields.Int(load_only=True) + store = fields.Nested(PlainStoreSchema(), dump_only=True) + items = fields.List(fields.Nested(PlainItemSchema()), dump_only=True) + + +class TagAndItemSchema(Schema): + message = fields.Str() + item = fields.Nested(ItemSchema) + tag = fields.Nested(TagSchema) + + +class UserSchema(Schema): + id = fields.Int(dump_only=True) + username = fields.Str(required=True) + password = fields.Str(required=True) \ No newline at end of file diff --git a/docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/CONTRIBUTING.md b/docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/CONTRIBUTING.md deleted file mode 100644 index 7e550e79..00000000 --- a/docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/CONTRIBUTING.md +++ /dev/null @@ -1,7 +0,0 @@ -# CONTRIBUTING - -## How to run the Dockerfile locally - -``` -docker run -dp 5000:5000 -w /app -v "$(pwd):/app" IMAGE_NAME sh -c "flask run" -``` diff --git a/docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/app.py b/docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/app.py index 65d7d0ca..0adde16e 100644 --- a/docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/app.py +++ b/docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/app.py @@ -9,7 +9,6 @@ from db import db from blocklist import BLOCKLIST -import models from resources.item import blp as ItemBlueprint from resources.store import blp as StoreBlueprint @@ -97,10 +96,6 @@ def missing_token_callback(error): 401, ) - @app.before_first_request - def create_tables(): - db.create_all() - api.register_blueprint(ItemBlueprint) api.register_blueprint(StoreBlueprint) api.register_blueprint(TagBlueprint) diff --git a/docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/migrations/env.py b/docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/migrations/env.py index 2ec83a7e..753d8f06 100644 --- a/docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/migrations/env.py +++ b/docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/end/migrations/env.py @@ -48,7 +48,6 @@ def run_migrations_offline(): context.configure( url=url, target_metadata=target_metadata, - compare_type=True, literal_binds=True ) @@ -81,7 +80,6 @@ def process_revision_directives(context, revision, directives): connection=connection, target_metadata=target_metadata, process_revision_directives=process_revision_directives, - compare_type=True, **current_app.extensions['migrate'].configure_args ) diff --git a/docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/CONTRIBUTING.md b/docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/CONTRIBUTING.md deleted file mode 100644 index 7e550e79..00000000 --- a/docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/CONTRIBUTING.md +++ /dev/null @@ -1,7 +0,0 @@ -# CONTRIBUTING - -## How to run the Dockerfile locally - -``` -docker run -dp 5000:5000 -w /app -v "$(pwd):/app" IMAGE_NAME sh -c "flask run" -``` diff --git a/docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/app.py b/docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/app.py index 65d7d0ca..0adde16e 100644 --- a/docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/app.py +++ b/docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/app.py @@ -9,7 +9,6 @@ from db import db from blocklist import BLOCKLIST -import models from resources.item import blp as ItemBlueprint from resources.store import blp as StoreBlueprint @@ -97,10 +96,6 @@ def missing_token_callback(error): 401, ) - @app.before_first_request - def create_tables(): - db.create_all() - api.register_blueprint(ItemBlueprint) api.register_blueprint(StoreBlueprint) api.register_blueprint(TagBlueprint) diff --git a/docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/migrations/env.py b/docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/migrations/env.py index 2ec83a7e..753d8f06 100644 --- a/docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/migrations/env.py +++ b/docs/docs/12_task_queues_emails/01_send_emails_python_mailgun/start/migrations/env.py @@ -48,7 +48,6 @@ def run_migrations_offline(): context.configure( url=url, target_metadata=target_metadata, - compare_type=True, literal_binds=True ) @@ -81,7 +80,6 @@ def process_revision_directives(context, revision, directives): connection=connection, target_metadata=target_metadata, process_revision_directives=process_revision_directives, - compare_type=True, **current_app.extensions['migrate'].configure_args ) diff --git a/docs/docs/12_task_queues_emails/02_send_email_user_registration/end/CONTRIBUTING.md b/docs/docs/12_task_queues_emails/02_send_email_user_registration/end/CONTRIBUTING.md deleted file mode 100644 index 7e550e79..00000000 --- a/docs/docs/12_task_queues_emails/02_send_email_user_registration/end/CONTRIBUTING.md +++ /dev/null @@ -1,7 +0,0 @@ -# CONTRIBUTING - -## How to run the Dockerfile locally - -``` -docker run -dp 5000:5000 -w /app -v "$(pwd):/app" IMAGE_NAME sh -c "flask run" -``` diff --git a/docs/docs/12_task_queues_emails/02_send_email_user_registration/end/app.py b/docs/docs/12_task_queues_emails/02_send_email_user_registration/end/app.py index 65d7d0ca..0adde16e 100644 --- a/docs/docs/12_task_queues_emails/02_send_email_user_registration/end/app.py +++ b/docs/docs/12_task_queues_emails/02_send_email_user_registration/end/app.py @@ -9,7 +9,6 @@ from db import db from blocklist import BLOCKLIST -import models from resources.item import blp as ItemBlueprint from resources.store import blp as StoreBlueprint @@ -97,10 +96,6 @@ def missing_token_callback(error): 401, ) - @app.before_first_request - def create_tables(): - db.create_all() - api.register_blueprint(ItemBlueprint) api.register_blueprint(StoreBlueprint) api.register_blueprint(TagBlueprint) diff --git a/docs/docs/12_task_queues_emails/02_send_email_user_registration/end/migrations/env.py b/docs/docs/12_task_queues_emails/02_send_email_user_registration/end/migrations/env.py index 2ec83a7e..753d8f06 100644 --- a/docs/docs/12_task_queues_emails/02_send_email_user_registration/end/migrations/env.py +++ b/docs/docs/12_task_queues_emails/02_send_email_user_registration/end/migrations/env.py @@ -48,7 +48,6 @@ def run_migrations_offline(): context.configure( url=url, target_metadata=target_metadata, - compare_type=True, literal_binds=True ) @@ -81,7 +80,6 @@ def process_revision_directives(context, revision, directives): connection=connection, target_metadata=target_metadata, process_revision_directives=process_revision_directives, - compare_type=True, **current_app.extensions['migrate'].configure_args ) diff --git a/docs/docs/12_task_queues_emails/02_send_email_user_registration/start/CONTRIBUTING.md b/docs/docs/12_task_queues_emails/02_send_email_user_registration/start/CONTRIBUTING.md deleted file mode 100644 index 7e550e79..00000000 --- a/docs/docs/12_task_queues_emails/02_send_email_user_registration/start/CONTRIBUTING.md +++ /dev/null @@ -1,7 +0,0 @@ -# CONTRIBUTING - -## How to run the Dockerfile locally - -``` -docker run -dp 5000:5000 -w /app -v "$(pwd):/app" IMAGE_NAME sh -c "flask run" -``` diff --git a/docs/docs/12_task_queues_emails/02_send_email_user_registration/start/app.py b/docs/docs/12_task_queues_emails/02_send_email_user_registration/start/app.py index 65d7d0ca..0adde16e 100644 --- a/docs/docs/12_task_queues_emails/02_send_email_user_registration/start/app.py +++ b/docs/docs/12_task_queues_emails/02_send_email_user_registration/start/app.py @@ -9,7 +9,6 @@ from db import db from blocklist import BLOCKLIST -import models from resources.item import blp as ItemBlueprint from resources.store import blp as StoreBlueprint @@ -97,10 +96,6 @@ def missing_token_callback(error): 401, ) - @app.before_first_request - def create_tables(): - db.create_all() - api.register_blueprint(ItemBlueprint) api.register_blueprint(StoreBlueprint) api.register_blueprint(TagBlueprint) diff --git a/docs/docs/12_task_queues_emails/02_send_email_user_registration/start/migrations/env.py b/docs/docs/12_task_queues_emails/02_send_email_user_registration/start/migrations/env.py index 2ec83a7e..753d8f06 100644 --- a/docs/docs/12_task_queues_emails/02_send_email_user_registration/start/migrations/env.py +++ b/docs/docs/12_task_queues_emails/02_send_email_user_registration/start/migrations/env.py @@ -48,7 +48,6 @@ def run_migrations_offline(): context.configure( url=url, target_metadata=target_metadata, - compare_type=True, literal_binds=True ) @@ -81,7 +80,6 @@ def process_revision_directives(context, revision, directives): connection=connection, target_metadata=target_metadata, process_revision_directives=process_revision_directives, - compare_type=True, **current_app.extensions['migrate'].configure_args ) diff --git a/docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/CONTRIBUTING.md b/docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/CONTRIBUTING.md deleted file mode 100644 index 7e550e79..00000000 --- a/docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/CONTRIBUTING.md +++ /dev/null @@ -1,7 +0,0 @@ -# CONTRIBUTING - -## How to run the Dockerfile locally - -``` -docker run -dp 5000:5000 -w /app -v "$(pwd):/app" IMAGE_NAME sh -c "flask run" -``` diff --git a/docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/app.py b/docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/app.py index 65d7d0ca..0adde16e 100644 --- a/docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/app.py +++ b/docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/app.py @@ -9,7 +9,6 @@ from db import db from blocklist import BLOCKLIST -import models from resources.item import blp as ItemBlueprint from resources.store import blp as StoreBlueprint @@ -97,10 +96,6 @@ def missing_token_callback(error): 401, ) - @app.before_first_request - def create_tables(): - db.create_all() - api.register_blueprint(ItemBlueprint) api.register_blueprint(StoreBlueprint) api.register_blueprint(TagBlueprint) diff --git a/docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/migrations/env.py b/docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/migrations/env.py index 2ec83a7e..753d8f06 100644 --- a/docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/migrations/env.py +++ b/docs/docs/12_task_queues_emails/04_populate_rq_task_queue/end/migrations/env.py @@ -48,7 +48,6 @@ def run_migrations_offline(): context.configure( url=url, target_metadata=target_metadata, - compare_type=True, literal_binds=True ) @@ -81,7 +80,6 @@ def process_revision_directives(context, revision, directives): connection=connection, target_metadata=target_metadata, process_revision_directives=process_revision_directives, - compare_type=True, **current_app.extensions['migrate'].configure_args ) diff --git a/docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/CONTRIBUTING.md b/docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/CONTRIBUTING.md deleted file mode 100644 index 7e550e79..00000000 --- a/docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/CONTRIBUTING.md +++ /dev/null @@ -1,7 +0,0 @@ -# CONTRIBUTING - -## How to run the Dockerfile locally - -``` -docker run -dp 5000:5000 -w /app -v "$(pwd):/app" IMAGE_NAME sh -c "flask run" -``` diff --git a/docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/app.py b/docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/app.py index 65d7d0ca..0adde16e 100644 --- a/docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/app.py +++ b/docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/app.py @@ -9,7 +9,6 @@ from db import db from blocklist import BLOCKLIST -import models from resources.item import blp as ItemBlueprint from resources.store import blp as StoreBlueprint @@ -97,10 +96,6 @@ def missing_token_callback(error): 401, ) - @app.before_first_request - def create_tables(): - db.create_all() - api.register_blueprint(ItemBlueprint) api.register_blueprint(StoreBlueprint) api.register_blueprint(TagBlueprint) diff --git a/docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/migrations/env.py b/docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/migrations/env.py index 2ec83a7e..753d8f06 100644 --- a/docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/migrations/env.py +++ b/docs/docs/12_task_queues_emails/04_populate_rq_task_queue/start/migrations/env.py @@ -48,7 +48,6 @@ def run_migrations_offline(): context.configure( url=url, target_metadata=target_metadata, - compare_type=True, literal_binds=True ) @@ -81,7 +80,6 @@ def process_revision_directives(context, revision, directives): connection=connection, target_metadata=target_metadata, process_revision_directives=process_revision_directives, - compare_type=True, **current_app.extensions['migrate'].configure_args ) diff --git a/docs/docs/12_task_queues_emails/05_rq_background_worker/end/CONTRIBUTING.md b/docs/docs/12_task_queues_emails/05_rq_background_worker/end/CONTRIBUTING.md deleted file mode 100644 index 7e550e79..00000000 --- a/docs/docs/12_task_queues_emails/05_rq_background_worker/end/CONTRIBUTING.md +++ /dev/null @@ -1,7 +0,0 @@ -# CONTRIBUTING - -## How to run the Dockerfile locally - -``` -docker run -dp 5000:5000 -w /app -v "$(pwd):/app" IMAGE_NAME sh -c "flask run" -``` diff --git a/docs/docs/12_task_queues_emails/05_rq_background_worker/end/app.py b/docs/docs/12_task_queues_emails/05_rq_background_worker/end/app.py index 65d7d0ca..0adde16e 100644 --- a/docs/docs/12_task_queues_emails/05_rq_background_worker/end/app.py +++ b/docs/docs/12_task_queues_emails/05_rq_background_worker/end/app.py @@ -9,7 +9,6 @@ from db import db from blocklist import BLOCKLIST -import models from resources.item import blp as ItemBlueprint from resources.store import blp as StoreBlueprint @@ -97,10 +96,6 @@ def missing_token_callback(error): 401, ) - @app.before_first_request - def create_tables(): - db.create_all() - api.register_blueprint(ItemBlueprint) api.register_blueprint(StoreBlueprint) api.register_blueprint(TagBlueprint) diff --git a/docs/docs/12_task_queues_emails/05_rq_background_worker/end/migrations/env.py b/docs/docs/12_task_queues_emails/05_rq_background_worker/end/migrations/env.py index 2ec83a7e..753d8f06 100644 --- a/docs/docs/12_task_queues_emails/05_rq_background_worker/end/migrations/env.py +++ b/docs/docs/12_task_queues_emails/05_rq_background_worker/end/migrations/env.py @@ -48,7 +48,6 @@ def run_migrations_offline(): context.configure( url=url, target_metadata=target_metadata, - compare_type=True, literal_binds=True ) @@ -81,7 +80,6 @@ def process_revision_directives(context, revision, directives): connection=connection, target_metadata=target_metadata, process_revision_directives=process_revision_directives, - compare_type=True, **current_app.extensions['migrate'].configure_args ) diff --git a/docs/docs/12_task_queues_emails/05_rq_background_worker/start/CONTRIBUTING.md b/docs/docs/12_task_queues_emails/05_rq_background_worker/start/CONTRIBUTING.md deleted file mode 100644 index 7e550e79..00000000 --- a/docs/docs/12_task_queues_emails/05_rq_background_worker/start/CONTRIBUTING.md +++ /dev/null @@ -1,7 +0,0 @@ -# CONTRIBUTING - -## How to run the Dockerfile locally - -``` -docker run -dp 5000:5000 -w /app -v "$(pwd):/app" IMAGE_NAME sh -c "flask run" -``` diff --git a/docs/docs/12_task_queues_emails/05_rq_background_worker/start/app.py b/docs/docs/12_task_queues_emails/05_rq_background_worker/start/app.py index 65d7d0ca..0adde16e 100644 --- a/docs/docs/12_task_queues_emails/05_rq_background_worker/start/app.py +++ b/docs/docs/12_task_queues_emails/05_rq_background_worker/start/app.py @@ -9,7 +9,6 @@ from db import db from blocklist import BLOCKLIST -import models from resources.item import blp as ItemBlueprint from resources.store import blp as StoreBlueprint @@ -97,10 +96,6 @@ def missing_token_callback(error): 401, ) - @app.before_first_request - def create_tables(): - db.create_all() - api.register_blueprint(ItemBlueprint) api.register_blueprint(StoreBlueprint) api.register_blueprint(TagBlueprint) diff --git a/docs/docs/12_task_queues_emails/05_rq_background_worker/start/migrations/env.py b/docs/docs/12_task_queues_emails/05_rq_background_worker/start/migrations/env.py index 2ec83a7e..753d8f06 100644 --- a/docs/docs/12_task_queues_emails/05_rq_background_worker/start/migrations/env.py +++ b/docs/docs/12_task_queues_emails/05_rq_background_worker/start/migrations/env.py @@ -48,7 +48,6 @@ def run_migrations_offline(): context.configure( url=url, target_metadata=target_metadata, - compare_type=True, literal_binds=True ) @@ -81,7 +80,6 @@ def process_revision_directives(context, revision, directives): connection=connection, target_metadata=target_metadata, process_revision_directives=process_revision_directives, - compare_type=True, **current_app.extensions['migrate'].configure_args ) diff --git a/docs/docs/12_task_queues_emails/06_sending_html_emails/end/CONTRIBUTING.md b/docs/docs/12_task_queues_emails/06_sending_html_emails/end/CONTRIBUTING.md deleted file mode 100644 index 7e550e79..00000000 --- a/docs/docs/12_task_queues_emails/06_sending_html_emails/end/CONTRIBUTING.md +++ /dev/null @@ -1,7 +0,0 @@ -# CONTRIBUTING - -## How to run the Dockerfile locally - -``` -docker run -dp 5000:5000 -w /app -v "$(pwd):/app" IMAGE_NAME sh -c "flask run" -``` diff --git a/docs/docs/12_task_queues_emails/06_sending_html_emails/end/app.py b/docs/docs/12_task_queues_emails/06_sending_html_emails/end/app.py index 65d7d0ca..0adde16e 100644 --- a/docs/docs/12_task_queues_emails/06_sending_html_emails/end/app.py +++ b/docs/docs/12_task_queues_emails/06_sending_html_emails/end/app.py @@ -9,7 +9,6 @@ from db import db from blocklist import BLOCKLIST -import models from resources.item import blp as ItemBlueprint from resources.store import blp as StoreBlueprint @@ -97,10 +96,6 @@ def missing_token_callback(error): 401, ) - @app.before_first_request - def create_tables(): - db.create_all() - api.register_blueprint(ItemBlueprint) api.register_blueprint(StoreBlueprint) api.register_blueprint(TagBlueprint) diff --git a/docs/docs/12_task_queues_emails/06_sending_html_emails/end/migrations/env.py b/docs/docs/12_task_queues_emails/06_sending_html_emails/end/migrations/env.py index 2ec83a7e..753d8f06 100644 --- a/docs/docs/12_task_queues_emails/06_sending_html_emails/end/migrations/env.py +++ b/docs/docs/12_task_queues_emails/06_sending_html_emails/end/migrations/env.py @@ -48,7 +48,6 @@ def run_migrations_offline(): context.configure( url=url, target_metadata=target_metadata, - compare_type=True, literal_binds=True ) @@ -81,7 +80,6 @@ def process_revision_directives(context, revision, directives): connection=connection, target_metadata=target_metadata, process_revision_directives=process_revision_directives, - compare_type=True, **current_app.extensions['migrate'].configure_args ) diff --git a/docs/docs/12_task_queues_emails/06_sending_html_emails/start/CONTRIBUTING.md b/docs/docs/12_task_queues_emails/06_sending_html_emails/start/CONTRIBUTING.md deleted file mode 100644 index 7e550e79..00000000 --- a/docs/docs/12_task_queues_emails/06_sending_html_emails/start/CONTRIBUTING.md +++ /dev/null @@ -1,7 +0,0 @@ -# CONTRIBUTING - -## How to run the Dockerfile locally - -``` -docker run -dp 5000:5000 -w /app -v "$(pwd):/app" IMAGE_NAME sh -c "flask run" -``` diff --git a/docs/docs/12_task_queues_emails/06_sending_html_emails/start/app.py b/docs/docs/12_task_queues_emails/06_sending_html_emails/start/app.py index 65d7d0ca..0adde16e 100644 --- a/docs/docs/12_task_queues_emails/06_sending_html_emails/start/app.py +++ b/docs/docs/12_task_queues_emails/06_sending_html_emails/start/app.py @@ -9,7 +9,6 @@ from db import db from blocklist import BLOCKLIST -import models from resources.item import blp as ItemBlueprint from resources.store import blp as StoreBlueprint @@ -97,10 +96,6 @@ def missing_token_callback(error): 401, ) - @app.before_first_request - def create_tables(): - db.create_all() - api.register_blueprint(ItemBlueprint) api.register_blueprint(StoreBlueprint) api.register_blueprint(TagBlueprint) diff --git a/docs/docs/12_task_queues_emails/06_sending_html_emails/start/migrations/env.py b/docs/docs/12_task_queues_emails/06_sending_html_emails/start/migrations/env.py index 2ec83a7e..753d8f06 100644 --- a/docs/docs/12_task_queues_emails/06_sending_html_emails/start/migrations/env.py +++ b/docs/docs/12_task_queues_emails/06_sending_html_emails/start/migrations/env.py @@ -48,7 +48,6 @@ def run_migrations_offline(): context.configure( url=url, target_metadata=target_metadata, - compare_type=True, literal_binds=True ) @@ -81,7 +80,6 @@ def process_revision_directives(context, revision, directives): connection=connection, target_metadata=target_metadata, process_revision_directives=process_revision_directives, - compare_type=True, **current_app.extensions['migrate'].configure_args ) diff --git a/docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/CONTRIBUTING.md b/docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/CONTRIBUTING.md deleted file mode 100644 index 7e550e79..00000000 --- a/docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/CONTRIBUTING.md +++ /dev/null @@ -1,7 +0,0 @@ -# CONTRIBUTING - -## How to run the Dockerfile locally - -``` -docker run -dp 5000:5000 -w /app -v "$(pwd):/app" IMAGE_NAME sh -c "flask run" -``` diff --git a/docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/app.py b/docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/app.py index 65d7d0ca..0adde16e 100644 --- a/docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/app.py +++ b/docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/app.py @@ -9,7 +9,6 @@ from db import db from blocklist import BLOCKLIST -import models from resources.item import blp as ItemBlueprint from resources.store import blp as StoreBlueprint @@ -97,10 +96,6 @@ def missing_token_callback(error): 401, ) - @app.before_first_request - def create_tables(): - db.create_all() - api.register_blueprint(ItemBlueprint) api.register_blueprint(StoreBlueprint) api.register_blueprint(TagBlueprint) diff --git a/docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/migrations/env.py b/docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/migrations/env.py index 2ec83a7e..753d8f06 100644 --- a/docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/migrations/env.py +++ b/docs/docs/12_task_queues_emails/07_deploy_background_worker_render/end/migrations/env.py @@ -48,7 +48,6 @@ def run_migrations_offline(): context.configure( url=url, target_metadata=target_metadata, - compare_type=True, literal_binds=True ) @@ -81,7 +80,6 @@ def process_revision_directives(context, revision, directives): connection=connection, target_metadata=target_metadata, process_revision_directives=process_revision_directives, - compare_type=True, **current_app.extensions['migrate'].configure_args ) diff --git a/docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/CONTRIBUTING.md b/docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/CONTRIBUTING.md deleted file mode 100644 index 7e550e79..00000000 --- a/docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/CONTRIBUTING.md +++ /dev/null @@ -1,7 +0,0 @@ -# CONTRIBUTING - -## How to run the Dockerfile locally - -``` -docker run -dp 5000:5000 -w /app -v "$(pwd):/app" IMAGE_NAME sh -c "flask run" -``` diff --git a/docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/app.py b/docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/app.py index 65d7d0ca..0adde16e 100644 --- a/docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/app.py +++ b/docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/app.py @@ -9,7 +9,6 @@ from db import db from blocklist import BLOCKLIST -import models from resources.item import blp as ItemBlueprint from resources.store import blp as StoreBlueprint @@ -97,10 +96,6 @@ def missing_token_callback(error): 401, ) - @app.before_first_request - def create_tables(): - db.create_all() - api.register_blueprint(ItemBlueprint) api.register_blueprint(StoreBlueprint) api.register_blueprint(TagBlueprint) diff --git a/docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/migrations/env.py b/docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/migrations/env.py index 2ec83a7e..753d8f06 100644 --- a/docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/migrations/env.py +++ b/docs/docs/12_task_queues_emails/07_deploy_background_worker_render/start/migrations/env.py @@ -48,7 +48,6 @@ def run_migrations_offline(): context.configure( url=url, target_metadata=target_metadata, - compare_type=True, literal_binds=True ) @@ -81,7 +80,6 @@ def process_revision_directives(context, revision, directives): connection=connection, target_metadata=target_metadata, process_revision_directives=process_revision_directives, - compare_type=True, **current_app.extensions['migrate'].configure_args ) diff --git a/docs/src/components/VideoEmbed/index.js b/docs/src/components/VideoEmbed/index.js index e589c0ec..1af34375 100644 --- a/docs/src/components/VideoEmbed/index.js +++ b/docs/src/components/VideoEmbed/index.js @@ -14,7 +14,7 @@ export default function VideoEmbed({ url }) { width: "100%", }} allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;" - allowfullscreen="true" + allowFullScreen={true} > );